From f192bcf932b9f28e2bd48d5bfbd2e9e3161e9dd4 Mon Sep 17 00:00:00 2001 From: Benjamin Petit Date: Tue, 1 Apr 2025 18:09:01 +0200 Subject: [PATCH 1/7] Add notion of minimum cert version Add new SAN for dev cert + json output for the tool --- .../CertificateManager.cs | 71 +++++++++++----- .../MacOSCertificateManager.cs | 2 +- .../UnixCertificateManager.cs | 2 +- .../WindowsCertificateManager.cs | 2 +- .../test/CertificateManagerTests.cs | 9 +- src/Tools/dotnet-dev-certs/src/Program.cs | 83 ++++++++++++++++++- 6 files changed, 136 insertions(+), 33 deletions(-) diff --git a/src/Shared/CertificateGeneration/CertificateManager.cs b/src/Shared/CertificateGeneration/CertificateManager.cs index 491eb5adb974..6ffaca132407 100644 --- a/src/Shared/CertificateGeneration/CertificateManager.cs +++ b/src/Shared/CertificateGeneration/CertificateManager.cs @@ -9,6 +9,9 @@ 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 @@ -16,7 +19,8 @@ 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"; @@ -24,7 +28,12 @@ internal abstract class CertificateManager 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; @@ -49,6 +58,13 @@ public int AspNetHttpsCertificateVersion internal set; } + public int MinimumAspNetHttpsCertificateVersion + { + get; + // For testing purposes only + internal set; + } + public string Subject { get; } public CertificateManager() : this(LocalhostHttpsDistinguishedName, CurrentAspNetCoreCertificateVersion) @@ -57,9 +73,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; } /// @@ -147,30 +170,30 @@ bool HasOid(X509Certificate2 certificate, string oid) => certificate.Extensions.OfType() .Any(e => string.Equals(oid, e.Oid?.Value, StringComparison.Ordinal)); - static byte GetCertificateVersion(X509Certificate2 c) - { - var byteArray = c.Extensions.OfType() - .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; + } + + private static byte GetCertificateVersion(X509Certificate2 c) + { + var byteArray = c.Extensions.OfType() + .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 certificates, bool requireExportable) @@ -487,7 +510,7 @@ public void CleanupHttpsCertificates() /// Implementations may choose to throw, rather than returning . protected abstract TrustLevel TrustCertificateCore(X509Certificate2 certificate); - protected abstract bool IsExportable(X509Certificate2 c); + internal abstract bool IsExportable(X509Certificate2 c); protected abstract void RemoveCertificateFromTrustedRoots(X509Certificate2 certificate); @@ -649,6 +672,8 @@ internal X509Certificate2 CreateAspNetCoreHttpsDevelopmentCertificate(DateTimeOf var extensions = new List(); 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( diff --git a/src/Shared/CertificateGeneration/MacOSCertificateManager.cs b/src/Shared/CertificateGeneration/MacOSCertificateManager.cs index a38e22762190..36b0c92d895c 100644 --- a/src/Shared/CertificateGeneration/MacOSCertificateManager.cs +++ b/src/Shared/CertificateGeneration/MacOSCertificateManager.cs @@ -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) { diff --git a/src/Shared/CertificateGeneration/UnixCertificateManager.cs b/src/Shared/CertificateGeneration/UnixCertificateManager.cs index 0ea92b87483e..149e0fab3ba6 100644 --- a/src/Shared/CertificateGeneration/UnixCertificateManager.cs +++ b/src/Shared/CertificateGeneration/UnixCertificateManager.cs @@ -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) { diff --git a/src/Shared/CertificateGeneration/WindowsCertificateManager.cs b/src/Shared/CertificateGeneration/WindowsCertificateManager.cs index 85a72be37c66..1cf1ebd9480e 100644 --- a/src/Shared/CertificateGeneration/WindowsCertificateManager.cs +++ b/src/Shared/CertificateGeneration/WindowsCertificateManager.cs @@ -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. diff --git a/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs b/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs index bcc2d6ef05b5..f8ad0eb24388 100644 --- a/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs +++ b/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs @@ -387,7 +387,7 @@ public void EnsureCreateHttpsCertificate_ReturnsExpiredCertificateIfVersionIsInc Output.WriteLine(creation.ToString()); ListCertificates(); - _manager.AspNetHttpsCertificateVersion = 2; + _manager.MinimumAspNetHttpsCertificateVersion = 2; var httpsCertificateList = _manager.ListCertificates(StoreName.My, StoreLocation.CurrentUser, isValid: true); Assert.Empty(httpsCertificateList); @@ -419,7 +419,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(); @@ -460,11 +460,12 @@ public void ListCertificates_AlwaysReturnsTheCertificate_WithHighestVersion() 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.MinimumAspNetHttpsCertificateVersion = 1; var httpsCertificateList = _manager.ListCertificates(StoreName.My, StoreLocation.CurrentUser, isValid: true); Assert.Equal(2, httpsCertificateList.Count); @@ -532,6 +533,8 @@ public CertFixture() internal void CleanupCertificates() { + Manager.AspNetHttpsCertificateVersion = 1; + Manager.MinimumAspNetHttpsCertificateVersion = 1; Manager.RemoveAllCertificates(StoreName.My, StoreLocation.CurrentUser); if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { diff --git a/src/Tools/dotnet-dev-certs/src/Program.cs b/src/Tools/dotnet-dev-certs/src/Program.cs index 222e3c355e57..1ebf5cd67bff 100644 --- a/src/Tools/dotnet-dev-certs/src/Program.cs +++ b/src/Tools/dotnet-dev-certs/src/Program.cs @@ -1,9 +1,13 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics; using System.Linq; using System.Runtime.InteropServices; using System.Security.Cryptography.X509Certificates; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; using Microsoft.AspNetCore.Certificates.Generation; using Microsoft.Extensions.CommandLineUtils; using Microsoft.Extensions.Tools.Internal; @@ -43,6 +47,8 @@ internal sealed class Program public static readonly TimeSpan HttpsCertificateValidity = TimeSpan.FromDays(365); + private static bool _parsableOutput; + public static int Main(string[] args) { if (args.Contains("--debug")) @@ -110,12 +116,18 @@ public static int Main(string[] args) "Display warnings and errors only.", CommandOptionType.NoValue); + var parsableOutput = c.Option("--parsable", + "Produce a parsable output, to be used by other tools.", + CommandOptionType.NoValue); + c.HelpOption("-h|--help"); c.OnExecute(() => { var reporter = new ConsoleReporter(PhysicalConsole.Singleton, verbose.HasValue(), quiet.HasValue()); + _parsableOutput = parsableOutput.HasValue(); + if (verbose.HasValue()) { var listener = new ReporterEventListener(reporter); @@ -328,11 +340,18 @@ private static int CheckHttpsCertificate(CommandOption trust, CommandOption verb private static void ReportCertificates(IReporter reporter, IReadOnlyList certificates, string certificateState) { - reporter.Output(certificates.Count switch + if (_parsableOutput) { - 1 => $"A {certificateState} certificate was found: {CertificateManager.GetDescription(certificates[0])}", - _ => $"{certificates.Count} {certificateState} certificates were found: {CertificateManager.ToCertificateDescription(certificates)}" - }); + reporter.Output(JsonSerializer.Serialize(CertificateReport.FromX509Certificate2List(certificates))); + } + else + { + reporter.Output(certificates.Count switch + { + 1 => $"A {certificateState} certificate was found: {CertificateManager.GetDescription(certificates[0])}", + _ => $"{certificates.Count} {certificateState} certificates were found: {CertificateManager.ToCertificateDescription(certificates)}" + }); + } } private static int EnsureHttpsCertificate(CommandOption exportPath, CommandOption password, CommandOption noPassword, CommandOption trust, CommandOption exportFormat, IReporter reporter) @@ -452,3 +471,59 @@ private static int EnsureHttpsCertificate(CommandOption exportPath, CommandOptio } } } + +/// +/// A Serializable friendly version of the cert report output +/// +internal class CertificateReport +{ + public string Thumbprint { get; init; } + public string Subject { get; init; } + public List X509SubjectAlternativeNameExtension { get; init; } + public int Version { get; init; } + public DateTime ValidityNotBefore { get; init; } + public DateTime ValidityNotAfter { get; init; } + public bool IsHttpsDevelopmentCertificate { get; init; } + public bool IsExportable { get; init; } + + public static CertificateReport FromX509Certificate2(X509Certificate2 cert) + { + return new CertificateReport + { + Thumbprint = cert.Thumbprint, + Subject = cert.Subject, + X509SubjectAlternativeNameExtension = GetSanExtension(cert), + Version = cert.Version, + ValidityNotBefore = cert.NotBefore, + ValidityNotAfter = cert.NotAfter, + IsHttpsDevelopmentCertificate = CertificateManager.IsHttpsDevelopmentCertificate(cert), + IsExportable = CertificateManager.Instance.IsExportable(cert) + }; + + static List GetSanExtension(X509Certificate2 cert) + { + var dnsNames = new List(); + foreach (var extension in cert.Extensions) + { + if (extension is X509SubjectAlternativeNameExtension sanExtension) + { + foreach (var dns in sanExtension.EnumerateDnsNames()) + { + dnsNames.Add(dns); + } + } + } + return dnsNames; + } + } + + public static List FromX509Certificate2List(IEnumerable certs) + { + var list = new List(); + foreach (var cert in certs) + { + list.Add(FromX509Certificate2(cert)); + } + return list; + } +} From 37f340d98ae4ec1be19426a73edcd8418d3b6338 Mon Sep 17 00:00:00 2001 From: Benjamin Petit Date: Wed, 2 Apr 2025 17:56:08 +0200 Subject: [PATCH 2/7] Cleaner json output version --- .../CertificateManager.cs | 2 +- src/Tools/dotnet-dev-certs/src/Program.cs | 83 ++++++++++++------- 2 files changed, 52 insertions(+), 33 deletions(-) diff --git a/src/Shared/CertificateGeneration/CertificateManager.cs b/src/Shared/CertificateGeneration/CertificateManager.cs index 6ffaca132407..76ff6bc3f134 100644 --- a/src/Shared/CertificateGeneration/CertificateManager.cs +++ b/src/Shared/CertificateGeneration/CertificateManager.cs @@ -177,7 +177,7 @@ bool IsValidCertificate(X509Certificate2 certificate, DateTimeOffset currentDate GetCertificateVersion(certificate) >= MinimumAspNetHttpsCertificateVersion; } - private static byte GetCertificateVersion(X509Certificate2 c) + internal static byte GetCertificateVersion(X509Certificate2 c) { var byteArray = c.Extensions.OfType() .Where(e => string.Equals(AspNetHttpsOid, e.Oid?.Value, StringComparison.Ordinal)) diff --git a/src/Tools/dotnet-dev-certs/src/Program.cs b/src/Tools/dotnet-dev-certs/src/Program.cs index 1ebf5cd67bff..d5ce5de418a7 100644 --- a/src/Tools/dotnet-dev-certs/src/Program.cs +++ b/src/Tools/dotnet-dev-certs/src/Program.cs @@ -47,8 +47,6 @@ internal sealed class Program public static readonly TimeSpan HttpsCertificateValidity = TimeSpan.FromDays(365); - private static bool _parsableOutput; - public static int Main(string[] args) { if (args.Contains("--debug")) @@ -116,8 +114,8 @@ public static int Main(string[] args) "Display warnings and errors only.", CommandOptionType.NoValue); - var parsableOutput = c.Option("--parsable", - "Produce a parsable output, to be used by other tools.", + var checkJsonOutput = c.Option("--check-json-output", + "Same as running --check --trust, but output the results in json.", CommandOptionType.NoValue); c.HelpOption("-h|--help"); @@ -126,17 +124,26 @@ public static int Main(string[] args) { var reporter = new ConsoleReporter(PhysicalConsole.Singleton, verbose.HasValue(), quiet.HasValue()); - _parsableOutput = parsableOutput.HasValue(); - if (verbose.HasValue()) { var listener = new ReporterEventListener(reporter); listener.EnableEvents(CertificateManager.Log, System.Diagnostics.Tracing.EventLevel.Verbose); } + if (checkJsonOutput.HasValue()) + { + if (exportPath.HasValue() || trust?.HasValue() == true || format.HasValue() || noPassword.HasValue() || check.HasValue() || clean.HasValue() || + (!import.HasValue() && password.HasValue()) || + (import.HasValue() && !password.HasValue())) + { + reporter.Error(InvalidUsageErrorMessage); + return CriticalError; + } + } + if (clean.HasValue()) { - if (exportPath.HasValue() || trust?.HasValue() == true || format.HasValue() || noPassword.HasValue() || check.HasValue() || + if (exportPath.HasValue() || trust?.HasValue() == true || format.HasValue() || noPassword.HasValue() || check.HasValue() || checkJsonOutput.HasValue() || (!import.HasValue() && password.HasValue()) || (import.HasValue() && !password.HasValue())) { @@ -147,7 +154,7 @@ public static int Main(string[] args) if (check.HasValue()) { - if (exportPath.HasValue() || password.HasValue() || noPassword.HasValue() || clean.HasValue() || format.HasValue() || import.HasValue()) + if (exportPath.HasValue() || password.HasValue() || noPassword.HasValue() || clean.HasValue() || format.HasValue() || import.HasValue() || checkJsonOutput.HasValue()) { reporter.Error(InvalidUsageErrorMessage); return CriticalError; @@ -191,6 +198,11 @@ public static int Main(string[] args) return ImportCertificate(import, password, reporter); } + if (checkJsonOutput.HasValue()) + { + return CheckHttpsCertificateJsonOutput(reporter); + } + return EnsureHttpsCertificate(exportPath, password, noPassword, trust, format, reporter); }); }); @@ -340,18 +352,21 @@ private static int CheckHttpsCertificate(CommandOption trust, CommandOption verb private static void ReportCertificates(IReporter reporter, IReadOnlyList certificates, string certificateState) { - if (_parsableOutput) + reporter.Output(certificates.Count switch { - reporter.Output(JsonSerializer.Serialize(CertificateReport.FromX509Certificate2List(certificates))); - } - else - { - reporter.Output(certificates.Count switch - { - 1 => $"A {certificateState} certificate was found: {CertificateManager.GetDescription(certificates[0])}", - _ => $"{certificates.Count} {certificateState} certificates were found: {CertificateManager.ToCertificateDescription(certificates)}" - }); - } + 1 => $"A {certificateState} certificate was found: {CertificateManager.GetDescription(certificates[0])}", + _ => $"{certificates.Count} {certificateState} certificates were found: {CertificateManager.ToCertificateDescription(certificates)}" + }); + } + + private static int CheckHttpsCertificateJsonOutput(IReporter reporter) + { + var availableCertificates = CertificateManager.Instance.ListCertificates(StoreName.My, StoreLocation.CurrentUser, isValid: true); + + var certReports = availableCertificates.Select(CertificateReport.FromX509Certificate2).ToList(); + reporter.Output(JsonSerializer.Serialize(certReports, options: new JsonSerializerOptions { WriteIndented = true })); + + return Success; } private static int EnsureHttpsCertificate(CommandOption exportPath, CommandOption password, CommandOption noPassword, CommandOption trust, CommandOption exportFormat, IReporter reporter) @@ -361,7 +376,7 @@ private static int EnsureHttpsCertificate(CommandOption exportPath, CommandOptio if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { - var certificates = manager.ListCertificates(StoreName.My, StoreLocation.CurrentUser, isValid: true, exportPath.HasValue()); + var certificates = manager.ListCertificates(StoreName.My, StoreLocation.CurrentUser, isValid: false, exportPath.HasValue()); foreach (var certificate in certificates) { var status = manager.CheckCertificateState(certificate); @@ -485,19 +500,33 @@ internal class CertificateReport public DateTime ValidityNotAfter { get; init; } public bool IsHttpsDevelopmentCertificate { get; init; } public bool IsExportable { get; init; } + public string TrustLevel { get; private set; } public static CertificateReport FromX509Certificate2(X509Certificate2 cert) { + var certificateManager = CertificateManager.Instance; + var status = certificateManager.CheckCertificateState(cert); + string statusString; + if (!status.Success) + { + statusString = "Invalid"; + } + else + { + var trustStatus = certificateManager.GetTrustLevel(cert); + statusString = trustStatus.ToString(); + } return new CertificateReport { Thumbprint = cert.Thumbprint, Subject = cert.Subject, X509SubjectAlternativeNameExtension = GetSanExtension(cert), - Version = cert.Version, + Version = CertificateManager.GetCertificateVersion(cert), ValidityNotBefore = cert.NotBefore, ValidityNotAfter = cert.NotAfter, IsHttpsDevelopmentCertificate = CertificateManager.IsHttpsDevelopmentCertificate(cert), - IsExportable = CertificateManager.Instance.IsExportable(cert) + IsExportable = certificateManager.IsExportable(cert), + TrustLevel = statusString }; static List GetSanExtension(X509Certificate2 cert) @@ -516,14 +545,4 @@ static List GetSanExtension(X509Certificate2 cert) return dnsNames; } } - - public static List FromX509Certificate2List(IEnumerable certs) - { - var list = new List(); - foreach (var cert in certs) - { - list.Add(FromX509Certificate2(cert)); - } - return list; - } } From b19bebd681be4e85a7ec1a1b71b23ff7aa3f444e Mon Sep 17 00:00:00 2001 From: Benjamin Petit Date: Tue, 8 Apr 2025 15:13:45 +0200 Subject: [PATCH 3/7] Revert unecessary change --- src/Tools/dotnet-dev-certs/src/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tools/dotnet-dev-certs/src/Program.cs b/src/Tools/dotnet-dev-certs/src/Program.cs index d5ce5de418a7..a5ba74ade86e 100644 --- a/src/Tools/dotnet-dev-certs/src/Program.cs +++ b/src/Tools/dotnet-dev-certs/src/Program.cs @@ -376,7 +376,7 @@ private static int EnsureHttpsCertificate(CommandOption exportPath, CommandOptio if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { - var certificates = manager.ListCertificates(StoreName.My, StoreLocation.CurrentUser, isValid: false, exportPath.HasValue()); + var certificates = manager.ListCertificates(StoreName.My, StoreLocation.CurrentUser, isValid: true, exportPath.HasValue()); foreach (var certificate in certificates) { var status = manager.CheckCertificateState(certificate); From a859ba3b7be5db23240c94f001dea2e3692dcb25 Mon Sep 17 00:00:00 2001 From: Benjamin Petit Date: Tue, 15 Apr 2025 18:44:03 +0200 Subject: [PATCH 4/7] Rename option name and add a test --- .../CertificateManager.cs | 18 +++++++++++-- .../test/CertificateManagerTests.cs | 27 +++++++++++++++++-- src/Tools/dotnet-dev-certs/src/Program.cs | 2 +- 3 files changed, 42 insertions(+), 5 deletions(-) diff --git a/src/Shared/CertificateGeneration/CertificateManager.cs b/src/Shared/CertificateGeneration/CertificateManager.cs index 76ff6bc3f134..1d8c713a1e88 100644 --- a/src/Shared/CertificateGeneration/CertificateManager.cs +++ b/src/Shared/CertificateGeneration/CertificateManager.cs @@ -55,14 +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; + internal set + { + ArgumentOutOfRangeException.ThrowIfGreaterThan( + value, + AspNetHttpsCertificateVersion, + $"{nameof(MinimumAspNetHttpsCertificateVersion)} cannot be greater than {nameof(AspNetHttpsCertificateVersion)}"); + field = value; + } } public string Subject { get; } diff --git a/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs b/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs index f8ad0eb24388..65c8c8356e76 100644 --- a/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs +++ b/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs @@ -387,6 +387,7 @@ public void EnsureCreateHttpsCertificate_ReturnsExpiredCertificateIfVersionIsInc Output.WriteLine(creation.ToString()); ListCertificates(); + _manager.AspNetHttpsCertificateVersion = 2; _manager.MinimumAspNetHttpsCertificateVersion = 2; var httpsCertificateList = _manager.ListCertificates(StoreName.My, StoreLocation.CurrentUser, isValid: true); @@ -400,17 +401,39 @@ 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] + 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() @@ -441,7 +464,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); } @@ -533,8 +556,8 @@ public CertFixture() internal void CleanupCertificates() { - Manager.AspNetHttpsCertificateVersion = 1; Manager.MinimumAspNetHttpsCertificateVersion = 1; + Manager.AspNetHttpsCertificateVersion = 1; Manager.RemoveAllCertificates(StoreName.My, StoreLocation.CurrentUser); if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { diff --git a/src/Tools/dotnet-dev-certs/src/Program.cs b/src/Tools/dotnet-dev-certs/src/Program.cs index a5ba74ade86e..bf6a9d964a5c 100644 --- a/src/Tools/dotnet-dev-certs/src/Program.cs +++ b/src/Tools/dotnet-dev-certs/src/Program.cs @@ -114,7 +114,7 @@ public static int Main(string[] args) "Display warnings and errors only.", CommandOptionType.NoValue); - var checkJsonOutput = c.Option("--check-json-output", + var checkJsonOutput = c.Option("--check-trust-machine-readable", "Same as running --check --trust, but output the results in json.", CommandOptionType.NoValue); From f123d27dd0eeb5c5cec82172de892edf318f4267 Mon Sep 17 00:00:00 2001 From: Benjamin Petit Date: Tue, 15 Apr 2025 18:49:28 +0200 Subject: [PATCH 5/7] Modify ListCertificates_AlwaysReturnsTheCertificate_WithHighestVersion to create 3 certs --- .../test/CertificateManagerTests.cs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs b/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs index 65c8c8356e76..51a5e3407bc1 100644 --- a/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs +++ b/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs @@ -478,6 +478,7 @@ 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(); @@ -488,7 +489,13 @@ public void ListCertificates_AlwaysReturnsTheCertificate_WithHighestVersion() Output.WriteLine(creation.ToString()); ListCertificates(); - _manager.MinimumAspNetHttpsCertificateVersion = 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); @@ -499,13 +506,13 @@ public void ListCertificates_AlwaysReturnsTheCertificate_WithHighestVersion() firstCertificate.Extensions.OfType(), e => e.Critical == false && e.Oid.Value == CertificateManager.AspNetHttpsOid && - e.RawData[0] == 2); + e.RawData[0] == 3); Assert.Contains( secondCertificate.Extensions.OfType(), e => e.Critical == false && e.Oid.Value == CertificateManager.AspNetHttpsOid && - e.RawData[0] == 1); + e.RawData[0] == 2); } [ConditionalFact] From c5ccb296a97e4c1fd4d4843a99d23f5618a9bf16 Mon Sep 17 00:00:00 2001 From: Benjamin Petit Date: Thu, 17 Apr 2025 12:13:18 +0200 Subject: [PATCH 6/7] Skip on Helix the added test --- src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs b/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs index 51a5e3407bc1..37bdd2cafa0a 100644 --- a/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs +++ b/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs @@ -415,6 +415,7 @@ public void EnsureCreateHttpsCertificate_ReturnsExpiredCertificateForEmptyVersio } [Fact] + [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6720", Queues = "All.OSX")] public void EnsureCreateHttpsCertificate_DoNotOverrideValidOldCertificate() { _fixture.CleanupCertificates(); From 5baecf05381d6f820abed42675c44c7c1b495aa4 Mon Sep 17 00:00:00 2001 From: Benjamin Petit Date: Thu, 22 May 2025 13:49:00 +0200 Subject: [PATCH 7/7] Skip EnsureCreateHttpsCertificate_DoNotOverrideValidOldCertificate on OSX --- src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs | 2 +- src/submodules/googletest | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs b/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs index 37bdd2cafa0a..9e32f87d0ca4 100644 --- a/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs +++ b/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs @@ -414,7 +414,7 @@ public void EnsureCreateHttpsCertificate_ReturnsExpiredCertificateForEmptyVersio Assert.Empty(httpsCertificateList); } - [Fact] + [ConditionalFact] [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6720", Queues = "All.OSX")] public void EnsureCreateHttpsCertificate_DoNotOverrideValidOldCertificate() { diff --git a/src/submodules/googletest b/src/submodules/googletest index 571930618fa9..c2ceb2b09bfe 160000 --- a/src/submodules/googletest +++ b/src/submodules/googletest @@ -1 +1 @@ -Subproject commit 571930618fa96eabcd05b573285edbee9fc13bae +Subproject commit c2ceb2b09bfe6660f08c024cc758e5e9632acc07