diff --git a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Ed25519.cs b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Ed25519.cs
index 613f8330a..f3a0b7011 100644
--- a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Ed25519.cs
+++ b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Ed25519.cs
@@ -34,19 +34,6 @@ public static byte[] Sign(byte[] message, byte[] expandedPrivateKey)
return signature;
}
- public static byte[] ExpandedPrivateKeyFromSeed(byte[] privateKeySeed)
- {
- byte[] privateKey;
- byte[] publicKey;
- KeyPairFromSeed(out publicKey, out privateKey, privateKeySeed);
-#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER
- CryptographicOperations.ZeroMemory(publicKey);
-#else
- CryptoBytes.Wipe(publicKey);
-#endif
- return privateKey;
- }
-
public static void KeyPairFromSeed(out byte[] publicKey, out byte[] expandedPrivateKey, byte[] privateKeySeed)
{
if (privateKeySeed == null)
diff --git a/src/MySqlConnector.Authentication.Ed25519/Ed25519AuthenticationPlugin.cs b/src/MySqlConnector.Authentication.Ed25519/Ed25519AuthenticationPlugin.cs
index f2ea62c3e..6728b2b9d 100644
--- a/src/MySqlConnector.Authentication.Ed25519/Ed25519AuthenticationPlugin.cs
+++ b/src/MySqlConnector.Authentication.Ed25519/Ed25519AuthenticationPlugin.cs
@@ -10,7 +10,7 @@ namespace MySqlConnector.Authentication.Ed25519;
/// Provides an implementation of the client_ed25519 authentication plugin for MariaDB.
///
/// See Authentication Plugin - ed25519.
-public sealed class Ed25519AuthenticationPlugin : IAuthenticationPlugin2
+public sealed class Ed25519AuthenticationPlugin : IAuthenticationPlugin3
{
///
/// Registers the Ed25519 authentication plugin with MySqlConnector. You must call this method once before
@@ -32,20 +32,20 @@ public static void Install()
///
public byte[] CreateResponse(string password, ReadOnlySpan authenticationData)
{
- CreateResponseAndHash(password, authenticationData, out _, out var authenticationResponse);
+ CreateResponseAndPasswordHash(password, authenticationData, out var authenticationResponse, out _);
return authenticationResponse;
}
///
- /// Creates the Ed25519 password hash.
+ /// Creates the authentication response and hashes the client's password (e.g., for TLS certificate fingerprint verification).
///
- public byte[] CreatePasswordHash(string password, ReadOnlySpan authenticationData)
- {
- CreateResponseAndHash(password, authenticationData, out var passwordHash, out _);
- return passwordHash;
- }
-
- private static void CreateResponseAndHash(string password, ReadOnlySpan authenticationData, out byte[] passwordHash, out byte[] authenticationResponse)
+ /// The client's password.
+ /// The authentication data supplied by the server; this is the auth method data
+ /// from the Authentication
+ /// Method Switch Request Packet.
+ /// The authentication response.
+ /// The authentication-method-specific hash of the client's password.
+ public void CreateResponseAndPasswordHash(string password, ReadOnlySpan authenticationData, out byte[] authenticationResponse, out byte[] passwordHash)
{
// Java reference: https://github.com/MariaDB/mariadb-connector-j/blob/master/src/main/java/org/mariadb/jdbc/internal/com/send/authentication/Ed25519PasswordPlugin.java
// C reference: https://github.com/MariaDB/server/blob/592fe954ef82be1bc08b29a8e54f7729eb1e1343/plugin/auth_ed25519/ref10/sign.c#L7
diff --git a/src/MySqlConnector.Authentication.Ed25519/ParsecAuthenticationPlugin.cs b/src/MySqlConnector.Authentication.Ed25519/ParsecAuthenticationPlugin.cs
index 4c7adc3ac..742f829c0 100644
--- a/src/MySqlConnector.Authentication.Ed25519/ParsecAuthenticationPlugin.cs
+++ b/src/MySqlConnector.Authentication.Ed25519/ParsecAuthenticationPlugin.cs
@@ -8,7 +8,7 @@ namespace MySqlConnector.Authentication.Ed25519;
///
/// Provides an implementation of the Parsec authentication plugin for MariaDB.
///
-public sealed class ParsecAuthenticationPlugin : IAuthenticationPlugin
+public sealed class ParsecAuthenticationPlugin : IAuthenticationPlugin3
{
///
/// Registers the Parsec authentication plugin with MySqlConnector. You must call this method once before
@@ -29,6 +29,15 @@ public static void Install()
/// Creates the authentication response.
///
public byte[] CreateResponse(string password, ReadOnlySpan authenticationData)
+ {
+ CreateResponseAndPasswordHash(password, authenticationData, out var response, out _);
+ return response;
+ }
+
+ ///
+ /// Creates the authentication response.
+ ///
+ public void CreateResponseAndPasswordHash(string password, ReadOnlySpan authenticationData, out byte[] authenticationResponse, out byte[] passwordHash)
{
// first 32 bytes are server scramble
var serverScramble = authenticationData.Slice(0, 32);
@@ -54,28 +63,33 @@ public byte[] CreateResponse(string password, ReadOnlySpan authenticationD
var salt = extendedSalt.Slice(2);
// derive private key using PBKDF2-SHA512
- byte[] privateKey;
+ byte[] privateKeySeed;
#if NET6_0_OR_GREATER
- privateKey = Rfc2898DeriveBytes.Pbkdf2(Encoding.UTF8.GetBytes(password), salt, iterationCount, HashAlgorithmName.SHA512, 32);
+ privateKeySeed = Rfc2898DeriveBytes.Pbkdf2(Encoding.UTF8.GetBytes(password), salt, iterationCount, HashAlgorithmName.SHA512, 32);
#else
using (var pbkdf2 = new Rfc2898DeriveBytes(Encoding.UTF8.GetBytes(password), salt.ToArray(), iterationCount, HashAlgorithmName.SHA512))
- privateKey = pbkdf2.GetBytes(32);
+ privateKeySeed = pbkdf2.GetBytes(32);
#endif
- var expandedPrivateKey = Chaos.NaCl.Ed25519.ExpandedPrivateKeyFromSeed(privateKey);
+ Chaos.NaCl.Ed25519.KeyPairFromSeed(out var publicKey, out var privateKey, privateKeySeed);
// generate Ed25519 keypair and sign concatenated scrambles
var message = new byte[serverScramble.Length + clientScramble.Length];
serverScramble.CopyTo(message);
clientScramble.CopyTo(message.AsSpan(serverScramble.Length));
- var signature = Chaos.NaCl.Ed25519.Sign(message, expandedPrivateKey);
+ var signature = Chaos.NaCl.Ed25519.Sign(message, privateKey);
+
+#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER
+ CryptographicOperations.ZeroMemory(privateKey);
+#endif
// return client scramble followed by signature
- var response = new byte[clientScramble.Length + signature.Length];
- clientScramble.CopyTo(response.AsSpan());
- signature.CopyTo(response.AsSpan(clientScramble.Length));
-
- return response;
+ authenticationResponse = new byte[clientScramble.Length + signature.Length];
+ clientScramble.CopyTo(authenticationResponse.AsSpan());
+ signature.CopyTo(authenticationResponse.AsSpan(clientScramble.Length));
+
+ // "password hash" for parsec is the extended salt followed by the public key
+ passwordHash = [(byte) 'P', (byte) iterationCount, .. salt, .. publicKey];
}
private ParsecAuthenticationPlugin()
diff --git a/src/MySqlConnector/Authentication/IAuthenticationPlugin.cs b/src/MySqlConnector/Authentication/IAuthenticationPlugin.cs
index 6dfface4e..bc432c0d1 100644
--- a/src/MySqlConnector/Authentication/IAuthenticationPlugin.cs
+++ b/src/MySqlConnector/Authentication/IAuthenticationPlugin.cs
@@ -24,6 +24,7 @@ public interface IAuthenticationPlugin
///
/// is an extension to that returns a hash of the client's password.
///
+[Obsolete("Use IAuthenticationPlugin3 instead.")]
public interface IAuthenticationPlugin2 : IAuthenticationPlugin
{
///
@@ -36,3 +37,21 @@ public interface IAuthenticationPlugin2 : IAuthenticationPlugin
/// The authentication-method-specific hash of the client's password.
byte[] CreatePasswordHash(string password, ReadOnlySpan authenticationData);
}
+
+///
+/// is an extension to that also returns a hash of the client's password.
+///
+/// If an authentication plugin supports this interface, the base method will not be called.
+public interface IAuthenticationPlugin3 : IAuthenticationPlugin
+{
+ ///
+ /// Creates the authentication response and hashes the client's password (e.g., for TLS certificate fingerprint verification).
+ ///
+ /// The client's password.
+ /// The authentication data supplied by the server; this is the auth method data
+ /// from the Authentication
+ /// Method Switch Request Packet.
+ /// The authentication response.
+ /// The authentication-method-specific hash of the client's password.
+ void CreateResponseAndPasswordHash(string password, ReadOnlySpan authenticationData, out byte[] authenticationResponse, out byte[] passwordHash);
+}
diff --git a/src/MySqlConnector/Core/ServerSession.cs b/src/MySqlConnector/Core/ServerSession.cs
index cfb6e9a8c..e5ade4084 100644
--- a/src/MySqlConnector/Core/ServerSession.cs
+++ b/src/MySqlConnector/Core/ServerSession.cs
@@ -448,13 +448,13 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella
var initialHandshake = InitialHandshakePayload.Create(payload.Span);
// if PluginAuth is supported, then use the specified auth plugin; else, fall back to protocol capabilities to determine the auth type to use
- m_currentAuthenticationMethod = (initialHandshake.ProtocolCapabilities & ProtocolCapabilities.PluginAuth) != 0 ? initialHandshake.AuthPluginName! :
+ var currentAuthenticationMethod = (initialHandshake.ProtocolCapabilities & ProtocolCapabilities.PluginAuth) != 0 ? initialHandshake.AuthPluginName! :
(initialHandshake.ProtocolCapabilities & ProtocolCapabilities.SecureConnection) == 0 ? "mysql_old_password" :
"mysql_native_password";
- Log.ServerSentAuthPluginName(m_logger, Id, m_currentAuthenticationMethod);
- if (m_currentAuthenticationMethod is not "mysql_native_password" and not "sha256_password" and not "caching_sha2_password")
+ Log.ServerSentAuthPluginName(m_logger, Id, currentAuthenticationMethod);
+ if (currentAuthenticationMethod is not "mysql_native_password" and not "sha256_password" and not "caching_sha2_password")
{
- Log.UnsupportedAuthenticationMethod(m_logger, Id, m_currentAuthenticationMethod);
+ Log.UnsupportedAuthenticationMethod(m_logger, Id, currentAuthenticationMethod);
throw new NotSupportedException($"Authentication method '{initialHandshake.AuthPluginName}' is not supported.");
}
@@ -529,7 +529,8 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella
cs.ConnectionAttributes = CreateConnectionAttributes(cs.ApplicationName);
var password = GetPassword(cs, connection);
- using (var handshakeResponsePayload = HandshakeResponse41Payload.Create(initialHandshake, cs, password, m_compressionMethod, connection.ZstandardPlugin?.CompressionLevel, m_characterSet, m_supportsConnectionAttributes ? cs.ConnectionAttributes : null))
+ AuthenticationUtility.CreateResponseAndPasswordHash(password, initialHandshake.AuthPluginData, out var authenticationResponse, out m_passwordHash);
+ using (var handshakeResponsePayload = HandshakeResponse41Payload.Create(initialHandshake, cs, authenticationResponse, m_compressionMethod, connection.ZstandardPlugin?.CompressionLevel, m_characterSet, m_supportsConnectionAttributes ? cs.ConnectionAttributes : null))
await SendReplyAsync(handshakeResponsePayload, ioBehavior, cancellationToken).ConfigureAwait(false);
payload = await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
@@ -560,7 +561,7 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella
// there is no shared secret that can be used to validate the certificate
Log.CertificateErrorNoPassword(m_logger, Id, m_sslPolicyErrors);
}
- else if (ValidateFingerprint(ok.StatusInfo, initialHandshake.AuthPluginData.AsSpan(0, 20), password))
+ else if (ValidateFingerprint(ok.StatusInfo, initialHandshake.AuthPluginData.AsSpan(0, 20)))
{
Log.CertificateErrorValidThumbprint(m_logger, Id, m_sslPolicyErrors);
ignoreCertificateError = true;
@@ -626,36 +627,20 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella
///
/// The validation hash received from the server.
/// The auth plugin data from the initial handshake.
- /// The user's password.
/// true if the validation hash matches the locally-computed value; otherwise, false.
- private bool ValidateFingerprint(byte[]? validationHash, ReadOnlySpan challenge, string password)
+ private bool ValidateFingerprint(byte[]? validationHash, ReadOnlySpan challenge)
{
// expect 0x01 followed by 64 hex characters giving a SHA2 hash
if (validationHash?.Length != 65 || validationHash[0] != 1)
return false;
- byte[]? passwordHashResult = null;
- switch (m_currentAuthenticationMethod)
- {
- case "mysql_native_password":
- passwordHashResult = AuthenticationUtility.HashPassword([], password, onlyHashPassword: true);
- break;
-
- case "client_ed25519":
- AuthenticationPlugins.TryGetPlugin(m_currentAuthenticationMethod, out var ed25519Plugin);
- if (ed25519Plugin is IAuthenticationPlugin2 plugin2)
- passwordHashResult = plugin2.CreatePasswordHash(password, challenge);
- break;
- }
- if (passwordHashResult is null)
+ // the authentication plugin must have provided a password hash (via IAuthenticationPlugin3) that we saved for future use
+ if (m_passwordHash is null)
return false;
- Span combined = stackalloc byte[32 + challenge.Length + passwordHashResult.Length];
- passwordHashResult.CopyTo(combined);
- challenge.CopyTo(combined[passwordHashResult.Length..]);
- m_remoteCertificateSha2Thumbprint!.CopyTo(combined[(passwordHashResult.Length + challenge.Length)..]);
-
+ // hash password hash || scramble || certificate thumbprint
Span hashBytes = stackalloc byte[32];
+ Span combined = [.. m_passwordHash, .. challenge, .. m_remoteCertificateSha2Thumbprint!];
#if NET5_0_OR_GREATER
SHA256.TryHashData(combined, hashBytes, out _);
#else
@@ -804,8 +789,8 @@ public async Task TryResetConnectionAsync(ConnectionSettings cs, MySqlConn
DatabaseOverride = null;
}
var password = GetPassword(cs, connection);
- var hashedPassword = AuthenticationUtility.CreateAuthenticationResponse(AuthPluginData!, password);
- using (var changeUserPayload = ChangeUserPayload.Create(cs.UserID, hashedPassword, cs.Database, m_characterSet, m_supportsConnectionAttributes ? cs.ConnectionAttributes : null))
+ AuthenticationUtility.CreateResponseAndPasswordHash(password, AuthPluginData, out var nativeResponse, out m_passwordHash);
+ using (var changeUserPayload = ChangeUserPayload.Create(cs.UserID, nativeResponse, cs.Database, m_characterSet, m_supportsConnectionAttributes ? cs.ConnectionAttributes : null))
await SendAsync(changeUserPayload, ioBehavior, cancellationToken).ConfigureAwait(false);
payload = await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
if (payload.HeaderByte == AuthenticationMethodSwitchRequestPayload.Signature)
@@ -849,13 +834,12 @@ private async Task 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_currentAuthenticationMethod = switchRequest.Name;
switch (switchRequest.Name)
{
case "mysql_native_password":
AuthPluginData = switchRequest.Data;
- var hashedPassword = AuthenticationUtility.CreateAuthenticationResponse(AuthPluginData, password);
- payload = new(hashedPassword);
+ AuthenticationUtility.CreateResponseAndPasswordHash(password, AuthPluginData, out var nativeResponse, out m_passwordHash);
+ payload = new(nativeResponse);
await SendReplyAsync(payload, ioBehavior, cancellationToken).ConfigureAwait(false);
return await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
@@ -908,14 +892,15 @@ private async Task SwitchAuthenticationAsync(ConnectionSettings cs,
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.");
case "client_ed25519":
- if (!AuthenticationPlugins.TryGetPlugin(switchRequest.Name, out var ed25519Plugin))
+ if (!AuthenticationPlugins.TryGetPlugin(switchRequest.Name, out var ed25519Plugin) || ed25519Plugin is not IAuthenticationPlugin3 ed25519Plugin3)
throw new NotSupportedException("You must install the MySqlConnector.Authentication.Ed25519 package and call Ed25519AuthenticationPlugin.Install to use client_ed25519 authentication.");
- payload = new(ed25519Plugin.CreateResponse(password, switchRequest.Data));
+ ed25519Plugin3.CreateResponseAndPasswordHash(password, switchRequest.Data, out var ed25519Response, out m_passwordHash);
+ payload = new(ed25519Response);
await SendReplyAsync(payload, ioBehavior, cancellationToken).ConfigureAwait(false);
return await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
case "parsec":
- if (!AuthenticationPlugins.TryGetPlugin(switchRequest.Name, out var parsecPlugin))
+ if (!AuthenticationPlugins.TryGetPlugin(switchRequest.Name, out var parsecPlugin) || parsecPlugin is not IAuthenticationPlugin3 parsecPlugin3)
throw new NotSupportedException("You must install the MySqlConnector.Authentication.Ed25519 package and call ParsecAuthenticationPlugin.Install to use parsec authentication.");
payload = new([]);
await SendReplyAsync(payload, ioBehavior, cancellationToken).ConfigureAwait(false);
@@ -925,7 +910,8 @@ private async Task SwitchAuthenticationAsync(ConnectionSettings cs,
switchRequest.Data.CopyTo(combinedData);
payload.Span.CopyTo(combinedData.Slice(switchRequest.Data.Length));
- payload = new(parsecPlugin.CreateResponse(password, combinedData));
+ parsecPlugin3.CreateResponseAndPasswordHash(password, combinedData, out var parsecResponse, out m_passwordHash);
+ payload = new(parsecResponse);
await SendReplyAsync(payload, ioBehavior, cancellationToken).ConfigureAwait(false);
return await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
@@ -2192,7 +2178,7 @@ protected override void OnStatementBegin(int index)
private PayloadData m_setNamesPayload;
private byte[]? m_pipelinedResetConnectionBytes;
private Dictionary? m_preparedStatements;
- private string? m_currentAuthenticationMethod;
+ private byte[]? m_passwordHash;
private byte[]? m_remoteCertificateSha2Thumbprint;
private SslPolicyErrors m_sslPolicyErrors;
}
diff --git a/src/MySqlConnector/Protocol/Payloads/HandshakeResponse41Payload.cs b/src/MySqlConnector/Protocol/Payloads/HandshakeResponse41Payload.cs
index 609fcfd45..048459357 100644
--- a/src/MySqlConnector/Protocol/Payloads/HandshakeResponse41Payload.cs
+++ b/src/MySqlConnector/Protocol/Payloads/HandshakeResponse41Payload.cs
@@ -55,12 +55,11 @@ private static ByteBufferWriter CreateCapabilitiesPayload(ProtocolCapabilities s
public static PayloadData CreateWithSsl(ProtocolCapabilities serverCapabilities, ConnectionSettings cs, CompressionMethod compressionMethod, CharacterSet characterSet) =>
CreateCapabilitiesPayload(serverCapabilities, cs, compressionMethod, characterSet, ProtocolCapabilities.Ssl).ToPayloadData();
- public static PayloadData Create(InitialHandshakePayload handshake, ConnectionSettings cs, string password, CompressionMethod compressionMethod, int? compressionLevel, CharacterSet characterSet, byte[]? connectionAttributes)
+ public static PayloadData Create(InitialHandshakePayload handshake, ConnectionSettings cs, byte[] authenticationResponse, CompressionMethod compressionMethod, int? compressionLevel, CharacterSet characterSet, byte[]? connectionAttributes)
{
// TODO: verify server capabilities
var writer = CreateCapabilitiesPayload(handshake.ProtocolCapabilities, cs, compressionMethod, characterSet);
writer.WriteNullTerminatedString(cs.UserID);
- var authenticationResponse = AuthenticationUtility.CreateAuthenticationResponse(handshake.AuthPluginData, password);
writer.Write((byte) authenticationResponse.Length);
writer.Write(authenticationResponse);
diff --git a/src/MySqlConnector/Protocol/Serialization/AuthenticationUtility.cs b/src/MySqlConnector/Protocol/Serialization/AuthenticationUtility.cs
index 659d2350d..d1f325682 100644
--- a/src/MySqlConnector/Protocol/Serialization/AuthenticationUtility.cs
+++ b/src/MySqlConnector/Protocol/Serialization/AuthenticationUtility.cs
@@ -24,24 +24,26 @@ public static byte[] GetNullTerminatedPasswordBytes(string password)
return passwordBytes;
}
- public static byte[] CreateAuthenticationResponse(ReadOnlySpan challenge, string password) =>
- string.IsNullOrEmpty(password) ? [] : HashPassword(challenge, password, onlyHashPassword: false);
-
///
/// Hashes a password with the "Secure Password Authentication" method.
///
- /// The 20-byte random challenge (from the "auth-plugin-data" in the initial handshake).
/// The password to hash.
- /// If true, is ignored and only the twice-hashed password
- /// is returned, instead of performing the full "secure password authentication" algorithm that XORs the hashed password against
- /// a hash derived from the challenge.
- /// A 20-byte password hash.
+ /// The 20-byte random challenge (from the "auth-plugin-data" in the initial handshake).
+ /// The authentication response.
+ /// The twice-hashed password.
/// See Secure Password Authentication.
#if NET5_0_OR_GREATER
[SkipLocalsInit]
#endif
- public static byte[] HashPassword(ReadOnlySpan challenge, string password, bool onlyHashPassword)
+ public static void CreateResponseAndPasswordHash(string password, ReadOnlySpan authenticationData, out byte[] authenticationResponse, out byte[] passwordHash)
{
+ if (string.IsNullOrEmpty(password))
+ {
+ authenticationResponse = [];
+ passwordHash = [];
+ return;
+ }
+
#if !NET5_0_OR_GREATER
using var sha1 = SHA1.Create();
#endif
@@ -58,10 +60,9 @@ public static byte[] HashPassword(ReadOnlySpan challenge, string password,
sha1.TryComputeHash(passwordBytes, hashedPassword, out _);
sha1.TryComputeHash(hashedPassword, combined[20..], out _);
#endif
- if (onlyHashPassword)
- return combined[20..].ToArray();
+ passwordHash = combined[20..].ToArray();
- challenge[..20].CopyTo(combined);
+ authenticationData[..20].CopyTo(combined);
Span xorBytes = stackalloc byte[20];
#if NET5_0_OR_GREATER
SHA1.TryHashData(combined, xorBytes, out _);
@@ -71,7 +72,7 @@ public static byte[] HashPassword(ReadOnlySpan challenge, string password,
for (var i = 0; i < hashedPassword.Length; i++)
hashedPassword[i] ^= xorBytes[i];
- return hashedPassword.ToArray();
+ authenticationResponse = hashedPassword.ToArray();
}
public static byte[] CreateScrambleResponse(ReadOnlySpan nonce, string password) =>
diff --git a/tests/IntegrationTests/SslTests.cs b/tests/IntegrationTests/SslTests.cs
index 6ce73b803..506b3203a 100644
--- a/tests/IntegrationTests/SslTests.cs
+++ b/tests/IntegrationTests/SslTests.cs
@@ -250,6 +250,20 @@ public async Task ConnectZeroConfigurationSslEd25519()
using var connection = new MySqlConnection(csb.ConnectionString);
await connection.OpenAsync();
}
+
+ [SkippableFact(ServerFeatures.TlsFingerprintValidation | ServerFeatures.ParsecAuthentication)]
+ public async Task ConnectZeroConfigurationSslParsec()
+ {
+ MySqlConnector.Authentication.Ed25519.ParsecAuthenticationPlugin.Install();
+ var csb = AppConfig.CreateConnectionStringBuilder();
+ csb.CertificateFile = null;
+ csb.SslMode = MySqlSslMode.VerifyFull;
+ csb.SslCa = "";
+ csb.UserID = "parsec-user";
+ csb.Password = "P@rs3c-Pa55";
+ using var connection = new MySqlConnection(csb.ConnectionString);
+ await connection.OpenAsync();
+ }
#endif
#endif