Skip to content

Commit

Permalink
feat: Use built-in PEM certificate import on .NET 6 and onwards (#1139)
Browse files Browse the repository at this point in the history
Co-authored-by: Andre Hofmeister <[email protected]>
  • Loading branch information
0xced and HofmeisterAn authored Mar 11, 2024
1 parent 1cfc850 commit 6e6ccb5
Show file tree
Hide file tree
Showing 4 changed files with 82 additions and 61 deletions.
66 changes: 8 additions & 58 deletions src/Testcontainers/Builders/MTlsEndpointAuthenticationProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/// <inheritdoc cref="IDockerRegistryAuthenticationProvider" />
[PublicAPI]
internal sealed class MTlsEndpointAuthenticationProvider : TlsEndpointAuthenticationProvider
{
private static readonly X509CertificateParser CertificateParser = new X509CertificateParser();

/// <summary>
/// Initializes a new instance of the <see cref="MTlsEndpointAuthenticationProvider" /> class.
/// </summary>
Expand Down Expand Up @@ -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
}
}
}
68 changes: 68 additions & 0 deletions src/Testcontainers/Polyfills/X509Certificate2.cs
Original file line number Diff line number Diff line change
@@ -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
8 changes: 5 additions & 3 deletions src/Testcontainers/Testcontainers.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" VersionOverride="2023.3.0" PrivateAssets="All"/>
<PackageReference Include="BouncyCastle.Cryptography"/>
<PackageReference Include="Docker.DotNet.X509"/>
<PackageReference Include="Docker.DotNet"/>
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces"/>
<PackageReference Include="Microsoft.Bcl.HashCode"/>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions"/>
<PackageReference Include="SharpZipLib"/>
<PackageReference Include="SSH.NET"/>
</ItemGroup>
<ItemGroup Condition="$(TargetFrameworkIdentifier) == '.NETStandard'">
<PackageReference Include="BouncyCastle.Cryptography"/>
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces"/>
<PackageReference Include="Microsoft.Bcl.HashCode"/>
<PackageReference Include="System.Text.Json"/>
</ItemGroup>
</Project>
1 change: 1 addition & 0 deletions tests/Testcontainers.Tests/Testcontainers.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk"/>
<PackageReference Include="BouncyCastle.Cryptography"/>
<PackageReference Include="coverlet.collector"/>
<PackageReference Include="xunit.runner.visualstudio"/>
<PackageReference Include="xunit"/>
Expand Down

0 comments on commit 6e6ccb5

Please sign in to comment.