Skip to content

Commit 3e64bcf

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 92562a6 commit 3e64bcf

File tree

7 files changed

+102
-80
lines changed

7 files changed

+102
-80
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

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

@@ -560,7 +560,7 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella
560560
// there is no shared secret that can be used to validate the certificate
561561
Log.CertificateErrorNoPassword(m_logger, Id, m_sslPolicyErrors);
562562
}
563-
else if (ValidateFingerprint(ok.StatusInfo, initialHandshake.AuthPluginData.AsSpan(0, 20), password))
563+
else if (ValidateFingerprint(ok.StatusInfo, initialHandshake.AuthPluginData.AsSpan(0, 20)))
564564
{
565565
Log.CertificateErrorValidThumbprint(m_logger, Id, m_sslPolicyErrors);
566566
ignoreCertificateError = true;
@@ -626,36 +626,20 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella
626626
/// </summary>
627627
/// <param name="validationHash">The validation hash received from the server.</param>
628628
/// <param name="challenge">The auth plugin data from the initial handshake.</param>
629-
/// <param name="password">The user's password.</param>
630629
/// <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)
630+
private bool ValidateFingerprint(byte[]? validationHash, ReadOnlySpan<byte> challenge)
632631
{
633632
// expect 0x01 followed by 64 hex characters giving a SHA2 hash
634633
if (validationHash?.Length != 65 || validationHash[0] != 1)
635634
return false;
636635

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)
636+
// the authentication plugin must have provided a password hash (via IAuthenticationPlugin3) that we saved for future use
637+
if (m_passwordHash is null)
651638
return false;
652639

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-
640+
// hash password hash || scramble || certificate thumbprint
658641
Span<byte> hashBytes = stackalloc byte[32];
642+
Span<byte> combined = [.. m_passwordHash, .. challenge, .. m_remoteCertificateSha2Thumbprint!];
659643
#if NET5_0_OR_GREATER
660644
SHA256.TryHashData(combined, hashBytes, out _);
661645
#else
@@ -849,13 +833,12 @@ private async Task<PayloadData> SwitchAuthenticationAsync(ConnectionSettings cs,
849833
// if the server didn't support the hashed password; rehash with the new challenge
850834
var switchRequest = AuthenticationMethodSwitchRequestPayload.Create(payload.Span);
851835
Log.SwitchingToAuthenticationMethod(m_logger, Id, switchRequest.Name);
852-
m_currentAuthenticationMethod = switchRequest.Name;
853836
switch (switchRequest.Name)
854837
{
855838
case "mysql_native_password":
856839
AuthPluginData = switchRequest.Data;
857-
var hashedPassword = AuthenticationUtility.CreateAuthenticationResponse(AuthPluginData, password);
858-
payload = new(hashedPassword);
840+
AuthenticationUtility.CreateResponseAndPasswordHash(password, AuthPluginData, out var nativeResponse, out m_passwordHash);
841+
payload = new(nativeResponse);
859842
await SendReplyAsync(payload, ioBehavior, cancellationToken).ConfigureAwait(false);
860843
return await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
861844

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

910893
case "client_ed25519":
911-
if (!AuthenticationPlugins.TryGetPlugin(switchRequest.Name, out var ed25519Plugin))
894+
if (!AuthenticationPlugins.TryGetPlugin(switchRequest.Name, out var ed25519Plugin) || ed25519Plugin is not IAuthenticationPlugin3 ed25519Plugin3)
912895
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));
896+
ed25519Plugin3.CreateResponseAndPasswordHash(password, switchRequest.Data, out var ed25519Response, out m_passwordHash);
897+
payload = new(ed25519Response);
914898
await SendReplyAsync(payload, ioBehavior, cancellationToken).ConfigureAwait(false);
915899
return await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
916900

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

928-
payload = new(parsecPlugin.CreateResponse(password, combinedData));
912+
parsecPlugin3.CreateResponseAndPasswordHash(password, combinedData, out var parsecResponse, out m_passwordHash);
913+
payload = new(parsecResponse);
929914
await SendReplyAsync(payload, ioBehavior, cancellationToken).ConfigureAwait(false);
930915
return await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
931916

@@ -2192,7 +2177,7 @@ protected override void OnStatementBegin(int index)
21922177
private PayloadData m_setNamesPayload;
21932178
private byte[]? m_pipelinedResetConnectionBytes;
21942179
private Dictionary<string, PreparedStatements>? m_preparedStatements;
2195-
private string? m_currentAuthenticationMethod;
2180+
private byte[]? m_passwordHash;
21962181
private byte[]? m_remoteCertificateSha2Thumbprint;
21972182
private SslPolicyErrors m_sslPolicyErrors;
21982183
}

src/MySqlConnector/Protocol/Serialization/AuthenticationUtility.cs

+15-12
Original file line numberDiff line numberDiff line change
@@ -24,23 +24,27 @@ public static byte[] GetNullTerminatedPasswordBytes(string password)
2424
return passwordBytes;
2525
}
2626

27-
public static byte[] CreateAuthenticationResponse(ReadOnlySpan<byte> challenge, string password) =>
28-
string.IsNullOrEmpty(password) ? [] : HashPassword(challenge, password, onlyHashPassword: false);
27+
public static byte[] CreateAuthenticationResponse(ReadOnlySpan<byte> challenge, string password)
28+
{
29+
if (string.IsNullOrEmpty(password))
30+
return [];
31+
32+
CreateResponseAndPasswordHash(password, challenge, out var authenticationResponse, out _);
33+
return authenticationResponse;
34+
}
2935

3036
/// <summary>
3137
/// Hashes a password with the "Secure Password Authentication" method.
3238
/// </summary>
33-
/// <param name="challenge">The 20-byte random challenge (from the "auth-plugin-data" in the initial handshake).</param>
3439
/// <param name="password">The password to hash.</param>
35-
/// <param name="onlyHashPassword">If true, <paramref name="challenge"/> is ignored and only the twice-hashed password
36-
/// is returned, instead of performing the full "secure password authentication" algorithm that XORs the hashed password against
37-
/// a hash derived from the challenge.</param>
38-
/// <returns>A 20-byte password hash.</returns>
40+
/// <param name="authenticationData">The 20-byte random challenge (from the "auth-plugin-data" in the initial handshake).</param>
41+
/// <param name="authenticationResponse">The authentication response.</param>
42+
/// <param name="passwordHash">The twice-hashed password.</param>
3943
/// <remarks>See <a href="https://dev.mysql.com/doc/internals/en/secure-password-authentication.html">Secure Password Authentication</a>.</remarks>
4044
#if NET5_0_OR_GREATER
4145
[SkipLocalsInit]
4246
#endif
43-
public static byte[] HashPassword(ReadOnlySpan<byte> challenge, string password, bool onlyHashPassword)
47+
public static void CreateResponseAndPasswordHash(string password, ReadOnlySpan<byte> authenticationData, out byte[] authenticationResponse, out byte[] passwordHash)
4448
{
4549
#if !NET5_0_OR_GREATER
4650
using var sha1 = SHA1.Create();
@@ -58,10 +62,9 @@ public static byte[] HashPassword(ReadOnlySpan<byte> challenge, string password,
5862
sha1.TryComputeHash(passwordBytes, hashedPassword, out _);
5963
sha1.TryComputeHash(hashedPassword, combined[20..], out _);
6064
#endif
61-
if (onlyHashPassword)
62-
return combined[20..].ToArray();
65+
passwordHash = combined[20..].ToArray();
6366

64-
challenge[..20].CopyTo(combined);
67+
authenticationData[..20].CopyTo(combined);
6568
Span<byte> xorBytes = stackalloc byte[20];
6669
#if NET5_0_OR_GREATER
6770
SHA1.TryHashData(combined, xorBytes, out _);
@@ -71,7 +74,7 @@ public static byte[] HashPassword(ReadOnlySpan<byte> challenge, string password,
7174
for (var i = 0; i < hashedPassword.Length; i++)
7275
hashedPassword[i] ^= xorBytes[i];
7376

74-
return hashedPassword.ToArray();
77+
authenticationResponse = hashedPassword.ToArray();
7578
}
7679

7780
public static byte[] CreateScrambleResponse(ReadOnlySpan<byte> nonce, string password) =>

tests/IntegrationTests/SslTests.cs

+14
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,20 @@ public async Task ConnectZeroConfigurationSslEd25519()
250250
using var connection = new MySqlConnection(csb.ConnectionString);
251251
await connection.OpenAsync();
252252
}
253+
254+
[SkippableFact(ServerFeatures.TlsFingerprintValidation | ServerFeatures.ParsecAuthentication)]
255+
public async Task ConnectZeroConfigurationSslParsec()
256+
{
257+
MySqlConnector.Authentication.Ed25519.ParsecAuthenticationPlugin.Install();
258+
var csb = AppConfig.CreateConnectionStringBuilder();
259+
csb.CertificateFile = null;
260+
csb.SslMode = MySqlSslMode.VerifyFull;
261+
csb.SslCa = "";
262+
csb.UserID = "parsec-user";
263+
csb.Password = "P@rs3c-Pa55";
264+
using var connection = new MySqlConnection(csb.ConnectionString);
265+
await connection.OpenAsync();
266+
}
253267
#endif
254268
#endif
255269

0 commit comments

Comments
 (0)