diff --git a/.ci/config/config.compression+ssl.json b/.ci/config/config.compression+ssl.json
index 71cd817a0..f7d4263c8 100644
--- a/.ci/config/config.compression+ssl.json
+++ b/.ci/config/config.compression+ssl.json
@@ -4,7 +4,7 @@
"SocketPath": "./../../../../.ci/run/mysql/mysqld.sock",
"PasswordlessUser": "no_password",
"SecondaryDatabase": "testdb2",
- "UnsupportedFeatures": "CachingSha2Password,Redirection,RsaEncryption,Tls12,Tls13,UuidToBin",
+ "UnsupportedFeatures": "CachingSha2Password,Redirection,RsaEncryption,Tls12,Tls13,TlsFingerprintValidation,UuidToBin",
"MySqlBulkLoaderLocalCsvFile": "../../../TestData/LoadData_UTF8_BOM_Unix.CSV",
"MySqlBulkLoaderLocalTsvFile": "../../../TestData/LoadData_UTF8_BOM_Unix.TSV",
"CertificatesPath": "../../../../.ci/server/certs"
diff --git a/.ci/config/config.compression.json b/.ci/config/config.compression.json
index bf1073a12..09326f1f6 100644
--- a/.ci/config/config.compression.json
+++ b/.ci/config/config.compression.json
@@ -4,7 +4,7 @@
"SocketPath": "./../../../../.ci/run/mysql/mysqld.sock",
"PasswordlessUser": "no_password",
"SecondaryDatabase": "testdb2",
- "UnsupportedFeatures": "Ed25519,QueryAttributes,Redirection,StreamingResults,Tls11,UnixDomainSocket,ZeroDateTime",
+ "UnsupportedFeatures": "Ed25519,QueryAttributes,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,UnixDomainSocket,ZeroDateTime",
"MySqlBulkLoaderLocalCsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.CSV",
"MySqlBulkLoaderLocalTsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.TSV"
}
diff --git a/.ci/config/config.json b/.ci/config/config.json
index 035c05855..bc38f605a 100644
--- a/.ci/config/config.json
+++ b/.ci/config/config.json
@@ -4,7 +4,7 @@
"SocketPath": "./../../../../.ci/run/mysql/mysqld.sock",
"PasswordlessUser": "no_password",
"SecondaryDatabase": "testdb2",
- "UnsupportedFeatures": "Ed25519,QueryAttributes,Redirection,StreamingResults,Tls11,UnixDomainSocket,ZeroDateTime",
+ "UnsupportedFeatures": "Ed25519,QueryAttributes,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,UnixDomainSocket,ZeroDateTime",
"MySqlBulkLoaderLocalCsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.CSV",
"MySqlBulkLoaderLocalTsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.TSV"
}
diff --git a/.ci/config/config.ssl.json b/.ci/config/config.ssl.json
index 705e0a168..a7511faa4 100644
--- a/.ci/config/config.ssl.json
+++ b/.ci/config/config.ssl.json
@@ -4,7 +4,7 @@
"SocketPath": "./../../../../.ci/run/mysql/mysqld.sock",
"PasswordlessUser": "no_password",
"SecondaryDatabase": "testdb2",
- "UnsupportedFeatures": "CachingSha2Password,Redirection,RsaEncryption,Tls12,Tls13,UuidToBin",
+ "UnsupportedFeatures": "CachingSha2Password,Redirection,RsaEncryption,Tls12,Tls13,TlsFingerprintValidation,UuidToBin",
"MySqlBulkLoaderLocalCsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.CSV",
"MySqlBulkLoaderLocalTsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.TSV",
"CertificatesPath": "../../../../.ci/server/certs"
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index 47aa77a39..5e57f867e 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -62,7 +62,7 @@ jobs:
arguments: 'tests\IntegrationTests\IntegrationTests.csproj -c MySqlData'
testRunTitle: 'MySql.Data integration tests'
env:
- DATA__UNSUPPORTEDFEATURES: 'Ed25519,QueryAttributes,StreamingResults,UnixDomainSocket'
+ DATA__UNSUPPORTEDFEATURES: 'Ed25519,QueryAttributes,StreamingResults,TlsFingerprintValidation,UnixDomainSocket'
DATA__CONNECTIONSTRING: 'server=localhost;port=3306;user id=root;password=test;database=mysqltest;ssl mode=none;DefaultCommandTimeout=3600'
DATA__CERTIFICATESPATH: '$(Build.Repository.LocalPath)\.ci\server\certs\'
DATA__MYSQLBULKLOADERLOCALCSVFILE: '$(Build.Repository.LocalPath)\tests\TestData\LoadData_UTF8_BOM_Unix.CSV'
@@ -136,7 +136,7 @@ jobs:
arguments: '-c Release --no-restore'
testRunTitle: ${{ format('{0}, $(Agent.OS), {1}, {2}', 'mysql:8.0', 'net472/net8.0', 'No SSL') }}
env:
- DATA__UNSUPPORTEDFEATURES: 'Ed25519,QueryAttributes,Redirection,StreamingResults,Tls11,UnixDomainSocket'
+ DATA__UNSUPPORTEDFEATURES: 'Ed25519,QueryAttributes,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,UnixDomainSocket'
DATA__CONNECTIONSTRING: 'server=localhost;port=3306;user id=mysqltest;password=test;database=mysqltest;ssl mode=none;DefaultCommandTimeout=3600;AllowPublicKeyRetrieval=True;UseCompression=True'
- job: windows_integration_tests_2
@@ -174,7 +174,7 @@ jobs:
arguments: '-c Release --no-restore'
testRunTitle: ${{ format('{0}, $(Agent.OS), {1}, {2}', 'mysql:8.0', 'net6.0', 'No SSL') }}
env:
- DATA__UNSUPPORTEDFEATURES: 'Ed25519,QueryAttributes,Redirection,StreamingResults,Tls11,UnixDomainSocket'
+ DATA__UNSUPPORTEDFEATURES: 'Ed25519,QueryAttributes,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,UnixDomainSocket'
DATA__CONNECTIONSTRING: 'server=localhost;port=3306;user id=mysqltest;password=test;database=mysqltest;ssl mode=none;DefaultCommandTimeout=3600;AllowPublicKeyRetrieval=True'
- job: linux_integration_tests
@@ -187,23 +187,23 @@ jobs:
'MySQL 8.0':
image: 'mysql:8.0'
connectionStringExtra: 'AllowPublicKeyRetrieval=True'
- unsupportedFeatures: 'Ed25519,Redirection,StreamingResults,Tls11,ZeroDateTime'
+ unsupportedFeatures: 'Ed25519,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,ZeroDateTime'
'MySQL 8.4':
image: 'mysql:8.4'
connectionStringExtra: 'AllowPublicKeyRetrieval=True'
- unsupportedFeatures: 'Ed25519,Redirection,StreamingResults,Tls11,ZeroDateTime'
+ unsupportedFeatures: 'Ed25519,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,ZeroDateTime'
'MySQL 9.0':
image: 'mysql:9.0'
connectionStringExtra: 'AllowPublicKeyRetrieval=True'
- unsupportedFeatures: 'Ed25519,Redirection,StreamingResults,Tls11,ZeroDateTime'
+ unsupportedFeatures: 'Ed25519,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,ZeroDateTime'
'MariaDB 10.6':
image: 'mariadb:10.6'
connectionStringExtra: ''
- unsupportedFeatures: 'CachingSha2Password,CancelSleepSuccessfully,Json,RoundDateTime,QueryAttributes,Sha256Password,Tls11,UuidToBin,Redirection'
+ unsupportedFeatures: 'CachingSha2Password,CancelSleepSuccessfully,Json,RoundDateTime,QueryAttributes,Redirection,Sha256Password,Tls11,TlsFingerprintValidation,UuidToBin'
'MariaDB 10.11':
image: 'mariadb:10.11'
connectionStringExtra: ''
- unsupportedFeatures: 'CachingSha2Password,CancelSleepSuccessfully,Json,RoundDateTime,QueryAttributes,Sha256Password,Tls11,UuidToBin,Redirection'
+ unsupportedFeatures: 'CachingSha2Password,CancelSleepSuccessfully,Json,RoundDateTime,QueryAttributes,Redirection,Sha256Password,Tls11,TlsFingerprintValidation,UuidToBin'
'MariaDB 11.4':
image: 'mariadb:11.4'
connectionStringExtra: ''
diff --git a/src/MySqlConnector.Authentication.Ed25519/Ed25519AuthenticationPlugin.cs b/src/MySqlConnector.Authentication.Ed25519/Ed25519AuthenticationPlugin.cs
index 117953366..f2ea62c3e 100644
--- a/src/MySqlConnector.Authentication.Ed25519/Ed25519AuthenticationPlugin.cs
+++ b/src/MySqlConnector.Authentication.Ed25519/Ed25519AuthenticationPlugin.cs
@@ -1,6 +1,7 @@
using System;
using System.Security.Cryptography;
using System.Text;
+using System.Threading;
using Chaos.NaCl.Internal.Ed25519Ref10;
namespace MySqlConnector.Authentication.Ed25519;
@@ -9,7 +10,7 @@ namespace MySqlConnector.Authentication.Ed25519;
/// Provides an implementation of the client_ed25519 authentication plugin for MariaDB.
///
/// See Authentication Plugin - ed25519.
-public sealed class Ed25519AuthenticationPlugin : IAuthenticationPlugin
+public sealed class Ed25519AuthenticationPlugin : IAuthenticationPlugin2
{
///
/// Registers the Ed25519 authentication plugin with MySqlConnector. You must call this method once before
@@ -17,11 +18,8 @@ public sealed class Ed25519AuthenticationPlugin : IAuthenticationPlugin
///
public static void Install()
{
- if (!s_isInstalled)
- {
+ if (Interlocked.CompareExchange(ref s_isInstalled, 1, 0) == 0)
AuthenticationPlugins.Register(new Ed25519AuthenticationPlugin());
- s_isInstalled = true;
- }
}
///
@@ -33,6 +31,21 @@ public static void Install()
/// Creates the authentication response.
///
public byte[] CreateResponse(string password, ReadOnlySpan authenticationData)
+ {
+ CreateResponseAndHash(password, authenticationData, out _, out var authenticationResponse);
+ return authenticationResponse;
+ }
+
+ ///
+ /// Creates the Ed25519 password hash.
+ ///
+ public byte[] CreatePasswordHash(string password, ReadOnlySpan authenticationData)
+ {
+ CreateResponseAndHash(password, authenticationData, out var passwordHash, out _);
+ return passwordHash;
+ }
+
+ private static void CreateResponseAndHash(string password, ReadOnlySpan authenticationData, out byte[] passwordHash, out byte[] authenticationResponse)
{
// Java reference: https://github.com/MariaDB/mariadb-connector-j/blob/master/src/main/java/org/mariadb/jdbc/internal/com/send/authentication/Ed25519PasswordPlugin.java
// C reference: https://github.com/MariaDB/server/blob/592fe954ef82be1bc08b29a8e54f7729eb1e1343/plugin/auth_ed25519/ref10/sign.c#L7
@@ -111,6 +124,9 @@ public byte[] CreateResponse(string password, ReadOnlySpan authenticationD
GroupOperations.ge_scalarmult_base(out var A, az, 0);
GroupOperations.ge_p3_tobytes(sm, 32, ref A);
+ passwordHash = new byte[32];
+ Array.Copy(sm, 32, passwordHash, 0, 32);
+
/*** Java
nonce = scalar.reduce(nonce);
GroupElement elementRvalue = spec.getB().scalarMultiply(nonce);
@@ -154,12 +170,12 @@ public byte[] CreateResponse(string password, ReadOnlySpan authenticationD
var result = new byte[64];
Buffer.BlockCopy(sm, 0, result, 0, result.Length);
- return result;
+ authenticationResponse = result;
}
private Ed25519AuthenticationPlugin()
{
}
- private static bool s_isInstalled;
+ private static int s_isInstalled;
}
diff --git a/src/MySqlConnector/Authentication/IAuthenticationPlugin.cs b/src/MySqlConnector/Authentication/IAuthenticationPlugin.cs
index 063bd4c8b..6dfface4e 100644
--- a/src/MySqlConnector/Authentication/IAuthenticationPlugin.cs
+++ b/src/MySqlConnector/Authentication/IAuthenticationPlugin.cs
@@ -20,3 +20,19 @@ public interface IAuthenticationPlugin
/// The authentication response.
byte[] CreateResponse(string password, ReadOnlySpan authenticationData);
}
+
+///
+/// is an extension to that returns a hash of the client's password.
+///
+public interface IAuthenticationPlugin2 : IAuthenticationPlugin
+{
+ ///
+ /// Hashes the client's password (e.g., for TLS certificate fingerprint verification).
+ ///
+ /// The client's password.
+ /// The authentication data supplied by the server; this is the auth method data
+ /// from the Authentication
+ /// Method Switch Request Packet.
+ /// The authentication-method-specific hash of the client's password.
+ byte[] CreatePasswordHash(string password, ReadOnlySpan authenticationData);
+}
diff --git a/src/MySqlConnector/Core/ServerSession.cs b/src/MySqlConnector/Core/ServerSession.cs
index dcd64d009..69a288345 100644
--- a/src/MySqlConnector/Core/ServerSession.cs
+++ b/src/MySqlConnector/Core/ServerSession.cs
@@ -438,13 +438,13 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella
var initialHandshake = InitialHandshakePayload.Create(payload.Span);
// if PluginAuth is supported, then use the specified auth plugin; else, fall back to protocol capabilities to determine the auth type to use
- var authPluginName = (initialHandshake.ProtocolCapabilities & ProtocolCapabilities.PluginAuth) != 0 ? initialHandshake.AuthPluginName! :
+ m_currentAuthenticationMethod = (initialHandshake.ProtocolCapabilities & ProtocolCapabilities.PluginAuth) != 0 ? initialHandshake.AuthPluginName! :
(initialHandshake.ProtocolCapabilities & ProtocolCapabilities.SecureConnection) == 0 ? "mysql_old_password" :
"mysql_native_password";
- Log.ServerSentAuthPluginName(m_logger, Id, authPluginName);
- if (authPluginName is not "mysql_native_password" and not "sha256_password" and not "caching_sha2_password")
+ Log.ServerSentAuthPluginName(m_logger, Id, m_currentAuthenticationMethod);
+ if (m_currentAuthenticationMethod is not "mysql_native_password" and not "sha256_password" and not "caching_sha2_password")
{
- Log.UnsupportedAuthenticationMethod(m_logger, Id, authPluginName);
+ Log.UnsupportedAuthenticationMethod(m_logger, Id, m_currentAuthenticationMethod);
throw new NotSupportedException($"Authentication method '{initialHandshake.AuthPluginName}' is not supported.");
}
@@ -528,6 +528,46 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella
}
var ok = OkPayload.Create(payload.Span, this);
+
+ if (m_sslPolicyErrors != SslPolicyErrors.None)
+ {
+ // SSL would normally have thrown error, but this was suppressed in ValidateRemoteCertificate; now we need to verify the server certificate
+ // pass only if :
+ // * connection method is MitM-proof (e.g. unix socket)
+ // * auth plugin is MitM-proof and check SHA2(user's hashed password, scramble, certificate fingerprint)
+ // see https://mariadb.org/mission-impossible-zero-configuration-ssl/
+ var ignoreCertificateError = false;
+
+ if (cs.ConnectionProtocol == MySqlConnectionProtocol.UnixSocket)
+ {
+ Log.CertificateErrorUnixSocket(m_logger, Id, m_sslPolicyErrors);
+ ignoreCertificateError = true;
+ }
+ else if (string.IsNullOrEmpty(password))
+ {
+ // there is no shared secret that can be used to validate the certificate
+ Log.CertificateErrorNoPassword(m_logger, Id, m_sslPolicyErrors);
+ }
+ else if (ValidateFingerprint(ok.StatusInfo, initialHandshake.AuthPluginData.AsSpan(0, 20), password))
+ {
+ Log.CertificateErrorValidThumbprint(m_logger, Id, m_sslPolicyErrors);
+ ignoreCertificateError = true;
+ }
+
+ if (!ignoreCertificateError)
+ {
+ ShutdownSocket();
+ HostName = "";
+ lock (m_lock)
+ m_state = State.Failed;
+
+ // throw a MySqlException with an AuthenticationException InnerException to mimic what would have happened if ValidateRemoteCertificate returned false
+ var innerException = new AuthenticationException($"The remote certificate was rejected due to the following error: {m_sslPolicyErrors}");
+ Log.CouldNotInitializeTlsConnection(m_logger, innerException, Id);
+ throw new MySqlException(MySqlErrorCode.UnableToConnectToHost, "SSL Authentication Error", innerException);
+ }
+ }
+
var redirectionUrl = ok.RedirectionUrl;
if (m_useCompression)
@@ -567,6 +607,70 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella
}
}
+ ///
+ /// Validate SSL validation hash (from OK packet).
+ ///
+ /// The validation hash received from the server.
+ /// The auth plugin data from the initial handshake.
+ /// The user's password.
+ /// true if the validation hash matches the locally-computed value; otherwise, false.
+ private bool ValidateFingerprint(byte[]? validationHash, ReadOnlySpan challenge, string password)
+ {
+ // expect 0x01 followed by 64 hex characters giving a SHA2 hash
+ if (validationHash?.Length != 65 || validationHash[0] != 1)
+ return false;
+
+ byte[]? passwordHashResult = null;
+ switch (m_currentAuthenticationMethod)
+ {
+ case "mysql_native_password":
+ passwordHashResult = AuthenticationUtility.HashPassword([], password, onlyHashPassword: true);
+ break;
+
+ case "client_ed25519":
+ AuthenticationPlugins.TryGetPlugin(m_currentAuthenticationMethod, out var ed25519Plugin);
+ if (ed25519Plugin is IAuthenticationPlugin2 plugin2)
+ passwordHashResult = plugin2.CreatePasswordHash(password, challenge);
+ break;
+ }
+ if (passwordHashResult is null)
+ return false;
+
+ Span combined = stackalloc byte[32 + challenge.Length + passwordHashResult.Length];
+ passwordHashResult.CopyTo(combined);
+ challenge.CopyTo(combined[passwordHashResult.Length..]);
+ m_remoteCertificateSha2Thumbprint!.CopyTo(combined[(passwordHashResult.Length + challenge.Length)..]);
+
+ Span hashBytes = stackalloc byte[32];
+#if NET5_0_OR_GREATER
+ SHA256.TryHashData(combined, hashBytes, out _);
+#else
+ using var sha256 = SHA256.Create();
+ sha256.TryComputeHash(combined, hashBytes, out _);
+#endif
+
+ Span serverHash = combined[0..32];
+ return TryConvertFromHexString(validationHash.AsSpan(1), serverHash) && serverHash.SequenceEqual(hashBytes);
+
+ static bool TryConvertFromHexString(ReadOnlySpan hexChars, Span data)
+ {
+ ReadOnlySpan hexDigits = "0123456789ABCDEFabcdef"u8;
+ for (var i = 0; i < hexChars.Length; i += 2)
+ {
+ var high = hexDigits.IndexOf(hexChars[i]);
+ var low = hexDigits.IndexOf(hexChars[i + 1]);
+ if (high == -1 || low == -1)
+ return false;
+ if (high > 15)
+ high -= 6;
+ if (low > 15)
+ low -= 6;
+ data[i / 2] = (byte) ((high << 4) | low);
+ }
+ return true;
+ }
+ }
+
public static async ValueTask ConnectAndRedirectAsync(ILogger connectionLogger, ILogger poolLogger, IConnectionPoolMetadata pool, ConnectionSettings cs, ILoadBalancer? loadBalancer, MySqlConnection connection, Action? logMessage, long startingTimestamp, Activity? activity, IOBehavior ioBehavior, CancellationToken cancellationToken)
{
var session = new ServerSession(connectionLogger, pool);
@@ -729,6 +833,7 @@ private async Task SwitchAuthenticationAsync(ConnectionSettings cs,
// if the server didn't support the hashed password; rehash with the new challenge
var switchRequest = AuthenticationMethodSwitchRequestPayload.Create(payload.Span);
Log.SwitchingToAuthenticationMethod(m_logger, Id, switchRequest.Name);
+ m_currentAuthenticationMethod = switchRequest.Name;
switch (switchRequest.Name)
{
case "mysql_native_password":
@@ -1485,6 +1590,21 @@ caCertificateChain is not null &&
if (cs.SslMode == MySqlSslMode.VerifyCA)
rcbPolicyErrors &= ~SslPolicyErrors.RemoteCertificateNameMismatch;
+ if (rcbCertificate is X509Certificate2 cert2)
+ {
+ // saving sha256 thumbprint and SSL errors until thumbprint validation
+#if NET7_0_OR_GREATER
+ m_remoteCertificateSha2Thumbprint = SHA256.HashData(cert2.RawDataMemory.Span);
+#elif NET5_0_OR_GREATER
+ m_remoteCertificateSha2Thumbprint = SHA256.HashData(cert2.RawData);
+#else
+ using var sha256 = SHA256.Create();
+ m_remoteCertificateSha2Thumbprint = sha256.ComputeHash(cert2.RawData);
+#endif
+ m_sslPolicyErrors = rcbPolicyErrors;
+ return true;
+ }
+
return rcbPolicyErrors == SslPolicyErrors.None;
}
@@ -1558,11 +1678,22 @@ await sslStream.AuthenticateAsClientAsync(clientAuthenticationOptions.TargetHost
m_payloadHandler!.ByteHandler = sslByteHandler;
m_isSecureConnection = true;
m_sslStream = sslStream;
+ if (m_sslPolicyErrors != SslPolicyErrors.None)
+ {
#if NETCOREAPP3_0_OR_GREATER
- Log.ConnectedTlsBasic(m_logger, Id, sslStream.SslProtocol, sslStream.NegotiatedCipherSuite);
+ Log.ConnectedTlsBasicPreliminary(m_logger, Id, m_sslPolicyErrors, sslStream.SslProtocol, sslStream.NegotiatedCipherSuite);
#else
- Log.ConnectedTlsDetailed(m_logger, Id, sslStream.SslProtocol, sslStream.CipherAlgorithm, sslStream.HashAlgorithm, sslStream.KeyExchangeAlgorithm, sslStream.KeyExchangeStrength);
+ Log.ConnectedTlsDetailedPreliminary(m_logger, Id, m_sslPolicyErrors, sslStream.SslProtocol, sslStream.CipherAlgorithm, sslStream.HashAlgorithm, sslStream.KeyExchangeAlgorithm, sslStream.KeyExchangeStrength);
#endif
+ }
+ else
+ {
+#if NETCOREAPP3_0_OR_GREATER
+ Log.ConnectedTlsBasic(m_logger, Id, sslStream.SslProtocol, sslStream.NegotiatedCipherSuite);
+#else
+ Log.ConnectedTlsDetailed(m_logger, Id, sslStream.SslProtocol, sslStream.CipherAlgorithm, sslStream.HashAlgorithm, sslStream.KeyExchangeAlgorithm, sslStream.KeyExchangeStrength);
+#endif
+ }
}
catch (Exception ex)
{
@@ -2006,4 +2137,7 @@ protected override void OnStatementBegin(int index)
private PayloadData m_setNamesPayload;
private byte[]? m_pipelinedResetConnectionBytes;
private Dictionary? m_preparedStatements;
+ private string? m_currentAuthenticationMethod;
+ private byte[]? m_remoteCertificateSha2Thumbprint;
+ private SslPolicyErrors m_sslPolicyErrors;
}
diff --git a/src/MySqlConnector/Logging/EventIds.cs b/src/MySqlConnector/Logging/EventIds.cs
index 568acdcf3..de87e8590 100644
--- a/src/MySqlConnector/Logging/EventIds.cs
+++ b/src/MySqlConnector/Logging/EventIds.cs
@@ -81,6 +81,11 @@ internal static class EventIds
public const int CreatingConnectionAttributes = 2153;
public const int ObtainingPasswordViaProvidePasswordCallback = 2154;
public const int FailedToObtainPassword = 2155;
+ public const int ConnectedTlsBasicPreliminary = 2156;
+ public const int ConnectedTlsDetailedPreliminary = 2157;
+ public const int CertificateErrorUnixSocket = 2158;
+ public const int CertificateErrorNoPassword = 2159;
+ public const int CertificateErrorValidThumbprint = 2160;
// Command execution events, 2200-2299
public const int CannotExecuteNewCommandInState = 2200;
diff --git a/src/MySqlConnector/Logging/Log.cs b/src/MySqlConnector/Logging/Log.cs
index 57b10a088..e9b4f88bc 100644
--- a/src/MySqlConnector/Logging/Log.cs
+++ b/src/MySqlConnector/Logging/Log.cs
@@ -201,6 +201,23 @@ internal static partial class Log
[LoggerMessage(EventIds.FailedToObtainPassword, LogLevel.Error, "Session {SessionId} failed to obtain password via ProvidePasswordCallback: {ExceptionMessage}")]
public static partial void FailedToObtainPassword(ILogger logger, Exception exception, string sessionId, string exceptionMessage);
+#if NETCOREAPP3_0_OR_GREATER
+ [LoggerMessage(EventIds.ConnectedTlsBasicPreliminary, LogLevel.Debug, "Session {SessionId} provisionally connected TLS with error {SslPolicyErrors} using {SslProtocol} and {NegotiatedCipherSuite}")]
+ public static partial void ConnectedTlsBasicPreliminary(ILogger logger, string sessionId, SslPolicyErrors sslPolicyErrors, SslProtocols sslProtocol, TlsCipherSuite negotiatedCipherSuite);
+#endif
+
+ [LoggerMessage(EventIds.ConnectedTlsDetailedPreliminary, LogLevel.Debug, "Session {SessionId} provisionally connected TLS with error {SslPolicyErrors} using {SslProtocol}, {CipherAlgorithm}, {HashAlgorithm}, {KeyExchangeAlgorithm}, {KeyExchangeStrength}")]
+ public static partial void ConnectedTlsDetailedPreliminary(ILogger logger, string sessionId, SslPolicyErrors sslPolicyErrors, SslProtocols sslProtocol, CipherAlgorithmType cipherAlgorithm, HashAlgorithmType hashAlgorithm, ExchangeAlgorithmType keyExchangeAlgorithm, int keyExchangeStrength);
+
+ [LoggerMessage(EventIds.CertificateErrorUnixSocket, LogLevel.Trace, "Session {SessionId} ignoring remote certificate error {SslPolicyErrors} due to Unix socket connection")]
+ public static partial void CertificateErrorUnixSocket(ILogger logger, string sessionId, SslPolicyErrors sslPolicyErrors);
+
+ [LoggerMessage(EventIds.CertificateErrorNoPassword, LogLevel.Trace, "Session {SessionId} acknowledging remote certificate error {SslPolicyErrors} due to passwordless connection")]
+ public static partial void CertificateErrorNoPassword(ILogger logger, string sessionId, SslPolicyErrors sslPolicyErrors);
+
+ [LoggerMessage(EventIds.CertificateErrorValidThumbprint, LogLevel.Trace, "Session {SessionId} ignoring remote certificate error {SslPolicyErrors} due to valid signature in OK packet")]
+ public static partial void CertificateErrorValidThumbprint(ILogger logger, string sessionId, SslPolicyErrors sslPolicyErrors);
+
[LoggerMessage(EventIds.IgnoringCancellationForCommand, LogLevel.Trace, "Ignoring cancellation for closed connection or invalid command {CommandId}")]
public static partial void IgnoringCancellationForCommand(ILogger logger, int commandId);
diff --git a/src/MySqlConnector/Protocol/Payloads/OkPayload.cs b/src/MySqlConnector/Protocol/Payloads/OkPayload.cs
index 37db8862f..b1bc70139 100644
--- a/src/MySqlConnector/Protocol/Payloads/OkPayload.cs
+++ b/src/MySqlConnector/Protocol/Payloads/OkPayload.cs
@@ -12,7 +12,7 @@ internal sealed class OkPayload
public ulong LastInsertId { get; }
public ServerStatus ServerStatus { get; }
public int WarningCount { get; }
- public string? StatusInfo { get; }
+ public byte[]? StatusInfo { get; }
public string? NewSchema { get; }
public CharacterSet? NewCharacterSet { get; }
public int? NewConnectionId { get; }
@@ -152,7 +152,7 @@ public static void Verify(ReadOnlySpan span, IServerCapabilities serverCap
if (createPayload)
{
- var statusInfo = statusBytes.Length == 0 ? null : Encoding.UTF8.GetString(statusBytes);
+ var statusInfo = statusBytes.Length == 0 ? null : statusBytes.ToArray();
// detect the connection character set as utf8mb4 (or utf8) if all three system variables are set to the same value
var characterSet = clientCharacterSet == CharacterSet.Utf8Mb4Binary && connectionCharacterSet == CharacterSet.Utf8Mb4Binary && resultsCharacterSet == CharacterSet.Utf8Mb4Binary ? CharacterSet.Utf8Mb4Binary :
@@ -175,7 +175,7 @@ public static void Verify(ReadOnlySpan span, IServerCapabilities serverCap
}
}
- private OkPayload(ulong affectedRowCount, ulong lastInsertId, ServerStatus serverStatus, int warningCount, string? statusInfo, string? newSchema, CharacterSet newCharacterSet, int? connectionId, string? redirectionUrl)
+ private OkPayload(ulong affectedRowCount, ulong lastInsertId, ServerStatus serverStatus, int warningCount, byte[]? statusInfo, string? newSchema, CharacterSet newCharacterSet, int? connectionId, string? redirectionUrl)
{
AffectedRowCount = affectedRowCount;
LastInsertId = lastInsertId;
diff --git a/src/MySqlConnector/Protocol/Serialization/AuthenticationUtility.cs b/src/MySqlConnector/Protocol/Serialization/AuthenticationUtility.cs
index fc0301a90..659d2350d 100644
--- a/src/MySqlConnector/Protocol/Serialization/AuthenticationUtility.cs
+++ b/src/MySqlConnector/Protocol/Serialization/AuthenticationUtility.cs
@@ -25,25 +25,27 @@ public static byte[] GetNullTerminatedPasswordBytes(string password)
}
public static byte[] CreateAuthenticationResponse(ReadOnlySpan challenge, string password) =>
- string.IsNullOrEmpty(password) ? [] : HashPassword(challenge, password);
+ string.IsNullOrEmpty(password) ? [] : HashPassword(challenge, password, onlyHashPassword: false);
///
/// Hashes a password with the "Secure Password Authentication" method.
///
/// The 20-byte random challenge (from the "auth-plugin-data" in the initial handshake).
/// The password to hash.
+ /// If true, is ignored and only the twice-hashed password
+ /// is returned, instead of performing the full "secure password authentication" algorithm that XORs the hashed password against
+ /// a hash derived from the challenge.
/// A 20-byte password hash.
/// See Secure Password Authentication.
#if NET5_0_OR_GREATER
[SkipLocalsInit]
#endif
- public static byte[] HashPassword(ReadOnlySpan challenge, string password)
+ public static byte[] HashPassword(ReadOnlySpan challenge, string password, bool onlyHashPassword)
{
#if !NET5_0_OR_GREATER
using var sha1 = SHA1.Create();
#endif
Span combined = stackalloc byte[40];
- challenge.CopyTo(combined);
var passwordByteCount = Encoding.UTF8.GetByteCount(password);
Span passwordBytes = stackalloc byte[passwordByteCount];
@@ -56,7 +58,10 @@ public static byte[] HashPassword(ReadOnlySpan challenge, string password)
sha1.TryComputeHash(passwordBytes, hashedPassword, out _);
sha1.TryComputeHash(hashedPassword, combined[20..], out _);
#endif
+ if (onlyHashPassword)
+ return combined[20..].ToArray();
+ challenge[..20].CopyTo(combined);
Span xorBytes = stackalloc byte[20];
#if NET5_0_OR_GREATER
SHA1.TryHashData(combined, xorBytes, out _);
diff --git a/tests/IntegrationTests/MySqlDataSourceTests.cs b/tests/IntegrationTests/MySqlDataSourceTests.cs
index dcd8d7f2e..c753fe97c 100644
--- a/tests/IntegrationTests/MySqlDataSourceTests.cs
+++ b/tests/IntegrationTests/MySqlDataSourceTests.cs
@@ -147,7 +147,7 @@ public async Task ConnectSslRemoteCertificateValidationCallback(MySqlSslMode ssl
using var dataSource = builder.Build();
using var connection = dataSource.CreateConnection();
- if (expectedSuccess)
+ if (expectedSuccess || AppConfig.SupportedFeatures.HasFlag(ServerFeatures.TlsFingerprintValidation))
await connection.OpenAsync();
else
await Assert.ThrowsAsync(connection.OpenAsync);
diff --git a/tests/IntegrationTests/ServerFeatures.cs b/tests/IntegrationTests/ServerFeatures.cs
index ac4fa4863..120b541bf 100644
--- a/tests/IntegrationTests/ServerFeatures.cs
+++ b/tests/IntegrationTests/ServerFeatures.cs
@@ -37,7 +37,12 @@ public enum ServerFeatures
CancelSleepSuccessfully = 0x40_0000,
///
- /// Server permit redirection, available on first OK_Packet
+ /// Server permits redirection (sent as a server variable in first OK packet).
///
Redirection = 0x80_0000,
+
+ ///
+ /// Server provides hash of TLS certificate in first OK packet.
+ ///
+ TlsFingerprintValidation = 0x100_0000,
}
diff --git a/tests/IntegrationTests/SslTests.cs b/tests/IntegrationTests/SslTests.cs
index e22c034fb..6c428f386 100644
--- a/tests/IntegrationTests/SslTests.cs
+++ b/tests/IntegrationTests/SslTests.cs
@@ -181,7 +181,10 @@ public async Task ConnectSslBadCaCertificate()
csb.SslMode = MySqlSslMode.VerifyCA;
csb.SslCa = Path.Combine(AppConfig.CertsPath, "non-ca-client-cert.pem");
using var connection = new MySqlConnection(csb.ConnectionString);
- await Assert.ThrowsAsync(async () => await connection.OpenAsync());
+ if (AppConfig.SupportedFeatures.HasFlag(ServerFeatures.TlsFingerprintValidation))
+ await connection.OpenAsync();
+ else
+ await Assert.ThrowsAsync(async () => await connection.OpenAsync());
}
#if !MYSQL_DATA
@@ -198,13 +201,44 @@ public async Task ConnectSslRemoteCertificateValidationCallback(MySqlSslMode ssl
using var connection = new MySqlConnection(csb.ConnectionString);
connection.RemoteCertificateValidationCallback = (s, c, h, e) => true;
- if (expectedSuccess)
+ if (expectedSuccess || AppConfig.SupportedFeatures.HasFlag(ServerFeatures.TlsFingerprintValidation))
await connection.OpenAsync();
else
await Assert.ThrowsAsync(async () => await connection.OpenAsync());
}
#endif
+ [SkippableFact(ServerFeatures.TlsFingerprintValidation)]
+ public async Task ConnectZeroConfigurationSslNative()
+ {
+ // permit connection without any Ssl configuration.
+ // reference https://mariadb.org/mission-impossible-zero-configuration-ssl/
+ var csb = AppConfig.CreateConnectionStringBuilder();
+ csb.CertificateFile = null;
+ csb.SslMode = MySqlSslMode.VerifyFull;
+ csb.SslCa = "";
+ csb.UserID = "ssltest";
+ csb.Password = "test";
+ using var connection = new MySqlConnection(csb.ConnectionString);
+ await connection.OpenAsync();
+ }
+
+#if !MYSQL_DATA
+ [SkippableFact(ServerFeatures.TlsFingerprintValidation | ServerFeatures.Ed25519)]
+ public async Task ConnectZeroConfigurationSslEd25519()
+ {
+ MySqlConnector.Authentication.Ed25519.Ed25519AuthenticationPlugin.Install();
+ var csb = AppConfig.CreateConnectionStringBuilder();
+ csb.CertificateFile = null;
+ csb.SslMode = MySqlSslMode.VerifyFull;
+ csb.SslCa = "";
+ csb.UserID = "ed25519user";
+ csb.Password = "Ed255!9";
+ using var connection = new MySqlConnection(csb.ConnectionString);
+ await connection.OpenAsync();
+ }
+#endif
+
[SkippableFact(ConfigSettings.RequiresSsl)]
public async Task ConnectSslTlsVersion()
{
diff --git a/tests/README.md b/tests/README.md
index bd5267639..da0fcb0ab 100644
--- a/tests/README.md
+++ b/tests/README.md
@@ -27,6 +27,7 @@ Otherwise, set the following options appropriately:
* `ErrorCodes`: server returns error codes in error packet (some MySQL proxies do not)
* `Json`: the `JSON` data type (MySQL 5.7 and later)
* `LargePackets`: large packets (over 4MB)
+ * `Redirection`: server supports sending redirection information in a server variable in the first OK packet
* `RoundDateTime`: server rounds `datetime` values to the specified precision (not implemented in MariaDB)
* `RsaEncryption`: server supports RSA public key encryption (for `sha256_password` and `caching_sha2_password`)
* `SessionTrack`: server supports `CLIENT_SESSION_TRACK` capability (MySQL 5.7 and later)
@@ -36,6 +37,7 @@ Otherwise, set the following options appropriately:
* `Tls11`: server supports TLS 1.1
* `Tls12`: server supports TLS 1.2
* `Tls13`: server supports TLS 1.3
+ * `TlsFingerprintValidation`: server provides a hash of the TLS certificate fingerprint in the first OK packet
* `UnixDomainSocket`: server is accessible via a Unix domain socket
* `UuidToBin`: server supports `UUID_TO_BIN` (MySQL 8.0 and later)