-
-
Notifications
You must be signed in to change notification settings - Fork 288
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(#370): Added support docker MTLS endpoint
- Loading branch information
Showing
4 changed files
with
164 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
159 changes: 159 additions & 0 deletions
159
src/Testcontainers/Builders/MTlsEndpointAuthenticationProvider.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters