Skip to content

Commit 80c8747

Browse files
authored
feat: Add traceability and log messages (#15)
1 parent a9ebc1f commit 80c8747

32 files changed

+942
-1082
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,4 @@ jobs:
2424
- name: Build
2525
run: dotnet build -c Release --framework ${{ matrix.framework }}
2626
- name: Test
27-
run: dotnet test -c Release --framework ${{ matrix.framework }} --no-build
27+
run: dotnet test -c Release --framework ${{ matrix.framework }} --no-build --logger console

src/Docker.DotNet.BasicAuth/BasicAuthCredentials.cs

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,10 @@ public class BasicAuthCredentials : Credentials
55
private readonly bool _isTls;
66

77
private readonly MaybeSecureString _username;
8+
89
private readonly MaybeSecureString _password;
910

10-
public override HttpMessageHandler GetHandler(HttpMessageHandler innerHandler)
11-
{
12-
return new BasicAuthHandler(_username, _password, innerHandler);
13-
}
11+
private bool _disposed;
1412

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

30+
public override void Dispose()
31+
{
32+
Dispose(true);
33+
GC.SuppressFinalize(this);
34+
}
35+
3236
public override bool IsTlsCredentials()
3337
{
3438
return _isTls;
3539
}
3640

37-
public override void Dispose()
41+
public override HttpMessageHandler GetHandler(HttpMessageHandler handler)
42+
{
43+
return new BasicAuthHandler(_username, _password, handler);
44+
}
45+
46+
protected virtual void Dispose(bool disposing)
3847
{
39-
_username.Dispose();
40-
_password.Dispose();
48+
if (_disposed)
49+
{
50+
return;
51+
}
52+
53+
if (disposing)
54+
{
55+
_username.Dispose();
56+
_password.Dispose();
57+
}
58+
59+
_disposed = true;
4160
}
4261
}

src/Docker.DotNet.X509/CertificateCredentials.cs

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,37 +4,47 @@ public class CertificateCredentials : Credentials
44
{
55
private readonly X509Certificate2 _certificate;
66

7-
public CertificateCredentials(X509Certificate2 clientCertificate)
7+
private bool _disposed;
8+
9+
public CertificateCredentials(X509Certificate2 certificate)
810
{
9-
_certificate = clientCertificate;
11+
_certificate = certificate;
1012
}
1113

12-
public RemoteCertificateValidationCallback ServerCertificateValidationCallback { get; set; }
13-
14-
public override HttpMessageHandler GetHandler(HttpMessageHandler innerHandler)
14+
public override void Dispose()
1515
{
16-
var handler = (ManagedHandler)innerHandler;
17-
handler.ClientCertificates = new X509CertificateCollection
18-
{
19-
_certificate
20-
};
16+
Dispose(true);
17+
GC.SuppressFinalize(this);
18+
}
2119

22-
handler.ServerCertificateValidationCallback = ServerCertificateValidationCallback;
20+
public override bool IsTlsCredentials()
21+
{
22+
return true;
23+
}
2324

24-
if (handler.ServerCertificateValidationCallback == null)
25+
public override HttpMessageHandler GetHandler(HttpMessageHandler handler)
26+
{
27+
if (handler is HttpClientHandler httpClientHandler)
2528
{
26-
handler.ServerCertificateValidationCallback = ServicePointManager.ServerCertificateValidationCallback;
29+
httpClientHandler.ClientCertificates.Add(_certificate);
30+
httpClientHandler.ServerCertificateCustomValidationCallback = (_, _, _, _) => true;
2731
}
2832

2933
return handler;
3034
}
3135

32-
public override bool IsTlsCredentials()
36+
protected virtual void Dispose(bool disposing)
3337
{
34-
return true;
35-
}
38+
if (_disposed)
39+
{
40+
return;
41+
}
3642

37-
public override void Dispose()
38-
{
43+
if (disposing)
44+
{
45+
_certificate.Dispose();
46+
}
47+
48+
_disposed = true;
3949
}
4050
}

src/Docker.DotNet.X509/Docker.DotNet.X509.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
<PackageId>Docker.DotNet.Enhanced.X509</PackageId>
55
<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>
66
</PropertyGroup>
7+
<ItemGroup Condition="$(TargetFrameworkIdentifier) == '.NETStandard'">
8+
<PackageReference Include="BouncyCastle.Cryptography" Version="2.5.1" />
9+
</ItemGroup>
710
<ItemGroup>
811
<ProjectReference Include="..\Docker.DotNet\Docker.DotNet.csproj" />
912
</ItemGroup>

src/Docker.DotNet.X509/RSAUtil.cs

Lines changed: 14 additions & 168 deletions
Original file line numberDiff line numberDiff line change
@@ -2,179 +2,25 @@ namespace Docker.DotNet.X509;
22

33
public static class RSAUtil
44
{
5-
private const byte Padding = 0x00;
6-
75
public static X509Certificate2 GetCertFromPFX(string pfxFilePath, string password)
86
{
7+
#if NET9_0_OR_GREATER
8+
return X509CertificateLoader.LoadPkcs12FromFile(pfxFilePath, password);
9+
#else
910
return new X509Certificate2(pfxFilePath, password);
11+
#endif
1012
}
1113

12-
public static X509Certificate2 GetCertFromPFXSecure(string pfxFilePath, SecureString password)
13-
{
14-
return new X509Certificate2(pfxFilePath, password);
15-
}
16-
17-
public static X509Certificate2 GetCertFromPEMFiles(string certFilePath, string keyFilePath)
18-
{
19-
var cert = new X509Certificate2(certFilePath);
20-
cert.PrivateKey = ReadFromPemFile(keyFilePath);
21-
return cert;
22-
}
23-
24-
private static RSACryptoServiceProvider ReadFromPemFile(string pemFilePath)
25-
{
26-
var allBytes = File.ReadAllBytes(pemFilePath);
27-
var mem = new MemoryStream(allBytes);
28-
var startIndex = 0;
29-
var endIndex = 0;
30-
31-
using (var rdr = new BinaryReader(mem))
32-
{
33-
if (!TryReadUntil(rdr, "-----BEGIN RSA PRIVATE KEY-----"))
34-
{
35-
throw new Exception("Invalid file format expected. No begin tag.");
36-
}
37-
38-
startIndex = (int)(mem.Position);
39-
40-
const string endTag = "-----END RSA PRIVATE KEY-----";
41-
if (!TryReadUntil(rdr, endTag))
42-
{
43-
throw new Exception("Invalid file format expected. No end tag.");
44-
}
45-
46-
endIndex = (int)(mem.Position - endTag.Length - 2);
47-
}
48-
49-
// Convert the bytes from base64;
50-
var convertedBytes = Convert.FromBase64String(Encoding.UTF8.GetString(allBytes, startIndex, endIndex - startIndex));
51-
mem = new MemoryStream(convertedBytes);
52-
using (var rdr = new BinaryReader(mem))
53-
{
54-
var val = rdr.ReadUInt16();
55-
if (val != 0x8230)
56-
{
57-
throw new Exception("Invalid byte ordering.");
58-
}
59-
60-
// Discard the next bits of the version.
61-
rdr.ReadUInt32();
62-
if (rdr.ReadByte() != Padding)
63-
{
64-
throw new InvalidDataException("Invalid ASN.1 format.");
65-
}
66-
67-
var rsa = new RSAParameters()
68-
{
69-
Modulus = rdr.ReadBytes(ReadIntegerCount(rdr)),
70-
Exponent = rdr.ReadBytes(ReadIntegerCount(rdr)),
71-
D = rdr.ReadBytes(ReadIntegerCount(rdr)),
72-
P = rdr.ReadBytes(ReadIntegerCount(rdr)),
73-
Q = rdr.ReadBytes(ReadIntegerCount(rdr)),
74-
DP = rdr.ReadBytes(ReadIntegerCount(rdr)),
75-
DQ = rdr.ReadBytes(ReadIntegerCount(rdr)),
76-
InverseQ = rdr.ReadBytes(ReadIntegerCount(rdr))
77-
};
78-
79-
// Use "1" to indicate RSA.
80-
var csp = new CspParameters(1)
81-
{
82-
83-
// Set the KeyContainerName so that native code that looks up the private key
84-
// can find it. This produces a keyset file on disk as a side effect.
85-
KeyContainerName = pemFilePath
86-
};
87-
var rsaProvider = new RSACryptoServiceProvider(csp)
88-
{
89-
90-
// Setting to false makes sure the keystore file will be cleaned up
91-
// when the current process exits.
92-
PersistKeyInCsp = false
93-
};
94-
95-
// Import the private key into the keyset.
96-
rsaProvider.ImportParameters(rsa);
97-
98-
return rsaProvider;
99-
}
100-
}
101-
102-
/// <summary>
103-
/// Reads an integer count encoding in DER ASN.1 format.
104-
/// <summary>
105-
private static int ReadIntegerCount(BinaryReader rdr)
14+
public static X509Certificate2 GetCertFromPEM(string certFilePath, string keyFilePath)
10615
{
107-
const byte highBitOctet = 0x80;
108-
const byte ASN1_INTEGER = 0x02;
109-
110-
if (rdr.ReadByte() != ASN1_INTEGER)
111-
{
112-
throw new Exception("Integer tag expected.");
113-
}
114-
115-
int count = 0;
116-
var val = rdr.ReadByte();
117-
if ((val & highBitOctet) == highBitOctet)
118-
{
119-
byte numOfOctets = (byte)(val - highBitOctet);
120-
if (numOfOctets > 4)
121-
{
122-
throw new InvalidDataException("Too many octets.");
123-
}
124-
125-
for (var i = 0; i < numOfOctets; i++)
126-
{
127-
count <<= 8;
128-
count += rdr.ReadByte();
129-
}
130-
}
131-
else
132-
{
133-
count = val;
134-
}
135-
136-
while (rdr.ReadByte() == Padding)
137-
{
138-
count--;
139-
}
140-
141-
// The last read was a valid byte. Go back here.
142-
rdr.BaseStream.Seek(-1, SeekOrigin.Current);
143-
144-
return count;
145-
}
146-
147-
/// <summary>
148-
/// Reads until the matching PEM tag is found.
149-
/// <summary>
150-
private static bool TryReadUntil(BinaryReader rdr, string tag)
151-
{
152-
char delim = '\n';
153-
char c;
154-
char[] line = new char[64];
155-
int index;
156-
157-
try
158-
{
159-
do
160-
{
161-
index = 0;
162-
while ((c = rdr.ReadChar()) != delim)
163-
{
164-
if(c == '\r')
165-
{
166-
continue;
167-
}
168-
line[index] = c;
169-
index++;
170-
}
171-
} while (new string(line, 0, index) != tag);
172-
173-
return true;
174-
}
175-
catch (EndOfStreamException)
176-
{
177-
return false;
178-
}
16+
#if NETSTANDARD
17+
return Polyfills.X509Certificate2.CreateFromPemFile(certFilePath, keyFilePath);
18+
#elif NET9_0_OR_GREATER
19+
var certificate = X509Certificate2.CreateFromPemFile(certFilePath, keyFilePath);
20+
return OperatingSystem.IsWindows() ? X509CertificateLoader.LoadPkcs12(certificate.Export(X509ContentType.Pfx), null) : certificate;
21+
#elif NET6_0_OR_GREATER
22+
var certificate = X509Certificate2.CreateFromPemFile(certFilePath, keyFilePath);
23+
return OperatingSystem.IsWindows() ? new X509Certificate2(certificate.Export(X509ContentType.Pfx)) : certificate;
24+
#endif
17925
}
18026
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
#if NETSTANDARD
2+
namespace Docker.DotNet.X509.Polyfills;
3+
4+
using Org.BouncyCastle.Crypto;
5+
using Org.BouncyCastle.Crypto.Parameters;
6+
using Org.BouncyCastle.OpenSsl;
7+
using Org.BouncyCastle.Pkcs;
8+
using Org.BouncyCastle.Security;
9+
using Org.BouncyCastle.X509;
10+
11+
public static class X509Certificate2
12+
{
13+
private static readonly X509CertificateParser CertificateParser = new X509CertificateParser();
14+
15+
public static System.Security.Cryptography.X509Certificates.X509Certificate2 CreateFromPemFile(string certPemFilePath, string keyPemFilePath)
16+
{
17+
if (!File.Exists(certPemFilePath))
18+
{
19+
throw new FileNotFoundException(certPemFilePath);
20+
}
21+
22+
if (!File.Exists(keyPemFilePath))
23+
{
24+
throw new FileNotFoundException(keyPemFilePath);
25+
}
26+
27+
using var keyPairStream = new StreamReader(keyPemFilePath);
28+
29+
using var certificateStream = new MemoryStream();
30+
31+
var store = new Pkcs12StoreBuilder().Build();
32+
33+
var certificate = CertificateParser.ReadCertificate(File.ReadAllBytes(certPemFilePath));
34+
35+
var password = Guid.NewGuid().ToString("D");
36+
37+
var keyObject = new PemReader(keyPairStream).ReadObject();
38+
39+
var certificateEntry = new X509CertificateEntry(certificate);
40+
41+
var keyParameter = ResolveKeyParameter(keyObject);
42+
43+
var keyEntry = new AsymmetricKeyEntry(keyParameter);
44+
45+
store.SetKeyEntry(certificate.SubjectDN + "_key", keyEntry, new[] { certificateEntry });
46+
store.Save(certificateStream, password.ToCharArray(), new SecureRandom());
47+
48+
return new System.Security.Cryptography.X509Certificates.X509Certificate2(Pkcs12Utilities.ConvertToDefiniteLength(certificateStream.ToArray()), password);
49+
}
50+
51+
private static AsymmetricKeyParameter ResolveKeyParameter(object keyObject)
52+
{
53+
switch (keyObject)
54+
{
55+
case AsymmetricCipherKeyPair ackp:
56+
return ackp.Private;
57+
case RsaPrivateCrtKeyParameters rpckp:
58+
return rpckp;
59+
default:
60+
throw new ArgumentOutOfRangeException(nameof(keyObject), $"Unsupported asymmetric key entry encountered while trying to resolve key from input object '{keyObject.GetType()}'.");
61+
}
62+
}
63+
}
64+
#endif

0 commit comments

Comments
 (0)