Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Use built-in PEM certificate import on .NET 6 and onwards #1139

Merged
merged 3 commits into from
Mar 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading