diff --git a/src/Testcontainers/Builders/MTlsEndpointAuthenticationProvider.cs b/src/Testcontainers/Builders/MTlsEndpointAuthenticationProvider.cs index 3f203c01d..f106b7599 100644 --- a/src/Testcontainers/Builders/MTlsEndpointAuthenticationProvider.cs +++ b/src/Testcontainers/Builders/MTlsEndpointAuthenticationProvider.cs @@ -7,19 +7,11 @@ namespace DotNet.Testcontainers.Builders using Docker.DotNet.X509; using DotNet.Testcontainers.Configurations; using JetBrains.Annotations; - using Org.BouncyCastle.Crypto; - using Org.BouncyCastle.Crypto.Parameters; - using Org.BouncyCastle.OpenSsl; - using Org.BouncyCastle.Pkcs; - using Org.BouncyCastle.Security; - using Org.BouncyCastle.X509; /// [PublicAPI] internal sealed class MTlsEndpointAuthenticationProvider : TlsEndpointAuthenticationProvider { - private static readonly X509CertificateParser CertificateParser = new X509CertificateParser(); - /// /// Initializes a new instance of the class. /// @@ -57,57 +49,15 @@ protected override X509Certificate2 GetClientCertificate() { var clientCertificateFilePath = Path.Combine(CertificatesDirectoryPath, ClientCertificateFileName); var clientCertificateKeyFilePath = Path.Combine(CertificatesDirectoryPath, ClientCertificateKeyFileName); - return CreateFromPemFile(clientCertificateFilePath, clientCertificateKeyFilePath); - } - - private static X509Certificate2 CreateFromPemFile(string certPemFilePath, string keyPemFilePath) - { - if (!File.Exists(certPemFilePath)) - { - throw new FileNotFoundException(certPemFilePath); - } - - if (!File.Exists(keyPemFilePath)) - { - throw new FileNotFoundException(keyPemFilePath); - } - - using (var keyPairStream = new StreamReader(keyPemFilePath)) - { - var store = new Pkcs12StoreBuilder().Build(); - - var certificate = CertificateParser.ReadCertificate(File.ReadAllBytes(certPemFilePath)); - - var password = Guid.NewGuid().ToString("D"); - var keyObject = new PemReader(keyPairStream).ReadObject(); - - var certificateEntry = new X509CertificateEntry(certificate); - - var keyParameter = ResolveKeyParameter(keyObject); - - var keyEntry = new AsymmetricKeyEntry(keyParameter); - store.SetKeyEntry(certificate.SubjectDN + "_key", keyEntry, new[] { certificateEntry }); - - using (var certificateStream = new MemoryStream()) - { - store.Save(certificateStream, password.ToCharArray(), new SecureRandom()); - return new X509Certificate2(Pkcs12Utilities.ConvertToDefiniteLength(certificateStream.ToArray()), password); - } - } - } - - private static AsymmetricKeyParameter ResolveKeyParameter(object keyObject) - { - switch (keyObject) - { - case AsymmetricCipherKeyPair ackp: - return ackp.Private; - case RsaPrivateCrtKeyParameters rpckp: - return rpckp; - default: - throw new ArgumentOutOfRangeException(nameof(keyObject), $"Unsupported asymmetric key entry encountered while trying to resolve key from input object '{keyObject.GetType()}'."); - } +#if NETSTANDARD + return Polyfills.X509Certificate2.CreateFromPemFile(clientCertificateFilePath, clientCertificateKeyFilePath); +#else + var certificate = X509Certificate2.CreateFromPemFile(clientCertificateFilePath, clientCertificateKeyFilePath); + // The certificate must be exported to PFX on Windows to avoid "No credentials are available in the security package": + // https://stackoverflow.com/questions/72096812/loading-x509certificate2-from-pem-file-results-in-no-credentials-are-available/72101855#72101855. + return OperatingSystem.IsWindows() ? new X509Certificate2(certificate.Export(X509ContentType.Pfx)) : certificate; +#endif } } } diff --git a/src/Testcontainers/Polyfills/X509Certificate2.cs b/src/Testcontainers/Polyfills/X509Certificate2.cs new file mode 100644 index 000000000..bcbb0da00 --- /dev/null +++ b/src/Testcontainers/Polyfills/X509Certificate2.cs @@ -0,0 +1,68 @@ +#if NETSTANDARD +namespace DotNet.Testcontainers.Polyfills +{ + using System; + using System.IO; + using Org.BouncyCastle.Crypto; + using Org.BouncyCastle.Crypto.Parameters; + using Org.BouncyCastle.OpenSsl; + using Org.BouncyCastle.Pkcs; + using Org.BouncyCastle.Security; + using Org.BouncyCastle.X509; + + public static class X509Certificate2 + { + private static readonly X509CertificateParser CertificateParser = new X509CertificateParser(); + + public static System.Security.Cryptography.X509Certificates.X509Certificate2 CreateFromPemFile(string certPemFilePath, string keyPemFilePath) + { + if (!File.Exists(certPemFilePath)) + { + throw new FileNotFoundException(certPemFilePath); + } + + if (!File.Exists(keyPemFilePath)) + { + throw new FileNotFoundException(keyPemFilePath); + } + + using (var keyPairStream = new StreamReader(keyPemFilePath)) + { + var store = new Pkcs12StoreBuilder().Build(); + + var certificate = CertificateParser.ReadCertificate(File.ReadAllBytes(certPemFilePath)); + + var password = Guid.NewGuid().ToString("D"); + + var keyObject = new PemReader(keyPairStream).ReadObject(); + + var certificateEntry = new X509CertificateEntry(certificate); + + var keyParameter = ResolveKeyParameter(keyObject); + + var keyEntry = new AsymmetricKeyEntry(keyParameter); + store.SetKeyEntry(certificate.SubjectDN + "_key", keyEntry, new[] { certificateEntry }); + + using (var certificateStream = new MemoryStream()) + { + store.Save(certificateStream, password.ToCharArray(), new SecureRandom()); + return new System.Security.Cryptography.X509Certificates.X509Certificate2(Pkcs12Utilities.ConvertToDefiniteLength(certificateStream.ToArray()), password); + } + } + } + + private static AsymmetricKeyParameter ResolveKeyParameter(object keyObject) + { + switch (keyObject) + { + case AsymmetricCipherKeyPair ackp: + return ackp.Private; + case RsaPrivateCrtKeyParameters rpckp: + return rpckp; + default: + throw new ArgumentOutOfRangeException(nameof(keyObject), $"Unsupported asymmetric key entry encountered while trying to resolve key from input object '{keyObject.GetType()}'."); + } + } + } +} +#endif diff --git a/src/Testcontainers/Testcontainers.csproj b/src/Testcontainers/Testcontainers.csproj index 12f06cd47..7280cbb10 100644 --- a/src/Testcontainers/Testcontainers.csproj +++ b/src/Testcontainers/Testcontainers.csproj @@ -6,14 +6,16 @@ - - - + + + + + diff --git a/tests/Testcontainers.Tests/Testcontainers.Tests.csproj b/tests/Testcontainers.Tests/Testcontainers.Tests.csproj index 21b9b100f..caa03761c 100644 --- a/tests/Testcontainers.Tests/Testcontainers.Tests.csproj +++ b/tests/Testcontainers.Tests/Testcontainers.Tests.csproj @@ -11,6 +11,7 @@ +