Skip to content

Commit

Permalink
Add stronger TLS check for Gossip in Cluster mode (#392)
Browse files Browse the repository at this point in the history
* Add TLS cert validation when Garnet runs as client and does gossip

* formatting fixes

* add comment

* address renaming concerns

* fix whitespace and exception types

* fix comment casing

* added

---------

Co-authored-by: Lukas Maas <lumaas@microsoft.com>
  • Loading branch information
msft-paddy14 and lmaas committed May 23, 2024
1 parent dcac12e commit 9822a43
Showing 1 changed file with 57 additions and 16 deletions.
73 changes: 57 additions & 16 deletions libs/server/TLS/GarnetTlsOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ SslServerAuthenticationOptions GetSslServerAuthenticationOptions()
{
ClientCertificateRequired = ClientCertificateRequired,
CertificateRevocationCheckMode = CertificateRevocationCheckMode,
RemoteCertificateValidationCallback = ValidateCertificateCallback(IssuerCertificatePath),
RemoteCertificateValidationCallback = ValidateClientCertificateCallback(IssuerCertificatePath),
ServerCertificateSelectionCallback = (sender, hostName) =>
{
return serverCertificateSelector.GetSslServerCertificate();
Expand All @@ -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) =>
{
Expand All @@ -161,21 +161,65 @@ SslClientAuthenticationOptions GetSslClientAuthenticationOptions()
};
}


/// <summary>
/// Callback to verify the TLS certificate
/// </summary>
/// <param name="issuerCertificatePath"></param>
/// <param name="issuerCertificatePath">The path to issuer certificate file. </param>
/// <returns></returns>
/// <exception cref="ArgumentException"></exception>
/// <exception cref="ArgumentNullException"></exception>
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));
}

/// <summary>
/// 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.
/// </summary>
/// <param name="certificate2">The remote certificate to validate.</param>
/// <param name="targetHostName">The expected target host name. </param>
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);
}

/// <summary>
/// Callback to verify the TLS certificate
/// </summary>
/// <param name="issuerCertificatePath">The path to issuer certificate file.</param>
/// <returns>The RemoteCertificateValidationCallback delegate to invoke.</returns>
RemoteCertificateValidationCallback ValidateClientCertificateCallback(string issuerCertificatePath)
{
if (!ClientCertificateRequired)
{
logger?.LogWarning("ClientCertificateRequired is false. Remote certificate validation will always succeed.");
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));
}

/// <summary>
/// Loads an issuer X.509 certificate using its file name.
/// </summary>
/// <param name="issuerCertificatePath">The path to issuer certificate file.</param>
X509Certificate2 GetCertificateIssuer(string issuerCertificatePath)
{
X509Certificate2 issuer = null;
if (!string.IsNullOrEmpty(issuerCertificatePath))
{
Expand All @@ -192,20 +236,17 @@ 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;
}

/// <summary>
/// Check the chain certificate and determine if the certificate is valid. NOTE: This is prototype code based on
/// 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.
/// </summary>
/// <param name="certificateToValidate"></param>
/// <param name="authority"></param>
/// <returns></returns>
/// <param name="certificateToValidate">X509Certificate2 certificate to be validated.</param>
/// <param name="authority">X509Certificate2 representing the root cert.</param>
/// <returns>A boolean indicating whether the certificate has a valid issuer.</returns>
bool ValidateCertificateIssuer(X509Certificate2 certificateToValidate, X509Certificate2 authority)
{
using X509Chain chain = new();
Expand All @@ -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)
Expand All @@ -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;
Expand Down

0 comments on commit 9822a43

Please sign in to comment.