Skip to content

Commit 328b4b5

Browse files
authored
Make it easier to use user certificate files (#2873)
* Make it easier to use user certificate files * make key file optional; PEM can include the private key * - release notes - use Enum.TryParse for the X509 flags
1 parent e93de40 commit 328b4b5

File tree

6 files changed

+67
-13
lines changed

6 files changed

+67
-13
lines changed

docs/ReleaseNotes.md

+2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ Current package versions:
99
## Unreleased
1010
No pending unreleased changes
1111

12+
- Add `ConfigurationOptions.SetUserPemCertificate(...)` and `ConfigurationOptions.SetUserPfxCertificate(...)` methods to simplify using client certificates ([#2873 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2873))
13+
1214
## 2.8.31
1315

1416
- 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)

src/StackExchange.Redis/ConfigurationOptions.cs

+43
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,49 @@ public bool HighIntegrity
301301
/// <param name="issuerCertificatePath">The file system path to find the certificate at.</param>
302302
public void TrustIssuer(string issuerCertificatePath) => CertificateValidationCallback = TrustIssuerCallback(issuerCertificatePath);
303303

304+
#if NET5_0_OR_GREATER
305+
/// <summary>
306+
/// Supply a user certificate from a PEM file pair and enable TLS.
307+
/// </summary>
308+
/// <param name="userCertificatePath">The path for the the user certificate (commonly a .crt file).</param>
309+
/// <param name="userKeyPath">The path for the the user key (commonly a .key file).</param>
310+
public void SetUserPemCertificate(string userCertificatePath, string? userKeyPath = null)
311+
{
312+
CertificateSelectionCallback = CreatePemUserCertificateCallback(userCertificatePath, userKeyPath);
313+
Ssl = true;
314+
}
315+
#endif
316+
317+
/// <summary>
318+
/// Supply a user certificate from a PFX file and optional password and enable TLS.
319+
/// </summary>
320+
/// <param name="userCertificatePath">The path for the the user certificate (commonly a .pfx file).</param>
321+
/// <param name="password">The password for the certificate file.</param>
322+
public void SetUserPfxCertificate(string userCertificatePath, string? password = null)
323+
{
324+
CertificateSelectionCallback = CreatePfxUserCertificateCallback(userCertificatePath, password);
325+
Ssl = true;
326+
}
327+
328+
#if NET5_0_OR_GREATER
329+
internal static LocalCertificateSelectionCallback CreatePemUserCertificateCallback(string userCertificatePath, string? userKeyPath)
330+
{
331+
// PEM handshakes not universally supported and causes a runtime error about ephemeral certificates; to avoid, export as PFX
332+
using var pem = X509Certificate2.CreateFromPemFile(userCertificatePath, userKeyPath);
333+
#pragma warning disable SYSLIB0057 // Type or member is obsolete
334+
var pfx = new X509Certificate2(pem.Export(X509ContentType.Pfx));
335+
#pragma warning restore SYSLIB0057 // Type or member is obsolete
336+
337+
return (sender, targetHost, localCertificates, remoteCertificate, acceptableIssuers) => pfx;
338+
}
339+
#endif
340+
341+
internal static LocalCertificateSelectionCallback CreatePfxUserCertificateCallback(string userCertificatePath, string? password, X509KeyStorageFlags storageFlags = X509KeyStorageFlags.DefaultKeySet)
342+
{
343+
var pfx = new X509Certificate2(userCertificatePath, password ?? "", storageFlags);
344+
return (sender, targetHost, localCertificates, remoteCertificate, acceptableIssuers) => pfx;
345+
}
346+
304347
/// <summary>
305348
/// Create a certificate validation check that checks against the supplied issuer even when not known by the machine.
306349
/// </summary>

src/StackExchange.Redis/PhysicalConnection.cs

+17-10
Original file line numberDiff line numberDiff line change
@@ -1504,21 +1504,28 @@ public ConnectionStatus GetStatus()
15041504
{
15051505
try
15061506
{
1507-
var pfxPath = Environment.GetEnvironmentVariable("SERedis_ClientCertPfxPath");
1508-
var pfxPassword = Environment.GetEnvironmentVariable("SERedis_ClientCertPassword");
1509-
var pfxStorageFlags = Environment.GetEnvironmentVariable("SERedis_ClientCertStorageFlags");
1510-
1511-
X509KeyStorageFlags? flags = null;
1512-
if (!string.IsNullOrEmpty(pfxStorageFlags))
1507+
var certificatePath = Environment.GetEnvironmentVariable("SERedis_ClientCertPfxPath");
1508+
if (!string.IsNullOrEmpty(certificatePath) && File.Exists(certificatePath))
15131509
{
1514-
flags = Enum.Parse(typeof(X509KeyStorageFlags), pfxStorageFlags) as X509KeyStorageFlags?;
1510+
var password = Environment.GetEnvironmentVariable("SERedis_ClientCertPassword");
1511+
var pfxStorageFlags = Environment.GetEnvironmentVariable("SERedis_ClientCertStorageFlags");
1512+
X509KeyStorageFlags storageFlags = X509KeyStorageFlags.DefaultKeySet;
1513+
if (!string.IsNullOrEmpty(pfxStorageFlags) && Enum.TryParse<X509KeyStorageFlags>(pfxStorageFlags, true, out var typedFlags))
1514+
{
1515+
storageFlags = typedFlags;
1516+
}
1517+
1518+
return ConfigurationOptions.CreatePfxUserCertificateCallback(certificatePath, password, storageFlags);
15151519
}
15161520

1517-
if (!string.IsNullOrEmpty(pfxPath) && File.Exists(pfxPath))
1521+
#if NET5_0_OR_GREATER
1522+
certificatePath = Environment.GetEnvironmentVariable("SERedis_ClientCertPemPath");
1523+
if (!string.IsNullOrEmpty(certificatePath) && File.Exists(certificatePath))
15181524
{
1519-
return (sender, targetHost, localCertificates, remoteCertificate, acceptableIssuers) =>
1520-
new X509Certificate2(pfxPath, pfxPassword ?? "", flags ?? X509KeyStorageFlags.DefaultKeySet);
1525+
var passwordPath = Environment.GetEnvironmentVariable("SERedis_ClientCertPasswordPath");
1526+
return ConfigurationOptions.CreatePemUserCertificateCallback(certificatePath, passwordPath);
15211527
}
1528+
#endif
15221529
}
15231530
catch (Exception ex)
15241531
{

src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1893,4 +1893,4 @@ virtual StackExchange.Redis.RedisResult.Length.get -> int
18931893
virtual StackExchange.Redis.RedisResult.this[int index].get -> StackExchange.Redis.RedisResult!
18941894
StackExchange.Redis.ConnectionMultiplexer.AddLibraryNameSuffix(string! suffix) -> void
18951895
StackExchange.Redis.IConnectionMultiplexer.AddLibraryNameSuffix(string! suffix) -> void
1896-
1896+
StackExchange.Redis.ConfigurationOptions.SetUserPfxCertificate(string! userCertificatePath, string? password = null) -> void
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
StackExchange.Redis.ConfigurationOptions.SslClientAuthenticationOptions.get -> System.Func<string!, System.Net.Security.SslClientAuthenticationOptions!>?
22
StackExchange.Redis.ConfigurationOptions.SslClientAuthenticationOptions.set -> void
3-
System.Runtime.CompilerServices.IsExternalInit (forwarded, contained in System.Runtime)
3+
System.Runtime.CompilerServices.IsExternalInit (forwarded, contained in System.Runtime)
4+
StackExchange.Redis.ConfigurationOptions.SetUserPemCertificate(string! userCertificatePath, string? userKeyPath = null) -> void
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
StackExchange.Redis.ConfigurationOptions.SslClientAuthenticationOptions.get -> System.Func<string!, System.Net.Security.SslClientAuthenticationOptions!>?
22
StackExchange.Redis.ConfigurationOptions.SslClientAuthenticationOptions.set -> void
3-
System.Runtime.CompilerServices.IsExternalInit (forwarded, contained in System.Runtime)
3+
System.Runtime.CompilerServices.IsExternalInit (forwarded, contained in System.Runtime)
4+
StackExchange.Redis.ConfigurationOptions.SetUserPemCertificate(string! userCertificatePath, string? userKeyPath = null) -> void

0 commit comments

Comments
 (0)