Skip to content

Commit 7947a8a

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 208f2ec commit 7947a8a

File tree

8 files changed

+106
-86
lines changed

8 files changed

+106
-86
lines changed

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

-13
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

+10-10
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ 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+
public sealed class Ed25519AuthenticationPlugin : IAuthenticationPlugin3
1414
{
1515
/// <summary>
1616
/// Registers the Ed25519 authentication plugin with MySqlConnector. You must call this method once before
@@ -32,20 +32,20 @@ public static void Install()
3232
/// </summary>
3333
public byte[] CreateResponse(string password, ReadOnlySpan<byte> authenticationData)
3434
{
35-
CreateResponseAndHash(password, authenticationData, out _, out var authenticationResponse);
35+
CreateResponseAndPasswordHash(password, authenticationData, out var authenticationResponse, out _);
3636
return authenticationResponse;
3737
}
3838

3939
/// <summary>
40-
/// Creates the Ed25519 password hash.
40+
/// Creates the authentication response and hashes the client's password (e.g., for TLS certificate fingerprint verification).
4141
/// </summary>
42-
public byte[] CreatePasswordHash(string password, ReadOnlySpan<byte> authenticationData)
43-
{
44-
CreateResponseAndHash(password, authenticationData, out var passwordHash, out _);
45-
return passwordHash;
46-
}
47-
48-
private static void CreateResponseAndHash(string password, ReadOnlySpan<byte> authenticationData, out byte[] passwordHash, out byte[] authenticationResponse)
42+
/// <param name="password">The client's password.</param>
43+
/// <param name="authenticationData">The authentication data supplied by the server; this is the <code>auth method data</code>
44+
/// from the <a href="https://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::AuthSwitchRequest">Authentication
45+
/// Method Switch Request Packet</a>.</param>
46+
/// <param name="authenticationResponse">The authentication response.</param>
47+
/// <param name="passwordHash">The authentication-method-specific hash of the client's password.</param>
48+
public void CreateResponseAndPasswordHash(string password, ReadOnlySpan<byte> authenticationData, out byte[] authenticationResponse, out byte[] passwordHash)
4949
{
5050
// Java reference: https://github.com/MariaDB/mariadb-connector-j/blob/master/src/main/java/org/mariadb/jdbc/internal/com/send/authentication/Ed25519PasswordPlugin.java
5151
// C reference: https://github.com/MariaDB/server/blob/592fe954ef82be1bc08b29a8e54f7729eb1e1343/plugin/auth_ed25519/ref10/sign.c#L7

src/MySqlConnector.Authentication.Ed25519/ParsecAuthenticationPlugin.cs

+25-11
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,28 +63,33 @@ 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
#else
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
#endif
64-
var expandedPrivateKey = Chaos.NaCl.Ed25519.ExpandedPrivateKeyFromSeed(privateKey);
73+
Chaos.NaCl.Ed25519.KeyPairFromSeed(out var publicKey, out var privateKey, privateKeySeed);
6574

6675
// generate Ed25519 keypair and sign concatenated scrambles
6776
var message = new byte[serverScramble.Length + clientScramble.Length];
6877
serverScramble.CopyTo(message);
6978
clientScramble.CopyTo(message.AsSpan(serverScramble.Length));
7079

71-
var signature = Chaos.NaCl.Ed25519.Sign(message, expandedPrivateKey);
80+
var signature = Chaos.NaCl.Ed25519.Sign(message, privateKey);
81+
82+
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER
83+
CryptographicOperations.ZeroMemory(privateKey);
84+
#endif
7285

7386
// return client scramble followed by signature
74-
var response = new byte[clientScramble.Length + signature.Length];
75-
clientScramble.CopyTo(response.AsSpan());
76-
signature.CopyTo(response.AsSpan(clientScramble.Length));
77-
78-
return response;
87+
authenticationResponse = new byte[clientScramble.Length + signature.Length];
88+
clientScramble.CopyTo(authenticationResponse.AsSpan());
89+
signature.CopyTo(authenticationResponse.AsSpan(clientScramble.Length));
90+
91+
// "password hash" for parsec is the extended salt followed by the public key
92+
passwordHash = [(byte) 'P', (byte) iterationCount, .. salt, .. publicKey];
7993
}
8094

8195
private ParsecAuthenticationPlugin()

src/MySqlConnector/Authentication/IAuthenticationPlugin.cs

+19
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

+23-37
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

@@ -529,7 +529,8 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella
529529
cs.ConnectionAttributes = CreateConnectionAttributes(cs.ApplicationName);
530530

531531
var password = GetPassword(cs, connection);
532-
using (var handshakeResponsePayload = HandshakeResponse41Payload.Create(initialHandshake, cs, password, m_compressionMethod, connection.ZstandardPlugin?.CompressionLevel, m_characterSet, m_supportsConnectionAttributes ? cs.ConnectionAttributes : null))
532+
AuthenticationUtility.CreateResponseAndPasswordHash(password, initialHandshake.AuthPluginData, out var authenticationResponse, out m_passwordHash);
533+
using (var handshakeResponsePayload = HandshakeResponse41Payload.Create(initialHandshake, cs, authenticationResponse, m_compressionMethod, connection.ZstandardPlugin?.CompressionLevel, m_characterSet, m_supportsConnectionAttributes ? cs.ConnectionAttributes : null))
533534
await SendReplyAsync(handshakeResponsePayload, ioBehavior, cancellationToken).ConfigureAwait(false);
534535
payload = await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
535536

@@ -560,7 +561,7 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella
560561
// there is no shared secret that can be used to validate the certificate
561562
Log.CertificateErrorNoPassword(m_logger, Id, m_sslPolicyErrors);
562563
}
563-
else if (ValidateFingerprint(ok.StatusInfo, initialHandshake.AuthPluginData.AsSpan(0, 20), password))
564+
else if (ValidateFingerprint(ok.StatusInfo, initialHandshake.AuthPluginData.AsSpan(0, 20)))
564565
{
565566
Log.CertificateErrorValidThumbprint(m_logger, Id, m_sslPolicyErrors);
566567
ignoreCertificateError = true;
@@ -626,36 +627,20 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella
626627
/// </summary>
627628
/// <param name="validationHash">The validation hash received from the server.</param>
628629
/// <param name="challenge">The auth plugin data from the initial handshake.</param>
629-
/// <param name="password">The user's password.</param>
630630
/// <returns><c>true</c> if the validation hash matches the locally-computed value; otherwise, <c>false</c>.</returns>
631-
private bool ValidateFingerprint(byte[]? validationHash, ReadOnlySpan<byte> challenge, string password)
631+
private bool ValidateFingerprint(byte[]? validationHash, ReadOnlySpan<byte> challenge)
632632
{
633633
// expect 0x01 followed by 64 hex characters giving a SHA2 hash
634634
if (validationHash?.Length != 65 || validationHash[0] != 1)
635635
return false;
636636

637-
byte[]? passwordHashResult = null;
638-
switch (m_currentAuthenticationMethod)
639-
{
640-
case "mysql_native_password":
641-
passwordHashResult = AuthenticationUtility.HashPassword([], password, onlyHashPassword: true);
642-
break;
643-
644-
case "client_ed25519":
645-
AuthenticationPlugins.TryGetPlugin(m_currentAuthenticationMethod, out var ed25519Plugin);
646-
if (ed25519Plugin is IAuthenticationPlugin2 plugin2)
647-
passwordHashResult = plugin2.CreatePasswordHash(password, challenge);
648-
break;
649-
}
650-
if (passwordHashResult is null)
637+
// the authentication plugin must have provided a password hash (via IAuthenticationPlugin3) that we saved for future use
638+
if (m_passwordHash is null)
651639
return false;
652640

653-
Span<byte> combined = stackalloc byte[32 + challenge.Length + passwordHashResult.Length];
654-
passwordHashResult.CopyTo(combined);
655-
challenge.CopyTo(combined[passwordHashResult.Length..]);
656-
m_remoteCertificateSha2Thumbprint!.CopyTo(combined[(passwordHashResult.Length + challenge.Length)..]);
657-
641+
// hash password hash || scramble || certificate thumbprint
658642
Span<byte> hashBytes = stackalloc byte[32];
643+
Span<byte> combined = [.. m_passwordHash, .. challenge, .. m_remoteCertificateSha2Thumbprint!];
659644
#if NET5_0_OR_GREATER
660645
SHA256.TryHashData(combined, hashBytes, out _);
661646
#else
@@ -804,8 +789,8 @@ public async Task<bool> TryResetConnectionAsync(ConnectionSettings cs, MySqlConn
804789
DatabaseOverride = null;
805790
}
806791
var password = GetPassword(cs, connection);
807-
var hashedPassword = AuthenticationUtility.CreateAuthenticationResponse(AuthPluginData!, password);
808-
using (var changeUserPayload = ChangeUserPayload.Create(cs.UserID, hashedPassword, cs.Database, m_characterSet, m_supportsConnectionAttributes ? cs.ConnectionAttributes : null))
792+
AuthenticationUtility.CreateResponseAndPasswordHash(password, AuthPluginData, out var nativeResponse, out m_passwordHash);
793+
using (var changeUserPayload = ChangeUserPayload.Create(cs.UserID, nativeResponse, cs.Database, m_characterSet, m_supportsConnectionAttributes ? cs.ConnectionAttributes : null))
809794
await SendAsync(changeUserPayload, ioBehavior, cancellationToken).ConfigureAwait(false);
810795
payload = await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
811796
if (payload.HeaderByte == AuthenticationMethodSwitchRequestPayload.Signature)
@@ -849,13 +834,12 @@ private async Task<PayloadData> SwitchAuthenticationAsync(ConnectionSettings cs,
849834
// if the server didn't support the hashed password; rehash with the new challenge
850835
var switchRequest = AuthenticationMethodSwitchRequestPayload.Create(payload.Span);
851836
Log.SwitchingToAuthenticationMethod(m_logger, Id, switchRequest.Name);
852-
m_currentAuthenticationMethod = switchRequest.Name;
853837
switch (switchRequest.Name)
854838
{
855839
case "mysql_native_password":
856840
AuthPluginData = switchRequest.Data;
857-
var hashedPassword = AuthenticationUtility.CreateAuthenticationResponse(AuthPluginData, password);
858-
payload = new(hashedPassword);
841+
AuthenticationUtility.CreateResponseAndPasswordHash(password, AuthPluginData, out var nativeResponse, out m_passwordHash);
842+
payload = new(nativeResponse);
859843
await SendReplyAsync(payload, ioBehavior, cancellationToken).ConfigureAwait(false);
860844
return await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
861845

@@ -908,14 +892,15 @@ private async Task<PayloadData> SwitchAuthenticationAsync(ConnectionSettings cs,
908892
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.");
909893

910894
case "client_ed25519":
911-
if (!AuthenticationPlugins.TryGetPlugin(switchRequest.Name, out var ed25519Plugin))
895+
if (!AuthenticationPlugins.TryGetPlugin(switchRequest.Name, out var ed25519Plugin) || ed25519Plugin is not IAuthenticationPlugin3 ed25519Plugin3)
912896
throw new NotSupportedException("You must install the MySqlConnector.Authentication.Ed25519 package and call Ed25519AuthenticationPlugin.Install to use client_ed25519 authentication.");
913-
payload = new(ed25519Plugin.CreateResponse(password, switchRequest.Data));
897+
ed25519Plugin3.CreateResponseAndPasswordHash(password, switchRequest.Data, out var ed25519Response, out m_passwordHash);
898+
payload = new(ed25519Response);
914899
await SendReplyAsync(payload, ioBehavior, cancellationToken).ConfigureAwait(false);
915900
return await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
916901

917902
case "parsec":
918-
if (!AuthenticationPlugins.TryGetPlugin(switchRequest.Name, out var parsecPlugin))
903+
if (!AuthenticationPlugins.TryGetPlugin(switchRequest.Name, out var parsecPlugin) || parsecPlugin is not IAuthenticationPlugin3 parsecPlugin3)
919904
throw new NotSupportedException("You must install the MySqlConnector.Authentication.Ed25519 package and call ParsecAuthenticationPlugin.Install to use parsec authentication.");
920905
payload = new([]);
921906
await SendReplyAsync(payload, ioBehavior, cancellationToken).ConfigureAwait(false);
@@ -925,7 +910,8 @@ private async Task<PayloadData> SwitchAuthenticationAsync(ConnectionSettings cs,
925910
switchRequest.Data.CopyTo(combinedData);
926911
payload.Span.CopyTo(combinedData.Slice(switchRequest.Data.Length));
927912

928-
payload = new(parsecPlugin.CreateResponse(password, combinedData));
913+
parsecPlugin3.CreateResponseAndPasswordHash(password, combinedData, out var parsecResponse, out m_passwordHash);
914+
payload = new(parsecResponse);
929915
await SendReplyAsync(payload, ioBehavior, cancellationToken).ConfigureAwait(false);
930916
return await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
931917

@@ -2192,7 +2178,7 @@ protected override void OnStatementBegin(int index)
21922178
private PayloadData m_setNamesPayload;
21932179
private byte[]? m_pipelinedResetConnectionBytes;
21942180
private Dictionary<string, PreparedStatements>? m_preparedStatements;
2195-
private string? m_currentAuthenticationMethod;
2181+
private byte[]? m_passwordHash;
21962182
private byte[]? m_remoteCertificateSha2Thumbprint;
21972183
private SslPolicyErrors m_sslPolicyErrors;
21982184
}

src/MySqlConnector/Protocol/Payloads/HandshakeResponse41Payload.cs

+1-2
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,11 @@ private static ByteBufferWriter CreateCapabilitiesPayload(ProtocolCapabilities s
5555
public static PayloadData CreateWithSsl(ProtocolCapabilities serverCapabilities, ConnectionSettings cs, CompressionMethod compressionMethod, CharacterSet characterSet) =>
5656
CreateCapabilitiesPayload(serverCapabilities, cs, compressionMethod, characterSet, ProtocolCapabilities.Ssl).ToPayloadData();
5757

58-
public static PayloadData Create(InitialHandshakePayload handshake, ConnectionSettings cs, string password, CompressionMethod compressionMethod, int? compressionLevel, CharacterSet characterSet, byte[]? connectionAttributes)
58+
public static PayloadData Create(InitialHandshakePayload handshake, ConnectionSettings cs, byte[] authenticationResponse, CompressionMethod compressionMethod, int? compressionLevel, CharacterSet characterSet, byte[]? connectionAttributes)
5959
{
6060
// TODO: verify server capabilities
6161
var writer = CreateCapabilitiesPayload(handshake.ProtocolCapabilities, cs, compressionMethod, characterSet);
6262
writer.WriteNullTerminatedString(cs.UserID);
63-
var authenticationResponse = AuthenticationUtility.CreateAuthenticationResponse(handshake.AuthPluginData, password);
6463
writer.Write((byte) authenticationResponse.Length);
6564
writer.Write(authenticationResponse);
6665

0 commit comments

Comments
 (0)