Skip to content

Commit d424bac

Browse files
committed
Enable TLS validation with parsec.
Introduce new IAuthenticationPlugin3 interface and deprecate IAuthenticationPlugin2. Authentication plugins will now compute the password hash and the authentication response in one call, and the session will cache the password hash for later use. Signed-off-by: Bradley Grainger <[email protected]>
1 parent 227f5b0 commit d424bac

File tree

8 files changed

+119
-84
lines changed

8 files changed

+119
-84
lines changed

src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Ed25519.cs

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -34,19 +34,6 @@ public static byte[] Sign(byte[] message, byte[] expandedPrivateKey)
3434
return signature;
3535
}
3636

37-
public static byte[] ExpandedPrivateKeyFromSeed(byte[] privateKeySeed)
38-
{
39-
byte[] privateKey;
40-
byte[] publicKey;
41-
KeyPairFromSeed(out publicKey, out privateKey, privateKeySeed);
42-
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER
43-
CryptographicOperations.ZeroMemory(publicKey);
44-
#else
45-
CryptoBytes.Wipe(publicKey);
46-
#endif
47-
return privateKey;
48-
}
49-
5037
public static void KeyPairFromSeed(out byte[] publicKey, out byte[] expandedPrivateKey, byte[] privateKeySeed)
5138
{
5239
if (privateKeySeed == null)

src/MySqlConnector.Authentication.Ed25519/Ed25519AuthenticationPlugin.cs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ namespace MySqlConnector.Authentication.Ed25519;
1010
/// Provides an implementation of the <c>client_ed25519</c> authentication plugin for MariaDB.
1111
/// </summary>
1212
/// <remarks>See <a href="https://mariadb.com/kb/en/library/authentication-plugin-ed25519/">Authentication Plugin - ed25519</a>.</remarks>
13-
public sealed class Ed25519AuthenticationPlugin : IAuthenticationPlugin2
13+
#pragma warning disable CS0618 // Type or member is obsolete
14+
public sealed class Ed25519AuthenticationPlugin : IAuthenticationPlugin3, IAuthenticationPlugin2
15+
#pragma warning restore CS0618 // Type or member is obsolete
1416
{
1517
/// <summary>
1618
/// Registers the Ed25519 authentication plugin with MySqlConnector. You must call this method once before
@@ -32,7 +34,7 @@ public static void Install()
3234
/// </summary>
3335
public byte[] CreateResponse(string password, ReadOnlySpan<byte> authenticationData)
3436
{
35-
CreateResponseAndHash(password, authenticationData, out _, out var authenticationResponse);
37+
CreateResponseAndPasswordHash(password, authenticationData, out var authenticationResponse, out _);
3638
return authenticationResponse;
3739
}
3840

@@ -41,11 +43,20 @@ public byte[] CreateResponse(string password, ReadOnlySpan<byte> authenticationD
4143
/// </summary>
4244
public byte[] CreatePasswordHash(string password, ReadOnlySpan<byte> authenticationData)
4345
{
44-
CreateResponseAndHash(password, authenticationData, out var passwordHash, out _);
46+
CreateResponseAndPasswordHash(password, authenticationData, out _, out var passwordHash);
4547
return passwordHash;
4648
}
4749

48-
private static void CreateResponseAndHash(string password, ReadOnlySpan<byte> authenticationData, out byte[] passwordHash, out byte[] authenticationResponse)
50+
/// <summary>
51+
/// Creates the authentication response and hashes the client's password (e.g., for TLS certificate fingerprint verification).
52+
/// </summary>
53+
/// <param name="password">The client's password.</param>
54+
/// <param name="authenticationData">The authentication data supplied by the server; this is the <code>auth method data</code>
55+
/// from the <a href="https://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::AuthSwitchRequest">Authentication
56+
/// Method Switch Request Packet</a>.</param>
57+
/// <param name="authenticationResponse">The authentication response.</param>
58+
/// <param name="passwordHash">The authentication-method-specific hash of the client's password.</param>
59+
public void CreateResponseAndPasswordHash(string password, ReadOnlySpan<byte> authenticationData, out byte[] authenticationResponse, out byte[] passwordHash)
4960
{
5061
// Java reference: https://github.com/MariaDB/mariadb-connector-j/blob/master/src/main/java/org/mariadb/jdbc/internal/com/send/authentication/Ed25519PasswordPlugin.java
5162
// C reference: https://github.com/MariaDB/server/blob/592fe954ef82be1bc08b29a8e54f7729eb1e1343/plugin/auth_ed25519/ref10/sign.c#L7

src/MySqlConnector.Authentication.Ed25519/ParsecAuthenticationPlugin.cs

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ namespace MySqlConnector.Authentication.Ed25519;
88
/// <summary>
99
/// Provides an implementation of the Parsec authentication plugin for MariaDB.
1010
/// </summary>
11-
public sealed class ParsecAuthenticationPlugin : IAuthenticationPlugin
11+
public sealed class ParsecAuthenticationPlugin : IAuthenticationPlugin3
1212
{
1313
/// <summary>
1414
/// Registers the Parsec authentication plugin with MySqlConnector. You must call this method once before
@@ -29,6 +29,15 @@ public static void Install()
2929
/// Creates the authentication response.
3030
/// </summary>
3131
public byte[] CreateResponse(string password, ReadOnlySpan<byte> authenticationData)
32+
{
33+
CreateResponseAndPasswordHash(password, authenticationData, out var response, out _);
34+
return response;
35+
}
36+
37+
/// <summary>
38+
/// Creates the authentication response.
39+
/// </summary>
40+
public void CreateResponseAndPasswordHash(string password, ReadOnlySpan<byte> authenticationData, out byte[] authenticationResponse, out byte[] passwordHash)
3241
{
3342
// first 32 bytes are server scramble
3443
var serverScramble = authenticationData.Slice(0, 32);
@@ -54,32 +63,37 @@ public byte[] CreateResponse(string password, ReadOnlySpan<byte> authenticationD
5463
var salt = extendedSalt.Slice(2);
5564

5665
// derive private key using PBKDF2-SHA512
57-
byte[] privateKey;
66+
byte[] privateKeySeed;
5867
#if NET6_0_OR_GREATER
59-
privateKey = Rfc2898DeriveBytes.Pbkdf2(Encoding.UTF8.GetBytes(password), salt, iterationCount, HashAlgorithmName.SHA512, 32);
68+
privateKeySeed = Rfc2898DeriveBytes.Pbkdf2(Encoding.UTF8.GetBytes(password), salt, iterationCount, HashAlgorithmName.SHA512, 32);
6069
#elif NET472_OR_GREATER || NETSTANDARD2_1_OR_GREATER
6170
using (var pbkdf2 = new Rfc2898DeriveBytes(Encoding.UTF8.GetBytes(password), salt.ToArray(), iterationCount, HashAlgorithmName.SHA512))
62-
privateKey = pbkdf2.GetBytes(32);
71+
privateKeySeed = pbkdf2.GetBytes(32);
6372
#else
64-
privateKey = Microsoft.AspNetCore.Cryptography.KeyDerivation.KeyDerivation.Pbkdf2(
73+
privateKeySeed = Microsoft.AspNetCore.Cryptography.KeyDerivation.KeyDerivation.Pbkdf2(
6574
password, salt.ToArray(), Microsoft.AspNetCore.Cryptography.KeyDerivation.KeyDerivationPrf.HMACSHA512,
6675
iterationCount, numBytesRequested: 32);
6776
#endif
68-
var expandedPrivateKey = Chaos.NaCl.Ed25519.ExpandedPrivateKeyFromSeed(privateKey);
77+
Chaos.NaCl.Ed25519.KeyPairFromSeed(out var publicKey, out var privateKey, privateKeySeed);
6978

7079
// generate Ed25519 keypair and sign concatenated scrambles
7180
var message = new byte[serverScramble.Length + clientScramble.Length];
7281
serverScramble.CopyTo(message);
7382
clientScramble.CopyTo(message.AsSpan(serverScramble.Length));
7483

75-
var signature = Chaos.NaCl.Ed25519.Sign(message, expandedPrivateKey);
84+
var signature = Chaos.NaCl.Ed25519.Sign(message, privateKey);
85+
86+
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER
87+
CryptographicOperations.ZeroMemory(privateKey);
88+
#endif
7689

7790
// return client scramble followed by signature
78-
var response = new byte[clientScramble.Length + signature.Length];
79-
clientScramble.CopyTo(response.AsSpan());
80-
signature.CopyTo(response.AsSpan(clientScramble.Length));
81-
82-
return response;
91+
authenticationResponse = new byte[clientScramble.Length + signature.Length];
92+
clientScramble.CopyTo(authenticationResponse.AsSpan());
93+
signature.CopyTo(authenticationResponse.AsSpan(clientScramble.Length));
94+
95+
// "password hash" for parsec is the extended salt followed by the public key
96+
passwordHash = [(byte) 'P', (byte) iterationCount, .. salt, .. publicKey];
8397
}
8498

8599
private ParsecAuthenticationPlugin()

src/MySqlConnector/Authentication/IAuthenticationPlugin.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ public interface IAuthenticationPlugin
2424
/// <summary>
2525
/// <see cref="IAuthenticationPlugin2"/> is an extension to <see cref="IAuthenticationPlugin"/> that returns a hash of the client's password.
2626
/// </summary>
27+
[Obsolete("Use IAuthenticationPlugin3 instead.")]
2728
public interface IAuthenticationPlugin2 : IAuthenticationPlugin
2829
{
2930
/// <summary>
@@ -36,3 +37,21 @@ public interface IAuthenticationPlugin2 : IAuthenticationPlugin
3637
/// <returns>The authentication-method-specific hash of the client's password.</returns>
3738
byte[] CreatePasswordHash(string password, ReadOnlySpan<byte> authenticationData);
3839
}
40+
41+
/// <summary>
42+
/// <see cref="IAuthenticationPlugin3"/> is an extension to <see cref="IAuthenticationPlugin"/> that also returns a hash of the client's password.
43+
/// </summary>
44+
/// <remarks>If an authentication plugin supports this interface, the base <see cref="IAuthenticationPlugin.CreateResponse(string, ReadOnlySpan{byte})"/> method will not be called.</remarks>
45+
public interface IAuthenticationPlugin3 : IAuthenticationPlugin
46+
{
47+
/// <summary>
48+
/// Creates the authentication response and hashes the client's password (e.g., for TLS certificate fingerprint verification).
49+
/// </summary>
50+
/// <param name="password">The client's password.</param>
51+
/// <param name="authenticationData">The authentication data supplied by the server; this is the <code>auth method data</code>
52+
/// from the <a href="https://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::AuthSwitchRequest">Authentication
53+
/// Method Switch Request Packet</a>.</param>
54+
/// <param name="authenticationResponse">The authentication response.</param>
55+
/// <param name="passwordHash">The authentication-method-specific hash of the client's password.</param>
56+
void CreateResponseAndPasswordHash(string password, ReadOnlySpan<byte> authenticationData, out byte[] authenticationResponse, out byte[] passwordHash);
57+
}

src/MySqlConnector/Core/ServerSession.cs

Lines changed: 29 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -448,13 +448,13 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella
448448
var initialHandshake = InitialHandshakePayload.Create(payload.Span);
449449

450450
// if PluginAuth is supported, then use the specified auth plugin; else, fall back to protocol capabilities to determine the auth type to use
451-
m_currentAuthenticationMethod = (initialHandshake.ProtocolCapabilities & ProtocolCapabilities.PluginAuth) != 0 ? initialHandshake.AuthPluginName! :
451+
var currentAuthenticationMethod = (initialHandshake.ProtocolCapabilities & ProtocolCapabilities.PluginAuth) != 0 ? initialHandshake.AuthPluginName! :
452452
(initialHandshake.ProtocolCapabilities & ProtocolCapabilities.SecureConnection) == 0 ? "mysql_old_password" :
453453
"mysql_native_password";
454-
Log.ServerSentAuthPluginName(m_logger, Id, m_currentAuthenticationMethod);
455-
if (m_currentAuthenticationMethod is not "mysql_native_password" and not "sha256_password" and not "caching_sha2_password")
454+
Log.ServerSentAuthPluginName(m_logger, Id, currentAuthenticationMethod);
455+
if (currentAuthenticationMethod is not "mysql_native_password" and not "sha256_password" and not "caching_sha2_password")
456456
{
457-
Log.UnsupportedAuthenticationMethod(m_logger, Id, m_currentAuthenticationMethod);
457+
Log.UnsupportedAuthenticationMethod(m_logger, Id, currentAuthenticationMethod);
458458
throw new NotSupportedException($"Authentication method '{initialHandshake.AuthPluginName}' is not supported.");
459459
}
460460

@@ -532,7 +532,13 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella
532532
var useCachingSha2 = initialHandshake.AuthPluginName == "caching_sha2_password";
533533

534534
var password = GetPassword(cs, connection);
535-
using (var handshakeResponsePayload = HandshakeResponse41Payload.Create(initialHandshake, cs, password, useCachingSha2, m_compressionMethod, connection.ZstandardPlugin?.CompressionLevel, m_characterSet, m_supportsConnectionAttributes ? cs.ConnectionAttributes : null))
535+
byte[] authenticationResponse;
536+
if (useCachingSha2)
537+
authenticationResponse = AuthenticationUtility.CreateScrambleResponse(Utility.TrimZeroByte(initialHandshake.AuthPluginData.AsSpan()), password);
538+
else
539+
AuthenticationUtility.CreateResponseAndPasswordHash(password, initialHandshake.AuthPluginData, out authenticationResponse, out m_passwordHash);
540+
541+
using (var handshakeResponsePayload = HandshakeResponse41Payload.Create(initialHandshake, cs, authenticationResponse, m_compressionMethod, connection.ZstandardPlugin?.CompressionLevel, m_characterSet, m_supportsConnectionAttributes ? cs.ConnectionAttributes : null))
536542
await SendReplyAsync(handshakeResponsePayload, ioBehavior, cancellationToken).ConfigureAwait(false);
537543
payload = await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
538544

@@ -553,7 +559,7 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella
553559
}
554560
else if (!m_isSecureConnection && password.Length != 0)
555561
{
556-
var publicKey = await GetRsaPublicKeyAsync(m_currentAuthenticationMethod, cs, ioBehavior, cancellationToken).ConfigureAwait(false);
562+
var publicKey = await GetRsaPublicKeyAsync(currentAuthenticationMethod, cs, ioBehavior, cancellationToken).ConfigureAwait(false);
557563
payload = await SendEncryptedPasswordAsync(AuthPluginData, publicKey, password, ioBehavior, cancellationToken).ConfigureAwait(false);
558564
}
559565
else
@@ -583,7 +589,7 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella
583589
// there is no shared secret that can be used to validate the certificate
584590
Log.CertificateErrorNoPassword(m_logger, Id, m_sslPolicyErrors);
585591
}
586-
else if (ValidateFingerprint(ok.StatusInfo, initialHandshake.AuthPluginData.AsSpan(0, 20), password))
592+
else if (ValidateFingerprint(ok.StatusInfo, initialHandshake.AuthPluginData.AsSpan(0, 20)))
587593
{
588594
Log.CertificateErrorValidThumbprint(m_logger, Id, m_sslPolicyErrors);
589595
ignoreCertificateError = true;
@@ -649,36 +655,20 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella
649655
/// </summary>
650656
/// <param name="validationHash">The validation hash received from the server.</param>
651657
/// <param name="challenge">The auth plugin data from the initial handshake.</param>
652-
/// <param name="password">The user's password.</param>
653658
/// <returns><c>true</c> if the validation hash matches the locally-computed value; otherwise, <c>false</c>.</returns>
654-
private bool ValidateFingerprint(byte[]? validationHash, ReadOnlySpan<byte> challenge, string password)
659+
private bool ValidateFingerprint(byte[]? validationHash, ReadOnlySpan<byte> challenge)
655660
{
656661
// expect 0x01 followed by 64 hex characters giving a SHA2 hash
657662
if (validationHash?.Length != 65 || validationHash[0] != 1)
658663
return false;
659664

660-
byte[]? passwordHashResult = null;
661-
switch (m_currentAuthenticationMethod)
662-
{
663-
case "mysql_native_password":
664-
passwordHashResult = AuthenticationUtility.HashPassword([], password, onlyHashPassword: true);
665-
break;
666-
667-
case "client_ed25519":
668-
AuthenticationPlugins.TryGetPlugin(m_currentAuthenticationMethod, out var ed25519Plugin);
669-
if (ed25519Plugin is IAuthenticationPlugin2 plugin2)
670-
passwordHashResult = plugin2.CreatePasswordHash(password, challenge);
671-
break;
672-
}
673-
if (passwordHashResult is null)
665+
// the authentication plugin must have provided a password hash (via IAuthenticationPlugin3) that we saved for future use
666+
if (m_passwordHash is null)
674667
return false;
675668

676-
Span<byte> combined = stackalloc byte[32 + challenge.Length + passwordHashResult.Length];
677-
passwordHashResult.CopyTo(combined);
678-
challenge.CopyTo(combined[passwordHashResult.Length..]);
679-
m_remoteCertificateSha2Thumbprint!.CopyTo(combined[(passwordHashResult.Length + challenge.Length)..]);
680-
669+
// hash password hash || scramble || certificate thumbprint
681670
Span<byte> hashBytes = stackalloc byte[32];
671+
Span<byte> combined = [.. m_passwordHash, .. challenge, .. m_remoteCertificateSha2Thumbprint!];
682672
#if NET5_0_OR_GREATER
683673
SHA256.TryHashData(combined, hashBytes, out _);
684674
#else
@@ -827,8 +817,8 @@ public async Task<bool> TryResetConnectionAsync(ConnectionSettings cs, MySqlConn
827817
DatabaseOverride = null;
828818
}
829819
var password = GetPassword(cs, connection);
830-
var hashedPassword = AuthenticationUtility.CreateAuthenticationResponse(AuthPluginData!, password);
831-
using (var changeUserPayload = ChangeUserPayload.Create(cs.UserID, hashedPassword, cs.Database, m_characterSet, m_supportsConnectionAttributes ? cs.ConnectionAttributes : null))
820+
AuthenticationUtility.CreateResponseAndPasswordHash(password, AuthPluginData, out var nativeResponse, out m_passwordHash);
821+
using (var changeUserPayload = ChangeUserPayload.Create(cs.UserID, nativeResponse, cs.Database, m_characterSet, m_supportsConnectionAttributes ? cs.ConnectionAttributes : null))
832822
await SendAsync(changeUserPayload, ioBehavior, cancellationToken).ConfigureAwait(false);
833823
payload = await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
834824
if (payload.HeaderByte == AuthenticationMethodSwitchRequestPayload.Signature)
@@ -872,13 +862,12 @@ private async Task<PayloadData> SwitchAuthenticationAsync(ConnectionSettings cs,
872862
// if the server didn't support the hashed password; rehash with the new challenge
873863
var switchRequest = AuthenticationMethodSwitchRequestPayload.Create(payload.Span);
874864
Log.SwitchingToAuthenticationMethod(m_logger, Id, switchRequest.Name);
875-
m_currentAuthenticationMethod = switchRequest.Name;
876865
switch (switchRequest.Name)
877866
{
878867
case "mysql_native_password":
879868
AuthPluginData = switchRequest.Data;
880-
var hashedPassword = AuthenticationUtility.CreateAuthenticationResponse(AuthPluginData, password);
881-
payload = new(hashedPassword);
869+
AuthenticationUtility.CreateResponseAndPasswordHash(password, AuthPluginData, out var nativeResponse, out m_passwordHash);
870+
payload = new(nativeResponse);
882871
await SendReplyAsync(payload, ioBehavior, cancellationToken).ConfigureAwait(false);
883872
return await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
884873

@@ -931,14 +920,15 @@ private async Task<PayloadData> SwitchAuthenticationAsync(ConnectionSettings cs,
931920
throw new NotSupportedException("'MySQL Server is requesting the insecure pre-4.1 auth mechanism (mysql_old_password). The user password must be upgraded; see https://dev.mysql.com/doc/refman/5.7/en/account-upgrades.html.");
932921

933922
case "client_ed25519":
934-
if (!AuthenticationPlugins.TryGetPlugin(switchRequest.Name, out var ed25519Plugin))
923+
if (!AuthenticationPlugins.TryGetPlugin(switchRequest.Name, out var ed25519Plugin) || ed25519Plugin is not IAuthenticationPlugin3 ed25519Plugin3)
935924
throw new NotSupportedException("You must install the MySqlConnector.Authentication.Ed25519 package and call Ed25519AuthenticationPlugin.Install to use client_ed25519 authentication.");
936-
payload = new(ed25519Plugin.CreateResponse(password, switchRequest.Data));
925+
ed25519Plugin3.CreateResponseAndPasswordHash(password, switchRequest.Data, out var ed25519Response, out m_passwordHash);
926+
payload = new(ed25519Response);
937927
await SendReplyAsync(payload, ioBehavior, cancellationToken).ConfigureAwait(false);
938928
return await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
939929

940930
case "parsec":
941-
if (!AuthenticationPlugins.TryGetPlugin(switchRequest.Name, out var parsecPlugin))
931+
if (!AuthenticationPlugins.TryGetPlugin(switchRequest.Name, out var parsecPlugin) || parsecPlugin is not IAuthenticationPlugin3 parsecPlugin3)
942932
throw new NotSupportedException("You must install the MySqlConnector.Authentication.Ed25519 package and call ParsecAuthenticationPlugin.Install to use parsec authentication.");
943933
payload = new([]);
944934
await SendReplyAsync(payload, ioBehavior, cancellationToken).ConfigureAwait(false);
@@ -948,7 +938,8 @@ private async Task<PayloadData> SwitchAuthenticationAsync(ConnectionSettings cs,
948938
switchRequest.Data.CopyTo(combinedData);
949939
payload.Span.CopyTo(combinedData.Slice(switchRequest.Data.Length));
950940

951-
payload = new(parsecPlugin.CreateResponse(password, combinedData));
941+
parsecPlugin3.CreateResponseAndPasswordHash(password, combinedData, out var parsecResponse, out m_passwordHash);
942+
payload = new(parsecResponse);
952943
await SendReplyAsync(payload, ioBehavior, cancellationToken).ConfigureAwait(false);
953944
return await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
954945

@@ -2216,7 +2207,7 @@ protected override void OnStatementBegin(int index)
22162207
private PayloadData m_setNamesPayload;
22172208
private byte[]? m_pipelinedResetConnectionBytes;
22182209
private Dictionary<string, PreparedStatements>? m_preparedStatements;
2219-
private string? m_currentAuthenticationMethod;
2210+
private byte[]? m_passwordHash;
22202211
private byte[]? m_remoteCertificateSha2Thumbprint;
22212212
private SslPolicyErrors m_sslPolicyErrors;
22222213
}

0 commit comments

Comments
 (0)