From 4b618c7e5940e5c03e1e52770d2d2e92d43b118a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vlastimil=20Kalu=C5=BEa?= Date: Tue, 2 Aug 2022 08:31:20 +0200 Subject: [PATCH] feat(#370): Added support docker MTLS endpoint --- Packages.props | 2 + .../MTlsEndpointAuthenticationProvider.cs | 159 ++++++++++++++++++ .../Configurations/TestcontainersSettings.cs | 1 + src/Testcontainers/Testcontainers.csproj | 2 + 4 files changed, 164 insertions(+) create mode 100644 src/Testcontainers/Builders/MTlsEndpointAuthenticationProvider.cs diff --git a/Packages.props b/Packages.props index 76fc75462..6234daee4 100644 --- a/Packages.props +++ b/Packages.props @@ -5,7 +5,9 @@ + + diff --git a/src/Testcontainers/Builders/MTlsEndpointAuthenticationProvider.cs b/src/Testcontainers/Builders/MTlsEndpointAuthenticationProvider.cs new file mode 100644 index 000000000..7b5a02d13 --- /dev/null +++ b/src/Testcontainers/Builders/MTlsEndpointAuthenticationProvider.cs @@ -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; + + /// + 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 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(() => new MSX509.X509Certificate2(Path.Combine(this.dockerCertPath, DefaultCaCertFileName))); + this.clientCertificate = this.GetClientCertificate(); + } + + /// + public override bool IsApplicable() + { + return this.dockerEngine != null && this.dockerTlsVerifyEnabled; + } + + /// + 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().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; + } + } + } +} diff --git a/src/Testcontainers/Configurations/TestcontainersSettings.cs b/src/Testcontainers/Configurations/TestcontainersSettings.cs index b4e6a821b..e4f508725 100644 --- a/src/Testcontainers/Configurations/TestcontainersSettings.cs +++ b/src/Testcontainers/Configurations/TestcontainersSettings.cs @@ -27,6 +27,7 @@ public static class TestcontainersSettings private static readonly IDockerEndpointAuthenticationConfiguration DockerEndpointAuthConfig = new IDockerEndpointAuthenticationProvider[] { + new MTlsEndpointAuthenticationProvider(), new TlsEndpointAuthenticationProvider(), new EnvironmentEndpointAuthenticationProvider(), new NpipeEndpointAuthenticationProvider(), diff --git a/src/Testcontainers/Testcontainers.csproj b/src/Testcontainers/Testcontainers.csproj index 1c67783b2..37b0cf66c 100644 --- a/src/Testcontainers/Testcontainers.csproj +++ b/src/Testcontainers/Testcontainers.csproj @@ -9,8 +9,10 @@ + +