Skip to content

Commit 89beadf

Browse files
committed
Using TLS without configuration
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 <[email protected]>
1 parent e7b1fc7 commit 89beadf

File tree

14 files changed

+242
-14
lines changed

14 files changed

+242
-14
lines changed

.ci/config/config.compression+ssl.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"SocketPath": "./../../../../.ci/run/mysql/mysqld.sock",
55
"PasswordlessUser": "no_password",
66
"SecondaryDatabase": "testdb2",
7-
"UnsupportedFeatures": "RsaEncryption,CachingSha2Password,Tls12,Tls13,UuidToBin",
7+
"UnsupportedFeatures": "RsaEncryption,CachingSha2Password,Tls12,Tls13,UuidToBin,TlsFingerprintValidation",
88
"MySqlBulkLoaderLocalCsvFile": "../../../TestData/LoadData_UTF8_BOM_Unix.CSV",
99
"MySqlBulkLoaderLocalTsvFile": "../../../TestData/LoadData_UTF8_BOM_Unix.TSV",
1010
"CertificatesPath": "../../../../.ci/server/certs"

.ci/config/config.compression.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"SocketPath": "./../../../../.ci/run/mysql/mysqld.sock",
55
"PasswordlessUser": "no_password",
66
"SecondaryDatabase": "testdb2",
7-
"UnsupportedFeatures": "Ed25519,QueryAttributes,StreamingResults,Tls11,UnixDomainSocket,ZeroDateTime",
7+
"UnsupportedFeatures": "Ed25519,QueryAttributes,StreamingResults,Tls11,UnixDomainSocket,ZeroDateTime,TlsFingerprintValidation",
88
"MySqlBulkLoaderLocalCsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.CSV",
99
"MySqlBulkLoaderLocalTsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.TSV"
1010
}

.ci/config/config.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"SocketPath": "./../../../../.ci/run/mysql/mysqld.sock",
55
"PasswordlessUser": "no_password",
66
"SecondaryDatabase": "testdb2",
7-
"UnsupportedFeatures": "Ed25519,QueryAttributes,StreamingResults,Tls11,UnixDomainSocket,ZeroDateTime",
7+
"UnsupportedFeatures": "Ed25519,QueryAttributes,StreamingResults,Tls11,UnixDomainSocket,ZeroDateTime,TlsFingerprintValidation",
88
"MySqlBulkLoaderLocalCsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.CSV",
99
"MySqlBulkLoaderLocalTsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.TSV"
1010
}

.ci/config/config.ssl.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"SocketPath": "./../../../../.ci/run/mysql/mysqld.sock",
55
"PasswordlessUser": "no_password",
66
"SecondaryDatabase": "testdb2",
7-
"UnsupportedFeatures": "RsaEncryption,CachingSha2Password,Tls12,Tls13,UuidToBin",
7+
"UnsupportedFeatures": "RsaEncryption,CachingSha2Password,Tls12,Tls13,UuidToBin,TlsFingerprintValidation",
88
"MySqlBulkLoaderLocalCsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.CSV",
99
"MySqlBulkLoaderLocalTsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.TSV",
1010
"CertificatesPath": "../../../../.ci/server/certs"

azure-pipelines.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -187,23 +187,23 @@ jobs:
187187
'MySQL 8.0':
188188
image: 'mysql:8.0'
189189
connectionStringExtra: 'AllowPublicKeyRetrieval=True'
190-
unsupportedFeatures: 'Ed25519,StreamingResults,Tls11,ZeroDateTime,Redirection'
190+
unsupportedFeatures: 'Ed25519,StreamingResults,Tls11,ZeroDateTime,Redirection,TlsFingerprintValidation'
191191
'MySQL 8.4':
192192
image: 'mysql:8.4'
193193
connectionStringExtra: 'AllowPublicKeyRetrieval=True'
194-
unsupportedFeatures: 'Ed25519,StreamingResults,Tls11,ZeroDateTime,Redirection'
194+
unsupportedFeatures: 'Ed25519,StreamingResults,Tls11,ZeroDateTime,Redirection,TlsFingerprintValidation'
195195
'MySQL 9.0':
196196
image: 'mysql:9.0'
197197
connectionStringExtra: 'AllowPublicKeyRetrieval=True'
198-
unsupportedFeatures: 'Ed25519,StreamingResults,Tls11,ZeroDateTime,Redirection'
198+
unsupportedFeatures: 'Ed25519,StreamingResults,Tls11,ZeroDateTime,Redirection,TlsFingerprintValidation'
199199
'MariaDB 10.6':
200200
image: 'mariadb:10.6'
201201
connectionStringExtra: ''
202-
unsupportedFeatures: 'CachingSha2Password,CancelSleepSuccessfully,Json,RoundDateTime,QueryAttributes,Sha256Password,Tls11,UuidToBin,Redirection'
202+
unsupportedFeatures: 'CachingSha2Password,CancelSleepSuccessfully,Json,RoundDateTime,QueryAttributes,Sha256Password,Tls11,UuidToBin,Redirection,TlsFingerprintValidation'
203203
'MariaDB 10.11':
204204
image: 'mariadb:10.11'
205205
connectionStringExtra: ''
206-
unsupportedFeatures: 'CachingSha2Password,CancelSleepSuccessfully,Json,RoundDateTime,QueryAttributes,Sha256Password,Tls11,UuidToBin,Redirection'
206+
unsupportedFeatures: 'CachingSha2Password,CancelSleepSuccessfully,Json,RoundDateTime,QueryAttributes,Sha256Password,Tls11,UuidToBin,Redirection,TlsFingerprintValidation'
207207
'MariaDB 11.4':
208208
image: 'mariadb:11.4'
209209
connectionStringExtra: ''

src/MySqlConnector.Authentication.Ed25519/Ed25519AuthenticationPlugin.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,29 @@ public byte[] CreateResponse(string password, ReadOnlySpan<byte> authenticationD
157157
return result;
158158
}
159159

160+
/// <summary>
161+
/// Creates the ed25519 password hash.
162+
/// </summary>
163+
public byte[] CreatePasswordHash(string password, ReadOnlySpan<byte> authenticationData)
164+
{
165+
byte[] passwordBytes = Encoding.UTF8.GetBytes(password);
166+
using var sha512 = SHA512.Create();
167+
byte[] az = sha512.ComputeHash(passwordBytes);
168+
ScalarOperations.sc_clamp(az, 0);
169+
170+
byte[] sm = new byte[64 + authenticationData.Length];
171+
authenticationData.CopyTo(sm.AsSpan().Slice(64));
172+
Buffer.BlockCopy(az, 32, sm, 32, 32);
173+
sha512.ComputeHash(sm, 32, authenticationData.Length + 32);
174+
175+
GroupOperations.ge_scalarmult_base(out var A, az, 0);
176+
GroupOperations.ge_p3_tobytes(sm, 32, ref A);
177+
178+
byte[] res = new byte[32];
179+
Array.Copy(sm, 32, res, 0, 32);
180+
return res;
181+
}
182+
160183
private Ed25519AuthenticationPlugin()
161184
{
162185
}

src/MySqlConnector/Authentication/IAuthenticationPlugin.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,14 @@ public interface IAuthenticationPlugin
1919
/// Method Switch Request Packet</a>.</param>
2020
/// <returns>The authentication response.</returns>
2121
byte[] CreateResponse(string password, ReadOnlySpan<byte> authenticationData);
22+
23+
/// <summary>
24+
/// create password hash for fingerprint verification
25+
/// </summary>
26+
/// <param name="password">The client's password.</param>
27+
/// <param name="authenticationData">The authentication data supplied by the server; this is the <code>auth method data</code>
28+
/// from the <a href="https://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::AuthSwitchRequest">Authentication
29+
/// Method Switch Request Packet</a>.</param>
30+
/// <returns>password hash</returns>
31+
byte[] CreatePasswordHash(string password, ReadOnlySpan<byte> authenticationData);
2232
}

src/MySqlConnector/Core/ServerSession.cs

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818
using MySqlConnector.Protocol.Payloads;
1919
using MySqlConnector.Protocol.Serialization;
2020
using MySqlConnector.Utilities;
21+
#if NET5_0_OR_GREATER
22+
using System.Runtime.CompilerServices;
23+
#endif
2124

2225
namespace MySqlConnector.Core;
2326

@@ -534,6 +537,44 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella
534537
}
535538

536539
var ok = OkPayload.Create(payload.Span, this);
540+
if (m_rcbPolicyErrors != SslPolicyErrors.None)
541+
{
542+
// SSL would normally have thrown error, so connector need to ensure server certificates
543+
// pass only if :
544+
// * connection method is MitM-proof (e.g. unix socket)
545+
// * auth plugin is MitM-proof and check SHA2(user's hashed password, scramble, certificate fingerprint)
546+
if (cs.ConnectionProtocol != MySqlConnectionProtocol.UnixSocket)
547+
{
548+
if (string.IsNullOrEmpty(password) ||
549+
!ValidateFingerPrint(ok.StatusInfo, initialHandshake.AuthPluginData, password!))
550+
{
551+
// fingerprint validation fail.
552+
// now throwing SSL exception depending on m_rcbPolicyErrors
553+
ShutdownSocket();
554+
HostName = "";
555+
lock (m_lock) m_state = State.Failed;
556+
MySqlException ex;
557+
switch (m_rcbPolicyErrors)
558+
{
559+
case SslPolicyErrors.RemoteCertificateNotAvailable:
560+
// impossible
561+
ex = new MySqlException(MySqlErrorCode.UnableToConnectToHost, "SSL not validated, no remote certificate available");
562+
break;
563+
564+
case SslPolicyErrors.RemoteCertificateNameMismatch:
565+
ex = new MySqlException(MySqlErrorCode.UnableToConnectToHost, "SSL not validated, certificate name mismatch");
566+
break;
567+
568+
default:
569+
ex = new MySqlException(MySqlErrorCode.UnableToConnectToHost, "SSL not validated, certificate chain validation fail");
570+
break;
571+
}
572+
Log.CouldNotInitializeTlsConnection(m_logger, ex, Id);
573+
throw ex;
574+
}
575+
}
576+
}
577+
537578
var redirectionUrl = ok.RedirectionUrl;
538579

539580
if (m_useCompression)
@@ -573,6 +614,57 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella
573614
}
574615
}
575616

617+
/// <summary>
618+
/// Validate SSL validation has
619+
/// </summary>
620+
/// <param name="validationHash">received validation hash</param>
621+
/// <param name="challenge">initial seed</param>
622+
/// <param name="password">password</param>
623+
/// <returns>true if validated</returns>
624+
private bool ValidateFingerPrint(byte[]? validationHash, ReadOnlySpan<byte> challenge, string password)
625+
{
626+
if (validationHash is null || validationHash.Length == 0) return false;
627+
628+
// ensure using SHA256 encryption
629+
if (validationHash[0] != 0x01)
630+
throw new FormatException($"Unexpected validation hash format. expected 0x01 but got 0x{validationHash[0]:X2}");
631+
632+
byte[] passwordHashResult;
633+
switch (m_pluginName)
634+
{
635+
case "mysql_native_password":
636+
passwordHashResult = AuthenticationUtility.HashPassword(challenge, password, false);
637+
break;
638+
639+
case "client_ed25519":
640+
AuthenticationPlugins.TryGetPlugin("client_ed25519", out var ed25519Plugin);
641+
passwordHashResult = ed25519Plugin!.CreatePasswordHash(password, challenge);
642+
break;
643+
644+
default:
645+
return false;
646+
}
647+
648+
Span<byte> combined = stackalloc byte[32 + (challenge.Length - 1) + passwordHashResult.Length];
649+
passwordHashResult.CopyTo(combined);
650+
challenge.CopyTo(combined[passwordHashResult.Length..]);
651+
m_sha2Thumbprint!.CopyTo(combined[(passwordHashResult.Length + challenge.Length - 1)..]);
652+
653+
byte[] hashBytes;
654+
#if NET5_0_OR_GREATER
655+
hashBytes = SHA256.HashData(combined);
656+
#else
657+
using (var sha256 = SHA256.Create())
658+
{
659+
hashBytes = sha256.ComputeHash(combined.ToArray());
660+
}
661+
#endif
662+
663+
var clientGeneratedHash = hashBytes.Aggregate(string.Empty, (str, hashByte) => str + hashByte.ToString("X2", CultureInfo.InvariantCulture));
664+
var serverGeneratedHash = Encoding.ASCII.GetString(validationHash, 1, validationHash.Length - 1);
665+
return string.Equals(clientGeneratedHash, serverGeneratedHash, StringComparison.Ordinal);
666+
}
667+
576668
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)
577669
{
578670
var session = createSession();
@@ -734,6 +826,7 @@ private async Task<PayloadData> SwitchAuthenticationAsync(ConnectionSettings cs,
734826
// if the server didn't support the hashed password; rehash with the new challenge
735827
var switchRequest = AuthenticationMethodSwitchRequestPayload.Create(payload.Span);
736828
Log.SwitchingToAuthenticationMethod(m_logger, Id, switchRequest.Name);
829+
m_pluginName = switchRequest.Name;
737830
switch (switchRequest.Name)
738831
{
739832
case "mysql_native_password":
@@ -1490,6 +1583,21 @@ caCertificateChain is not null &&
14901583
if (cs.SslMode == MySqlSslMode.VerifyCA)
14911584
rcbPolicyErrors &= ~SslPolicyErrors.RemoteCertificateNameMismatch;
14921585

1586+
if (rcbCertificate is X509Certificate2 cert2)
1587+
{
1588+
// saving sha256 thumbprint and SSL errors until thumbprint validation
1589+
#if !NET5_0_OR_GREATER
1590+
using (var sha256 = SHA256.Create())
1591+
{
1592+
m_sha2Thumbprint = sha256.ComputeHash(cert2.RawData);
1593+
}
1594+
#else
1595+
m_sha2Thumbprint = SHA256.HashData(cert2.RawData);
1596+
#endif
1597+
m_rcbPolicyErrors = rcbPolicyErrors;
1598+
return true;
1599+
}
1600+
14931601
return rcbPolicyErrors == SslPolicyErrors.None;
14941602
}
14951603

@@ -2012,4 +2120,7 @@ protected override void OnStatementBegin(int index)
20122120
private PayloadData m_setNamesPayload;
20132121
private byte[]? m_pipelinedResetConnectionBytes;
20142122
private Dictionary<string, PreparedStatements>? m_preparedStatements;
2123+
private string m_pluginName = "mysql_native_password";
2124+
private byte[]? m_sha2Thumbprint;
2125+
private SslPolicyErrors m_rcbPolicyErrors;
20152126
}

src/MySqlConnector/Protocol/Payloads/OkPayload.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ internal sealed class OkPayload
1212
public ulong LastInsertId { get; }
1313
public ServerStatus ServerStatus { get; }
1414
public int WarningCount { get; }
15-
public string? StatusInfo { get; }
15+
public byte[]? StatusInfo { get; }
1616
public string? NewSchema { get; }
1717
public CharacterSet? NewCharacterSet { get; }
1818
public int? NewConnectionId { get; }
@@ -152,7 +152,7 @@ public static void Verify(ReadOnlySpan<byte> span, IServerCapabilities serverCap
152152

153153
if (createPayload)
154154
{
155-
var statusInfo = statusBytes.Length == 0 ? null : Encoding.UTF8.GetString(statusBytes);
155+
var statusInfo = statusBytes.Length == 0 ? null : statusBytes.ToArray();
156156

157157
// detect the connection character set as utf8mb4 (or utf8) if all three system variables are set to the same value
158158
var characterSet = clientCharacterSet == CharacterSet.Utf8Mb4Binary && connectionCharacterSet == CharacterSet.Utf8Mb4Binary && resultsCharacterSet == CharacterSet.Utf8Mb4Binary ? CharacterSet.Utf8Mb4Binary :
@@ -175,7 +175,7 @@ public static void Verify(ReadOnlySpan<byte> span, IServerCapabilities serverCap
175175
}
176176
}
177177

178-
private OkPayload(ulong affectedRowCount, ulong lastInsertId, ServerStatus serverStatus, int warningCount, string? statusInfo, string? newSchema, CharacterSet newCharacterSet, int? connectionId, string? redirectionUrl)
178+
private OkPayload(ulong affectedRowCount, ulong lastInsertId, ServerStatus serverStatus, int warningCount, byte[]? statusInfo, string? newSchema, CharacterSet newCharacterSet, int? connectionId, string? redirectionUrl)
179179
{
180180
AffectedRowCount = affectedRowCount;
181181
LastInsertId = lastInsertId;

src/MySqlConnector/Protocol/Serialization/AuthenticationUtility.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,19 +25,20 @@ public static byte[] GetNullTerminatedPasswordBytes(string password)
2525
}
2626

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

3030
/// <summary>
3131
/// Hashes a password with the "Secure Password Authentication" method.
3232
/// </summary>
3333
/// <param name="challenge">The 20-byte random challenge (from the "auth-plugin-data" in the initial handshake).</param>
3434
/// <param name="password">The password to hash.</param>
35+
/// <param name="withXor">must xor results.</param>
3536
/// <returns>A 20-byte password hash.</returns>
3637
/// <remarks>See <a href="https://dev.mysql.com/doc/internals/en/secure-password-authentication.html">Secure Password Authentication</a>.</remarks>
3738
#if NET5_0_OR_GREATER
3839
[SkipLocalsInit]
3940
#endif
40-
public static byte[] HashPassword(ReadOnlySpan<byte> challenge, string password)
41+
public static byte[] HashPassword(ReadOnlySpan<byte> challenge, string password, bool withXor)
4142
{
4243
#if !NET5_0_OR_GREATER
4344
using var sha1 = SHA1.Create();
@@ -56,6 +57,7 @@ public static byte[] HashPassword(ReadOnlySpan<byte> challenge, string password)
5657
sha1.TryComputeHash(passwordBytes, hashedPassword, out _);
5758
sha1.TryComputeHash(hashedPassword, combined[20..], out _);
5859
#endif
60+
if (!withXor) return combined[20..].ToArray();
5961

6062
Span<byte> xorBytes = stackalloc byte[20];
6163
#if NET5_0_OR_GREATER

0 commit comments

Comments
 (0)