@@ -438,13 +438,13 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella
438
438
var initialHandshake = InitialHandshakePayload . Create ( payload . Span ) ;
439
439
440
440
// if PluginAuth is supported, then use the specified auth plugin; else, fall back to protocol capabilities to determine the auth type to use
441
- var authPluginName = ( initialHandshake . ProtocolCapabilities & ProtocolCapabilities . PluginAuth ) != 0 ? initialHandshake . AuthPluginName ! :
441
+ m_currentAuthenticationMethod = ( initialHandshake . ProtocolCapabilities & ProtocolCapabilities . PluginAuth ) != 0 ? initialHandshake . AuthPluginName ! :
442
442
( initialHandshake . ProtocolCapabilities & ProtocolCapabilities . SecureConnection ) == 0 ? "mysql_old_password" :
443
443
"mysql_native_password" ;
444
- Log . ServerSentAuthPluginName ( m_logger , Id , authPluginName ) ;
445
- if ( authPluginName is not "mysql_native_password" and not "sha256_password" and not "caching_sha2_password" )
444
+ Log . ServerSentAuthPluginName ( m_logger , Id , m_currentAuthenticationMethod ) ;
445
+ if ( m_currentAuthenticationMethod is not "mysql_native_password" and not "sha256_password" and not "caching_sha2_password" )
446
446
{
447
- Log . UnsupportedAuthenticationMethod ( m_logger , Id , authPluginName ) ;
447
+ Log . UnsupportedAuthenticationMethod ( m_logger , Id , m_currentAuthenticationMethod ) ;
448
448
throw new NotSupportedException ( $ "Authentication method '{ initialHandshake . AuthPluginName } ' is not supported.") ;
449
449
}
450
450
@@ -528,6 +528,46 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella
528
528
}
529
529
530
530
var ok = OkPayload . Create ( payload . Span , this ) ;
531
+
532
+ if ( m_sslPolicyErrors != SslPolicyErrors . None )
533
+ {
534
+ // SSL would normally have thrown error, but this was suppressed in ValidateRemoteCertificate; now we need to verify the server certificate
535
+ // pass only if :
536
+ // * connection method is MitM-proof (e.g. unix socket)
537
+ // * auth plugin is MitM-proof and check SHA2(user's hashed password, scramble, certificate fingerprint)
538
+ // see https://mariadb.org/mission-impossible-zero-configuration-ssl/
539
+ var ignoreCertificateError = false ;
540
+
541
+ if ( cs . ConnectionProtocol == MySqlConnectionProtocol . UnixSocket )
542
+ {
543
+ Log . CertificateErrorUnixSocket ( m_logger , Id , m_sslPolicyErrors ) ;
544
+ ignoreCertificateError = true ;
545
+ }
546
+ else if ( string . IsNullOrEmpty ( password ) )
547
+ {
548
+ // there is no shared secret that can be used to validate the certificate
549
+ Log . CertificateErrorNoPassword ( m_logger , Id , m_sslPolicyErrors ) ;
550
+ }
551
+ else if ( ValidateFingerprint ( ok . StatusInfo , initialHandshake . AuthPluginData . AsSpan ( 0 , 20 ) , password ) )
552
+ {
553
+ Log . CertificateErrorValidThumbprint ( m_logger , Id , m_sslPolicyErrors ) ;
554
+ ignoreCertificateError = true ;
555
+ }
556
+
557
+ if ( ! ignoreCertificateError )
558
+ {
559
+ ShutdownSocket ( ) ;
560
+ HostName = "" ;
561
+ lock ( m_lock )
562
+ m_state = State . Failed ;
563
+
564
+ // throw a MySqlException with an AuthenticationException InnerException to mimic what would have happened if ValidateRemoteCertificate returned false
565
+ var innerException = new AuthenticationException ( $ "The remote certificate was rejected due to the following error: { m_sslPolicyErrors } ") ;
566
+ Log . CouldNotInitializeTlsConnection ( m_logger , innerException , Id ) ;
567
+ throw new MySqlException ( MySqlErrorCode . UnableToConnectToHost , "SSL Authentication Error" , innerException ) ;
568
+ }
569
+ }
570
+
531
571
var redirectionUrl = ok . RedirectionUrl ;
532
572
533
573
if ( m_useCompression )
@@ -567,6 +607,70 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella
567
607
}
568
608
}
569
609
610
+ /// <summary>
611
+ /// Validate SSL validation hash (from OK packet).
612
+ /// </summary>
613
+ /// <param name="validationHash">The validation hash received from the server.</param>
614
+ /// <param name="challenge">The auth plugin data from the initial handshake.</param>
615
+ /// <param name="password">The user's password.</param>
616
+ /// <returns><c>true</c> if the validation hash matches the locally-computed value; otherwise, <c>false</c>.</returns>
617
+ private bool ValidateFingerprint ( byte [ ] ? validationHash , ReadOnlySpan < byte > challenge , string password )
618
+ {
619
+ // expect 0x01 followed by 64 hex characters giving a SHA2 hash
620
+ if ( validationHash ? . Length != 65 || validationHash [ 0 ] != 1 )
621
+ return false ;
622
+
623
+ byte [ ] ? passwordHashResult = null ;
624
+ switch ( m_currentAuthenticationMethod )
625
+ {
626
+ case "mysql_native_password" :
627
+ passwordHashResult = AuthenticationUtility . HashPassword ( [ ] , password , onlyHashPassword : true ) ;
628
+ break ;
629
+
630
+ case "client_ed25519" :
631
+ AuthenticationPlugins . TryGetPlugin ( m_currentAuthenticationMethod , out var ed25519Plugin ) ;
632
+ if ( ed25519Plugin is IAuthenticationPlugin2 plugin2 )
633
+ passwordHashResult = plugin2 . CreatePasswordHash ( password , challenge ) ;
634
+ break ;
635
+ }
636
+ if ( passwordHashResult is null )
637
+ return false ;
638
+
639
+ Span < byte > combined = stackalloc byte [ 32 + challenge . Length + passwordHashResult . Length ] ;
640
+ passwordHashResult . CopyTo ( combined ) ;
641
+ challenge . CopyTo ( combined [ passwordHashResult . Length ..] ) ;
642
+ m_remoteCertificateSha2Thumbprint ! . CopyTo ( combined [ ( passwordHashResult . Length + challenge . Length ) ..] ) ;
643
+
644
+ Span < byte > hashBytes = stackalloc byte [ 32 ] ;
645
+ #if NET5_0_OR_GREATER
646
+ SHA256 . TryHashData ( combined , hashBytes , out _ ) ;
647
+ #else
648
+ using var sha256 = SHA256 . Create ( ) ;
649
+ sha256 . TryComputeHash ( combined , hashBytes , out _ ) ;
650
+ #endif
651
+
652
+ Span < byte > serverHash = combined [ 0 ..32 ] ;
653
+ return TryConvertFromHexString ( validationHash . AsSpan ( 1 ) , serverHash ) && serverHash . SequenceEqual ( hashBytes ) ;
654
+
655
+ static bool TryConvertFromHexString ( ReadOnlySpan < byte > hexChars , Span < byte > data )
656
+ {
657
+ ReadOnlySpan < byte > hexDigits = "0123456789ABCDEFabcdef"u8 ;
658
+ for ( var i = 0 ; i < hexChars . Length ; i += 2 )
659
+ {
660
+ var high = hexDigits . IndexOf ( hexChars [ i ] ) ;
661
+ var low = hexDigits . IndexOf ( hexChars [ i + 1 ] ) ;
662
+ if ( high == - 1 || low == - 1 )
663
+ return false ;
664
+ if ( high > 15 )
665
+ high -= 6 ;
666
+ if ( low > 15 )
667
+ low -= 6 ;
668
+ data [ i / 2 ] = ( byte ) ( ( high << 4 ) | low ) ;
669
+ }
670
+ return true ;
671
+ }
672
+ }
673
+
570
674
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 )
571
675
{
572
676
var session = new ServerSession ( connectionLogger , pool ) ;
@@ -729,6 +833,7 @@ private async Task<PayloadData> SwitchAuthenticationAsync(ConnectionSettings cs,
729
833
// if the server didn't support the hashed password; rehash with the new challenge
730
834
var switchRequest = AuthenticationMethodSwitchRequestPayload . Create ( payload . Span ) ;
731
835
Log . SwitchingToAuthenticationMethod ( m_logger , Id , switchRequest . Name ) ;
836
+ m_currentAuthenticationMethod = switchRequest . Name ;
732
837
switch ( switchRequest . Name )
733
838
{
734
839
case "mysql_native_password" :
@@ -1485,6 +1590,21 @@ caCertificateChain is not null &&
1485
1590
if ( cs . SslMode == MySqlSslMode . VerifyCA )
1486
1591
rcbPolicyErrors &= ~ SslPolicyErrors . RemoteCertificateNameMismatch ;
1487
1592
1593
+ if ( rcbCertificate is X509Certificate2 cert2 )
1594
+ {
1595
+ // saving sha256 thumbprint and SSL errors until thumbprint validation
1596
+ #if NET7_0_OR_GREATER
1597
+ m_remoteCertificateSha2Thumbprint = SHA256 . HashData ( cert2 . RawDataMemory . Span ) ;
1598
+ #elif NET5_0_OR_GREATER
1599
+ m_remoteCertificateSha2Thumbprint = SHA256 . HashData ( cert2 . RawData ) ;
1600
+ #else
1601
+ using var sha256 = SHA256 . Create ( ) ;
1602
+ m_remoteCertificateSha2Thumbprint = sha256 . ComputeHash ( cert2 . RawData ) ;
1603
+ #endif
1604
+ m_sslPolicyErrors = rcbPolicyErrors ;
1605
+ return true ;
1606
+ }
1607
+
1488
1608
return rcbPolicyErrors == SslPolicyErrors . None ;
1489
1609
}
1490
1610
@@ -1558,11 +1678,22 @@ await sslStream.AuthenticateAsClientAsync(clientAuthenticationOptions.TargetHost
1558
1678
m_payloadHandler ! . ByteHandler = sslByteHandler ;
1559
1679
m_isSecureConnection = true ;
1560
1680
m_sslStream = sslStream ;
1681
+ if ( m_sslPolicyErrors != SslPolicyErrors . None )
1682
+ {
1561
1683
#if NETCOREAPP3_0_OR_GREATER
1562
- Log . ConnectedTlsBasic ( m_logger , Id , sslStream . SslProtocol , sslStream . NegotiatedCipherSuite ) ;
1684
+ Log . ConnectedTlsBasicPreliminary ( m_logger , Id , m_sslPolicyErrors , sslStream . SslProtocol , sslStream . NegotiatedCipherSuite ) ;
1563
1685
#else
1564
- Log . ConnectedTlsDetailed ( m_logger , Id , sslStream . SslProtocol , sslStream . CipherAlgorithm , sslStream . HashAlgorithm , sslStream . KeyExchangeAlgorithm , sslStream . KeyExchangeStrength ) ;
1686
+ Log . ConnectedTlsDetailedPreliminary ( m_logger , Id , m_sslPolicyErrors , sslStream . SslProtocol , sslStream . CipherAlgorithm , sslStream . HashAlgorithm , sslStream . KeyExchangeAlgorithm , sslStream . KeyExchangeStrength ) ;
1565
1687
#endif
1688
+ }
1689
+ else
1690
+ {
1691
+ #if NETCOREAPP3_0_OR_GREATER
1692
+ Log . ConnectedTlsBasic ( m_logger , Id , sslStream . SslProtocol , sslStream . NegotiatedCipherSuite ) ;
1693
+ #else
1694
+ Log . ConnectedTlsDetailed ( m_logger , Id , sslStream . SslProtocol , sslStream . CipherAlgorithm , sslStream . HashAlgorithm , sslStream . KeyExchangeAlgorithm , sslStream . KeyExchangeStrength ) ;
1695
+ #endif
1696
+ }
1566
1697
}
1567
1698
catch ( Exception ex )
1568
1699
{
@@ -2006,4 +2137,7 @@ protected override void OnStatementBegin(int index)
2006
2137
private PayloadData m_setNamesPayload ;
2007
2138
private byte [ ] ? m_pipelinedResetConnectionBytes ;
2008
2139
private Dictionary < string , PreparedStatements > ? m_preparedStatements ;
2140
+ private string ? m_currentAuthenticationMethod ;
2141
+ private byte [ ] ? m_remoteCertificateSha2Thumbprint ;
2142
+ private SslPolicyErrors m_sslPolicyErrors ;
2009
2143
}
0 commit comments