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