Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions docs/content/api/MySqlConnector/MySqlConnectionType.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 8 additions & 2 deletions docs/content/connection-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,10 @@ These are the options that need to be used in order to configure a connection to
<tr id="CertificateFile">
<td>Certificate File, CertificateFile</td>
<td></td>
<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>
<td>
<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>
<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>
</td>
</tr>
<tr id="CertificatePassword">
<td>Certificate Password, CertificatePassword</td>
Expand All @@ -127,7 +130,10 @@ These are the options that need to be used in order to configure a connection to
<tr id="SslCa">
<td>SSL CA, CA Certificate File, CACertificateFile, SslCa, Ssl-Ca</td>
<td></td>
<td>The path to a CA certificate file in a PEM Encoded (.pem) format. This should be used with <code>SslMode=VerifyCA</code> or <code>SslMode=VerifyFull</code> to enable verification of a CA certificate that is not trusted by the operating system’s certificate store.</td>
<td>
<p>The path to a CA certificate file in a PEM Encoded (.pem) format. This should be used with <code>SslMode=VerifyCA</code> or <code>SslMode=VerifyFull</code> to enable verification of a CA certificate that is not trusted by the operating system’s certificate store.</p>
<p>To provide a custom callback to validate the remote certificate, leave this option empty and set <code>SslMode</code> to <code>Required</code> (or <code>Preferred</code>), then set <a href="/api/mysqlconnector/mysqlconnection/remotecertificatevalidationcallback/"><code>MySqlConnection.RemoteCertificateValidationCallback</code></a> before calling <a href="/api/mysqlconnector/mysqlconnection/open/"><code>MySqlConnection.Open</code></a>. The property should be set to a delegate that will validate the remote certificate, as per <a href="https://docs.microsoft.com/en-us/dotnet/api/system.net.security.remotecertificatevalidationcallback" title="RemoteCertificateValidationCallback Delegate (MSDN)">the documentation</a>.</p>
</td>
</tr>
<tr id="CertificateStoreLocation">
<td>Certificate Store Location, CertificateStoreLocation</td>
Expand Down
55 changes: 45 additions & 10 deletions src/MySqlConnector/Core/ServerSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -218,9 +218,9 @@ public async Task PrepareAsync(IMySqlCommand command, IOBehavior ioBehavior, Can
{
payload = await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
}
catch (MySqlException exception)
catch (MySqlException ex)
{
ThrowIfStatementContainsDelimiter(exception, command);
ThrowIfStatementContainsDelimiter(ex, command);
throw;
}

Expand Down Expand Up @@ -482,7 +482,7 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella

try
{
await InitSslAsync(initialHandshake.ProtocolCapabilities, cs, sslProtocols, ioBehavior, cancellationToken).ConfigureAwait(false);
await InitSslAsync(initialHandshake.ProtocolCapabilities, cs, connection, sslProtocols, ioBehavior, cancellationToken).ConfigureAwait(false);
shouldRetrySsl = false;
}
catch (ArgumentException ex) when (ex.ParamName == "sslProtocolType" && sslProtocols == SslProtocols.None)
Expand Down Expand Up @@ -1185,7 +1185,7 @@ private async Task<bool> OpenNamedPipeAsync(ConnectionSettings cs, int startTick
return false;
}

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

if (clientCertificates is null && connection.ProvideClientCertificatesCallback is { } clientCertificatesProvider)
{
clientCertificates = new();
try
{
await clientCertificatesProvider(clientCertificates).ConfigureAwait(false);
}
catch (Exception ex)
{
m_logArguments[1] = ex.Message;
Log.Error(ex, "Session{0} failed to obtain client certificates via ProvideClientCertificatesCallback: {1}", m_logArguments);
throw new MySqlException("Failed to obtain client certificates via ProvideClientCertificatesCallback", ex);
}
}

X509Chain? caCertificateChain = null;
if (cs.CACertificateFile.Length != 0)
{
Expand Down Expand Up @@ -1354,8 +1369,28 @@ caCertificateChain is not null &&
return rcbPolicyErrors == SslPolicyErrors.None;
}

var sslStream = clientCertificates is null ? new SslStream(m_stream!, false, ValidateRemoteCertificate) :
new SslStream(m_stream!, false, ValidateRemoteCertificate, ValidateLocalCertificate);
// use the client's callback (if any) for Preferred or Required mode
RemoteCertificateValidationCallback validateRemoteCertificate = ValidateRemoteCertificate;
if (connection.RemoteCertificateValidationCallback is not null)
{
if (caCertificateChain is not null)
{
Log.Warn("Session{0} not using client-provided RemoteCertificateValidationCallback because SslCA is specified", m_logArguments);
}
else if (cs.SslMode is not MySqlSslMode.Preferred and not MySqlSslMode.Required)
{
m_logArguments[1] = cs.SslMode;
Log.Warn("Session{0} not using client-provided RemoteCertificateValidationCallback because SslMode is {1}", m_logArguments);
}
else
{
Log.Debug("Session{0} using client-provided RemoteCertificateValidationCallback", m_logArguments);
validateRemoteCertificate = connection.RemoteCertificateValidationCallback;
}
}

var sslStream = clientCertificates is null ? new SslStream(m_stream!, false, validateRemoteCertificate) :
new SslStream(m_stream!, false, validateRemoteCertificate, ValidateLocalCertificate);

var checkCertificateRevocation = cs.SslMode == MySqlSslMode.VerifyFull;

Expand Down Expand Up @@ -1767,11 +1802,11 @@ private string GetPassword(ConnectionSettings cs, MySqlConnection connection)
Log.Trace("Session{0} obtaining password via ProvidePasswordCallback", m_logArguments);
return passwordProvider(new(HostName, cs.Port, cs.UserID, cs.Database));
}
catch (Exception e)
catch (Exception ex)
{
m_logArguments[1] = e.Message;
Log.Error("Session{0} failed to obtain password via ProvidePasswordCallback: {1}", m_logArguments);
throw new MySqlException(MySqlErrorCode.ProvidePasswordCallbackFailed, "Failed to obtain password via ProvidePasswordCallback", e);
m_logArguments[1] = ex.Message;
Log.Error(ex, "Session{0} failed to obtain password via ProvidePasswordCallback: {1}", m_logArguments);
throw new MySqlException(MySqlErrorCode.ProvidePasswordCallbackFailed, "Failed to obtain password via ProvidePasswordCallback", ex);
}
}

Expand Down
22 changes: 22 additions & 0 deletions src/MySqlConnector/MySqlConnection.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using MySqlConnector.Core;
using MySqlConnector.Logging;
using MySqlConnector.Protocol.Payloads;
Expand Down Expand Up @@ -496,6 +498,16 @@ public override string ConnectionString
/// </summary>
public int ServerThread => Session.ConnectionId;

/// <summary>
/// Gets or sets the delegate used to provide client certificates for connecting to a server.
/// </summary>
/// <remarks>The provided <see cref="X509CertificateCollection"/> should be filled with the client certificate(s) needed to connect to the server.</remarks>
#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER
public Func<X509CertificateCollection, ValueTask>? ProvideClientCertificatesCallback { get; set; }
#else
public Func<X509CertificateCollection, Task>? ProvideClientCertificatesCallback { get; set; }
#endif

/// <summary>
/// Gets or sets the delegate used to generate a password for new database connections.
/// </summary>
Expand All @@ -511,6 +523,14 @@ public override string ConnectionString
/// </remarks>
public Func<MySqlProvidePasswordContext, string>? ProvidePasswordCallback { get; set;}

/// <summary>
/// Gets or sets the delegate used to verify that the server's certificate is valid.
/// </summary>
/// <remarks><see cref="MySqlConnectionStringBuilder.SslMode"/> must be set to <see cref="MySqlSslMode.Preferred"/>
/// or <see cref="MySqlSslMode.Required"/> in order for this delegate to be invoked. See the documentation for
/// <see cref="RemoteCertificateValidationCallback"/> for more information on the values passed to this delegate.</remarks>
public RemoteCertificateValidationCallback? RemoteCertificateValidationCallback { get; set; }

/// <summary>
/// Clears the connection pool that <paramref name="connection"/> belongs to.
/// </summary>
Expand Down Expand Up @@ -674,6 +694,7 @@ public async Task DisposeAsync()

public MySqlConnection Clone() => new(m_connectionString, m_hasBeenOpened)
{
ProvideClientCertificatesCallback = ProvideClientCertificatesCallback,
ProvidePasswordCallback = ProvidePasswordCallback,
};

Expand All @@ -697,6 +718,7 @@ public MySqlConnection CloneWith(string connectionString)
newBuilder.Password = currentBuilder.Password;
return new MySqlConnection(newBuilder.ConnectionString, m_hasBeenOpened && shouldCopyPassword && !currentBuilder.PersistSecurityInfo)
{
ProvideClientCertificatesCallback = ProvideClientCertificatesCallback,
ProvidePasswordCallback = ProvidePasswordCallback,
};
}
Expand Down
50 changes: 50 additions & 0 deletions tests/SideBySide/SslTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,36 @@ public async Task ConnectSslClientCertificate(string certFile, string certFilePa
await DoTestSsl(csb.ConnectionString);
}

#if !BASELINE
[SkippableTheory(ConfigSettings.RequiresSsl | ConfigSettings.KnownClientCertificate)]
[InlineData("ssl-client.pfx", null)]
[InlineData("ssl-client-pw-test.pfx", "test")]
public async Task ConnectSslClientCertificateCallback(string certificateFile, string certificateFilePassword)
{
var csb = AppConfig.CreateConnectionStringBuilder();
var certificateFilePath = Path.Combine(AppConfig.CertsPath, certificateFile);

using var connection = new MySqlConnection(csb.ConnectionString);
#if NETFRAMEWORK
connection.ProvideClientCertificatesCallback = x =>
{
x.Add(new X509Certificate2(certificateFilePath, certificateFilePassword));
return MySqlConnector.Utilities.Utility.CompletedTask;
};
#else
connection.ProvideClientCertificatesCallback = async x =>
{
var certificateBytes = await File.ReadAllBytesAsync(certificateFilePath);
x.Add(new X509Certificate2(certificateBytes, certificateFilePassword));
};
#endif

await connection.OpenAsync();
Assert.True(connection.SslIsEncrypted);
}
#endif

[SkippableTheory(ConfigSettings.RequiresSsl | ConfigSettings.KnownClientCertificate)]
[InlineData("ssl-client-cert.pem", "ssl-client-key.pem", null)]
[InlineData("ssl-client-cert.pem", "ssl-client-key-null.pem", null)]
#if !BASELINE
Expand Down Expand Up @@ -163,6 +192,27 @@ public async Task ConnectSslBadCaCertificate()
await Assert.ThrowsAsync<MySqlException>(async () => await connection.OpenAsync());
}

#if !BASELINE
[SkippableTheory(ServerFeatures.KnownCertificateAuthority, ConfigSettings.RequiresSsl)]
[InlineData(MySqlSslMode.VerifyCA, false, false)]
[InlineData(MySqlSslMode.VerifyCA, true, false)]
[InlineData(MySqlSslMode.Required, true, true)]
public async Task ConnectSslRemoteCertificateValidationCallback(MySqlSslMode sslMode, bool clearCA, bool expectedSuccess)
{
var csb = AppConfig.CreateConnectionStringBuilder();
csb.CertificateFile = Path.Combine(AppConfig.CertsPath, "ssl-client.pfx");
csb.SslMode = sslMode;
csb.SslCa = clearCA ? "" : Path.Combine(AppConfig.CertsPath, "non-ca-client-cert.pem");
using var connection = new MySqlConnection(csb.ConnectionString);
connection.RemoteCertificateValidationCallback = (s, c, h, e) => true;

if (expectedSuccess)
await connection.OpenAsync();
else
await Assert.ThrowsAsync<MySqlException>(async () => await connection.OpenAsync());
}
#endif

[SkippableFact(ConfigSettings.RequiresSsl)]
public async Task ConnectSslTlsVersion()
{
Expand Down