Skip to content

Commit

Permalink
Add password support for PFX files (#694)
Browse files Browse the repository at this point in the history
* Add password support for PFX files

* Remove callback

* Add key file password
  • Loading branch information
mtmk authored Dec 10, 2024
1 parent 150d5a8 commit 11ee387
Show file tree
Hide file tree
Showing 25 changed files with 846 additions and 593 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,18 @@ namespace NATS.Client.Core.Internal;
internal static class SslClientAuthenticationOptionsExtensions
{
#if !NETSTANDARD
public static SslClientAuthenticationOptions LoadClientCertFromPem(this SslClientAuthenticationOptions options, string certPem, string keyPem, bool offline = false, SslCertificateTrust? trust = null)
public static SslClientAuthenticationOptions LoadClientCertFromPem(this SslClientAuthenticationOptions options, string certPem, string keyPem, bool offline = false, SslCertificateTrust? trust = null, string? password = null)
{
var leafCert = X509Certificate2.CreateFromPem(certPem, keyPem);
X509Certificate2 leafCert;
if (!string.IsNullOrEmpty(password))
{
leafCert = X509Certificate2.CreateFromEncryptedPem(certPem, keyPem, password);
}
else
{
leafCert = X509Certificate2.CreateFromPem(certPem, keyPem);
}

var intermediateCerts = new X509Certificate2Collection();
intermediateCerts.ImportFromPem(certPem);
if (intermediateCerts.Count > 0)
Expand Down Expand Up @@ -39,11 +48,21 @@ public static SslClientAuthenticationOptions LoadClientCertFromPem(this SslClien
}
#endif

public static SslClientAuthenticationOptions LoadClientCertFromPfxFile(this SslClientAuthenticationOptions options, string certBundleFile, bool offline = false, SslCertificateTrust? trust = null)
public static SslClientAuthenticationOptions LoadClientCertFromPfxFile(this SslClientAuthenticationOptions options, string certBundleFile, bool offline = false, SslCertificateTrust? trust = null, string? password = null)
{
var leafCert = new X509Certificate2(certBundleFile);
X509Certificate2 leafCert;
var intermediateCerts = new X509Certificate2Collection();
intermediateCerts.Import(certBundleFile);

if (password != null)
{
leafCert = new X509Certificate2(certBundleFile, password);
intermediateCerts.Import(certBundleFile, password, X509KeyStorageFlags.DefaultKeySet);
}
else
{
leafCert = new X509Certificate2(certBundleFile);
intermediateCerts.Import(certBundleFile);
}

// Linux does not include the leaf by default, but Windows does
// compare leaf to first intermediate just to be sure to catch all platform differences
Expand Down
21 changes: 16 additions & 5 deletions src/NATS.Client.Core/NatsTlsOpts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,20 +62,30 @@ public sealed record NatsTlsOpts
/// File path to PEM-encoded Private Key
/// </summary>
/// <remarks>
/// Key should not be password protected
/// If key is password protected use <see cref="KeyFilePassword"/>.
/// Must be used in conjunction with <see cref="CertFile"/>.
/// </remarks>
public string? KeyFile { get; init; }

/// <summary>
/// Key file password
/// </summary>
public string? KeyFilePassword { get; init; }
#endif

/// <summary>
/// File path to PKCS#12 bundle containing X509 Client Certificate and Private Key
/// </summary>
/// <remarks>
/// Bundle should not be password protected
/// Use <see cref="CertBundleFilePassword"/> to specify the password for the bundle.
/// </remarks>
public string? CertBundleFile { get; init; }

/// <summary>
/// Password for the PKCS#12 bundle file
/// </summary>
public string? CertBundleFilePassword { get; init; }

/// <summary>
/// Callback to configure <see cref="SslClientAuthenticationOptions"/>
/// </summary>
Expand Down Expand Up @@ -159,14 +169,15 @@ internal async ValueTask<SslClientAuthenticationOptions> AuthenticateAsClientOpt
if (CertFile != null && KeyFile != null)
{
options.LoadClientCertFromPem(
await File.ReadAllTextAsync(CertFile).ConfigureAwait(false),
await File.ReadAllTextAsync(KeyFile).ConfigureAwait(false));
certPem: await File.ReadAllTextAsync(CertFile).ConfigureAwait(false),
keyPem: await File.ReadAllTextAsync(KeyFile).ConfigureAwait(false),
password: KeyFilePassword);
}
#endif

if (CertBundleFile != null)
{
options.LoadClientCertFromPfxFile(CertBundleFile);
options.LoadClientCertFromPfxFile(CertBundleFile, password: CertBundleFilePassword);
}

if (InsecureSkipVerify)
Expand Down
15 changes: 10 additions & 5 deletions tests/NATS.Client.Core.Tests/TlsOptsTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,29 +36,34 @@ static async ValueTask ValidateAsync(NatsTlsOpts opts)
}
}

[Fact]
public async Task Load_client_cert_and_key()
[Theory]
[InlineData("client-cert-bundle.pfx", null, "client-key.pem", null)]
[InlineData("client-cert-bundle.pfx", "", "client-key.pem", "")]
[InlineData("client-cert-bundle-pass.pfx", "1234", "client-key-pass.pem", "5678")]
public async Task Load_client_cert_and_key(string pfxFile, string? pfxFilepassword, string keyFile, string? keyFilePassword)
{
const string clientCertFile = "resources/certs/client-cert.pem";
const string clientCertBundleFile = "resources/certs/client-cert-bundle.pfx";
const string clientKeyFile = "resources/certs/client-key.pem";
var clientKeyFile = $"resources/certs/{keyFile}";
var clientCertBundleFile = $"resources/certs/{pfxFile}";

await ValidateAsync(new NatsTlsOpts
{
CertFile = clientCertFile,
KeyFile = clientKeyFile,
KeyFilePassword = keyFilePassword,
});

await ValidateAsync(new NatsTlsOpts
{
CertBundleFile = clientCertBundleFile,
CertBundleFilePassword = pfxFilepassword,
});

await ValidateAsync(new NatsTlsOpts
{
ConfigureClientAuthentication = async options =>
{
options.LoadClientCertFromPem(await File.ReadAllTextAsync(clientCertFile), await File.ReadAllTextAsync(clientKeyFile));
options.LoadClientCertFromPem(await File.ReadAllTextAsync(clientCertFile), await File.ReadAllTextAsync(clientKeyFile), password: keyFilePassword);
},
});

Expand Down
58 changes: 29 additions & 29 deletions tests/NATS.Client.Core.Tests/resources/certs/ca-cert.pem
Original file line number Diff line number Diff line change
@@ -1,29 +1,29 @@
-----BEGIN CERTIFICATE-----
MIIE+zCCAuOgAwIBAgIUaIS3fVNjOgYm5uFgS/kv6yuIKucwDQYJKoZIhvcNAQEL
BQAwDTELMAkGA1UEAwwCY2EwHhcNMjQwMzI3MTI1NjM3WhcNMzQwMzI1MTI1NjM3
WjANMQswCQYDVQQDDAJjYTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB
ALV2DvN/xQMBcCpjXe2wXo5khlfbw6CzhsPnieQScDLw1onxuO0+uUN0l4YaMqC4
gn560gct+Hb41wx2j40MU1zm6eYH0CalWuPpPDHx4f/sEVbd9LCHVem+IxrHAnyU
yp6RPQlJvsJr3m617YNIrZDANLrt0Ssk41XdvFsx6tFFTU5SgIYvWOHiIbqkNTAn
6iAdPKBKPM4jNRczocLWP+sbo8TtALMD+0blDjZ9Ue7WqUKnx6ns78oH4XQWyxD0
Bgd6UPGCNbuSJ1BdnE31/M/x8DLOOvkczNCXRp4nCn0VBEvwu1uS77aHN6xrNtN9
nWF/ykTMKlgw8oznSt1znD1QZU/uB2cwomnXGQZ/d4ocrSNW+gNM8JmxHTnO/FRk
UN5KWkRoHOFgzv7e4sW9p7UKzdmYAQRTHn+XgSg7/AFY7J0rQoj5ckrGgJ+MItTY
lVLNuPXPIi7x/bfXKiWDfhrXKMaP08hFB30gTCZ9vpMea7CEOJ6MhJH2yuZnKgjJ
ez65uCu7NMoFrN7y3IcDL/ZrEnaHRJLMBzvGgEoYZ6Ypf7oNJMi8uXnfhqclGpaj
SpLP+31yd2jHYkii2fAZAHzzXse+aT1OvVhShdlcNTSUFwjroMYlaTG92llX1FE4
gn51oj13QGBK9lpfh/uLjfM6+4LDjmQbHDzhTOEQpkqhAgMBAAGjUzBRMB0GA1Ud
DgQWBBTdKnIqsIQL44dltqE1+aFtjASH2TAfBgNVHSMEGDAWgBTdKnIqsIQL44dl
tqE1+aFtjASH2TAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQAi
+NFuLAn7yjpuBvmncClbMMTuZDo1PH79PIBVESGid2gRye0vXFNqXm20VKEDsM71
6o78bTu+zNBpXyqluqDVjSN+nkeiNytTqsKsUPPMUujtFibc7yC9j2Woj3j+akar
lc5OtDtW/Lkk7Ne9aBQGYa7l0bQuE5OuZquCRpn1HhovNV/otLuaLGJZWoiZ1fry
l0SV4JmEQi1D9T13SeJ2lu/KNHKfs9noCXzE4BjvHFcmk8kxInyfqxbBVJUOzAAl
ELVl83MRSFFX3FvYX3A1OWNY9F6w5TiuYSaGTFqMJwcWXL9KHm4F0gHxyjZCbgeI
O2HJtXq/MfCibgvXV3ghu8v/YPyMTmagAaxeWawMD/QyIJaNVeXdyoG6TnpccB58
F2jiMgobYuN4whj6tpkVXSS6PGobJhl9EaT6hPjfmcIRpTn6j0hVZyKwDOO0cWon
XRY5XNLVpCqQCZNuvzkHc38s5KEuL+BLwPTygVg9OpC8OvbVXS+xhj26edLvwagq
+FxEatM20ytgX1+nl4XKMtcOxqS0eIwlWm0eQS6PjtCLw1GLU/A2cJA27TjjHw6C
EJkaOr5JAv0NSo7pjUW3hu/71frwq2AiCvBPAsKAsPi2pRwHhezyoUNYiaRgzrHX
4IKi1S1XOAy5OVRDZ6SI4iahfOz+/eJtBltk06V3dQ==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIE+zCCAuOgAwIBAgIUIWMC5Ul+2Mlyo0La9R6WT4VRcZUwDQYJKoZIhvcNAQEL
BQAwDTELMAkGA1UEAwwCY2EwHhcNMjQxMjA5MjI0ODAyWhcNMzQxMjA3MjI0ODAy
WjANMQswCQYDVQQDDAJjYTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB
ANR/64xZwIoTnDFbv8fotCbUImuhwn3UcrTCc+kiXg8tFLnv3MuELzosJJYsYkJ6
qZR82qBlpHlD6Tl8VSYVuaPFllXbukBL0+5shLKXePiw1OvI02NJ3IetjVnRQIsv
Z3HsHOhZb2JjQr5XOl0bGc9oXOOdZZxfdmSr/XP2CToNmWHfO7Aref/EQzDzjsQf
nJVRjov+jq6lVSUScga2FEjF3ICV+33YAnkdtMys9efPsYDhCMrEatX64wMk6wX6
SWDFYdulNwCJ18ua+CKnPKtngEFv72Q89OSpLR3/3xYOy1dTtZ5TkkCGh4OznfUJ
J7qjZewKR4mBLjPGHehrwdF3IgAH7xgboD7sce3ERvw1NfmhL14LT12CT0CM1Z53
jjglKL4ZbFRERENEgoqUsdDuXbr0VKwNJ+Gqo0mWF9EKk+BbgLJBpifyMkOCqP+R
VnL3QJdhQUUl8+AUlqcgzde0z77VwVmegL008Ahd6GPtZ6sKWXM/tvb0wfmFx371
fGXVpy250AEU+W5Ie7OrrlZUbFcq/XIG9+S0lJiubv4POm5DjR2cm6L/XYt3Ixzb
htZfQWOwYx7u7qc5wXQjOc99dmFCHoGLyrcgOaJYC3vPc9jII2MAiPEDMcNMBauv
wOuqEoWInqzU5qKOuGuukQzoZTTp9hr7FqkzAgmBjLiPAgMBAAGjUzBRMB0GA1Ud
DgQWBBTNvA2CaWbBkPrv4t2ujpQTsgUhPTAfBgNVHSMEGDAWgBTNvA2CaWbBkPrv
4t2ujpQTsgUhPTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQDB
AeBzxrAJebheaID80T4D0ojFazm96X+VLYc1Kf32rxae4Jg7bC//EyV4tS4N8bvi
ODKIk7vwJEcc/8xLlxGzwaMEIcy4oz2H0hlD3XbgD6lk0PKiLYv+qPrLrFz9u9W0
rUFa5BN5qlYlW7RrzcJlaq8x6xMuJ8Clqmb/Y0vcGm9tFw5V1z7uYnKwsRwZ8M+7
JPKGQvb/lYekYC5mkdn7dxZ2jrdVJhxWNAiUqQJDIqa1svEx0eM8PKvn0gDfnOD8
Sn/K3GZpc+1aPx5h6Oxxnln7tmoD6rskPKa5XEjVT25m1uEfCKrGeJEGTVQItpQt
jWzyFUZKagU2ChSrA1h2AMfhY+jLQCqJGHRYnPnuLJ/iQSTz1OQusmsC7wIBOxdH
wzvTHf7lYsarR+I1aorn7AyVYF2bqXOecoNw88wrndYCdWKDs5dk6U8MPFeMMr1/
l7ZVl5nIYaZhgAuqnsWy83Yey56mdHkBIEQMnEKPp9n3aTGON6O/CEqGfVmWG3fO
iG37NpcGdvtVKCglV4DPRf7zRkIfLoveQcI5sSiT36V7swJe9FK+vcmhDs8j1B1J
BZDlQJI33Ll0qZRdSNvw6rCryaIVUsfMWDX09qSaiaqkbsmS1MXlj2czYLgsbbUz
hF6eEvpiId/9SyCNvHV4eGFg3nSLUJ4JYADm+s5tmA==
-----END CERTIFICATE-----
Binary file not shown.
Binary file not shown.
Loading

0 comments on commit 11ee387

Please sign in to comment.