Skip to content

Add host.docker.internal and host.containers.internal to the dev cert SAN #61265

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
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
87 changes: 63 additions & 24 deletions src/Shared/CertificateGeneration/CertificateManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,31 @@
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;

#nullable enable

namespace Microsoft.AspNetCore.Certificates.Generation;

internal abstract class CertificateManager
{
internal const int CurrentAspNetCoreCertificateVersion = 2;
internal const int CurrentAspNetCoreCertificateVersion = 3;
internal const int CurrentMinimumAspNetCoreCertificateVersion = 3;

// 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";

// dns names of the host from a container
private const string LocalHostDockerHttpsDnsName = "host.docker.internal";
private const string ContainersDockerHttpsDnsName = "host.containers.internal";

// main cert subject
private const string LocalhostHttpsDnsName = "localhost";
internal const string LocalhostHttpsDistinguishedName = "CN=" + LocalhostHttpsDnsName;

Expand All @@ -46,7 +55,28 @@ public int AspNetHttpsCertificateVersion
{
get;
// For testing purposes only
internal set;
internal set
{
ArgumentOutOfRangeException.ThrowIfLessThan(
value,
MinimumAspNetHttpsCertificateVersion,
$"{nameof(AspNetHttpsCertificateVersion)} cannot be lesser than {nameof(MinimumAspNetHttpsCertificateVersion)}");
field = value;
}
}

public int MinimumAspNetHttpsCertificateVersion
{
get;
// For testing purposes only
internal set
{
ArgumentOutOfRangeException.ThrowIfGreaterThan(
value,
AspNetHttpsCertificateVersion,
$"{nameof(MinimumAspNetHttpsCertificateVersion)} cannot be greater than {nameof(AspNetHttpsCertificateVersion)}");
field = value;
}
}

public string Subject { get; }
Expand All @@ -57,9 +87,16 @@ public CertificateManager() : this(LocalhostHttpsDistinguishedName, CurrentAspNe

// For testing purposes only
internal CertificateManager(string subject, int version)
: this(subject, version, version)
{
}

// For testing purposes only
internal CertificateManager(string subject, int generatedVersion, int minimumVersion)
{
Subject = subject;
AspNetHttpsCertificateVersion = version;
AspNetHttpsCertificateVersion = generatedVersion;
MinimumAspNetHttpsCertificateVersion = minimumVersion;
}

/// <remarks>
Expand Down Expand Up @@ -147,30 +184,30 @@ bool HasOid(X509Certificate2 certificate, string oid) =>
certificate.Extensions.OfType<X509Extension>()
.Any(e => string.Equals(oid, e.Oid?.Value, StringComparison.Ordinal));

static byte GetCertificateVersion(X509Certificate2 c)
{
var byteArray = c.Extensions.OfType<X509Extension>()
.Where(e => string.Equals(AspNetHttpsOid, e.Oid?.Value, StringComparison.Ordinal))
.Single()
.RawData;

if ((byteArray.Length == AspNetHttpsOidFriendlyName.Length && byteArray[0] == (byte)'A') || byteArray.Length == 0)
{
// No Version set, default to 0
return 0b0;
}
else
{
// Version is in the only byte of the byte array.
return byteArray[0];
}
}

bool IsValidCertificate(X509Certificate2 certificate, DateTimeOffset currentDate, bool requireExportable) =>
certificate.NotBefore <= currentDate &&
currentDate <= certificate.NotAfter &&
(!requireExportable || IsExportable(certificate)) &&
GetCertificateVersion(certificate) >= AspNetHttpsCertificateVersion;
GetCertificateVersion(certificate) >= MinimumAspNetHttpsCertificateVersion;
}

internal static byte GetCertificateVersion(X509Certificate2 c)
{
var byteArray = c.Extensions.OfType<X509Extension>()
.Where(e => string.Equals(AspNetHttpsOid, e.Oid?.Value, StringComparison.Ordinal))
.Single()
.RawData;

if ((byteArray.Length == AspNetHttpsOidFriendlyName.Length && byteArray[0] == (byte)'A') || byteArray.Length == 0)
{
// No Version set, default to 0
return 0b0;
}
else
{
// Version is in the only byte of the byte array.
return byteArray[0];
}
}

protected virtual void PopulateCertificatesFromStore(X509Store store, List<X509Certificate2> certificates, bool requireExportable)
Expand Down Expand Up @@ -487,7 +524,7 @@ public void CleanupHttpsCertificates()
/// <remarks>Implementations may choose to throw, rather than returning <see cref="TrustLevel.None"/>.</remarks>
protected abstract TrustLevel TrustCertificateCore(X509Certificate2 certificate);

protected abstract bool IsExportable(X509Certificate2 c);
internal abstract bool IsExportable(X509Certificate2 c);

protected abstract void RemoveCertificateFromTrustedRoots(X509Certificate2 certificate);

Expand Down Expand Up @@ -649,6 +686,8 @@ internal X509Certificate2 CreateAspNetCoreHttpsDevelopmentCertificate(DateTimeOf
var extensions = new List<X509Extension>();
var sanBuilder = new SubjectAlternativeNameBuilder();
sanBuilder.AddDnsName(LocalhostHttpsDnsName);
sanBuilder.AddDnsName(LocalHostDockerHttpsDnsName);
sanBuilder.AddDnsName(ContainersDockerHttpsDnsName);

var keyUsage = new X509KeyUsageExtension(X509KeyUsageFlags.KeyEncipherment | X509KeyUsageFlags.DigitalSignature, critical: true);
var enhancedKeyUsage = new X509EnhancedKeyUsageExtension(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ private static bool IsCertOnKeychain(string keychain, X509Certificate2 certifica
}

// We don't have a good way of checking on the underlying implementation if it is exportable, so just return true.
protected override bool IsExportable(X509Certificate2 c) => true;
internal override bool IsExportable(X509Certificate2 c) => true;

protected override X509Certificate2 SaveCertificateCore(X509Certificate2 certificate, StoreName storeName, StoreLocation storeLocation)
{
Expand Down
2 changes: 1 addition & 1 deletion src/Shared/CertificateGeneration/UnixCertificateManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ internal override void CorrectCertificateState(X509Certificate2 candidate)
// This is about correcting storage, not trust.
}

protected override bool IsExportable(X509Certificate2 c) => true;
internal override bool IsExportable(X509Certificate2 c) => true;

protected override TrustLevel TrustCertificateCore(X509Certificate2 certificate)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ internal WindowsCertificateManager(string subject, int version)
{
}

protected override bool IsExportable(X509Certificate2 c)
internal override bool IsExportable(X509Certificate2 c)
{
#if XPLAT
// For the first run experience we don't need to know if the certificate can be exported.
Expand Down
44 changes: 39 additions & 5 deletions src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,7 @@ public void EnsureCreateHttpsCertificate_ReturnsExpiredCertificateIfVersionIsInc
ListCertificates();

_manager.AspNetHttpsCertificateVersion = 2;
_manager.MinimumAspNetHttpsCertificateVersion = 2;

var httpsCertificateList = _manager.ListCertificates(StoreName.My, StoreLocation.CurrentUser, isValid: true);
Assert.Empty(httpsCertificateList);
Expand All @@ -400,17 +401,40 @@ public void EnsureCreateHttpsCertificate_ReturnsExpiredCertificateForEmptyVersio

var now = DateTimeOffset.UtcNow;
now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset);
_manager.MinimumAspNetHttpsCertificateVersion = 0;
_manager.AspNetHttpsCertificateVersion = 0;
var creation = _manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), path: null, trust: false, isInteractive: false);
Output.WriteLine(creation.ToString());
ListCertificates();

_manager.AspNetHttpsCertificateVersion = 1;
_manager.MinimumAspNetHttpsCertificateVersion = 1;

var httpsCertificateList = _manager.ListCertificates(StoreName.My, StoreLocation.CurrentUser, isValid: true);
Assert.Empty(httpsCertificateList);
}

[Fact]
[SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6720", Queues = "All.OSX")]
public void EnsureCreateHttpsCertificate_DoNotOverrideValidOldCertificate()
{
_fixture.CleanupCertificates();

var now = DateTimeOffset.UtcNow;
now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset);
var creation = _manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), path: null, trust: false, isInteractive: false);
Output.WriteLine(creation.ToString());
ListCertificates();

// Simulate a tool with the same min version as the already existing cert but with a more
// recent generation version
_manager.MinimumAspNetHttpsCertificateVersion = 1;
_manager.AspNetHttpsCertificateVersion = 2;
var alreadyExist = _manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), path: null, trust: false, isInteractive: false);
Output.WriteLine(alreadyExist.ToString());
Assert.Equal(EnsureCertificateResult.ValidCertificatePresent, alreadyExist);
}

[ConditionalFact]
[SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6720", Queues = "All.OSX")]
public void EnsureCreateHttpsCertificate_ReturnsValidIfVersionIsZero()
Expand All @@ -419,7 +443,7 @@ public void EnsureCreateHttpsCertificate_ReturnsValidIfVersionIsZero()

var now = DateTimeOffset.UtcNow;
now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset);
_manager.AspNetHttpsCertificateVersion = 0;
_manager.MinimumAspNetHttpsCertificateVersion = 0;
var creation = _manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), path: null, trust: false, isInteractive: false);
Output.WriteLine(creation.ToString());
ListCertificates();
Expand All @@ -441,7 +465,7 @@ public void EnsureCreateHttpsCertificate_ReturnValidIfCertIsNewer()
Output.WriteLine(creation.ToString());
ListCertificates();

_manager.AspNetHttpsCertificateVersion = 1;
_manager.MinimumAspNetHttpsCertificateVersion = 1;
var httpsCertificateList = _manager.ListCertificates(StoreName.My, StoreLocation.CurrentUser, isValid: true);
Assert.NotEmpty(httpsCertificateList);
}
Expand All @@ -455,16 +479,24 @@ public void ListCertificates_AlwaysReturnsTheCertificate_WithHighestVersion()
var now = DateTimeOffset.UtcNow;
now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset);
_manager.AspNetHttpsCertificateVersion = 1;
_manager.MinimumAspNetHttpsCertificateVersion = 1;
var creation = _manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), path: null, trust: false, isInteractive: false);
Output.WriteLine(creation.ToString());
ListCertificates();

_manager.AspNetHttpsCertificateVersion = 2;
_manager.MinimumAspNetHttpsCertificateVersion = 2;
creation = _manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), path: null, trust: false, isInteractive: false);
Output.WriteLine(creation.ToString());
ListCertificates();

_manager.AspNetHttpsCertificateVersion = 1;
_manager.AspNetHttpsCertificateVersion = 3;
_manager.MinimumAspNetHttpsCertificateVersion = 3;
creation = _manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), path: null, trust: false, isInteractive: false);
Output.WriteLine(creation.ToString());
ListCertificates();

_manager.MinimumAspNetHttpsCertificateVersion = 2;
var httpsCertificateList = _manager.ListCertificates(StoreName.My, StoreLocation.CurrentUser, isValid: true);
Assert.Equal(2, httpsCertificateList.Count);

Expand All @@ -475,13 +507,13 @@ public void ListCertificates_AlwaysReturnsTheCertificate_WithHighestVersion()
firstCertificate.Extensions.OfType<X509Extension>(),
e => e.Critical == false &&
e.Oid.Value == CertificateManager.AspNetHttpsOid &&
e.RawData[0] == 2);
e.RawData[0] == 3);

Assert.Contains(
secondCertificate.Extensions.OfType<X509Extension>(),
e => e.Critical == false &&
e.Oid.Value == CertificateManager.AspNetHttpsOid &&
e.RawData[0] == 1);
e.RawData[0] == 2);
}

[ConditionalFact]
Expand Down Expand Up @@ -532,6 +564,8 @@ public CertFixture()

internal void CleanupCertificates()
{
Manager.MinimumAspNetHttpsCertificateVersion = 1;
Manager.AspNetHttpsCertificateVersion = 1;
Manager.RemoveAllCertificates(StoreName.My, StoreLocation.CurrentUser);
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
Expand Down
Loading
Loading