diff --git a/src/Servers/Kestrel/Core/src/TlsConfigurationLoader.cs b/src/Servers/Kestrel/Core/src/TlsConfigurationLoader.cs index 087b06639483..2f19ea9acd26 100644 --- a/src/Servers/Kestrel/Core/src/TlsConfigurationLoader.cs +++ b/src/Servers/Kestrel/Core/src/TlsConfigurationLoader.cs @@ -176,20 +176,8 @@ public ListenOptions UseHttpsWithSni( private static bool IsDevelopmentCertificate(X509Certificate2 certificate) { - if (!string.Equals(certificate.Subject, "CN=localhost", StringComparison.Ordinal)) - { - return false; - } - - foreach (var ext in certificate.Extensions) - { - if (string.Equals(ext.Oid?.Value, CertificateManager.AspNetHttpsOid, StringComparison.Ordinal)) - { - return true; - } - } - - return false; + return string.Equals(certificate.Subject, CertificateManager.LocalhostHttpsDistinguishedName, StringComparison.Ordinal) && + CertificateManager.IsHttpsDevelopmentCertificate(certificate); } private static bool TryGetCertificatePath(string applicationName, [NotNullWhen(true)] out string? path) diff --git a/src/Shared/CertificateGeneration/CertificateManager.cs b/src/Shared/CertificateGeneration/CertificateManager.cs index c169d1a17047..e3b1526eb80b 100644 --- a/src/Shared/CertificateGeneration/CertificateManager.cs +++ b/src/Shared/CertificateGeneration/CertificateManager.cs @@ -26,7 +26,7 @@ internal abstract class CertificateManager private const string ServerAuthenticationEnhancedKeyUsageOidFriendlyName = "Server Authentication"; private const string LocalhostHttpsDnsName = "localhost"; - private const string LocalhostHttpsDistinguishedName = "CN=" + LocalhostHttpsDnsName; + internal const string LocalhostHttpsDistinguishedName = "CN=" + LocalhostHttpsDnsName; public const int RSAMinimumKeySizeInBits = 2048; @@ -62,9 +62,21 @@ internal CertificateManager(string subject, int version) AspNetHttpsCertificateVersion = version; } - public static bool IsHttpsDevelopmentCertificate(X509Certificate2 certificate) => - certificate.Extensions.OfType() - .Any(e => string.Equals(AspNetHttpsOid, e.Oid?.Value, StringComparison.Ordinal)); + /// + /// This only checks if the certificate has the OID for ASP.NET Core HTTPS development certificates - + /// it doesn't check the subject, validity, key usages, etc. + /// + public static bool IsHttpsDevelopmentCertificate(X509Certificate2 certificate) + { + foreach (var extension in certificate.Extensions.OfType()) + { + if (string.Equals(AspNetHttpsOid, extension.Oid?.Value, StringComparison.Ordinal)) + { + return true; + } + } + return false; + } public IList ListCertificates( StoreName storeName, @@ -398,7 +410,10 @@ internal ImportCertificateResult ImportCertificate(string certificatePath, strin return ImportCertificateResult.InvalidCertificate; } - if (!IsHttpsDevelopmentCertificate(certificate)) + // Note that we're checking Subject, rather than LocalhostHttpsDistinguishedName, + // because the tests use a different subject. + if (!string.Equals(certificate.Subject, Subject, StringComparison.Ordinal) || // Kestrel requires this + !IsHttpsDevelopmentCertificate(certificate)) { if (Log.IsEnabled()) { diff --git a/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs b/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs index afe4c872ac8f..6ea15546072b 100644 --- a/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs +++ b/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs @@ -116,7 +116,7 @@ private void ListCertificates() var certificates = store.Certificates; foreach (var certificate in certificates) { - Output.WriteLine($"Certificate: '{Convert.ToBase64String(certificate.Export(X509ContentType.Cert))}'."); + Output.WriteLine($"Certificate: {certificate.Subject} '{Convert.ToBase64String(certificate.Export(X509ContentType.Cert))}'."); certificate.Dispose(); } @@ -225,7 +225,7 @@ public void EnsureCreateHttpsCertificate_CanExportTheCertInPemFormat_WithoutKey( public void EnsureCreateHttpsCertificate_CanImport_ExportedPfx() { // Arrange - const string CertificateName = nameof(EnsureCreateHttpsCertificate_DoesNotCreateACertificate_WhenThereIsAnExistingHttpsCertificates) + ".pfx"; + const string CertificateName = nameof(EnsureCreateHttpsCertificate_CanImport_ExportedPfx) + ".pfx"; var certificatePassword = Guid.NewGuid().ToString(); _fixture.CleanupCertificates(); @@ -258,7 +258,7 @@ public void EnsureCreateHttpsCertificate_CanImport_ExportedPfx() public void EnsureCreateHttpsCertificate_CanImport_ExportedPfx_FailsIfThereAreCertificatesPresent() { // Arrange - const string CertificateName = nameof(EnsureCreateHttpsCertificate_DoesNotCreateACertificate_WhenThereIsAnExistingHttpsCertificates) + ".pfx"; + const string CertificateName = nameof(EnsureCreateHttpsCertificate_CanImport_ExportedPfx_FailsIfThereAreCertificatesPresent) + ".pfx"; var certificatePassword = Guid.NewGuid().ToString(); _fixture.CleanupCertificates(); @@ -280,6 +280,47 @@ public void EnsureCreateHttpsCertificate_CanImport_ExportedPfx_FailsIfThereAreCe Assert.Equal(ImportCertificateResult.ExistingCertificatesPresent, result); } + [ConditionalFact] + [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6720", Queues = "All.OSX")] + public void EnsureCreateHttpsCertificate_CannotImportIfTheSubjectNameIsWrong() + { + // Arrange + const string CertificateName = nameof(EnsureCreateHttpsCertificate_CannotImportIfTheSubjectNameIsWrong) + ".pfx"; + var certificatePassword = Guid.NewGuid().ToString(); + + _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(); + + var httpsCertificate = _manager.ListCertificates(StoreName.My, StoreLocation.CurrentUser, isValid: false).Single(c => c.Subject == TestCertificateSubject); + + _manager.CleanupHttpsCertificates(); + + using var privateKey = httpsCertificate.GetRSAPrivateKey(); + var csr = new CertificateRequest(httpsCertificate.Subject + "Not", privateKey, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + foreach (var extension in httpsCertificate.Extensions) + { + csr.CertificateExtensions.Add(extension); + } + var wrongSubjectCertificate = csr.CreateSelfSigned(httpsCertificate.NotBefore, httpsCertificate.NotAfter); + + Assert.True(CertificateManager.IsHttpsDevelopmentCertificate(wrongSubjectCertificate)); + Assert.NotEqual(_manager.Subject, wrongSubjectCertificate.Subject); + + File.WriteAllBytes(CertificateName, wrongSubjectCertificate.Export(X509ContentType.Pfx, certificatePassword)); + + // Act + var result = _manager.ImportCertificate(CertificateName, certificatePassword); + + // Assert + Assert.Equal(ImportCertificateResult.NoDevelopmentHttpsCertificate, result); + Assert.Empty(_manager.ListCertificates(StoreName.My, StoreLocation.CurrentUser, isValid: false)); + } + [ConditionalFact] [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6720", Queues = "All.OSX")] public void EnsureCreateHttpsCertificate_CanExportTheCertInPemFormat_WithoutPassword()