From f7518fa4b111dae4f5553baecb35b72aebfc2559 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Sat, 12 Apr 2025 09:58:08 +0100 Subject: [PATCH 1/3] Make it easier to use user certificate files --- .../ConfigurationOptions.cs | 43 +++++++++++++++++++ src/StackExchange.Redis/PhysicalConnection.cs | 31 ++++++++----- .../PublicAPI/PublicAPI.Shipped.txt | 2 +- .../PublicAPI/net6.0/PublicAPI.Shipped.txt | 3 +- .../PublicAPI/net8.0/PublicAPI.Shipped.txt | 3 +- 5 files changed, 69 insertions(+), 13 deletions(-) diff --git a/src/StackExchange.Redis/ConfigurationOptions.cs b/src/StackExchange.Redis/ConfigurationOptions.cs index 199f1a378..1606e868d 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) + { + 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..4170834a1 100644 --- a/src/StackExchange.Redis/PhysicalConnection.cs +++ b/src/StackExchange.Redis/PhysicalConnection.cs @@ -1504,21 +1504,32 @@ 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)) + { + var tmp = Enum.Parse(typeof(X509KeyStorageFlags), pfxStorageFlags) as X509KeyStorageFlags?; + if (tmp is not null) storageFlags = tmp.GetValueOrDefault(); + } + + 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"); + if (!string.IsNullOrEmpty(passwordPath) && File.Exists(passwordPath)) + { + 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..e7cfc8d41 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) -> 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..e7cfc8d41 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) -> void \ No newline at end of file From 9d5240e9806d06f0d637c6bb3d7df152e548c26c Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Sat, 12 Apr 2025 10:32:01 +0100 Subject: [PATCH 2/3] make key file optional; PEM can include the private key --- src/StackExchange.Redis/ConfigurationOptions.cs | 4 ++-- src/StackExchange.Redis/PhysicalConnection.cs | 5 +---- .../PublicAPI/net6.0/PublicAPI.Shipped.txt | 2 +- .../PublicAPI/net8.0/PublicAPI.Shipped.txt | 2 +- 4 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/StackExchange.Redis/ConfigurationOptions.cs b/src/StackExchange.Redis/ConfigurationOptions.cs index 1606e868d..dfdab5f4e 100644 --- a/src/StackExchange.Redis/ConfigurationOptions.cs +++ b/src/StackExchange.Redis/ConfigurationOptions.cs @@ -307,7 +307,7 @@ public bool HighIntegrity /// /// 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) + public void SetUserPemCertificate(string userCertificatePath, string? userKeyPath = null) { CertificateSelectionCallback = CreatePemUserCertificateCallback(userCertificatePath, userKeyPath); Ssl = true; @@ -326,7 +326,7 @@ public void SetUserPfxCertificate(string userCertificatePath, string? password = } #if NET5_0_OR_GREATER - internal static LocalCertificateSelectionCallback CreatePemUserCertificateCallback(string userCertificatePath, string userKeyPath) + 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); diff --git a/src/StackExchange.Redis/PhysicalConnection.cs b/src/StackExchange.Redis/PhysicalConnection.cs index 4170834a1..392dd7950 100644 --- a/src/StackExchange.Redis/PhysicalConnection.cs +++ b/src/StackExchange.Redis/PhysicalConnection.cs @@ -1524,10 +1524,7 @@ public ConnectionStatus GetStatus() if (!string.IsNullOrEmpty(certificatePath) && File.Exists(certificatePath)) { var passwordPath = Environment.GetEnvironmentVariable("SERedis_ClientCertPasswordPath"); - if (!string.IsNullOrEmpty(passwordPath) && File.Exists(passwordPath)) - { - return ConfigurationOptions.CreatePemUserCertificateCallback(certificatePath, passwordPath); - } + return ConfigurationOptions.CreatePemUserCertificateCallback(certificatePath, passwordPath); } #endif } diff --git a/src/StackExchange.Redis/PublicAPI/net6.0/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/net6.0/PublicAPI.Shipped.txt index e7cfc8d41..fae4f65ce 100644 --- a/src/StackExchange.Redis/PublicAPI/net6.0/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/net6.0/PublicAPI.Shipped.txt @@ -1,4 +1,4 @@ StackExchange.Redis.ConfigurationOptions.SslClientAuthenticationOptions.get -> System.Func? StackExchange.Redis.ConfigurationOptions.SslClientAuthenticationOptions.set -> void System.Runtime.CompilerServices.IsExternalInit (forwarded, contained in System.Runtime) -StackExchange.Redis.ConfigurationOptions.SetUserPemCertificate(string! userCertificatePath, string! userKeyPath) -> void \ No newline at end of file +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 e7cfc8d41..fae4f65ce 100644 --- a/src/StackExchange.Redis/PublicAPI/net8.0/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/net8.0/PublicAPI.Shipped.txt @@ -1,4 +1,4 @@ StackExchange.Redis.ConfigurationOptions.SslClientAuthenticationOptions.get -> System.Func? StackExchange.Redis.ConfigurationOptions.SslClientAuthenticationOptions.set -> void System.Runtime.CompilerServices.IsExternalInit (forwarded, contained in System.Runtime) -StackExchange.Redis.ConfigurationOptions.SetUserPemCertificate(string! userCertificatePath, string! userKeyPath) -> void \ No newline at end of file +StackExchange.Redis.ConfigurationOptions.SetUserPemCertificate(string! userCertificatePath, string? userKeyPath = null) -> void \ No newline at end of file From 7a9208f432841c5f6e9380a007e41c6554ae5098 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 15 Apr 2025 17:00:48 +0100 Subject: [PATCH 3/3] - release notes - use Enum.TryParse for the X509 flags --- docs/ReleaseNotes.md | 2 ++ src/StackExchange.Redis/PhysicalConnection.cs | 5 ++--- 2 files changed, 4 insertions(+), 3 deletions(-) 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/PhysicalConnection.cs b/src/StackExchange.Redis/PhysicalConnection.cs index 392dd7950..df30ab207 100644 --- a/src/StackExchange.Redis/PhysicalConnection.cs +++ b/src/StackExchange.Redis/PhysicalConnection.cs @@ -1510,10 +1510,9 @@ public ConnectionStatus GetStatus() var password = Environment.GetEnvironmentVariable("SERedis_ClientCertPassword"); var pfxStorageFlags = Environment.GetEnvironmentVariable("SERedis_ClientCertStorageFlags"); X509KeyStorageFlags storageFlags = X509KeyStorageFlags.DefaultKeySet; - if (!string.IsNullOrEmpty(pfxStorageFlags)) + if (!string.IsNullOrEmpty(pfxStorageFlags) && Enum.TryParse(pfxStorageFlags, true, out var typedFlags)) { - var tmp = Enum.Parse(typeof(X509KeyStorageFlags), pfxStorageFlags) as X509KeyStorageFlags?; - if (tmp is not null) storageFlags = tmp.GetValueOrDefault(); + storageFlags = typedFlags; } return ConfigurationOptions.CreatePfxUserCertificateCallback(certificatePath, password, storageFlags);