Skip to content

Commit

Permalink
feat(#370): Added support docker MTLS endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
vlaskal committed Aug 2, 2022
1 parent e5ec367 commit 4b618c7
Show file tree
Hide file tree
Showing 4 changed files with 164 additions and 0 deletions.
2 changes: 2 additions & 0 deletions Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
<GlobalPackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" />
<GlobalPackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" />
<PackageReference Update="JetBrains.Annotations" Version="2022.1.0" PrivateAssets="all" />
<PackageReference Update="Portable.BouncyCastle" Version="1.9.0" />
<PackageReference Update="Docker.DotNet" Version="3.125.10" />
<PackageReference Update="Docker.DotNet.X509" Version="3.125.2" />
<PackageReference Update="Microsoft.Bcl.AsyncInterfaces" Version="1.1.1" />
<PackageReference Update="Microsoft.Extensions.Logging.Abstractions" Version="3.1.26" />
<PackageReference Update="SharpZipLib" Version="1.3.3" />
Expand Down
159 changes: 159 additions & 0 deletions src/Testcontainers/Builders/MTlsEndpointAuthenticationProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
namespace DotNet.Testcontainers.Builders
{
using System;
using System.IO;
using System.Linq;
using System.Net.Security;
using System.Text.RegularExpressions;
using Docker.DotNet.X509;
using DotNet.Testcontainers.Configurations;
using Org.BouncyCastle.Asn1;
using Org.BouncyCastle.Asn1.Pkcs;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Pkcs;
using Org.BouncyCastle.Security;
using Org.BouncyCastle.X509;
using MSX509 = System.Security.Cryptography.X509Certificates;

/// <inheritdoc cref="IDockerRegistryAuthenticationProvider" />
internal sealed class MTlsEndpointAuthenticationProvider : DockerEndpointAuthenticationProvider
{
private const string DefaultCertPath = ".docker";
private const string DefaultCaCertFileName = "ca.pem";
private const string DefaultClientCertFileName = "cert.pem";
private const string DefaultClientKeyFileName = "key.pem";
private static readonly Regex PemData = new Regex("-----BEGIN (.*)-----(.*)-----END (.*)-----", RegexOptions.Multiline);

private readonly Uri dockerEngine;
private readonly bool dockerTlsVerifyEnabled;
private readonly string dockerCertPath;
private readonly Lazy<MSX509.X509Certificate2> caCertificate;
private readonly MSX509.X509Certificate2 clientCertificate;

public MTlsEndpointAuthenticationProvider()
{
var dockerHostValue = Environment.GetEnvironmentVariable("DOCKER_HOST");
var dockerTlsVerifyValue = Environment.GetEnvironmentVariable("DOCKER_TLS_VERIFY");
var dockerCertPathValue = Environment.GetEnvironmentVariable("DOCKER_CERT_PATH");

this.dockerEngine = Uri.TryCreate(dockerHostValue, UriKind.RelativeOrAbsolute, out var dockerHost) ? dockerHost : null;
this.dockerTlsVerifyEnabled = int.TryParse(dockerTlsVerifyValue, out var dockerTlsVerify) && dockerTlsVerify == 1;
this.dockerCertPath = dockerCertPathValue ?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), DefaultCertPath);
this.caCertificate = new Lazy<MSX509.X509Certificate2>(() => new MSX509.X509Certificate2(Path.Combine(this.dockerCertPath, DefaultCaCertFileName)));
this.clientCertificate = this.GetClientCertificate();
}

/// <inheritdoc />
public override bool IsApplicable()
{
return this.dockerEngine != null && this.dockerTlsVerifyEnabled;
}

/// <inheritdoc />
public override IDockerEndpointAuthenticationConfiguration GetAuthConfig()
{
return new DockerEndpointAuthenticationConfiguration(this.dockerEngine, new CertificateCredentials(this.clientCertificate)
{
ServerCertificateValidationCallback = this.ServerCertificateValidationCallback,
});
}

private static MSX509.X509Certificate2 GetCertificateWithKey(X509Certificate certificate, AsymmetricKeyParameter privateKey)
{
var store = new Pkcs12StoreBuilder()
.SetUseDerEncoding(true)
.Build();

var certEntry = new X509CertificateEntry(certificate);
store.SetKeyEntry(certificate.SubjectDN.ToString(), new AsymmetricKeyEntry(privateKey), new[] { certEntry });

byte[] pfxBytes;
var password = Guid.NewGuid().ToString("N");

using (var stream = new MemoryStream())
{
store.Save(stream, password.ToCharArray(), new SecureRandom());
pfxBytes = stream.ToArray();
}

var result = Pkcs12Utilities.ConvertToDefiniteLength(pfxBytes);

return new MSX509.X509Certificate2(result, password, MSX509.X509KeyStorageFlags.Exportable);
}

private static X509Certificate ReadPemCert(string pathToPemFile)
{
var x509CertificateParser = new X509CertificateParser();
var cert = x509CertificateParser.ReadCertificate(File.ReadAllBytes(pathToPemFile));

return cert;
}

private static AsymmetricKeyParameter ReadPemRsaPrivateKey(string pathToPemFile)
{
var keyData = File.ReadAllText(pathToPemFile)
.Replace("\n", string.Empty);

var keyMatch = PemData.Match(keyData);
if (!keyMatch.Success)
{
throw new NotSupportedException("Not supported key content");
}

if (keyMatch.Groups[1].Value != "RSA PRIVATE KEY")
{
throw new NotSupportedException("Not supported key type. Only RSA PRIVATE KEY is supported.");
}

var keyContent = keyMatch.Groups[2].Value;
var keyRawData = Convert.FromBase64String(keyContent);
var seq = Asn1Sequence.GetInstance(keyRawData);

if (seq.Count != 9)
{
throw new NotSupportedException("Invalid RSA Private Key ASN1 sequence.");
}

var keyStructure = RsaPrivateKeyStructure.GetInstance(seq);
var privateKeyInfo = PrivateKeyInfoFactory.CreatePrivateKeyInfo(new RsaPrivateCrtKeyParameters(keyStructure));
var key = PrivateKeyFactory.CreateKey(privateKeyInfo);

return key;
}

private MSX509.X509Certificate2 GetClientCertificate()
{
var certFilePath = Path.Combine(this.dockerCertPath, DefaultClientCertFileName);
var certificate = ReadPemCert(certFilePath);

var keyFilePath = Path.Combine(this.dockerCertPath, DefaultClientKeyFileName);
var key = ReadPemRsaPrivateKey(keyFilePath);

return GetCertificateWithKey(certificate, key);
}

private bool ServerCertificateValidationCallback(object sender, MSX509.X509Certificate certificate, MSX509.X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
switch (sslPolicyErrors)
{
case SslPolicyErrors.None:
return true;
case SslPolicyErrors.RemoteCertificateNotAvailable:
case SslPolicyErrors.RemoteCertificateNameMismatch:
return false;
case SslPolicyErrors.RemoteCertificateChainErrors:
default:
var validationChain = new MSX509.X509Chain();
validationChain.ChainPolicy.RevocationMode = MSX509.X509RevocationMode.NoCheck;
validationChain.ChainPolicy.VerificationFlags = MSX509.X509VerificationFlags.AllowUnknownCertificateAuthority;
validationChain.ChainPolicy.ExtraStore.Add(this.caCertificate.Value);
validationChain.ChainPolicy.ExtraStore.AddRange(chain.ChainElements.OfType<MSX509.X509ChainElement>().Select(element => element.Certificate).ToArray());
var isVerified = validationChain.Build(certificate as MSX509.X509Certificate2 ?? new MSX509.X509Certificate2(certificate));
var isSignedByExpectedRoot = validationChain.ChainElements[validationChain.ChainElements.Count - 1].Certificate.RawData.SequenceEqual(this.caCertificate.Value.RawData);
var isSuccess = isVerified && isSignedByExpectedRoot;
return isSuccess;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public static class TestcontainersSettings
private static readonly IDockerEndpointAuthenticationConfiguration DockerEndpointAuthConfig =
new IDockerEndpointAuthenticationProvider[]
{
new MTlsEndpointAuthenticationProvider(),
new TlsEndpointAuthenticationProvider(),
new EnvironmentEndpointAuthenticationProvider(),
new NpipeEndpointAuthenticationProvider(),
Expand Down
2 changes: 2 additions & 0 deletions src/Testcontainers/Testcontainers.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" />
<PackageReference Include="Docker.DotNet" />
<PackageReference Include="Docker.DotNet.X509" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
<PackageReference Include="Portable.BouncyCastle" />
<PackageReference Include="SharpZipLib" />
<PackageReference Include="System.Text.Json" />
</ItemGroup>
Expand Down

0 comments on commit 4b618c7

Please sign in to comment.