@@ -438,13 +438,13 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella
438438 var initialHandshake = InitialHandshakePayload . Create ( payload . Span ) ;
439439
440440 // 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 ! :
442442 ( initialHandshake . ProtocolCapabilities & ProtocolCapabilities . SecureConnection ) == 0 ? "mysql_old_password" :
443443 "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" )
446446 {
447- Log . UnsupportedAuthenticationMethod ( m_logger , Id , authPluginName ) ;
447+ Log . UnsupportedAuthenticationMethod ( m_logger , Id , m_currentAuthenticationMethod ) ;
448448 throw new NotSupportedException ( $ "Authentication method '{ initialHandshake . AuthPluginName } ' is not supported.") ;
449449 }
450450
@@ -528,6 +528,46 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella
528528 }
529529
530530 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+
531571 var redirectionUrl = ok . RedirectionUrl ;
532572
533573 if ( m_useCompression )
@@ -567,6 +607,70 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella
567607 }
568608 }
569609
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+
570674 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 )
571675 {
572676 var session = new ServerSession ( connectionLogger , pool ) ;
@@ -729,6 +833,7 @@ private async Task<PayloadData> SwitchAuthenticationAsync(ConnectionSettings cs,
729833 // if the server didn't support the hashed password; rehash with the new challenge
730834 var switchRequest = AuthenticationMethodSwitchRequestPayload . Create ( payload . Span ) ;
731835 Log . SwitchingToAuthenticationMethod ( m_logger , Id , switchRequest . Name ) ;
836+ m_currentAuthenticationMethod = switchRequest . Name ;
732837 switch ( switchRequest . Name )
733838 {
734839 case "mysql_native_password" :
@@ -1485,6 +1590,21 @@ caCertificateChain is not null &&
14851590 if ( cs . SslMode == MySqlSslMode . VerifyCA )
14861591 rcbPolicyErrors &= ~ SslPolicyErrors . RemoteCertificateNameMismatch ;
14871592
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+
14881608 return rcbPolicyErrors == SslPolicyErrors . None ;
14891609 }
14901610
@@ -1558,11 +1678,22 @@ await sslStream.AuthenticateAsClientAsync(clientAuthenticationOptions.TargetHost
15581678 m_payloadHandler ! . ByteHandler = sslByteHandler ;
15591679 m_isSecureConnection = true ;
15601680 m_sslStream = sslStream ;
1681+ if ( m_sslPolicyErrors != SslPolicyErrors . None )
1682+ {
15611683#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 ) ;
15631685#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 ) ;
15651687#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+ }
15661697 }
15671698 catch ( Exception ex )
15681699 {
@@ -2006,4 +2137,7 @@ protected override void OnStatementBegin(int index)
20062137 private PayloadData m_setNamesPayload ;
20072138 private byte [ ] ? m_pipelinedResetConnectionBytes ;
20082139 private Dictionary < string , PreparedStatements > ? m_preparedStatements ;
2140+ private string ? m_currentAuthenticationMethod ;
2141+ private byte [ ] ? m_remoteCertificateSha2Thumbprint ;
2142+ private SslPolicyErrors m_sslPolicyErrors ;
20092143}
0 commit comments