diff --git a/libs/server/TLS/GarnetTlsOptions.cs b/libs/server/TLS/GarnetTlsOptions.cs index 403a3afa7..8eda2e8a8 100644 --- a/libs/server/TLS/GarnetTlsOptions.cs +++ b/libs/server/TLS/GarnetTlsOptions.cs @@ -138,7 +138,7 @@ SslServerAuthenticationOptions GetSslServerAuthenticationOptions() { ClientCertificateRequired = ClientCertificateRequired, CertificateRevocationCheckMode = CertificateRevocationCheckMode, - RemoteCertificateValidationCallback = ValidateCertificateCallback(IssuerCertificatePath), + RemoteCertificateValidationCallback = ValidateClientCertificateCallback(IssuerCertificatePath), ServerCertificateSelectionCallback = (sender, hostName) => { return serverCertificateSelector.GetSslServerCertificate(); @@ -152,7 +152,7 @@ SslClientAuthenticationOptions GetSslClientAuthenticationOptions() { TargetHost = ClusterTlsClientTargetHost, AllowRenegotiation = false, - RemoteCertificateValidationCallback = ValidateCertificateCallback(IssuerCertificatePath), + RemoteCertificateValidationCallback = ValidateServerCertificateCallback(ClusterTlsClientTargetHost, IssuerCertificatePath), // We use the same server certificate selector for the server's own client as well LocalCertificateSelectionCallback = (object sender, string targetHost, X509CertificateCollection localCertificates, X509Certificate remoteCertificate, string[] acceptableIssuers) => { @@ -161,14 +161,45 @@ SslClientAuthenticationOptions GetSslClientAuthenticationOptions() }; } + /// /// Callback to verify the TLS certificate /// - /// + /// The path to issuer certificate file. /// - /// - /// - RemoteCertificateValidationCallback ValidateCertificateCallback(string issuerCertificatePath) + RemoteCertificateValidationCallback ValidateServerCertificateCallback(string targetHostName, string issuerCertificatePath) + { + var issuer = GetCertificateIssuer(issuerCertificatePath); + return (object _, X509Certificate certificate, X509Chain __, SslPolicyErrors sslPolicyErrors) + => (sslPolicyErrors == SslPolicyErrors.None) || (sslPolicyErrors == SslPolicyErrors.RemoteCertificateChainErrors + && certificate is X509Certificate2 certificate2 + && ValidateCertificateName(certificate2, targetHostName) + && ValidateCertificateIssuer(certificate2, issuer)); + } + + /// + /// Validates certificate subject name by looking into DNS name property (preferred), if missing it falls back to + /// legacy SimpleName. The input certificate subject should match the expected host name provided in server config. + /// + /// The remote certificate to validate. + /// The expected target host name. + private bool ValidateCertificateName(X509Certificate2 certificate2, string targetHostName) + { + var subjectName = certificate2.GetNameInfo(X509NameType.DnsName, false); + if (string.IsNullOrWhiteSpace(subjectName)) + { + subjectName = certificate2.GetNameInfo(X509NameType.SimpleName, false); + } + + return subjectName.Equals(targetHostName, StringComparison.InvariantCultureIgnoreCase); + } + + /// + /// Callback to verify the TLS certificate + /// + /// The path to issuer certificate file. + /// The RemoteCertificateValidationCallback delegate to invoke. + RemoteCertificateValidationCallback ValidateClientCertificateCallback(string issuerCertificatePath) { if (!ClientCertificateRequired) { @@ -176,6 +207,19 @@ RemoteCertificateValidationCallback ValidateCertificateCallback(string issuerCer return (object _, X509Certificate certificate, X509Chain __, SslPolicyErrors sslPolicyErrors) => true; } + var issuer = GetCertificateIssuer(issuerCertificatePath); + return (object _, X509Certificate certificate, X509Chain __, SslPolicyErrors sslPolicyErrors) + => (sslPolicyErrors == SslPolicyErrors.None) || (sslPolicyErrors == SslPolicyErrors.RemoteCertificateChainErrors + && certificate is X509Certificate2 certificate2 + && ValidateCertificateIssuer(certificate2, issuer)); + } + + /// + /// Loads an issuer X.509 certificate using its file name. + /// + /// The path to issuer certificate file. + X509Certificate2 GetCertificateIssuer(string issuerCertificatePath) + { X509Certificate2 issuer = null; if (!string.IsNullOrEmpty(issuerCertificatePath)) { @@ -192,10 +236,7 @@ RemoteCertificateValidationCallback ValidateCertificateCallback(string issuerCer else logger?.LogWarning("ClientCertificateRequired is true and IssuerCertificatePath is not provided. The remote certificate chain will not be validated against issuer."); - return (object _, X509Certificate certificate, X509Chain __, SslPolicyErrors sslPolicyErrors) - => (sslPolicyErrors == SslPolicyErrors.None) || (sslPolicyErrors == SslPolicyErrors.RemoteCertificateChainErrors - && certificate is X509Certificate2 certificate2 - && ValidateCertificateIssuer(certificate2, issuer)); + return issuer; } /// @@ -203,9 +244,9 @@ RemoteCertificateValidationCallback ValidateCertificateCallback(string issuerCer /// https://stackoverflow.com/questions/6497040/how-do-i-validate-that-a-certificate-was-created-by-a-particular-certification-a /// Make sure to validate for your requirements before using in production. /// - /// - /// - /// + /// X509Certificate2 certificate to be validated. + /// X509Certificate2 representing the root cert. + /// A boolean indicating whether the certificate has a valid issuer. bool ValidateCertificateIssuer(X509Certificate2 certificateToValidate, X509Certificate2 authority) { using X509Chain chain = new(); @@ -228,7 +269,7 @@ bool ValidateCertificateIssuer(X509Certificate2 certificateToValidate, X509Certi string certificateErrorsString = "Unknown errors."; if (errors != null && errors.Length > 0) certificateErrorsString = String.Join(", ", errors); - throw new Exception("Trust chain did not complete to the known authority anchor. Errors: " + certificateErrorsString); + throw new GarnetException("Trust chain did not complete to the known authority anchor. Errors: " + certificateErrorsString); } if (authority != null) @@ -239,11 +280,11 @@ bool ValidateCertificateIssuer(X509Certificate2 certificateToValidate, X509Certi .Any(x => x.Certificate.Thumbprint == authority.Thumbprint); if (!valid) - throw new Exception("Trust chain did not complete to the known authority anchor. Thumbprints did not match."); + throw new GarnetException("Trust chain did not complete to the known authority anchor. Thumbprints did not match."); } return true; } - catch (Exception ex) + catch (GarnetException ex) { logger?.LogError(ex, "Error validating certificate issuer"); return false;