Skip to content

Commit

Permalink
Using TLS without configuration
Browse files Browse the repository at this point in the history
Connector implementation of https://jira.mariadb.org/browse/MDEV-31855

Since MariaDB 11.4.1, TLS use has greatly been simplified. Connector side doesn't require TLS configuration anymore, even with self-signed certificates.

connectors now validate ssl certificates using client password (using seed and server certificate SHA256 thumbprint).

limitations:
 * only possible when using mysql_native_password/client_ed25519 authentication
 * password is required

see https://mariadb.org/mission-impossible-zero-configuration-ssl/

Signed-off-by: rusher <diego.dupin@gmail.com>
  • Loading branch information
rusher committed Jul 26, 2024
1 parent e7b1fc7 commit 89beadf
Show file tree
Hide file tree
Showing 14 changed files with 242 additions and 14 deletions.
2 changes: 1 addition & 1 deletion .ci/config/config.compression+ssl.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"SocketPath": "./../../../../.ci/run/mysql/mysqld.sock",
"PasswordlessUser": "no_password",
"SecondaryDatabase": "testdb2",
"UnsupportedFeatures": "RsaEncryption,CachingSha2Password,Tls12,Tls13,UuidToBin",
"UnsupportedFeatures": "RsaEncryption,CachingSha2Password,Tls12,Tls13,UuidToBin,TlsFingerprintValidation",
"MySqlBulkLoaderLocalCsvFile": "../../../TestData/LoadData_UTF8_BOM_Unix.CSV",
"MySqlBulkLoaderLocalTsvFile": "../../../TestData/LoadData_UTF8_BOM_Unix.TSV",
"CertificatesPath": "../../../../.ci/server/certs"
Expand Down
2 changes: 1 addition & 1 deletion .ci/config/config.compression.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"SocketPath": "./../../../../.ci/run/mysql/mysqld.sock",
"PasswordlessUser": "no_password",
"SecondaryDatabase": "testdb2",
"UnsupportedFeatures": "Ed25519,QueryAttributes,StreamingResults,Tls11,UnixDomainSocket,ZeroDateTime",
"UnsupportedFeatures": "Ed25519,QueryAttributes,StreamingResults,Tls11,UnixDomainSocket,ZeroDateTime,TlsFingerprintValidation",
"MySqlBulkLoaderLocalCsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.CSV",
"MySqlBulkLoaderLocalTsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.TSV"
}
Expand Down
2 changes: 1 addition & 1 deletion .ci/config/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"SocketPath": "./../../../../.ci/run/mysql/mysqld.sock",
"PasswordlessUser": "no_password",
"SecondaryDatabase": "testdb2",
"UnsupportedFeatures": "Ed25519,QueryAttributes,StreamingResults,Tls11,UnixDomainSocket,ZeroDateTime",
"UnsupportedFeatures": "Ed25519,QueryAttributes,StreamingResults,Tls11,UnixDomainSocket,ZeroDateTime,TlsFingerprintValidation",
"MySqlBulkLoaderLocalCsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.CSV",
"MySqlBulkLoaderLocalTsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.TSV"
}
Expand Down
2 changes: 1 addition & 1 deletion .ci/config/config.ssl.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"SocketPath": "./../../../../.ci/run/mysql/mysqld.sock",
"PasswordlessUser": "no_password",
"SecondaryDatabase": "testdb2",
"UnsupportedFeatures": "RsaEncryption,CachingSha2Password,Tls12,Tls13,UuidToBin",
"UnsupportedFeatures": "RsaEncryption,CachingSha2Password,Tls12,Tls13,UuidToBin,TlsFingerprintValidation",
"MySqlBulkLoaderLocalCsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.CSV",
"MySqlBulkLoaderLocalTsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.TSV",
"CertificatesPath": "../../../../.ci/server/certs"
Expand Down
10 changes: 5 additions & 5 deletions azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -187,23 +187,23 @@ jobs:
'MySQL 8.0':
image: 'mysql:8.0'
connectionStringExtra: 'AllowPublicKeyRetrieval=True'
unsupportedFeatures: 'Ed25519,StreamingResults,Tls11,ZeroDateTime,Redirection'
unsupportedFeatures: 'Ed25519,StreamingResults,Tls11,ZeroDateTime,Redirection,TlsFingerprintValidation'
'MySQL 8.4':
image: 'mysql:8.4'
connectionStringExtra: 'AllowPublicKeyRetrieval=True'
unsupportedFeatures: 'Ed25519,StreamingResults,Tls11,ZeroDateTime,Redirection'
unsupportedFeatures: 'Ed25519,StreamingResults,Tls11,ZeroDateTime,Redirection,TlsFingerprintValidation'
'MySQL 9.0':
image: 'mysql:9.0'
connectionStringExtra: 'AllowPublicKeyRetrieval=True'
unsupportedFeatures: 'Ed25519,StreamingResults,Tls11,ZeroDateTime,Redirection'
unsupportedFeatures: 'Ed25519,StreamingResults,Tls11,ZeroDateTime,Redirection,TlsFingerprintValidation'
'MariaDB 10.6':
image: 'mariadb:10.6'
connectionStringExtra: ''
unsupportedFeatures: 'CachingSha2Password,CancelSleepSuccessfully,Json,RoundDateTime,QueryAttributes,Sha256Password,Tls11,UuidToBin,Redirection'
unsupportedFeatures: 'CachingSha2Password,CancelSleepSuccessfully,Json,RoundDateTime,QueryAttributes,Sha256Password,Tls11,UuidToBin,Redirection,TlsFingerprintValidation'
'MariaDB 10.11':
image: 'mariadb:10.11'
connectionStringExtra: ''
unsupportedFeatures: 'CachingSha2Password,CancelSleepSuccessfully,Json,RoundDateTime,QueryAttributes,Sha256Password,Tls11,UuidToBin,Redirection'
unsupportedFeatures: 'CachingSha2Password,CancelSleepSuccessfully,Json,RoundDateTime,QueryAttributes,Sha256Password,Tls11,UuidToBin,Redirection,TlsFingerprintValidation'
'MariaDB 11.4':
image: 'mariadb:11.4'
connectionStringExtra: ''
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,29 @@ public byte[] CreateResponse(string password, ReadOnlySpan<byte> authenticationD
return result;
}

/// <summary>
/// Creates the ed25519 password hash.
/// </summary>
public byte[] CreatePasswordHash(string password, ReadOnlySpan<byte> authenticationData)
{
byte[] passwordBytes = Encoding.UTF8.GetBytes(password);
using var sha512 = SHA512.Create();
byte[] az = sha512.ComputeHash(passwordBytes);
ScalarOperations.sc_clamp(az, 0);

byte[] sm = new byte[64 + authenticationData.Length];
authenticationData.CopyTo(sm.AsSpan().Slice(64));
Buffer.BlockCopy(az, 32, sm, 32, 32);
sha512.ComputeHash(sm, 32, authenticationData.Length + 32);

GroupOperations.ge_scalarmult_base(out var A, az, 0);
GroupOperations.ge_p3_tobytes(sm, 32, ref A);

byte[] res = new byte[32];
Array.Copy(sm, 32, res, 0, 32);
return res;
}

private Ed25519AuthenticationPlugin()
{
}
Expand Down
10 changes: 10 additions & 0 deletions src/MySqlConnector/Authentication/IAuthenticationPlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,14 @@ public interface IAuthenticationPlugin
/// Method Switch Request Packet</a>.</param>
/// <returns>The authentication response.</returns>
byte[] CreateResponse(string password, ReadOnlySpan<byte> authenticationData);

/// <summary>
/// create password hash for fingerprint verification
/// </summary>
/// <param name="password">The client's password.</param>
/// <param name="authenticationData">The authentication data supplied by the server; this is the <code>auth method data</code>
/// from the <a href="https://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::AuthSwitchRequest">Authentication
/// Method Switch Request Packet</a>.</param>
/// <returns>password hash</returns>
byte[] CreatePasswordHash(string password, ReadOnlySpan<byte> authenticationData);
}
111 changes: 111 additions & 0 deletions src/MySqlConnector/Core/ServerSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
using MySqlConnector.Protocol.Payloads;
using MySqlConnector.Protocol.Serialization;
using MySqlConnector.Utilities;
#if NET5_0_OR_GREATER
using System.Runtime.CompilerServices;
#endif

namespace MySqlConnector.Core;

Expand Down Expand Up @@ -534,6 +537,44 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella
}

var ok = OkPayload.Create(payload.Span, this);
if (m_rcbPolicyErrors != SslPolicyErrors.None)
{
// SSL would normally have thrown error, so connector need to ensure server certificates
// 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)
if (cs.ConnectionProtocol != MySqlConnectionProtocol.UnixSocket)
{
if (string.IsNullOrEmpty(password) ||
!ValidateFingerPrint(ok.StatusInfo, initialHandshake.AuthPluginData, password!))
{
// fingerprint validation fail.
// now throwing SSL exception depending on m_rcbPolicyErrors
ShutdownSocket();
HostName = "";
lock (m_lock) m_state = State.Failed;
MySqlException ex;
switch (m_rcbPolicyErrors)
{
case SslPolicyErrors.RemoteCertificateNotAvailable:
// impossible
ex = new MySqlException(MySqlErrorCode.UnableToConnectToHost, "SSL not validated, no remote certificate available");
break;

case SslPolicyErrors.RemoteCertificateNameMismatch:
ex = new MySqlException(MySqlErrorCode.UnableToConnectToHost, "SSL not validated, certificate name mismatch");
break;

default:
ex = new MySqlException(MySqlErrorCode.UnableToConnectToHost, "SSL not validated, certificate chain validation fail");
break;
}
Log.CouldNotInitializeTlsConnection(m_logger, ex, Id);
throw ex;
}
}
}

var redirectionUrl = ok.RedirectionUrl;

if (m_useCompression)
Expand Down Expand Up @@ -573,6 +614,57 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella
}
}

/// <summary>
/// Validate SSL validation has
/// </summary>
/// <param name="validationHash">received validation hash</param>
/// <param name="challenge">initial seed</param>
/// <param name="password">password</param>
/// <returns>true if validated</returns>
private bool ValidateFingerPrint(byte[]? validationHash, ReadOnlySpan<byte> challenge, string password)
{
if (validationHash is null || validationHash.Length == 0) return false;

// ensure using SHA256 encryption
if (validationHash[0] != 0x01)
throw new FormatException($"Unexpected validation hash format. expected 0x01 but got 0x{validationHash[0]:X2}");

byte[] passwordHashResult;
switch (m_pluginName)
{
case "mysql_native_password":
passwordHashResult = AuthenticationUtility.HashPassword(challenge, password, false);
break;

case "client_ed25519":
AuthenticationPlugins.TryGetPlugin("client_ed25519", out var ed25519Plugin);
passwordHashResult = ed25519Plugin!.CreatePasswordHash(password, challenge);
break;

default:
return false;
}

Span<byte> combined = stackalloc byte[32 + (challenge.Length - 1) + passwordHashResult.Length];
passwordHashResult.CopyTo(combined);
challenge.CopyTo(combined[passwordHashResult.Length..]);
m_sha2Thumbprint!.CopyTo(combined[(passwordHashResult.Length + challenge.Length - 1)..]);

byte[] hashBytes;
#if NET5_0_OR_GREATER
hashBytes = SHA256.HashData(combined);
#else
using (var sha256 = SHA256.Create())
{
hashBytes = sha256.ComputeHash(combined.ToArray());
}
#endif

var clientGeneratedHash = hashBytes.Aggregate(string.Empty, (str, hashByte) => str + hashByte.ToString("X2", CultureInfo.InvariantCulture));
var serverGeneratedHash = Encoding.ASCII.GetString(validationHash, 1, validationHash.Length - 1);
return string.Equals(clientGeneratedHash, serverGeneratedHash, StringComparison.Ordinal);
}

public static async ValueTask<ServerSession> ConnectAndRedirectAsync(Func<ServerSession> createSession, ILogger logger, int? poolId, ConnectionSettings cs, ILoadBalancer? loadBalancer, MySqlConnection connection, Action<ILogger, int, string, Exception?>? logMessage, long startingTimestamp, Activity? activity, IOBehavior ioBehavior, CancellationToken cancellationToken)
{
var session = createSession();
Expand Down Expand Up @@ -734,6 +826,7 @@ private async Task<PayloadData> 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_pluginName = switchRequest.Name;
switch (switchRequest.Name)
{
case "mysql_native_password":
Expand Down Expand Up @@ -1490,6 +1583,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 !NET5_0_OR_GREATER
using (var sha256 = SHA256.Create())
{
m_sha2Thumbprint = sha256.ComputeHash(cert2.RawData);
}
#else
m_sha2Thumbprint = SHA256.HashData(cert2.RawData);
#endif
m_rcbPolicyErrors = rcbPolicyErrors;
return true;
}

return rcbPolicyErrors == SslPolicyErrors.None;
}

Expand Down Expand Up @@ -2012,4 +2120,7 @@ protected override void OnStatementBegin(int index)
private PayloadData m_setNamesPayload;
private byte[]? m_pipelinedResetConnectionBytes;
private Dictionary<string, PreparedStatements>? m_preparedStatements;
private string m_pluginName = "mysql_native_password";
private byte[]? m_sha2Thumbprint;
private SslPolicyErrors m_rcbPolicyErrors;
}
6 changes: 3 additions & 3 deletions src/MySqlConnector/Protocol/Payloads/OkPayload.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down Expand Up @@ -152,7 +152,7 @@ public static void Verify(ReadOnlySpan<byte> 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 :
Expand All @@ -175,7 +175,7 @@ public static void Verify(ReadOnlySpan<byte> 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,20 @@ public static byte[] GetNullTerminatedPasswordBytes(string password)
}

public static byte[] CreateAuthenticationResponse(ReadOnlySpan<byte> challenge, string password) =>
string.IsNullOrEmpty(password) ? [] : HashPassword(challenge, password);
string.IsNullOrEmpty(password) ? [] : HashPassword(challenge, password, true);

/// <summary>
/// Hashes a password with the "Secure Password Authentication" method.
/// </summary>
/// <param name="challenge">The 20-byte random challenge (from the "auth-plugin-data" in the initial handshake).</param>
/// <param name="password">The password to hash.</param>
/// <param name="withXor">must xor results.</param>
/// <returns>A 20-byte password hash.</returns>
/// <remarks>See <a href="https://dev.mysql.com/doc/internals/en/secure-password-authentication.html">Secure Password Authentication</a>.</remarks>
#if NET5_0_OR_GREATER
[SkipLocalsInit]
#endif
public static byte[] HashPassword(ReadOnlySpan<byte> challenge, string password)
public static byte[] HashPassword(ReadOnlySpan<byte> challenge, string password, bool withXor)
{
#if !NET5_0_OR_GREATER
using var sha1 = SHA1.Create();
Expand All @@ -56,6 +57,7 @@ public static byte[] HashPassword(ReadOnlySpan<byte> challenge, string password)
sha1.TryComputeHash(passwordBytes, hashedPassword, out _);
sha1.TryComputeHash(hashedPassword, combined[20..], out _);
#endif
if (!withXor) return combined[20..].ToArray();

Span<byte> xorBytes = stackalloc byte[20];
#if NET5_0_OR_GREATER
Expand Down
15 changes: 15 additions & 0 deletions tests/IntegrationTests/Attributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ public SkippableFactAttribute(ServerFeatures serverFeatures)
{
}

public SkippableFactAttribute(ServerFeatures[] serverFeatureList)
: this(serverFeatureList, ConfigSettings.None)
{
}

public SkippableFactAttribute(ConfigSettings configSettings)
: this(ServerFeatures.None, configSettings)
{
Expand All @@ -22,6 +27,16 @@ public SkippableFactAttribute(ServerFeatures serverFeatures, ConfigSettings conf
Skip = TestUtilities.GetSkipReason(serverFeatures, configSettings);
}

public SkippableFactAttribute(ServerFeatures[] serverFeatureList, ConfigSettings configSettings)
{
Skip = null;
foreach (ServerFeatures serverFeatures in serverFeatureList)
{
Skip = TestUtilities.GetSkipReason(serverFeatures, configSettings);
if (Skip is not null) break;
}
}

public string MySqlData
{
get => null;
Expand Down
4 changes: 4 additions & 0 deletions tests/IntegrationTests/ServerFeatures.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,8 @@ public enum ServerFeatures
/// </summary>
Redirection = 0x80_0000,

/// <summary>
/// Server permit redirection, available on first OK_Packet
/// </summary>
TlsFingerprintValidation = 0x100_0000,
}
Loading

0 comments on commit 89beadf

Please sign in to comment.