Skip to content

Commit 6e6ccb5

Browse files
0xcedHofmeisterAn
andauthored
feat: Use built-in PEM certificate import on .NET 6 and onwards (#1139)
Co-authored-by: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com>
1 parent 1cfc850 commit 6e6ccb5

File tree

4 files changed

+82
-61
lines changed

4 files changed

+82
-61
lines changed

src/Testcontainers/Builders/MTlsEndpointAuthenticationProvider.cs

+8-58
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,11 @@ namespace DotNet.Testcontainers.Builders
77
using Docker.DotNet.X509;
88
using DotNet.Testcontainers.Configurations;
99
using JetBrains.Annotations;
10-
using Org.BouncyCastle.Crypto;
11-
using Org.BouncyCastle.Crypto.Parameters;
12-
using Org.BouncyCastle.OpenSsl;
13-
using Org.BouncyCastle.Pkcs;
14-
using Org.BouncyCastle.Security;
15-
using Org.BouncyCastle.X509;
1610

1711
/// <inheritdoc cref="IDockerRegistryAuthenticationProvider" />
1812
[PublicAPI]
1913
internal sealed class MTlsEndpointAuthenticationProvider : TlsEndpointAuthenticationProvider
2014
{
21-
private static readonly X509CertificateParser CertificateParser = new X509CertificateParser();
22-
2315
/// <summary>
2416
/// Initializes a new instance of the <see cref="MTlsEndpointAuthenticationProvider" /> class.
2517
/// </summary>
@@ -57,57 +49,15 @@ protected override X509Certificate2 GetClientCertificate()
5749
{
5850
var clientCertificateFilePath = Path.Combine(CertificatesDirectoryPath, ClientCertificateFileName);
5951
var clientCertificateKeyFilePath = Path.Combine(CertificatesDirectoryPath, ClientCertificateKeyFileName);
60-
return CreateFromPemFile(clientCertificateFilePath, clientCertificateKeyFilePath);
61-
}
62-
63-
private static X509Certificate2 CreateFromPemFile(string certPemFilePath, string keyPemFilePath)
64-
{
65-
if (!File.Exists(certPemFilePath))
66-
{
67-
throw new FileNotFoundException(certPemFilePath);
68-
}
69-
70-
if (!File.Exists(keyPemFilePath))
71-
{
72-
throw new FileNotFoundException(keyPemFilePath);
73-
}
74-
75-
using (var keyPairStream = new StreamReader(keyPemFilePath))
76-
{
77-
var store = new Pkcs12StoreBuilder().Build();
78-
79-
var certificate = CertificateParser.ReadCertificate(File.ReadAllBytes(certPemFilePath));
80-
81-
var password = Guid.NewGuid().ToString("D");
8252

83-
var keyObject = new PemReader(keyPairStream).ReadObject();
84-
85-
var certificateEntry = new X509CertificateEntry(certificate);
86-
87-
var keyParameter = ResolveKeyParameter(keyObject);
88-
89-
var keyEntry = new AsymmetricKeyEntry(keyParameter);
90-
store.SetKeyEntry(certificate.SubjectDN + "_key", keyEntry, new[] { certificateEntry });
91-
92-
using (var certificateStream = new MemoryStream())
93-
{
94-
store.Save(certificateStream, password.ToCharArray(), new SecureRandom());
95-
return new X509Certificate2(Pkcs12Utilities.ConvertToDefiniteLength(certificateStream.ToArray()), password);
96-
}
97-
}
98-
}
99-
100-
private static AsymmetricKeyParameter ResolveKeyParameter(object keyObject)
101-
{
102-
switch (keyObject)
103-
{
104-
case AsymmetricCipherKeyPair ackp:
105-
return ackp.Private;
106-
case RsaPrivateCrtKeyParameters rpckp:
107-
return rpckp;
108-
default:
109-
throw new ArgumentOutOfRangeException(nameof(keyObject), $"Unsupported asymmetric key entry encountered while trying to resolve key from input object '{keyObject.GetType()}'.");
110-
}
53+
#if NETSTANDARD
54+
return Polyfills.X509Certificate2.CreateFromPemFile(clientCertificateFilePath, clientCertificateKeyFilePath);
55+
#else
56+
var certificate = X509Certificate2.CreateFromPemFile(clientCertificateFilePath, clientCertificateKeyFilePath);
57+
// The certificate must be exported to PFX on Windows to avoid "No credentials are available in the security package":
58+
// https://stackoverflow.com/questions/72096812/loading-x509certificate2-from-pem-file-results-in-no-credentials-are-available/72101855#72101855.
59+
return OperatingSystem.IsWindows() ? new X509Certificate2(certificate.Export(X509ContentType.Pfx)) : certificate;
60+
#endif
11161
}
11262
}
11363
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
#if NETSTANDARD
2+
namespace DotNet.Testcontainers.Polyfills
3+
{
4+
using System;
5+
using System.IO;
6+
using Org.BouncyCastle.Crypto;
7+
using Org.BouncyCastle.Crypto.Parameters;
8+
using Org.BouncyCastle.OpenSsl;
9+
using Org.BouncyCastle.Pkcs;
10+
using Org.BouncyCastle.Security;
11+
using Org.BouncyCastle.X509;
12+
13+
public static class X509Certificate2
14+
{
15+
private static readonly X509CertificateParser CertificateParser = new X509CertificateParser();
16+
17+
public static System.Security.Cryptography.X509Certificates.X509Certificate2 CreateFromPemFile(string certPemFilePath, string keyPemFilePath)
18+
{
19+
if (!File.Exists(certPemFilePath))
20+
{
21+
throw new FileNotFoundException(certPemFilePath);
22+
}
23+
24+
if (!File.Exists(keyPemFilePath))
25+
{
26+
throw new FileNotFoundException(keyPemFilePath);
27+
}
28+
29+
using (var keyPairStream = new StreamReader(keyPemFilePath))
30+
{
31+
var store = new Pkcs12StoreBuilder().Build();
32+
33+
var certificate = CertificateParser.ReadCertificate(File.ReadAllBytes(certPemFilePath));
34+
35+
var password = Guid.NewGuid().ToString("D");
36+
37+
var keyObject = new PemReader(keyPairStream).ReadObject();
38+
39+
var certificateEntry = new X509CertificateEntry(certificate);
40+
41+
var keyParameter = ResolveKeyParameter(keyObject);
42+
43+
var keyEntry = new AsymmetricKeyEntry(keyParameter);
44+
store.SetKeyEntry(certificate.SubjectDN + "_key", keyEntry, new[] { certificateEntry });
45+
46+
using (var certificateStream = new MemoryStream())
47+
{
48+
store.Save(certificateStream, password.ToCharArray(), new SecureRandom());
49+
return new System.Security.Cryptography.X509Certificates.X509Certificate2(Pkcs12Utilities.ConvertToDefiniteLength(certificateStream.ToArray()), password);
50+
}
51+
}
52+
}
53+
54+
private static AsymmetricKeyParameter ResolveKeyParameter(object keyObject)
55+
{
56+
switch (keyObject)
57+
{
58+
case AsymmetricCipherKeyPair ackp:
59+
return ackp.Private;
60+
case RsaPrivateCrtKeyParameters rpckp:
61+
return rpckp;
62+
default:
63+
throw new ArgumentOutOfRangeException(nameof(keyObject), $"Unsupported asymmetric key entry encountered while trying to resolve key from input object '{keyObject.GetType()}'.");
64+
}
65+
}
66+
}
67+
}
68+
#endif

src/Testcontainers/Testcontainers.csproj

+5-3
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,16 @@
66
</PropertyGroup>
77
<ItemGroup>
88
<PackageReference Include="JetBrains.Annotations" VersionOverride="2023.3.0" PrivateAssets="All"/>
9-
<PackageReference Include="BouncyCastle.Cryptography"/>
109
<PackageReference Include="Docker.DotNet.X509"/>
1110
<PackageReference Include="Docker.DotNet"/>
12-
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces"/>
13-
<PackageReference Include="Microsoft.Bcl.HashCode"/>
1411
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions"/>
1512
<PackageReference Include="SharpZipLib"/>
1613
<PackageReference Include="SSH.NET"/>
14+
</ItemGroup>
15+
<ItemGroup Condition="$(TargetFrameworkIdentifier) == '.NETStandard'">
16+
<PackageReference Include="BouncyCastle.Cryptography"/>
17+
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces"/>
18+
<PackageReference Include="Microsoft.Bcl.HashCode"/>
1719
<PackageReference Include="System.Text.Json"/>
1820
</ItemGroup>
1921
</Project>

tests/Testcontainers.Tests/Testcontainers.Tests.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
</PropertyGroup>
1212
<ItemGroup>
1313
<PackageReference Include="Microsoft.NET.Test.Sdk"/>
14+
<PackageReference Include="BouncyCastle.Cryptography"/>
1415
<PackageReference Include="coverlet.collector"/>
1516
<PackageReference Include="xunit.runner.visualstudio"/>
1617
<PackageReference Include="xunit"/>

0 commit comments

Comments
 (0)