Skip to content

Commit 3b2856a

Browse files
committed
Regenerate root certificate without SubjectKeyIdentifier or KeyCertSign usage extensions, leaf certificates without a valid AuthorityKeyIdentifier
1 parent a309959 commit 3b2856a

2 files changed

Lines changed: 164 additions & 5 deletions

File tree

‎DevProxy.Proxy.Kestrel.Tests/CertificateAuthorityTests.cs‎

Lines changed: 141 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ public void FirstRun_CreatesValidRoot_AndPersistsToDisk()
5454
Assert.Equal("CN=Dev Proxy CA", root.Subject);
5555
Assert.True(root.HasPrivateKey);
5656
Assert.True(IsCertificateAuthority(root));
57+
Assert.True(HasSubjectKeyIdentifier(root));
58+
Assert.True(HasKeyCertSignUsage(root));
5759
Assert.True(root.NotAfter > DateTime.Now);
5860
Assert.True(File.Exists(_rootPath));
5961
}
@@ -105,6 +107,36 @@ public void NonCaRoot_Regenerates()
105107
Assert.True(IsCertificateAuthority(ca.RootCertificate));
106108
}
107109

110+
[Fact]
111+
public void CaRootWithoutSki_Regenerates()
112+
{
113+
File.WriteAllBytes(_rootPath, CreateCaWithoutSkiPfx());
114+
115+
using var ca = new CertificateAuthority(_rootPath, _leafDir);
116+
117+
Assert.True(HasSubjectKeyIdentifier(ca.RootCertificate));
118+
}
119+
120+
[Fact]
121+
public void CaRootWithoutKeyCertSignUsage_Regenerates()
122+
{
123+
File.WriteAllBytes(_rootPath, CreateCaWithoutKeyCertSignUsagePfx());
124+
125+
using var ca = new CertificateAuthority(_rootPath, _leafDir);
126+
127+
Assert.True(HasKeyCertSignUsage(ca.RootCertificate));
128+
}
129+
130+
[Fact]
131+
public void CaRootWithoutKeyUsageExtension_Regenerates()
132+
{
133+
File.WriteAllBytes(_rootPath, CreateCaWithoutKeyUsageExtensionPfx());
134+
135+
using var ca = new CertificateAuthority(_rootPath, _leafDir);
136+
137+
Assert.True(HasKeyCertSignUsage(ca.RootCertificate));
138+
}
139+
108140
[Fact]
109141
public void GetCertificateForHost_MintsLeafSignedByRoot_AndPersists()
110142
{
@@ -116,6 +148,7 @@ public void GetCertificateForHost_MintsLeafSignedByRoot_AndPersists()
116148
Assert.Equal("CN=example.com", leaf.Subject);
117149
Assert.Equal(ca.RootCertificate.Subject, leaf.Issuer);
118150
Assert.Contains("example.com", GetSanText(leaf), StringComparison.Ordinal);
151+
Assert.True(DoesAuthorityKeyIdentifierMatchCA(leaf, ca.RootCertificate));
119152
Assert.True(File.Exists(Path.Combine(_leafDir, "example.com.pfx")));
120153
}
121154

@@ -195,22 +228,111 @@ public void GetCertificateForHost_LeafDoesNotOutliveRoot()
195228
Assert.True(leaf.NotAfter <= ca.RootCertificate.NotAfter);
196229
}
197230

231+
[Fact]
232+
public void GetCertificateForHost_LeafWithoutAki_Regenerates()
233+
{
234+
using var ca = new CertificateAuthority(_rootPath, _leafDir);
235+
236+
_ = Directory.CreateDirectory(_leafDir);
237+
File.WriteAllBytes(Path.Combine(_leafDir, "example.com.pfx"), CreateLeafPfx(ca.RootCertificate, null));
238+
239+
var leaf = ca.GetCertificateForHost("example.com");
240+
241+
Assert.True(DoesAuthorityKeyIdentifierMatchCA(leaf, ca.RootCertificate));
242+
}
243+
244+
[Fact]
245+
public void GetCertificateForHost_LeafWithMismatchedAki_Regenerates()
246+
{
247+
var rootNotAfter = DateTimeOffset.UtcNow.AddDays(10);
248+
var other_ca = CreateRootCertificate(rootNotAfter);
249+
using var ca = new CertificateAuthority(_rootPath, _leafDir);
250+
251+
_ = Directory.CreateDirectory(_leafDir);
252+
File.WriteAllBytes(Path.Combine(_leafDir, "example.com.pfx"), CreateLeafPfx(ca.RootCertificate, other_ca));
253+
254+
var leaf = ca.GetCertificateForHost("example.com");
255+
256+
Assert.True(DoesAuthorityKeyIdentifierMatchCA(leaf, ca.RootCertificate));
257+
}
258+
198259
private static bool IsCertificateAuthority(X509Certificate2 cert) =>
199260
cert.Extensions.OfType<X509BasicConstraintsExtension>().FirstOrDefault()?.CertificateAuthority == true;
200261

262+
private static bool HasSubjectKeyIdentifier(X509Certificate2 cert) =>
263+
cert.Extensions.OfType<X509SubjectKeyIdentifierExtension>().Any();
264+
265+
private static bool HasKeyCertSignUsage(X509Certificate2 cert) =>
266+
(cert.Extensions
267+
.OfType<X509KeyUsageExtension>()
268+
.FirstOrDefault()?.KeyUsages & X509KeyUsageFlags.KeyCertSign) == X509KeyUsageFlags.KeyCertSign;
269+
270+
private static bool DoesAuthorityKeyIdentifierMatchCA(X509Certificate2 cert, X509Certificate2 ca)
271+
{
272+
var ski = ca.Extensions
273+
.OfType<X509SubjectKeyIdentifierExtension>()
274+
.FirstOrDefault()?.SubjectKeyIdentifierBytes;
275+
276+
var aki = cert.Extensions
277+
.OfType<X509AuthorityKeyIdentifierExtension>()
278+
.FirstOrDefault()?.KeyIdentifier;
279+
280+
return aki.HasValue
281+
&& ski.HasValue
282+
&& aki.Value.Span.SequenceEqual(ski.Value.Span);
283+
}
284+
201285
private static string GetSanText(X509Certificate2 cert)
202286
{
203287
var san = cert.Extensions.FirstOrDefault(e => e.Oid?.Value == "2.5.29.17");
204288
return san?.Format(false) ?? string.Empty;
205289
}
206290

207-
private static byte[] CreateRootPfx(DateTimeOffset NotAfter)
291+
private static X509Certificate2 CreateRootCertificate(DateTimeOffset NotAfter)
208292
{
209293
using var rsa = RSA.Create(2048);
210294
var req = new CertificateRequest("CN=Old Root", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
211295
req.CertificateExtensions.Add(new X509BasicConstraintsExtension(true, false, 0, true));
212296
req.CertificateExtensions.Add(new X509SubjectKeyIdentifierExtension(req.PublicKey, false));
213-
using var cert = req.CreateSelfSigned(NotAfter.AddDays(-10), NotAfter);
297+
req.CertificateExtensions.Add(new X509KeyUsageExtension(
298+
X509KeyUsageFlags.KeyCertSign | X509KeyUsageFlags.CrlSign, true));
299+
return req.CreateSelfSigned(NotAfter.AddDays(-10), NotAfter);
300+
}
301+
302+
private static byte[] CreateRootPfx(DateTimeOffset NotAfter)
303+
{
304+
using var cert = CreateRootCertificate(NotAfter);
305+
return cert.Export(X509ContentType.Pkcs12, string.Empty);
306+
}
307+
308+
private static byte[] CreateCaWithoutSkiPfx()
309+
{
310+
using var rsa = RSA.Create(2048);
311+
var req = new CertificateRequest("CN=CA Root Without SKI", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
312+
req.CertificateExtensions.Add(new X509BasicConstraintsExtension(true, false, 0, true));
313+
req.CertificateExtensions.Add(new X509KeyUsageExtension(
314+
X509KeyUsageFlags.KeyCertSign | X509KeyUsageFlags.CrlSign, true));
315+
using var cert = req.CreateSelfSigned(DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddYears(1));
316+
return cert.Export(X509ContentType.Pkcs12, string.Empty);
317+
}
318+
319+
private static byte[] CreateCaWithoutKeyCertSignUsagePfx()
320+
{
321+
using var rsa = RSA.Create(2048);
322+
var req = new CertificateRequest("CN=CA Root Without KeyCertSign", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
323+
req.CertificateExtensions.Add(new X509BasicConstraintsExtension(true, false, 0, true));
324+
req.CertificateExtensions.Add(new X509KeyUsageExtension(
325+
X509KeyUsageFlags.CrlSign, true));
326+
using var cert = req.CreateSelfSigned(DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddYears(1));
327+
return cert.Export(X509ContentType.Pkcs12, string.Empty);
328+
}
329+
330+
private static byte[] CreateCaWithoutKeyUsageExtensionPfx()
331+
{
332+
using var rsa = RSA.Create(2048);
333+
var req = new CertificateRequest("CN=CA Root Without KeyUsageExtension", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
334+
req.CertificateExtensions.Add(new X509BasicConstraintsExtension(true, false, 0, true));
335+
using var cert = req.CreateSelfSigned(DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddYears(1));
214336
return cert.Export(X509ContentType.Pkcs12, string.Empty);
215337
}
216338

@@ -222,4 +344,21 @@ private static byte[] CreateNonCaPfx()
222344
using var cert = req.CreateSelfSigned(DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddYears(1));
223345
return cert.Export(X509ContentType.Pkcs12, string.Empty);
224346
}
347+
348+
private static byte[] CreateLeafPfx(X509Certificate2 ca, X509Certificate2? aki_source)
349+
{
350+
using var rsa = RSA.Create(2048);
351+
var req = new CertificateRequest("CN=Leaf", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
352+
req.CertificateExtensions.Add(new X509BasicConstraintsExtension(false, false, 0, true));
353+
if (aki_source is not null) {
354+
req.CertificateExtensions.Add(
355+
X509AuthorityKeyIdentifierExtension.CreateFromCertificate(
356+
ca, includeKeyIdentifier: true, includeIssuerAndSerial: false));
357+
}
358+
var serialNumber = new byte[8];
359+
RandomNumberGenerator.Fill(serialNumber);
360+
using var cert = req.Create(ca, DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddDays(10), serialNumber);
361+
return cert.Export(X509ContentType.Pkcs12, string.Empty);
362+
}
363+
225364
}

‎DevProxy.Proxy.Kestrel/Internal/CertificateAuthority.cs‎

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,15 @@ private X509Certificate2 LoadOrCreateRoot()
148148
.OfType<X509BasicConstraintsExtension>()
149149
.FirstOrDefault()?.CertificateAuthority == true;
150150

151-
if (cert.HasPrivateKey && isCa && cert.NotAfter > DateTime.Now)
151+
var hasSKI = cert.Extensions
152+
.OfType<X509SubjectKeyIdentifierExtension>()
153+
.Any();
154+
155+
var hasKeyCertSign = (cert.Extensions
156+
.OfType<X509KeyUsageExtension>()
157+
.FirstOrDefault()?.KeyUsages & X509KeyUsageFlags.KeyCertSign) == X509KeyUsageFlags.KeyCertSign;
158+
159+
if (cert.HasPrivateKey && isCa && hasSKI && hasKeyCertSign && cert.NotAfter > DateTime.Now)
152160
{
153161
return cert;
154162
}
@@ -214,7 +222,7 @@ private X509Certificate2 LoadOrCreateLeaf(string host)
214222
return leaf;
215223
}
216224

217-
private static X509Certificate2? TryLoadLeaf(string path)
225+
private X509Certificate2? TryLoadLeaf(string path)
218226
{
219227
if (!File.Exists(path))
220228
{
@@ -228,7 +236,19 @@ private X509Certificate2 LoadOrCreateLeaf(string host)
228236
string.Empty,
229237
X509KeyStorageFlags.Exportable);
230238

231-
if (cert.HasPrivateKey && cert.NotAfter > DateTime.Now)
239+
var ski = _ca.Extensions
240+
.OfType<X509SubjectKeyIdentifierExtension>()
241+
.FirstOrDefault()?.SubjectKeyIdentifierBytes;
242+
243+
var aki = cert.Extensions
244+
.OfType<X509AuthorityKeyIdentifierExtension>()
245+
.FirstOrDefault()?.KeyIdentifier;
246+
247+
var akiMatchesCA = aki.HasValue
248+
&& ski.HasValue
249+
&& aki.Value.Span.SequenceEqual(ski.Value.Span);
250+
251+
if (cert.HasPrivateKey && akiMatchesCA && cert.NotAfter > DateTime.Now)
232252
{
233253
return cert;
234254
}

0 commit comments

Comments
 (0)