diff --git a/src/Shared/CertificateGeneration/CertificateManager.cs b/src/Shared/CertificateGeneration/CertificateManager.cs index cc63219bd9..b5c0d0088e 100644 --- a/src/Shared/CertificateGeneration/CertificateManager.cs +++ b/src/Shared/CertificateGeneration/CertificateManager.cs @@ -15,6 +15,7 @@ namespace Microsoft.AspNetCore.Certificates.Generation { internal abstract class CertificateManager { + internal const int CurrentAspNetCoreCertificateVersion = 2; internal const string AspNetHttpsOid = "1.3.6.1.4.1.311.84.1.1"; internal const string AspNetHttpsOidFriendlyName = "ASP.NET Core HTTPS development certificate"; @@ -45,7 +46,7 @@ namespace Microsoft.AspNetCore.Certificates.Generation public string Subject { get; } - public CertificateManager() : this(LocalhostHttpsDistinguishedName, 1) + public CertificateManager() : this(LocalhostHttpsDistinguishedName, CurrentAspNetCoreCertificateVersion) { } @@ -86,10 +87,8 @@ namespace Microsoft.AspNetCore.Certificates.Generation Log.CheckCertificatesValidity(); var now = DateTimeOffset.Now; var validCertificates = matchingCertificates - .Where(c => c.NotBefore <= now && - now <= c.NotAfter && - (!requireExportable || IsExportable(c)) - && MatchesVersion(c)) + .Where(c => IsValidCertificate(c, now, requireExportable)) + .OrderByDescending(c => GetCertificateVersion(c)) .ToArray(); var invalidCertificates = matchingCertificates.Except(validCertificates); @@ -123,7 +122,7 @@ namespace Microsoft.AspNetCore.Certificates.Generation certificate.Extensions.OfType() .Any(e => string.Equals(oid, e.Oid.Value, StringComparison.Ordinal)); - bool MatchesVersion(X509Certificate2 c) + static byte GetCertificateVersion(X509Certificate2 c) { var byteArray = c.Extensions.OfType() .Where(e => string.Equals(AspNetHttpsOid, e.Oid.Value, StringComparison.Ordinal)) @@ -133,14 +132,20 @@ namespace Microsoft.AspNetCore.Certificates.Generation if ((byteArray.Length == AspNetHttpsOidFriendlyName.Length && byteArray[0] == (byte)'A') || byteArray.Length == 0) { // No Version set, default to 0 - return 0 >= AspNetHttpsCertificateVersion; + return 0b0; } else { // Version is in the only byte of the byte array. - return byteArray[0] >= AspNetHttpsCertificateVersion; + return byteArray[0]; } } + + bool IsValidCertificate(X509Certificate2 certificate, DateTimeOffset currentDate, bool requireExportable) => + certificate.NotBefore <= currentDate && + currentDate <= certificate.NotAfter && + (!requireExportable || IsExportable(certificate)) && + GetCertificateVersion(certificate) >= AspNetHttpsCertificateVersion; } public IList GetHttpsCertificates() => @@ -448,7 +453,14 @@ namespace Microsoft.AspNetCore.Certificates.Generation } else { - bytes = certificate.Export(X509ContentType.Cert); + if (format == CertificateKeyExportFormat.Pem) + { + bytes = Encoding.ASCII.GetBytes(PemEncoding.Write("CERTIFICATE", certificate.Export(X509ContentType.Cert))); + } + else + { + bytes = certificate.Export(X509ContentType.Cert); + } } } catch (Exception e) diff --git a/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs b/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs index 79a0f4c6e3..4e08c899cd 100644 --- a/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs +++ b/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs @@ -191,6 +191,36 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests Assert.Equal(httpsCertificate.GetCertHashString(), exportedCertificate.GetCertHashString()); } + [ConditionalFact] + [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6720", Queues = "OSX.1014.Amd64;OSX.1014.Amd64.Open")] + public void EnsureCreateHttpsCertificate_CanExportTheCertInPemFormat_WithoutKey() + { + // Arrange + const string CertificateName = nameof(EnsureCreateHttpsCertificate_DoesNotCreateACertificate_WhenThereIsAnExistingHttpsCertificates) + ".pem"; + + _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); + + // Act + var result = _manager + .EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), CertificateName, trust: false, includePrivateKey: false, password: null, keyExportFormat: CertificateKeyExportFormat.Pem, isInteractive: false); + + // Assert + Assert.Equal(EnsureCertificateResult.ValidCertificatePresent, result); + Assert.True(File.Exists(CertificateName)); + + var exportedCertificate = new X509Certificate2(CertificateName); + Assert.NotNull(exportedCertificate); + Assert.False(exportedCertificate.HasPrivateKey); + } + [ConditionalFact] [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6720", Queues = "OSX.1014.Amd64;OSX.1014.Amd64.Open")] public void EnsureCreateHttpsCertificate_CanImport_ExportedPfx() @@ -351,6 +381,44 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests var httpsCertificateList = _manager.ListCertificates(StoreName.My, StoreLocation.CurrentUser, isValid: true); Assert.NotEmpty(httpsCertificateList); } + + [ConditionalFact] + [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6720", Queues = "OSX.1014.Amd64;OSX.1014.Amd64.Open")] + public void ListCertificates_AlwaysReturnsTheCertificate_WithHighestVersion() + { + _fixture.CleanupCertificates(); + + 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; + var creation = _manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), path: null, trust: false, isInteractive: false); + Output.WriteLine(creation.ToString()); + ListCertificates(); + + _manager.AspNetHttpsCertificateVersion = 2; + creation = _manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), path: null, trust: false, isInteractive: false); + Output.WriteLine(creation.ToString()); + ListCertificates(); + + _manager.AspNetHttpsCertificateVersion = 1; + var httpsCertificateList = _manager.ListCertificates(StoreName.My, StoreLocation.CurrentUser, isValid: true); + Assert.Equal(2, httpsCertificateList.Count); + + var firstCertificate = httpsCertificateList[0]; + var secondCertificate = httpsCertificateList[1]; + + Assert.Contains( + firstCertificate.Extensions.OfType(), + e => e.Critical == false && + e.Oid.Value == "1.3.6.1.4.1.311.84.1.1" && + e.RawData[0] == 2); + + Assert.Contains( + secondCertificate.Extensions.OfType(), + e => e.Critical == false && + e.Oid.Value == "1.3.6.1.4.1.311.84.1.1" && + e.RawData[0] == 1); + } } public class CertFixture : IDisposable diff --git a/src/Tools/dotnet-dev-certs/src/Program.cs b/src/Tools/dotnet-dev-certs/src/Program.cs index 617d5b789e..3f41193917 100644 --- a/src/Tools/dotnet-dev-certs/src/Program.cs +++ b/src/Tools/dotnet-dev-certs/src/Program.cs @@ -98,12 +98,9 @@ namespace Microsoft.AspNetCore.DeveloperCertificates.Tools CommandOptionType.SingleValue); CommandOption trust = null; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - trust = c.Option("-t|--trust", - "Trust the certificate on the current platform", - CommandOptionType.NoValue); - } + trust = c.Option("-t|--trust", + "Trust the certificate on the current platform", + CommandOptionType.NoValue); var verbose = c.Option("-v|--verbose", "Display more debug information.", @@ -292,24 +289,32 @@ namespace Microsoft.AspNetCore.DeveloperCertificates.Tools if (trust != null && trust.HasValue()) { - var store = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? StoreName.My : StoreName.Root; - var trustedCertificates = certificateManager.ListCertificates(store, StoreLocation.CurrentUser, isValid: true); - if (!certificates.Any(c => certificateManager.IsTrusted(c))) + if(!RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { - reporter.Output($@"The following certificates were found, but none of them is trusted: -{string.Join(Environment.NewLine, certificates.Select(c => $"{c.Subject} - {c.Thumbprint}"))}"); - return ErrorCertificateNotTrusted; + var store = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? StoreName.My : StoreName.Root; + var trustedCertificates = certificateManager.ListCertificates(store, StoreLocation.CurrentUser, isValid: true); + if (!certificates.Any(c => certificateManager.IsTrusted(c))) + { + reporter.Output($@"The following certificates were found, but none of them is trusted: + {string.Join(Environment.NewLine, certificates.Select(c => $"{c.Subject} - {c.Thumbprint}"))}"); + return ErrorCertificateNotTrusted; + } + else + { + reporter.Output("A trusted certificate was found."); + } } else { - reporter.Output("A trusted certificate was found."); + reporter.Warn("Checking the HTTPS development certificate trust status was requested. Checking whether the certificate is trusted or not is not supported on Linux distributions." + + "For instructions on how to manually validate the certificate is trusted on your Linux distribution, go to https://aka.ms/dev-certs-trust"); } } return Success; } - private static int EnsureHttpsCertificate(CommandOption exportPath, CommandOption password, CommandOption noPassword, CommandOption trust, CommandOption keyFormat, IReporter reporter) + private static int EnsureHttpsCertificate(CommandOption exportPath, CommandOption password, CommandOption noPassword, CommandOption trust, CommandOption exportFormat, IReporter reporter) { var now = DateTimeOffset.Now; var manager = CertificateManager.Instance; @@ -332,25 +337,34 @@ namespace Microsoft.AspNetCore.DeveloperCertificates.Tools } } - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && trust?.HasValue() == true) + if (trust?.HasValue() == true) { - reporter.Warn("Trusting the HTTPS development certificate was requested. If the certificate is not " + - "already trusted we will run the following command:" + Environment.NewLine + - "'sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain <>'" + - Environment.NewLine + "This command might prompt you for your password to install the certificate " + - "on the system keychain."); - } + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + reporter.Warn("Trusting the HTTPS development certificate was requested. If the certificate is not " + + "already trusted we will run the following command:" + Environment.NewLine + + "'sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain <>'" + + Environment.NewLine + "This command might prompt you for your password to install the certificate " + + "on the system keychain."); + } - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && trust?.HasValue() == true) - { - reporter.Warn("Trusting the HTTPS development certificate was requested. A confirmation prompt will be displayed " + - "if the certificate was not previously trusted. Click yes on the prompt to trust the certificate."); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + reporter.Warn("Trusting the HTTPS development certificate was requested. A confirmation prompt will be displayed " + + "if the certificate was not previously trusted. Click yes on the prompt to trust the certificate."); + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + reporter.Warn("Trusting the HTTPS development certificate was requested. Trusting the certificate on Linux distributions automatically is not supported. " + + "For instructions on how to manually trust the certificate on your Linux distribution, go to https://aka.ms/dev-certs-trust"); + } } var format = CertificateKeyExportFormat.Pfx; - if (keyFormat.HasValue() && !Enum.TryParse(keyFormat.Value(), ignoreCase: true, out format)) + if (exportFormat.HasValue() && !Enum.TryParse(exportFormat.Value(), ignoreCase: true, out format)) { - reporter.Error($"Unknown key format '{keyFormat.Value()}'."); + reporter.Error($"Unknown key format '{exportFormat.Value()}'."); return InvalidKeyExportFormat; } @@ -358,10 +372,10 @@ namespace Microsoft.AspNetCore.DeveloperCertificates.Tools now, now.Add(HttpsCertificateValidity), exportPath.Value(), - trust == null ? false : trust.HasValue(), + trust == null ? false : trust.HasValue() && !RuntimeInformation.IsOSPlatform(OSPlatform.Linux), password.HasValue() || (noPassword.HasValue() && format == CertificateKeyExportFormat.Pem), password.Value(), - keyFormat.HasValue() ? format : CertificateKeyExportFormat.Pfx); + exportFormat.HasValue() ? format : CertificateKeyExportFormat.Pfx); switch (result) {