diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index ecb6dda66..4b26d9348 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -9,6 +9,8 @@ Current package versions: ## Unreleased No pending unreleased changes +- Add `ConfigurationOptions.SetUserPemCertificate(...)` and `ConfigurationOptions.SetUserPfxCertificate(...)` methods to simplify using client certificates ([#2873 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2873)) + ## 2.8.31 - Fix: Respect `IReconnectRetryPolicy` timing in the case that a node that was present disconnects indefinitely ([#2853](https://github.com/StackExchange/StackExchange.Redis/pull/2853) & [#2856](https://github.com/StackExchange/StackExchange.Redis/pull/2856) by NickCraver) diff --git a/src/StackExchange.Redis/ConfigurationOptions.cs b/src/StackExchange.Redis/ConfigurationOptions.cs index 199f1a378..dfdab5f4e 100644 --- a/src/StackExchange.Redis/ConfigurationOptions.cs +++ b/src/StackExchange.Redis/ConfigurationOptions.cs @@ -301,6 +301,49 @@ public bool HighIntegrity /// The file system path to find the certificate at. public void TrustIssuer(string issuerCertificatePath) => CertificateValidationCallback = TrustIssuerCallback(issuerCertificatePath); +#if NET5_0_OR_GREATER + /// + /// Supply a user certificate from a PEM file pair and enable TLS. + /// + /// The path for the the user certificate (commonly a .crt file). + /// The path for the the user key (commonly a .key file). + public void SetUserPemCertificate(string userCertificatePath, string? userKeyPath = null) + { + CertificateSelectionCallback = CreatePemUserCertificateCallback(userCertificatePath, userKeyPath); + Ssl = true; + } +#endif + + /// + /// Supply a user certificate from a PFX file and optional password and enable TLS. + /// + /// The path for the the user certificate (commonly a .pfx file). + /// The password for the certificate file. + public void SetUserPfxCertificate(string userCertificatePath, string? password = null) + { + CertificateSelectionCallback = CreatePfxUserCertificateCallback(userCertificatePath, password); + Ssl = true; + } + +#if NET5_0_OR_GREATER + internal static LocalCertificateSelectionCallback CreatePemUserCertificateCallback(string userCertificatePath, string? userKeyPath) + { + // PEM handshakes not universally supported and causes a runtime error about ephemeral certificates; to avoid, export as PFX + using var pem = X509Certificate2.CreateFromPemFile(userCertificatePath, userKeyPath); +#pragma warning disable SYSLIB0057 // Type or member is obsolete + var pfx = new X509Certificate2(pem.Export(X509ContentType.Pfx)); +#pragma warning restore SYSLIB0057 // Type or member is obsolete + + return (sender, targetHost, localCertificates, remoteCertificate, acceptableIssuers) => pfx; + } +#endif + + internal static LocalCertificateSelectionCallback CreatePfxUserCertificateCallback(string userCertificatePath, string? password, X509KeyStorageFlags storageFlags = X509KeyStorageFlags.DefaultKeySet) + { + var pfx = new X509Certificate2(userCertificatePath, password ?? "", storageFlags); + return (sender, targetHost, localCertificates, remoteCertificate, acceptableIssuers) => pfx; + } + /// /// Create a certificate validation check that checks against the supplied issuer even when not known by the machine. /// diff --git a/src/StackExchange.Redis/PhysicalConnection.cs b/src/StackExchange.Redis/PhysicalConnection.cs index bac6ebac0..df30ab207 100644 --- a/src/StackExchange.Redis/PhysicalConnection.cs +++ b/src/StackExchange.Redis/PhysicalConnection.cs @@ -1504,21 +1504,28 @@ public ConnectionStatus GetStatus() { try { - var pfxPath = Environment.GetEnvironmentVariable("SERedis_ClientCertPfxPath"); - var pfxPassword = Environment.GetEnvironmentVariable("SERedis_ClientCertPassword"); - var pfxStorageFlags = Environment.GetEnvironmentVariable("SERedis_ClientCertStorageFlags"); - - X509KeyStorageFlags? flags = null; - if (!string.IsNullOrEmpty(pfxStorageFlags)) + var certificatePath = Environment.GetEnvironmentVariable("SERedis_ClientCertPfxPath"); + if (!string.IsNullOrEmpty(certificatePath) && File.Exists(certificatePath)) { - flags = Enum.Parse(typeof(X509KeyStorageFlags), pfxStorageFlags) as X509KeyStorageFlags?; + var password = Environment.GetEnvironmentVariable("SERedis_ClientCertPassword"); + var pfxStorageFlags = Environment.GetEnvironmentVariable("SERedis_ClientCertStorageFlags"); + X509KeyStorageFlags storageFlags = X509KeyStorageFlags.DefaultKeySet; + if (!string.IsNullOrEmpty(pfxStorageFlags) && Enum.TryParse(pfxStorageFlags, true, out var typedFlags)) + { + storageFlags = typedFlags; + } + + return ConfigurationOptions.CreatePfxUserCertificateCallback(certificatePath, password, storageFlags); } - if (!string.IsNullOrEmpty(pfxPath) && File.Exists(pfxPath)) +#if NET5_0_OR_GREATER + certificatePath = Environment.GetEnvironmentVariable("SERedis_ClientCertPemPath"); + if (!string.IsNullOrEmpty(certificatePath) && File.Exists(certificatePath)) { - return (sender, targetHost, localCertificates, remoteCertificate, acceptableIssuers) => - new X509Certificate2(pfxPath, pfxPassword ?? "", flags ?? X509KeyStorageFlags.DefaultKeySet); + var passwordPath = Environment.GetEnvironmentVariable("SERedis_ClientCertPasswordPath"); + return ConfigurationOptions.CreatePemUserCertificateCallback(certificatePath, passwordPath); } +#endif } catch (Exception ex) { diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index a24333c8e..8263defd3 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -1893,4 +1893,4 @@ virtual StackExchange.Redis.RedisResult.Length.get -> int virtual StackExchange.Redis.RedisResult.this[int index].get -> StackExchange.Redis.RedisResult! StackExchange.Redis.ConnectionMultiplexer.AddLibraryNameSuffix(string! suffix) -> void StackExchange.Redis.IConnectionMultiplexer.AddLibraryNameSuffix(string! suffix) -> void - +StackExchange.Redis.ConfigurationOptions.SetUserPfxCertificate(string! userCertificatePath, string? password = null) -> void diff --git a/src/StackExchange.Redis/PublicAPI/net6.0/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/net6.0/PublicAPI.Shipped.txt index 599891ac2..fae4f65ce 100644 --- a/src/StackExchange.Redis/PublicAPI/net6.0/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/net6.0/PublicAPI.Shipped.txt @@ -1,3 +1,4 @@ StackExchange.Redis.ConfigurationOptions.SslClientAuthenticationOptions.get -> System.Func? StackExchange.Redis.ConfigurationOptions.SslClientAuthenticationOptions.set -> void -System.Runtime.CompilerServices.IsExternalInit (forwarded, contained in System.Runtime) \ No newline at end of file +System.Runtime.CompilerServices.IsExternalInit (forwarded, contained in System.Runtime) +StackExchange.Redis.ConfigurationOptions.SetUserPemCertificate(string! userCertificatePath, string? userKeyPath = null) -> void \ No newline at end of file diff --git a/src/StackExchange.Redis/PublicAPI/net8.0/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/net8.0/PublicAPI.Shipped.txt index 599891ac2..fae4f65ce 100644 --- a/src/StackExchange.Redis/PublicAPI/net8.0/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/net8.0/PublicAPI.Shipped.txt @@ -1,3 +1,4 @@ StackExchange.Redis.ConfigurationOptions.SslClientAuthenticationOptions.get -> System.Func? StackExchange.Redis.ConfigurationOptions.SslClientAuthenticationOptions.set -> void -System.Runtime.CompilerServices.IsExternalInit (forwarded, contained in System.Runtime) \ No newline at end of file +System.Runtime.CompilerServices.IsExternalInit (forwarded, contained in System.Runtime) +StackExchange.Redis.ConfigurationOptions.SetUserPemCertificate(string! userCertificatePath, string? userKeyPath = null) -> void \ No newline at end of file