Skip to content

feat: Add traceability and log messages #15

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

Merged
merged 14 commits into from
Mar 31, 2025
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@ jobs:
- name: Build
run: dotnet build -c Release --framework ${{ matrix.framework }}
- name: Test
run: dotnet test -c Release --framework ${{ matrix.framework }} --no-build
run: dotnet test -c Release --framework ${{ matrix.framework }} --no-build --logger console
33 changes: 26 additions & 7 deletions src/Docker.DotNet.BasicAuth/BasicAuthCredentials.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,10 @@ public class BasicAuthCredentials : Credentials
private readonly bool _isTls;

private readonly MaybeSecureString _username;

private readonly MaybeSecureString _password;

public override HttpMessageHandler GetHandler(HttpMessageHandler innerHandler)
{
return new BasicAuthHandler(_username, _password, innerHandler);
}
private bool _disposed;

public BasicAuthCredentials(SecureString username, SecureString password, bool isTls = false)
: this(new MaybeSecureString(username), new MaybeSecureString(password), isTls)
Expand All @@ -29,14 +27,35 @@ private BasicAuthCredentials(MaybeSecureString username, MaybeSecureString passw
_password = password;
}

public override void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}

public override bool IsTlsCredentials()
{
return _isTls;
}

public override void Dispose()
public override HttpMessageHandler GetHandler(HttpMessageHandler handler)
{
return new BasicAuthHandler(_username, _password, handler);
}

protected virtual void Dispose(bool disposing)
{
_username.Dispose();
_password.Dispose();
if (_disposed)
{
return;
}

if (disposing)
{
_username.Dispose();
_password.Dispose();
}

_disposed = true;
}
}
46 changes: 28 additions & 18 deletions src/Docker.DotNet.X509/CertificateCredentials.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,47 @@ public class CertificateCredentials : Credentials
{
private readonly X509Certificate2 _certificate;

public CertificateCredentials(X509Certificate2 clientCertificate)
private bool _disposed;

public CertificateCredentials(X509Certificate2 certificate)
{
_certificate = clientCertificate;
_certificate = certificate;
}

public RemoteCertificateValidationCallback ServerCertificateValidationCallback { get; set; }

public override HttpMessageHandler GetHandler(HttpMessageHandler innerHandler)
public override void Dispose()
{
var handler = (ManagedHandler)innerHandler;
handler.ClientCertificates = new X509CertificateCollection
{
_certificate
};
Dispose(true);
GC.SuppressFinalize(this);
}

handler.ServerCertificateValidationCallback = ServerCertificateValidationCallback;
public override bool IsTlsCredentials()
{
return true;
}

if (handler.ServerCertificateValidationCallback == null)
public override HttpMessageHandler GetHandler(HttpMessageHandler handler)
{
if (handler is HttpClientHandler httpClientHandler)
{
handler.ServerCertificateValidationCallback = ServicePointManager.ServerCertificateValidationCallback;
httpClientHandler.ClientCertificates.Add(_certificate);
httpClientHandler.ServerCertificateCustomValidationCallback = (_, _, _, _) => true;
}

return handler;
}

public override bool IsTlsCredentials()
protected virtual void Dispose(bool disposing)
{
return true;
}
if (_disposed)
{
return;
}

public override void Dispose()
{
if (disposing)
{
_certificate.Dispose();
}

_disposed = true;
}
}
3 changes: 3 additions & 0 deletions src/Docker.DotNet.X509/Docker.DotNet.X509.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
<PackageId>Docker.DotNet.Enhanced.X509</PackageId>
<Description>Docker.DotNet.X509 is a library that allows you to use certificate authentication with a remote Docker engine programmatically in your .NET applications.</Description>
</PropertyGroup>
<ItemGroup Condition="$(TargetFrameworkIdentifier) == '.NETStandard'">
<PackageReference Include="BouncyCastle.Cryptography" Version="2.5.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Docker.DotNet\Docker.DotNet.csproj" />
</ItemGroup>
Expand Down
182 changes: 14 additions & 168 deletions src/Docker.DotNet.X509/RSAUtil.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,179 +2,25 @@ namespace Docker.DotNet.X509;

public static class RSAUtil
{
private const byte Padding = 0x00;

public static X509Certificate2 GetCertFromPFX(string pfxFilePath, string password)
{
#if NET9_0_OR_GREATER
return X509CertificateLoader.LoadPkcs12FromFile(pfxFilePath, password);
#else
return new X509Certificate2(pfxFilePath, password);
#endif
}

public static X509Certificate2 GetCertFromPFXSecure(string pfxFilePath, SecureString password)
{
return new X509Certificate2(pfxFilePath, password);
}

public static X509Certificate2 GetCertFromPEMFiles(string certFilePath, string keyFilePath)
{
var cert = new X509Certificate2(certFilePath);
cert.PrivateKey = ReadFromPemFile(keyFilePath);
return cert;
}

private static RSACryptoServiceProvider ReadFromPemFile(string pemFilePath)
{
var allBytes = File.ReadAllBytes(pemFilePath);
var mem = new MemoryStream(allBytes);
var startIndex = 0;
var endIndex = 0;

using (var rdr = new BinaryReader(mem))
{
if (!TryReadUntil(rdr, "-----BEGIN RSA PRIVATE KEY-----"))
{
throw new Exception("Invalid file format expected. No begin tag.");
}

startIndex = (int)(mem.Position);

const string endTag = "-----END RSA PRIVATE KEY-----";
if (!TryReadUntil(rdr, endTag))
{
throw new Exception("Invalid file format expected. No end tag.");
}

endIndex = (int)(mem.Position - endTag.Length - 2);
}

// Convert the bytes from base64;
var convertedBytes = Convert.FromBase64String(Encoding.UTF8.GetString(allBytes, startIndex, endIndex - startIndex));
mem = new MemoryStream(convertedBytes);
using (var rdr = new BinaryReader(mem))
{
var val = rdr.ReadUInt16();
if (val != 0x8230)
{
throw new Exception("Invalid byte ordering.");
}

// Discard the next bits of the version.
rdr.ReadUInt32();
if (rdr.ReadByte() != Padding)
{
throw new InvalidDataException("Invalid ASN.1 format.");
}

var rsa = new RSAParameters()
{
Modulus = rdr.ReadBytes(ReadIntegerCount(rdr)),
Exponent = rdr.ReadBytes(ReadIntegerCount(rdr)),
D = rdr.ReadBytes(ReadIntegerCount(rdr)),
P = rdr.ReadBytes(ReadIntegerCount(rdr)),
Q = rdr.ReadBytes(ReadIntegerCount(rdr)),
DP = rdr.ReadBytes(ReadIntegerCount(rdr)),
DQ = rdr.ReadBytes(ReadIntegerCount(rdr)),
InverseQ = rdr.ReadBytes(ReadIntegerCount(rdr))
};

// Use "1" to indicate RSA.
var csp = new CspParameters(1)
{

// Set the KeyContainerName so that native code that looks up the private key
// can find it. This produces a keyset file on disk as a side effect.
KeyContainerName = pemFilePath
};
var rsaProvider = new RSACryptoServiceProvider(csp)
{

// Setting to false makes sure the keystore file will be cleaned up
// when the current process exits.
PersistKeyInCsp = false
};

// Import the private key into the keyset.
rsaProvider.ImportParameters(rsa);

return rsaProvider;
}
}

/// <summary>
/// Reads an integer count encoding in DER ASN.1 format.
/// <summary>
private static int ReadIntegerCount(BinaryReader rdr)
public static X509Certificate2 GetCertFromPEM(string certFilePath, string keyFilePath)
{
const byte highBitOctet = 0x80;
const byte ASN1_INTEGER = 0x02;

if (rdr.ReadByte() != ASN1_INTEGER)
{
throw new Exception("Integer tag expected.");
}

int count = 0;
var val = rdr.ReadByte();
if ((val & highBitOctet) == highBitOctet)
{
byte numOfOctets = (byte)(val - highBitOctet);
if (numOfOctets > 4)
{
throw new InvalidDataException("Too many octets.");
}

for (var i = 0; i < numOfOctets; i++)
{
count <<= 8;
count += rdr.ReadByte();
}
}
else
{
count = val;
}

while (rdr.ReadByte() == Padding)
{
count--;
}

// The last read was a valid byte. Go back here.
rdr.BaseStream.Seek(-1, SeekOrigin.Current);

return count;
}

/// <summary>
/// Reads until the matching PEM tag is found.
/// <summary>
private static bool TryReadUntil(BinaryReader rdr, string tag)
{
char delim = '\n';
char c;
char[] line = new char[64];
int index;

try
{
do
{
index = 0;
while ((c = rdr.ReadChar()) != delim)
{
if(c == '\r')
{
continue;
}
line[index] = c;
index++;
}
} while (new string(line, 0, index) != tag);

return true;
}
catch (EndOfStreamException)
{
return false;
}
#if NETSTANDARD
return Polyfills.X509Certificate2.CreateFromPemFile(certFilePath, keyFilePath);
#elif NET9_0_OR_GREATER
var certificate = X509Certificate2.CreateFromPemFile(certFilePath, keyFilePath);
return OperatingSystem.IsWindows() ? X509CertificateLoader.LoadPkcs12(certificate.Export(X509ContentType.Pfx), null) : certificate;
#elif NET6_0_OR_GREATER
var certificate = X509Certificate2.CreateFromPemFile(certFilePath, keyFilePath);
return OperatingSystem.IsWindows() ? new X509Certificate2(certificate.Export(X509ContentType.Pfx)) : certificate;
#endif
}
}
64 changes: 64 additions & 0 deletions src/Docker.DotNet.X509/X509Certificate2.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#if NETSTANDARD
namespace Docker.DotNet.X509.Polyfills;

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);

using var certificateStream = new MemoryStream();

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 });
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
Loading
Loading