Skip to content
Open
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
23 changes: 20 additions & 3 deletions src/Shared/CertificateGeneration/CertificateManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,19 @@ 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";
internal const string AspNetHttpsOidFriendlyName = "ASP.NET Core HTTPS development certificate";

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";
Expand Down Expand Up @@ -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;

Expand Down
133 changes: 133 additions & 0 deletions src/Shared/test/Shared.Tests/CertificateManagerTests.cs
Original file line number Diff line number Diff line change
@@ -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<X509SubjectKeyIdentifierExtension>());

// Certificate should include an Authority Key Identifier extension
var authorityKeyIdentifier = Assert.Single(certificate.Extensions.OfType<X509AuthorityKeyIdentifierExtension>());

// 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<X509Extension>
{
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<X509AuthorityKeyIdentifierExtension>());

var subjectKeyIdentifier = Assert.Single(certificate.Extensions.OfType<X509SubjectKeyIdentifierExtension>());
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<X509Extension>
{
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<X509AuthorityKeyIdentifierExtension>());

var subjectKeyIdentifier = Assert.Single(certificate.Extensions.OfType<X509SubjectKeyIdentifierExtension>());
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<X509Extension>
{
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<X509SubjectKeyIdentifierExtension>());

var authorityKeyIdentifier = Assert.Single(certificate.Extensions.OfType<X509AuthorityKeyIdentifierExtension>());
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<X509Extension>();

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<X509SubjectKeyIdentifierExtension>());
var authorityKeyIdentifier = Assert.Single(certificate.Extensions.OfType<X509AuthorityKeyIdentifierExtension>());

// The Authority Key Identifier should match the Subject Key Identifier
Assert.True(authorityKeyIdentifier.KeyIdentifier?.Span.SequenceEqual(subjectKeyIdentifier.SubjectKeyIdentifierBytes.Span));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

<ItemGroup>
<Compile Include="$(SharedSourceRoot)ActivatorUtilities\**\*.cs" Link="Shared\ActivatorUtilities\%(Filename)%(Extension)" />
<Compile Include="$(SharedSourceRoot)CertificateGeneration\**\*.cs" Link="Shared\CertificateGeneration\%(Filename)%(Extension)" />
<Compile Include="$(SharedSourceRoot)CommandLineUtils\**\*.cs" Link="Shared\CommandLineUtils\%(Filename)%(Extension)" />
<Compile Include="$(SharedSourceRoot)ClosedGenericMatcher\*.cs" Link="Shared\ClosedGenericMatcher\%(Filename)%(Extension)" />
<Compile Include="$(SharedSourceRoot)CopyOnWriteDictionary\*.cs" Link="Shared\CopyOnWriteDictionary\%(Filename)%(Extension)" />
Expand Down
Loading