Skip to content

Commit 90b3332

Browse files
authored
Merge pull request #64263 from danegsta/danegsta/skid
Add Subject Key Identifier and Authority Key Identifier extensions to the generated dev cert
2 parents 48fa780 + 595b2be commit 90b3332

File tree

3 files changed

+154
-3
lines changed

3 files changed

+154
-3
lines changed

src/Shared/CertificateGeneration/CertificateManager.cs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,19 @@ namespace Microsoft.AspNetCore.Certificates.Generation;
1919

2020
internal abstract class CertificateManager
2121
{
22-
internal const int CurrentAspNetCoreCertificateVersion = 4;
23-
internal const int CurrentMinimumAspNetCoreCertificateVersion = 4;
22+
internal const int CurrentAspNetCoreCertificateVersion = 5;
23+
internal const int CurrentMinimumAspNetCoreCertificateVersion = 5;
2424

2525
// OID used for HTTPS certs
2626
internal const string AspNetHttpsOid = "1.3.6.1.4.1.311.84.1.1";
2727
internal const string AspNetHttpsOidFriendlyName = "ASP.NET Core HTTPS development certificate";
2828

2929
private const string ServerAuthenticationEnhancedKeyUsageOid = "1.3.6.1.5.5.7.3.1";
3030
private const string ServerAuthenticationEnhancedKeyUsageOidFriendlyName = "Server Authentication";
31-
31+
32+
internal const string SubjectKeyIdentifierOid = "2.5.29.14";
33+
internal const string AuthorityKeyIdentifierOid = "2.5.29.35";
34+
3235
// dns names of the host from a container
3336
private const string LocalhostDockerHttpsDnsName = "host.docker.internal";
3437
private const string ContainersDockerHttpsDnsName = "host.containers.internal";
@@ -828,6 +831,20 @@ internal static X509Certificate2 CreateSelfSignedCertificate(
828831
request.CertificateExtensions.Add(extension);
829832
}
830833

834+
// Only add the SKI and AKI extensions if neither is already present.
835+
// OpenSSL needs these to correctly identify the trust chain for a private key. If multiple certificates don't have a subject key identifier and share the same subject,
836+
// the wrong certificate can be chosen for the trust chain, leading to validation errors.
837+
if (!request.CertificateExtensions.Any(ext => ext.Oid?.Value is SubjectKeyIdentifierOid or AuthorityKeyIdentifierOid))
838+
{
839+
// RFC 5280 section 4.2.1.2
840+
var subjectKeyIdentifier = new X509SubjectKeyIdentifierExtension(request.PublicKey, X509SubjectKeyIdentifierHashAlgorithm.Sha256, critical: false);
841+
// RFC 5280 section 4.2.1.1
842+
var authorityKeyIdentifier = X509AuthorityKeyIdentifierExtension.CreateFromSubjectKeyIdentifier(subjectKeyIdentifier);
843+
844+
request.CertificateExtensions.Add(subjectKeyIdentifier);
845+
request.CertificateExtensions.Add(authorityKeyIdentifier);
846+
}
847+
831848
var result = request.CreateSelfSigned(notBefore, notAfter);
832849
return result;
833850

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Security.Cryptography;
5+
using System.Security.Cryptography.X509Certificates;
6+
using Microsoft.AspNetCore.Certificates.Generation;
7+
8+
namespace Microsoft.AspNetCore.Internal.Tests;
9+
10+
public class CertificateManagerTests
11+
{
12+
[Fact]
13+
public void CreateAspNetCoreHttpsDevelopmentCertificateIsValid()
14+
{
15+
var notBefore = DateTimeOffset.Now;
16+
var notAfter = notBefore.AddMinutes(5);
17+
var certificate = CertificateManager.Instance.CreateAspNetCoreHttpsDevelopmentCertificate(notBefore, notAfter);
18+
19+
// Certificate should be valid for the expected time range
20+
Assert.Equal(notBefore, certificate.NotBefore, TimeSpan.FromSeconds(1));
21+
Assert.Equal(notAfter, certificate.NotAfter, TimeSpan.FromSeconds(1));
22+
23+
// Certificate should have a private key
24+
Assert.True(certificate.HasPrivateKey);
25+
26+
// Certificate should be recognized as an ASP.NET Core HTTPS development certificate
27+
Assert.True(CertificateManager.IsHttpsDevelopmentCertificate(certificate));
28+
29+
// Certificate should include a Subject Key Identifier extension
30+
var subjectKeyIdentifier = Assert.Single(certificate.Extensions.OfType<X509SubjectKeyIdentifierExtension>());
31+
32+
// Certificate should include an Authority Key Identifier extension
33+
var authorityKeyIdentifier = Assert.Single(certificate.Extensions.OfType<X509AuthorityKeyIdentifierExtension>());
34+
35+
// The Authority Key Identifier should match the Subject Key Identifier
36+
Assert.True(authorityKeyIdentifier.KeyIdentifier?.Span.SequenceEqual(subjectKeyIdentifier.SubjectKeyIdentifierBytes.Span));
37+
}
38+
39+
[Fact]
40+
public void CreateSelfSignedCertificate_ExistingSubjectKeyIdentifierExtension()
41+
{
42+
var subject = new X500DistinguishedName("CN=TestCertificate");
43+
var notBefore = DateTimeOffset.Now;
44+
var notAfter = notBefore.AddMinutes(5);
45+
var testSubjectKeyId = new byte[] { 1, 2, 3, 4, 5 };
46+
var extensions = new List<X509Extension>
47+
{
48+
new X509SubjectKeyIdentifierExtension(testSubjectKeyId, critical: false),
49+
};
50+
51+
var certificate = CertificateManager.CreateSelfSignedCertificate(subject, extensions, notBefore, notAfter);
52+
53+
Assert.Equal(notBefore, certificate.NotBefore, TimeSpan.FromSeconds(1));
54+
Assert.Equal(notAfter, certificate.NotAfter, TimeSpan.FromSeconds(1));
55+
56+
// Certificate had an existing Subject Key Identifier extension, so AKID should not be added
57+
Assert.Empty(certificate.Extensions.OfType<X509AuthorityKeyIdentifierExtension>());
58+
59+
var subjectKeyIdentifier = Assert.Single(certificate.Extensions.OfType<X509SubjectKeyIdentifierExtension>());
60+
Assert.True(subjectKeyIdentifier.SubjectKeyIdentifierBytes.Span.SequenceEqual(testSubjectKeyId));
61+
}
62+
63+
[Fact]
64+
public void CreateSelfSignedCertificate_ExistingRawSubjectKeyIdentifierExtension()
65+
{
66+
var subject = new X500DistinguishedName("CN=TestCertificate");
67+
var notBefore = DateTimeOffset.Now;
68+
var notAfter = notBefore.AddMinutes(5);
69+
var testSubjectKeyId = new byte[] { 5, 4, 3, 2, 1 };
70+
// Pass the extension as a raw X509Extension to simulate pre-encoded data
71+
var extension = new X509SubjectKeyIdentifierExtension(testSubjectKeyId, critical: false);
72+
var extensions = new List<X509Extension>
73+
{
74+
new X509Extension(extension.Oid, extension.RawData, extension.Critical),
75+
};
76+
77+
var certificate = CertificateManager.CreateSelfSignedCertificate(subject, extensions, notBefore, notAfter);
78+
79+
Assert.Equal(notBefore, certificate.NotBefore, TimeSpan.FromSeconds(1));
80+
Assert.Equal(notAfter, certificate.NotAfter, TimeSpan.FromSeconds(1));
81+
82+
Assert.Empty(certificate.Extensions.OfType<X509AuthorityKeyIdentifierExtension>());
83+
84+
var subjectKeyIdentifier = Assert.Single(certificate.Extensions.OfType<X509SubjectKeyIdentifierExtension>());
85+
Assert.True(subjectKeyIdentifier.SubjectKeyIdentifierBytes.Span.SequenceEqual(testSubjectKeyId));
86+
}
87+
88+
[Fact]
89+
public void CreateSelfSignedCertificate_ExistingRawAuthorityKeyIdentifierExtension()
90+
{
91+
var subject = new X500DistinguishedName("CN=TestCertificate");
92+
var notBefore = DateTimeOffset.Now;
93+
var notAfter = notBefore.AddMinutes(5);
94+
var testSubjectKeyId = new byte[] { 9, 8, 7, 6, 5 };
95+
// Pass the extension as a raw X509Extension to simulate pre-encoded data
96+
var subjectExtension = new X509SubjectKeyIdentifierExtension(testSubjectKeyId, critical: false);
97+
var authorityExtension = X509AuthorityKeyIdentifierExtension.CreateFromSubjectKeyIdentifier(subjectExtension);
98+
var extensions = new List<X509Extension>
99+
{
100+
new X509Extension(authorityExtension.Oid, authorityExtension.RawData, authorityExtension.Critical),
101+
};
102+
103+
var certificate = CertificateManager.CreateSelfSignedCertificate(subject, extensions, notBefore, notAfter);
104+
105+
Assert.Equal(notBefore, certificate.NotBefore, TimeSpan.FromSeconds(1));
106+
Assert.Equal(notAfter, certificate.NotAfter, TimeSpan.FromSeconds(1));
107+
108+
Assert.Empty(certificate.Extensions.OfType<X509SubjectKeyIdentifierExtension>());
109+
110+
var authorityKeyIdentifier = Assert.Single(certificate.Extensions.OfType<X509AuthorityKeyIdentifierExtension>());
111+
Assert.True(authorityKeyIdentifier.KeyIdentifier?.Span.SequenceEqual(testSubjectKeyId));
112+
}
113+
114+
[Fact]
115+
public void CreateSelfSignedCertificate_NoSubjectKeyIdentifierExtension()
116+
{
117+
var subject = new X500DistinguishedName("CN=TestCertificate");
118+
var notBefore = DateTimeOffset.Now;
119+
var notAfter = notBefore.AddMinutes(5);
120+
var extensions = new List<X509Extension>();
121+
122+
var certificate = CertificateManager.CreateSelfSignedCertificate(subject, extensions, notBefore, notAfter);
123+
124+
Assert.Equal(notBefore, certificate.NotBefore, TimeSpan.FromSeconds(1));
125+
Assert.Equal(notAfter, certificate.NotAfter, TimeSpan.FromSeconds(1));
126+
127+
var subjectKeyIdentifier = Assert.Single(certificate.Extensions.OfType<X509SubjectKeyIdentifierExtension>());
128+
var authorityKeyIdentifier = Assert.Single(certificate.Extensions.OfType<X509AuthorityKeyIdentifierExtension>());
129+
130+
// The Authority Key Identifier should match the Subject Key Identifier
131+
Assert.True(authorityKeyIdentifier.KeyIdentifier?.Span.SequenceEqual(subjectKeyIdentifier.SubjectKeyIdentifierBytes.Span));
132+
}
133+
}

src/Shared/test/Shared.Tests/Microsoft.AspNetCore.Shared.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
<ItemGroup>
1212
<Compile Include="$(SharedSourceRoot)ActivatorUtilities\**\*.cs" Link="Shared\ActivatorUtilities\%(Filename)%(Extension)" />
13+
<Compile Include="$(SharedSourceRoot)CertificateGeneration\**\*.cs" Link="Shared\CertificateGeneration\%(Filename)%(Extension)" />
1314
<Compile Include="$(SharedSourceRoot)CommandLineUtils\**\*.cs" Link="Shared\CommandLineUtils\%(Filename)%(Extension)" />
1415
<Compile Include="$(SharedSourceRoot)ClosedGenericMatcher\*.cs" Link="Shared\ClosedGenericMatcher\%(Filename)%(Extension)" />
1516
<Compile Include="$(SharedSourceRoot)CopyOnWriteDictionary\*.cs" Link="Shared\CopyOnWriteDictionary\%(Filename)%(Extension)" />

0 commit comments

Comments
 (0)