diff --git a/src/Shared/CertificateGeneration/CertificateManager.cs b/src/Shared/CertificateGeneration/CertificateManager.cs index aba1240c7b12..c07337210cfb 100644 --- a/src/Shared/CertificateGeneration/CertificateManager.cs +++ b/src/Shared/CertificateGeneration/CertificateManager.cs @@ -19,8 +19,8 @@ namespace Microsoft.AspNetCore.Certificates.Generation; internal abstract class CertificateManager { - internal const int CurrentAspNetCoreCertificateVersion = 4; - internal const int CurrentMinimumAspNetCoreCertificateVersion = 4; + internal const int CurrentAspNetCoreCertificateVersion = 5; + internal const int CurrentMinimumAspNetCoreCertificateVersion = 5; // OID used for HTTPS certs internal const string AspNetHttpsOid = "1.3.6.1.4.1.311.84.1.1"; @@ -28,7 +28,10 @@ internal abstract class CertificateManager private const string ServerAuthenticationEnhancedKeyUsageOid = "1.3.6.1.5.5.7.3.1"; private const string ServerAuthenticationEnhancedKeyUsageOidFriendlyName = "Server Authentication"; - + + internal const string SubjectKeyIdentifierOid = "2.5.29.14"; + internal const string AuthorityKeyIdentifierOid = "2.5.29.35"; + // dns names of the host from a container private const string LocalhostDockerHttpsDnsName = "host.docker.internal"; private const string ContainersDockerHttpsDnsName = "host.containers.internal"; @@ -828,6 +831,20 @@ internal static X509Certificate2 CreateSelfSignedCertificate( request.CertificateExtensions.Add(extension); } + // Only add the SKI and AKI extensions if neither is already present. + // 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, + // the wrong certificate can be chosen for the trust chain, leading to validation errors. + if (!request.CertificateExtensions.Any(ext => ext.Oid?.Value is SubjectKeyIdentifierOid or AuthorityKeyIdentifierOid)) + { + // RFC 5280 section 4.2.1.2 + var subjectKeyIdentifier = new X509SubjectKeyIdentifierExtension(request.PublicKey, X509SubjectKeyIdentifierHashAlgorithm.Sha256, critical: false); + // RFC 5280 section 4.2.1.1 + var authorityKeyIdentifier = X509AuthorityKeyIdentifierExtension.CreateFromSubjectKeyIdentifier(subjectKeyIdentifier); + + request.CertificateExtensions.Add(subjectKeyIdentifier); + request.CertificateExtensions.Add(authorityKeyIdentifier); + } + var result = request.CreateSelfSigned(notBefore, notAfter); return result; diff --git a/src/Shared/test/Shared.Tests/CertificateManagerTests.cs b/src/Shared/test/Shared.Tests/CertificateManagerTests.cs new file mode 100644 index 000000000000..d4ef5975fab9 --- /dev/null +++ b/src/Shared/test/Shared.Tests/CertificateManagerTests.cs @@ -0,0 +1,133 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using Microsoft.AspNetCore.Certificates.Generation; + +namespace Microsoft.AspNetCore.Internal.Tests; + +public class CertificateManagerTests +{ + [Fact] + public void CreateAspNetCoreHttpsDevelopmentCertificateIsValid() + { + var notBefore = DateTimeOffset.Now; + var notAfter = notBefore.AddMinutes(5); + var certificate = CertificateManager.Instance.CreateAspNetCoreHttpsDevelopmentCertificate(notBefore, notAfter); + + // Certificate should be valid for the expected time range + Assert.Equal(notBefore, certificate.NotBefore, TimeSpan.FromSeconds(1)); + Assert.Equal(notAfter, certificate.NotAfter, TimeSpan.FromSeconds(1)); + + // Certificate should have a private key + Assert.True(certificate.HasPrivateKey); + + // Certificate should be recognized as an ASP.NET Core HTTPS development certificate + Assert.True(CertificateManager.IsHttpsDevelopmentCertificate(certificate)); + + // Certificate should include a Subject Key Identifier extension + var subjectKeyIdentifier = Assert.Single(certificate.Extensions.OfType()); + + // Certificate should include an Authority Key Identifier extension + var authorityKeyIdentifier = Assert.Single(certificate.Extensions.OfType()); + + // The Authority Key Identifier should match the Subject Key Identifier + Assert.True(authorityKeyIdentifier.KeyIdentifier?.Span.SequenceEqual(subjectKeyIdentifier.SubjectKeyIdentifierBytes.Span)); + } + + [Fact] + public void CreateSelfSignedCertificate_ExistingSubjectKeyIdentifierExtension() + { + var subject = new X500DistinguishedName("CN=TestCertificate"); + var notBefore = DateTimeOffset.Now; + var notAfter = notBefore.AddMinutes(5); + var testSubjectKeyId = new byte[] { 1, 2, 3, 4, 5 }; + var extensions = new List + { + new X509SubjectKeyIdentifierExtension(testSubjectKeyId, critical: false), + }; + + var certificate = CertificateManager.CreateSelfSignedCertificate(subject, extensions, notBefore, notAfter); + + Assert.Equal(notBefore, certificate.NotBefore, TimeSpan.FromSeconds(1)); + Assert.Equal(notAfter, certificate.NotAfter, TimeSpan.FromSeconds(1)); + + // Certificate had an existing Subject Key Identifier extension, so AKID should not be added + Assert.Empty(certificate.Extensions.OfType()); + + var subjectKeyIdentifier = Assert.Single(certificate.Extensions.OfType()); + Assert.True(subjectKeyIdentifier.SubjectKeyIdentifierBytes.Span.SequenceEqual(testSubjectKeyId)); + } + + [Fact] + public void CreateSelfSignedCertificate_ExistingRawSubjectKeyIdentifierExtension() + { + var subject = new X500DistinguishedName("CN=TestCertificate"); + var notBefore = DateTimeOffset.Now; + var notAfter = notBefore.AddMinutes(5); + var testSubjectKeyId = new byte[] { 5, 4, 3, 2, 1 }; + // Pass the extension as a raw X509Extension to simulate pre-encoded data + var extension = new X509SubjectKeyIdentifierExtension(testSubjectKeyId, critical: false); + var extensions = new List + { + new X509Extension(extension.Oid, extension.RawData, extension.Critical), + }; + + var certificate = CertificateManager.CreateSelfSignedCertificate(subject, extensions, notBefore, notAfter); + + Assert.Equal(notBefore, certificate.NotBefore, TimeSpan.FromSeconds(1)); + Assert.Equal(notAfter, certificate.NotAfter, TimeSpan.FromSeconds(1)); + + Assert.Empty(certificate.Extensions.OfType()); + + var subjectKeyIdentifier = Assert.Single(certificate.Extensions.OfType()); + Assert.True(subjectKeyIdentifier.SubjectKeyIdentifierBytes.Span.SequenceEqual(testSubjectKeyId)); + } + + [Fact] + public void CreateSelfSignedCertificate_ExistingRawAuthorityKeyIdentifierExtension() + { + var subject = new X500DistinguishedName("CN=TestCertificate"); + var notBefore = DateTimeOffset.Now; + var notAfter = notBefore.AddMinutes(5); + var testSubjectKeyId = new byte[] { 9, 8, 7, 6, 5 }; + // Pass the extension as a raw X509Extension to simulate pre-encoded data + var subjectExtension = new X509SubjectKeyIdentifierExtension(testSubjectKeyId, critical: false); + var authorityExtension = X509AuthorityKeyIdentifierExtension.CreateFromSubjectKeyIdentifier(subjectExtension); + var extensions = new List + { + new X509Extension(authorityExtension.Oid, authorityExtension.RawData, authorityExtension.Critical), + }; + + var certificate = CertificateManager.CreateSelfSignedCertificate(subject, extensions, notBefore, notAfter); + + Assert.Equal(notBefore, certificate.NotBefore, TimeSpan.FromSeconds(1)); + Assert.Equal(notAfter, certificate.NotAfter, TimeSpan.FromSeconds(1)); + + Assert.Empty(certificate.Extensions.OfType()); + + var authorityKeyIdentifier = Assert.Single(certificate.Extensions.OfType()); + Assert.True(authorityKeyIdentifier.KeyIdentifier?.Span.SequenceEqual(testSubjectKeyId)); + } + + [Fact] + public void CreateSelfSignedCertificate_NoSubjectKeyIdentifierExtension() + { + var subject = new X500DistinguishedName("CN=TestCertificate"); + var notBefore = DateTimeOffset.Now; + var notAfter = notBefore.AddMinutes(5); + var extensions = new List(); + + var certificate = CertificateManager.CreateSelfSignedCertificate(subject, extensions, notBefore, notAfter); + + Assert.Equal(notBefore, certificate.NotBefore, TimeSpan.FromSeconds(1)); + Assert.Equal(notAfter, certificate.NotAfter, TimeSpan.FromSeconds(1)); + + var subjectKeyIdentifier = Assert.Single(certificate.Extensions.OfType()); + var authorityKeyIdentifier = Assert.Single(certificate.Extensions.OfType()); + + // The Authority Key Identifier should match the Subject Key Identifier + Assert.True(authorityKeyIdentifier.KeyIdentifier?.Span.SequenceEqual(subjectKeyIdentifier.SubjectKeyIdentifierBytes.Span)); + } +} \ No newline at end of file diff --git a/src/Shared/test/Shared.Tests/Microsoft.AspNetCore.Shared.Tests.csproj b/src/Shared/test/Shared.Tests/Microsoft.AspNetCore.Shared.Tests.csproj index 72496024ef3b..6f418ef98db0 100644 --- a/src/Shared/test/Shared.Tests/Microsoft.AspNetCore.Shared.Tests.csproj +++ b/src/Shared/test/Shared.Tests/Microsoft.AspNetCore.Shared.Tests.csproj @@ -10,6 +10,7 @@ +