Skip to content

Commit 6abcff1

Browse files
committed
Add ProvideClientCertificatesCallback.
Signed-off-by: Bradley Grainger <bgrainger@gmail.com>
1 parent bb2a03b commit 6abcff1

File tree

6 files changed

+93
-9
lines changed

6 files changed

+93
-9
lines changed

docs/content/api/MySqlConnector/MySqlConnection/ProvideClientCertificatesCallback.md

Lines changed: 23 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/content/api/MySqlConnector/MySqlConnectionType.md

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/content/connection-options.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,10 @@ These are the options that need to be used in order to configure a connection to
107107
<tr id="CertificateFile">
108108
<td>Certificate File, CertificateFile</td>
109109
<td></td>
110-
<td>The path to a certificate file in PKCS #12 (.pfx) format containing a bundled Certificate and Private Key used for mutual authentication. To create a PKCS #12 bundle from a PEM encoded Certificate and Key, use <code>openssl pkcs12 -in cert.pem -inkey key.pem -export -out bundle.pfx</code>. This option should not be specified if <code>SslCert</code> and <code>SslKey</code> are used.</td>
110+
<td>
111+
<p>The path to a certificate file in PKCS #12 (.pfx) format containing a bundled Certificate and Private Key used for mutual authentication. To create a PKCS #12 bundle from a PEM encoded Certificate and Key, use <code>openssl pkcs12 -in cert.pem -inkey key.pem -export -out bundle.pfx</code>. This option should not be specified if <code>SslCert</code> and <code>SslKey</code> are used.</p>
112+
<p>If the certificate can't be loaded from a file path, leave this value empty and set <a href="/api/mysqlconnector/mysqlconnection/provideclientcertificatescallback/"><code>MySqlConnection.ProvideClientCertificatesCallback</code></a> before calling <a href="/api/mysqlconnector/mysqlconnection/open/"><code>MySqlConnection.Open</code></a>. The property should be set to an async delegate that will populate a <code>X509CertificateCollection</code> with the client certificate(s) needed to connect.</p>
113+
</td>
111114
</tr>
112115
<tr id="CertificatePassword">
113116
<td>Certificate Password, CertificatePassword</td>

src/MySqlConnector/Core/ServerSession.cs

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -218,9 +218,9 @@ public async Task PrepareAsync(IMySqlCommand command, IOBehavior ioBehavior, Can
218218
{
219219
payload = await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
220220
}
221-
catch (MySqlException exception)
221+
catch (MySqlException ex)
222222
{
223-
ThrowIfStatementContainsDelimiter(exception, command);
223+
ThrowIfStatementContainsDelimiter(ex, command);
224224
throw;
225225
}
226226

@@ -482,7 +482,7 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella
482482

483483
try
484484
{
485-
await InitSslAsync(initialHandshake.ProtocolCapabilities, cs, sslProtocols, ioBehavior, cancellationToken).ConfigureAwait(false);
485+
await InitSslAsync(initialHandshake.ProtocolCapabilities, cs, connection, sslProtocols, ioBehavior, cancellationToken).ConfigureAwait(false);
486486
shouldRetrySsl = false;
487487
}
488488
catch (ArgumentException ex) when (ex.ParamName == "sslProtocolType" && sslProtocols == SslProtocols.None)
@@ -1185,7 +1185,7 @@ private async Task<bool> OpenNamedPipeAsync(ConnectionSettings cs, int startTick
11851185
return false;
11861186
}
11871187

1188-
private async Task InitSslAsync(ProtocolCapabilities serverCapabilities, ConnectionSettings cs, SslProtocols sslProtocols, IOBehavior ioBehavior, CancellationToken cancellationToken)
1188+
private async Task InitSslAsync(ProtocolCapabilities serverCapabilities, ConnectionSettings cs, MySqlConnection connection, SslProtocols sslProtocols, IOBehavior ioBehavior, CancellationToken cancellationToken)
11891189
{
11901190
Log.Trace("Session{0} initializing TLS connection", m_logArguments);
11911191
X509CertificateCollection? clientCertificates = null;
@@ -1264,6 +1264,21 @@ private async Task InitSslAsync(ProtocolCapabilities serverCapabilities, Connect
12641264
}
12651265
}
12661266

1267+
if (clientCertificates is null && connection.ProvideClientCertificatesCallback is { } clientCertificatesProvider)
1268+
{
1269+
clientCertificates = new();
1270+
try
1271+
{
1272+
await clientCertificatesProvider(clientCertificates).ConfigureAwait(false);
1273+
}
1274+
catch (Exception ex)
1275+
{
1276+
m_logArguments[1] = ex.Message;
1277+
Log.Error(ex, "Session{0} failed to obtain client certificates via ProvideClientCertificatesCallback: {1}", m_logArguments);
1278+
throw new MySqlException("Failed to obtain client certificates via ProvideClientCertificatesCallback", ex);
1279+
}
1280+
}
1281+
12671282
X509Chain? caCertificateChain = null;
12681283
if (cs.CACertificateFile.Length != 0)
12691284
{
@@ -1767,11 +1782,11 @@ private string GetPassword(ConnectionSettings cs, MySqlConnection connection)
17671782
Log.Trace("Session{0} obtaining password via ProvidePasswordCallback", m_logArguments);
17681783
return passwordProvider(new(HostName, cs.Port, cs.UserID, cs.Database));
17691784
}
1770-
catch (Exception e)
1785+
catch (Exception ex)
17711786
{
1772-
m_logArguments[1] = e.Message;
1773-
Log.Error("Session{0} failed to obtain password via ProvidePasswordCallback: {1}", m_logArguments);
1774-
throw new MySqlException(MySqlErrorCode.ProvidePasswordCallbackFailed, "Failed to obtain password via ProvidePasswordCallback", e);
1787+
m_logArguments[1] = ex.Message;
1788+
Log.Error(ex, "Session{0} failed to obtain password via ProvidePasswordCallback: {1}", m_logArguments);
1789+
throw new MySqlException(MySqlErrorCode.ProvidePasswordCallbackFailed, "Failed to obtain password via ProvidePasswordCallback", ex);
17751790
}
17761791
}
17771792

src/MySqlConnector/MySqlConnection.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Diagnostics.CodeAnalysis;
44
using System.Net.Sockets;
55
using System.Security.Authentication;
6+
using System.Security.Cryptography.X509Certificates;
67
using MySqlConnector.Core;
78
using MySqlConnector.Logging;
89
using MySqlConnector.Protocol.Payloads;
@@ -496,6 +497,16 @@ public override string ConnectionString
496497
/// </summary>
497498
public int ServerThread => Session.ConnectionId;
498499

500+
/// <summary>
501+
/// Gets or sets the delegate used to provide client certificates for connecting to a server.
502+
/// </summary>
503+
/// <remarks>The provided <see cref="X509CertificateCollection"/> should be filled with the client certificate(s) needed to connect to the server.</remarks>
504+
#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER
505+
public Func<X509CertificateCollection, ValueTask>? ProvideClientCertificatesCallback { get; set; }
506+
#else
507+
public Func<X509CertificateCollection, Task>? ProvideClientCertificatesCallback { get; set; }
508+
#endif
509+
499510
/// <summary>
500511
/// Gets or sets the delegate used to generate a password for new database connections.
501512
/// </summary>
@@ -674,6 +685,7 @@ public async Task DisposeAsync()
674685

675686
public MySqlConnection Clone() => new(m_connectionString, m_hasBeenOpened)
676687
{
688+
ProvideClientCertificatesCallback = ProvideClientCertificatesCallback,
677689
ProvidePasswordCallback = ProvidePasswordCallback,
678690
};
679691

@@ -697,6 +709,7 @@ public MySqlConnection CloneWith(string connectionString)
697709
newBuilder.Password = currentBuilder.Password;
698710
return new MySqlConnection(newBuilder.ConnectionString, m_hasBeenOpened && shouldCopyPassword && !currentBuilder.PersistSecurityInfo)
699711
{
712+
ProvideClientCertificatesCallback = ProvideClientCertificatesCallback,
700713
ProvidePasswordCallback = ProvidePasswordCallback,
701714
};
702715
}

tests/SideBySide/SslTests.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,36 @@ public async Task ConnectSslClientCertificate(string certFile, string certFilePa
5252
await DoTestSsl(csb.ConnectionString);
5353
}
5454

55+
#if !BASELINE
5556
[SkippableTheory(ConfigSettings.RequiresSsl | ConfigSettings.KnownClientCertificate)]
57+
[InlineData("ssl-client.pfx", null)]
58+
[InlineData("ssl-client-pw-test.pfx", "test")]
59+
public async Task ConnectSslClientCertificateCallback(string certificateFile, string certificateFilePassword)
60+
{
61+
var csb = AppConfig.CreateConnectionStringBuilder();
62+
var certificateFilePath = Path.Combine(AppConfig.CertsPath, certificateFile);
63+
64+
using var connection = new MySqlConnection(csb.ConnectionString);
65+
#if NETFRAMEWORK
66+
connection.ProvideClientCertificatesCallback = x =>
67+
{
68+
x.Add(new X509Certificate2(certificateFilePath, certificateFilePassword));
69+
return MySqlConnector.Utilities.Utility.CompletedTask;
70+
};
71+
#else
72+
connection.ProvideClientCertificatesCallback = async x =>
73+
{
74+
var certificateBytes = await File.ReadAllBytesAsync(certificateFilePath);
75+
x.Add(new X509Certificate2(certificateBytes, certificateFilePassword));
76+
};
77+
#endif
78+
79+
await connection.OpenAsync();
80+
Assert.True(connection.SslIsEncrypted);
81+
}
82+
#endif
83+
84+
[SkippableTheory(ConfigSettings.RequiresSsl | ConfigSettings.KnownClientCertificate)]
5685
[InlineData("ssl-client-cert.pem", "ssl-client-key.pem", null)]
5786
[InlineData("ssl-client-cert.pem", "ssl-client-key-null.pem", null)]
5887
#if !BASELINE

0 commit comments

Comments
 (0)