Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
3ea4d86
Changing Permit redirection
rusher Jul 20, 2024
4e58e42
Allow 'localhost' as a server address.
bgrainger Jul 21, 2024
e7b1fc7
permit skipping redirection test
rusher Jul 23, 2024
89beadf
Using TLS without configuration
rusher Jul 26, 2024
098bd5b
Merge master into ssl.
bgrainger Jul 28, 2024
d429b35
Delete setup step that should be done outside of tests.
bgrainger Jul 28, 2024
45dddae
Delete unnecessary attribute constructor.
bgrainger Jul 28, 2024
aff3418
Make Ed25519AuthenticationPlugin.Install threadsafe.
bgrainger Jul 28, 2024
ec7f8e0
Use existing test accounts instead of creating new ones.
bgrainger Jul 28, 2024
c354752
Fix MySql.Data build.
bgrainger Jul 28, 2024
003934c
Delete SessionConnectionString property.
bgrainger Jul 28, 2024
71e680a
Revert word-wrapping.
bgrainger Jul 28, 2024
43ee64d
Allow SSL tests to pass via fingerprint validation.
bgrainger Jul 28, 2024
4a99d6a
Add IAuthenticationMethod2 interface to avoid breaking change.
bgrainger Jul 28, 2024
03d179e
Defer copy of challenge until it's needed.
bgrainger Jul 28, 2024
2aea924
Change parameter name for clarity and update documentation.
bgrainger Jul 28, 2024
b4d8baf
Eliminate allocations when computing fingerprint hash.
bgrainger Jul 28, 2024
b58af51
Optimise thumbprint creation under .NET 7.
bgrainger Jul 28, 2024
34524ef
Restore AuthenticationException as inner exception.
bgrainger Jul 28, 2024
abee0c7
Add logging for provisional TLS connection and failure reasons.
bgrainger Jul 28, 2024
7c2964f
Minor code cleanup.
bgrainger Jul 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .ci/config/config.compression+ssl.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"SocketPath": "./../../../../.ci/run/mysql/mysqld.sock",
"PasswordlessUser": "no_password",
"SecondaryDatabase": "testdb2",
"UnsupportedFeatures": "CachingSha2Password,Redirection,RsaEncryption,Tls12,Tls13,UuidToBin",
"UnsupportedFeatures": "CachingSha2Password,Redirection,RsaEncryption,Tls12,Tls13,TlsFingerprintValidation,UuidToBin",
"MySqlBulkLoaderLocalCsvFile": "../../../TestData/LoadData_UTF8_BOM_Unix.CSV",
"MySqlBulkLoaderLocalTsvFile": "../../../TestData/LoadData_UTF8_BOM_Unix.TSV",
"CertificatesPath": "../../../../.ci/server/certs"
Expand Down
2 changes: 1 addition & 1 deletion .ci/config/config.compression.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"SocketPath": "./../../../../.ci/run/mysql/mysqld.sock",
"PasswordlessUser": "no_password",
"SecondaryDatabase": "testdb2",
"UnsupportedFeatures": "Ed25519,QueryAttributes,Redirection,StreamingResults,Tls11,UnixDomainSocket,ZeroDateTime",
"UnsupportedFeatures": "Ed25519,QueryAttributes,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,UnixDomainSocket,ZeroDateTime",
"MySqlBulkLoaderLocalCsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.CSV",
"MySqlBulkLoaderLocalTsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.TSV"
}
Expand Down
2 changes: 1 addition & 1 deletion .ci/config/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"SocketPath": "./../../../../.ci/run/mysql/mysqld.sock",
"PasswordlessUser": "no_password",
"SecondaryDatabase": "testdb2",
"UnsupportedFeatures": "Ed25519,QueryAttributes,Redirection,StreamingResults,Tls11,UnixDomainSocket,ZeroDateTime",
"UnsupportedFeatures": "Ed25519,QueryAttributes,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,UnixDomainSocket,ZeroDateTime",
"MySqlBulkLoaderLocalCsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.CSV",
"MySqlBulkLoaderLocalTsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.TSV"
}
Expand Down
2 changes: 1 addition & 1 deletion .ci/config/config.ssl.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"SocketPath": "./../../../../.ci/run/mysql/mysqld.sock",
"PasswordlessUser": "no_password",
"SecondaryDatabase": "testdb2",
"UnsupportedFeatures": "CachingSha2Password,Redirection,RsaEncryption,Tls12,Tls13,UuidToBin",
"UnsupportedFeatures": "CachingSha2Password,Redirection,RsaEncryption,Tls12,Tls13,TlsFingerprintValidation,UuidToBin",
"MySqlBulkLoaderLocalCsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.CSV",
"MySqlBulkLoaderLocalTsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.TSV",
"CertificatesPath": "../../../../.ci/server/certs"
Expand Down
16 changes: 8 additions & 8 deletions azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ jobs:
arguments: 'tests\IntegrationTests\IntegrationTests.csproj -c MySqlData'
testRunTitle: 'MySql.Data integration tests'
env:
DATA__UNSUPPORTEDFEATURES: 'Ed25519,QueryAttributes,StreamingResults,UnixDomainSocket'
DATA__UNSUPPORTEDFEATURES: 'Ed25519,QueryAttributes,StreamingResults,TlsFingerprintValidation,UnixDomainSocket'
DATA__CONNECTIONSTRING: 'server=localhost;port=3306;user id=root;password=test;database=mysqltest;ssl mode=none;DefaultCommandTimeout=3600'
DATA__CERTIFICATESPATH: '$(Build.Repository.LocalPath)\.ci\server\certs\'
DATA__MYSQLBULKLOADERLOCALCSVFILE: '$(Build.Repository.LocalPath)\tests\TestData\LoadData_UTF8_BOM_Unix.CSV'
Expand Down Expand Up @@ -136,7 +136,7 @@ jobs:
arguments: '-c Release --no-restore'
testRunTitle: ${{ format('{0}, $(Agent.OS), {1}, {2}', 'mysql:8.0', 'net472/net8.0', 'No SSL') }}
env:
DATA__UNSUPPORTEDFEATURES: 'Ed25519,QueryAttributes,Redirection,StreamingResults,Tls11,UnixDomainSocket'
DATA__UNSUPPORTEDFEATURES: 'Ed25519,QueryAttributes,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,UnixDomainSocket'
DATA__CONNECTIONSTRING: 'server=localhost;port=3306;user id=mysqltest;password=test;database=mysqltest;ssl mode=none;DefaultCommandTimeout=3600;AllowPublicKeyRetrieval=True;UseCompression=True'

- job: windows_integration_tests_2
Expand Down Expand Up @@ -174,7 +174,7 @@ jobs:
arguments: '-c Release --no-restore'
testRunTitle: ${{ format('{0}, $(Agent.OS), {1}, {2}', 'mysql:8.0', 'net6.0', 'No SSL') }}
env:
DATA__UNSUPPORTEDFEATURES: 'Ed25519,QueryAttributes,Redirection,StreamingResults,Tls11,UnixDomainSocket'
DATA__UNSUPPORTEDFEATURES: 'Ed25519,QueryAttributes,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,UnixDomainSocket'
DATA__CONNECTIONSTRING: 'server=localhost;port=3306;user id=mysqltest;password=test;database=mysqltest;ssl mode=none;DefaultCommandTimeout=3600;AllowPublicKeyRetrieval=True'

- job: linux_integration_tests
Expand All @@ -187,23 +187,23 @@ jobs:
'MySQL 8.0':
image: 'mysql:8.0'
connectionStringExtra: 'AllowPublicKeyRetrieval=True'
unsupportedFeatures: 'Ed25519,Redirection,StreamingResults,Tls11,ZeroDateTime'
unsupportedFeatures: 'Ed25519,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,ZeroDateTime'
'MySQL 8.4':
image: 'mysql:8.4'
connectionStringExtra: 'AllowPublicKeyRetrieval=True'
unsupportedFeatures: 'Ed25519,Redirection,StreamingResults,Tls11,ZeroDateTime'
unsupportedFeatures: 'Ed25519,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,ZeroDateTime'
'MySQL 9.0':
image: 'mysql:9.0'
connectionStringExtra: 'AllowPublicKeyRetrieval=True'
unsupportedFeatures: 'Ed25519,Redirection,StreamingResults,Tls11,ZeroDateTime'
unsupportedFeatures: 'Ed25519,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,ZeroDateTime'
'MariaDB 10.6':
image: 'mariadb:10.6'
connectionStringExtra: ''
unsupportedFeatures: 'CachingSha2Password,CancelSleepSuccessfully,Json,RoundDateTime,QueryAttributes,Sha256Password,Tls11,UuidToBin,Redirection'
unsupportedFeatures: 'CachingSha2Password,CancelSleepSuccessfully,Json,RoundDateTime,QueryAttributes,Redirection,Sha256Password,Tls11,TlsFingerprintValidation,UuidToBin'
'MariaDB 10.11':
image: 'mariadb:10.11'
connectionStringExtra: ''
unsupportedFeatures: 'CachingSha2Password,CancelSleepSuccessfully,Json,RoundDateTime,QueryAttributes,Sha256Password,Tls11,UuidToBin,Redirection'
unsupportedFeatures: 'CachingSha2Password,CancelSleepSuccessfully,Json,RoundDateTime,QueryAttributes,Redirection,Sha256Password,Tls11,TlsFingerprintValidation,UuidToBin'
'MariaDB 11.4':
image: 'mariadb:11.4'
connectionStringExtra: ''
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using Chaos.NaCl.Internal.Ed25519Ref10;

namespace MySqlConnector.Authentication.Ed25519;
Expand All @@ -9,19 +10,16 @@ namespace MySqlConnector.Authentication.Ed25519;
/// Provides an implementation of the <c>client_ed25519</c> authentication plugin for MariaDB.
/// </summary>
/// <remarks>See <a href="https://mariadb.com/kb/en/library/authentication-plugin-ed25519/">Authentication Plugin - ed25519</a>.</remarks>
public sealed class Ed25519AuthenticationPlugin : IAuthenticationPlugin
public sealed class Ed25519AuthenticationPlugin : IAuthenticationPlugin2
{
/// <summary>
/// Registers the Ed25519 authentication plugin with MySqlConnector. You must call this method once before
/// opening a connection that uses Ed25519 authentication.
/// </summary>
public static void Install()
{
if (!s_isInstalled)
{
if (Interlocked.CompareExchange(ref s_isInstalled, 1, 0) == 0)
AuthenticationPlugins.Register(new Ed25519AuthenticationPlugin());
s_isInstalled = true;
}
}

/// <summary>
Expand All @@ -33,6 +31,24 @@ public static void Install()
/// Creates the authentication response.
/// </summary>
public byte[] CreateResponse(string password, ReadOnlySpan<byte> authenticationData)
{
CreateResponseAndHash(password, authenticationData, out _, out var authenticationResponse);
return authenticationResponse;
}

/// <summary>
/// Creates the Ed25519 password hash.
/// </summary>
public byte[] CreatePasswordHash(string password, ReadOnlySpan<byte> authenticationData)
{
CreateResponseAndHash(password, authenticationData, out var passwordHash, out _);
return passwordHash;
}

/// <summary>
/// Creates the authentication response.
/// </summary>
private static void CreateResponseAndHash(string password, ReadOnlySpan<byte> authenticationData, out byte[] passwordHash, out byte[] authenticationResponse)
{
// 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
Expand Down Expand Up @@ -111,6 +127,9 @@ public byte[] CreateResponse(string password, ReadOnlySpan<byte> authenticationD
GroupOperations.ge_scalarmult_base(out var A, az, 0);
GroupOperations.ge_p3_tobytes(sm, 32, ref A);

passwordHash = new byte[32];
Array.Copy(sm, 32, passwordHash, 0, 32);

/*** Java
nonce = scalar.reduce(nonce);
GroupElement elementRvalue = spec.getB().scalarMultiply(nonce);
Expand Down Expand Up @@ -154,12 +173,12 @@ public byte[] CreateResponse(string password, ReadOnlySpan<byte> authenticationD

var result = new byte[64];
Buffer.BlockCopy(sm, 0, result, 0, result.Length);
return result;
authenticationResponse = result;
}

private Ed25519AuthenticationPlugin()
{
}

private static bool s_isInstalled;
private static int s_isInstalled;
}
16 changes: 16 additions & 0 deletions src/MySqlConnector/Authentication/IAuthenticationPlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,19 @@ public interface IAuthenticationPlugin
/// <returns>The authentication response.</returns>
byte[] CreateResponse(string password, ReadOnlySpan<byte> authenticationData);
}

/// <summary>
/// <see cref="IAuthenticationPlugin2"/> is an extension to <see cref="IAuthenticationPlugin"/> that returns a hash of the client's password.
/// </summary>
public interface IAuthenticationPlugin2 : IAuthenticationPlugin
{
/// <summary>
/// Hashes the client's password (e.g., for TLS certificate fingerprint verification).
/// </summary>
/// <param name="password">The client's password.</param>
/// <param name="authenticationData">The authentication data supplied by the server; this is the <code>auth method data</code>
/// from the <a href="https://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::AuthSwitchRequest">Authentication
/// Method Switch Request Packet</a>.</param>
/// <returns>The authentication-method-specific hash of the client's password.</returns>
byte[] CreatePasswordHash(string password, ReadOnlySpan<byte> authenticationData);
}
123 changes: 123 additions & 0 deletions src/MySqlConnector/Core/ServerSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,43 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella
}

var ok = OkPayload.Create(payload.Span, this);
if (m_rcbPolicyErrors != SslPolicyErrors.None)
{
// SSL would normally have thrown error, so connector need to ensure server certificates
// pass only if :
// * connection method is MitM-proof (e.g. unix socket)
// * auth plugin is MitM-proof and check SHA2(user's hashed password, scramble, certificate fingerprint)
if (cs.ConnectionProtocol != MySqlConnectionProtocol.UnixSocket)
{
if (string.IsNullOrEmpty(password) || !ValidateFingerprint(ok.StatusInfo, initialHandshake.AuthPluginData.AsSpan(0, 20), password!))
{
// fingerprint validation fail.
// now throwing SSL exception depending on m_rcbPolicyErrors
ShutdownSocket();
HostName = "";
lock (m_lock) m_state = State.Failed;
MySqlException ex;
switch (m_rcbPolicyErrors)
{
case SslPolicyErrors.RemoteCertificateNotAvailable:
// impossible
ex = new MySqlException(MySqlErrorCode.UnableToConnectToHost, "SSL not validated, no remote certificate available");
break;

case SslPolicyErrors.RemoteCertificateNameMismatch:
ex = new MySqlException(MySqlErrorCode.UnableToConnectToHost, "SSL not validated, certificate name mismatch");
break;

default:
ex = new MySqlException(MySqlErrorCode.UnableToConnectToHost, "SSL not validated, certificate chain validation fail");
break;
}
Log.CouldNotInitializeTlsConnection(m_logger, ex, Id);
throw ex;
}
}
}

var redirectionUrl = ok.RedirectionUrl;

if (m_useCompression)
Expand Down Expand Up @@ -567,6 +604,73 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella
}
}

/// <summary>
/// Validate SSL validation has
/// </summary>
/// <param name="validationHash">received validation hash</param>
/// <param name="challenge">initial seed</param>
/// <param name="password">password</param>
/// <returns>true if validated</returns>
private bool ValidateFingerprint(byte[]? validationHash, ReadOnlySpan<byte> challenge, string password)
{
if (validationHash?.Length != 65)
return false;

// ensure using SHA256 encryption
if (validationHash[0] != 0x01)
throw new FormatException($"Unexpected validation hash format. expected 0x01 but got 0x{validationHash[0]:X2}");

byte[]? passwordHashResult = null;
switch (m_pluginName)
{
case "mysql_native_password":
passwordHashResult = AuthenticationUtility.HashPassword([], password, onlyHashPassword: true);
break;

case "client_ed25519":
AuthenticationPlugins.TryGetPlugin(m_pluginName, out var ed25519Plugin);
if (ed25519Plugin is IAuthenticationPlugin2 plugin2)
passwordHashResult = plugin2.CreatePasswordHash(password, challenge);
break;
}
if (passwordHashResult is null)
return false;

Span<byte> combined = stackalloc byte[32 + challenge.Length + passwordHashResult.Length];
passwordHashResult.CopyTo(combined);
challenge.CopyTo(combined[passwordHashResult.Length..]);
m_sha2Thumbprint!.CopyTo(combined[(passwordHashResult.Length + challenge.Length)..]);

Span<byte> hashBytes = stackalloc byte[32];
#if NET5_0_OR_GREATER
SHA256.TryHashData(combined, hashBytes, out _);
#else
using var sha256 = SHA256.Create();
sha256.TryComputeHash(combined, hashBytes, out _);
#endif

Span<byte> serverHash = combined[0..32];
return TryConvertFromHexString(validationHash.AsSpan(1), serverHash) && serverHash.SequenceEqual(hashBytes);

static bool TryConvertFromHexString(ReadOnlySpan<byte> hexChars, Span<byte> data)
{
ReadOnlySpan<byte> hexDigits = "0123456789ABCDEFabcdef"u8;
for (var i = 0; i < hexChars.Length; i += 2)
{
var high = hexDigits.IndexOf(hexChars[i]);
var low = hexDigits.IndexOf(hexChars[i + 1]);
if (high == -1 || low == -1)
return false;
if (high > 15)
high -= 6;
if (low > 15)
low -= 6;
data[i / 2] = (byte) ((high << 4) | low);
}
return true;
}
}

public static async ValueTask<ServerSession> ConnectAndRedirectAsync(ILogger connectionLogger, ILogger poolLogger, IConnectionPoolMetadata pool, ConnectionSettings cs, ILoadBalancer? loadBalancer, MySqlConnection connection, Action<ILogger, int, string, Exception?>? logMessage, long startingTimestamp, Activity? activity, IOBehavior ioBehavior, CancellationToken cancellationToken)
{
var session = new ServerSession(connectionLogger, pool);
Expand Down Expand Up @@ -729,6 +833,7 @@ private async Task<PayloadData> 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_pluginName = switchRequest.Name;
switch (switchRequest.Name)
{
case "mysql_native_password":
Expand Down Expand Up @@ -1485,6 +1590,21 @@ caCertificateChain is not null &&
if (cs.SslMode == MySqlSslMode.VerifyCA)
rcbPolicyErrors &= ~SslPolicyErrors.RemoteCertificateNameMismatch;

if (rcbCertificate is X509Certificate2 cert2)
{
// saving sha256 thumbprint and SSL errors until thumbprint validation
#if !NET5_0_OR_GREATER
using (var sha256 = SHA256.Create())
{
m_sha2Thumbprint = sha256.ComputeHash(cert2.RawData);
}
#else
m_sha2Thumbprint = SHA256.HashData(cert2.RawData);
#endif
m_rcbPolicyErrors = rcbPolicyErrors;
return true;
}

return rcbPolicyErrors == SslPolicyErrors.None;
}

Expand Down Expand Up @@ -2006,4 +2126,7 @@ protected override void OnStatementBegin(int index)
private PayloadData m_setNamesPayload;
private byte[]? m_pipelinedResetConnectionBytes;
private Dictionary<string, PreparedStatements>? m_preparedStatements;
private string m_pluginName = "mysql_native_password";
private byte[]? m_sha2Thumbprint;
private SslPolicyErrors m_rcbPolicyErrors;
}
6 changes: 3 additions & 3 deletions src/MySqlConnector/Protocol/Payloads/OkPayload.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ internal sealed class OkPayload
public ulong LastInsertId { get; }
public ServerStatus ServerStatus { get; }
public int WarningCount { get; }
public string? StatusInfo { get; }
public byte[]? StatusInfo { get; }
public string? NewSchema { get; }
public CharacterSet? NewCharacterSet { get; }
public int? NewConnectionId { get; }
Expand Down Expand Up @@ -152,7 +152,7 @@ public static void Verify(ReadOnlySpan<byte> span, IServerCapabilities serverCap

if (createPayload)
{
var statusInfo = statusBytes.Length == 0 ? null : Encoding.UTF8.GetString(statusBytes);
var statusInfo = statusBytes.Length == 0 ? null : statusBytes.ToArray();

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

private OkPayload(ulong affectedRowCount, ulong lastInsertId, ServerStatus serverStatus, int warningCount, string? statusInfo, string? newSchema, CharacterSet newCharacterSet, int? connectionId, string? redirectionUrl)
private OkPayload(ulong affectedRowCount, ulong lastInsertId, ServerStatus serverStatus, int warningCount, byte[]? statusInfo, string? newSchema, CharacterSet newCharacterSet, int? connectionId, string? redirectionUrl)
{
AffectedRowCount = affectedRowCount;
LastInsertId = lastInsertId;
Expand Down
Loading