| File: DeveloperCertificateService.cs | Web Access |
| Project: src\src\Aspire.Hosting\Aspire.Hosting.csproj (Aspire.Hosting) |
#pragma warning disable ASPIRECERTIFICATES001 #pragma warning disable ASPIREFILESYSTEM001 // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Aspire.Hosting.Utils; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using System.Collections.Immutable; using System.Security.Cryptography.X509Certificates; namespace Aspire.Hosting; internal class DeveloperCertificateService : IDeveloperCertificateService { private readonly Lazy<ImmutableList<X509Certificate2>> _certificates; private readonly Lazy<bool> _supportsContainerTrust; private readonly Lazy<bool> _supportsTlsTermination; private bool _latestCertificateIsUntrusted; public DeveloperCertificateService(ILogger<DeveloperCertificateService> logger, IConfiguration configuration, DistributedApplicationOptions options) { TrustCertificate = configuration.GetBool(KnownConfigNames.DeveloperCertificateDefaultTrust) ?? options.TrustDeveloperCertificate ?? true; _certificates = new Lazy<ImmutableList<X509Certificate2>>(() => { try { using var store = new X509Store(StoreName.My, StoreLocation.CurrentUser); store.Open(OpenFlags.ReadOnly); var now = DateTimeOffset.Now; // Get all valid ASP.NET Core development certificates. // Use .Where() instead of .Find() to preserve the original keychain-backed certificate // instances on macOS. Find() clones certificates which can invalidate keychain handles. var validCerts = FindDevCertificates(store, now).ToList(); // If any certificate has a Subject Key Identifier extension, exclude certificates without it if (validCerts.Any(c => c.HasSubjectKeyIdentifier())) { validCerts = validCerts.Where(c => c.HasSubjectKeyIdentifier()).ToList(); } // Order by version and expiration date descending to get the most recent, highest version first. // OpenSSL will only check the first self-signed certificate in the bundle that matches a given domain, // so we want to ensure the certificate that will be used by ASP.NET Core is the first one in the bundle. // Match the ordering logic ASP.NET Core uses, including DateTimeOffset.Now for current time: https://github.com/dotnet/aspnetcore/blob/0aefdae365ff9b73b52961acafd227309524ce3c/src/Shared/CertificateGeneration/CertificateManager.cs#L122 var bestCerts = validCerts .GroupBy(c => c.Extensions.OfType<X509SubjectKeyIdentifierExtension>().FirstOrDefault()?.SubjectKeyIdentifier) .SelectMany(g => g.OrderByVersion().Take(1)) .OrderByVersion() .ToList(); // Partition into trusted and untrusted using a single X509Chain instance. // RevocationMode is set to NoCheck since revocation doesn't apply to self-signed dev certs. using var chain = new X509Chain(); chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; // On Windows, chain.Build() can succeed even when the certificate isn't in the // trusted root store. Open the CurrentUser Root store so we can verify membership. X509Certificate2Collection? rootCerts = null; if (OperatingSystem.IsWindows()) { using var rootStore = new X509Store(StoreName.Root, StoreLocation.CurrentUser); rootStore.Open(OpenFlags.ReadOnly); rootCerts = rootStore.Certificates; } // Find the dev certs that are trusted var trustedCerts = new List<X509Certificate2>(); foreach (var cert in bestCerts) { try { if (!chain.Build(cert)) { continue; } // On Windows, also verify the certificate exists in the root store if (rootCerts is not null && !rootCerts.Any(rc => rc.RawDataMemory.Span.SequenceEqual(cert.RawDataMemory.Span))) { continue; } trustedCerts.Add(cert); } finally { // Reset the chain for the next certificate regardless of branch taken. chain.Reset(); } } // Dispose root store certificates after use if (rootCerts is not null) { foreach (var rc in rootCerts) { rc.Dispose(); } } // Flag if the newest/highest-version cert is not trusted if (bestCerts.Count > 0 && (trustedCerts.Count == 0 || trustedCerts[0].Thumbprint != bestCerts[0].Thumbprint)) { _latestCertificateIsUntrusted = true; } // Release the unused certificates foreach (var unusedCert in validCerts.Except(trustedCerts)) { unusedCert.Dispose(); } if (trustedCerts.Count == 0) { return ImmutableList<X509Certificate2>.Empty; } return trustedCerts.ToImmutableList(); } catch (Exception ex) { logger.LogWarning("Failed to load developer certificates from the CurrentUser/My certificate store. Automatic trust of development certificates will not be available. Reason: {Message}", ex.Message); return ImmutableList<X509Certificate2>.Empty; } }); _supportsContainerTrust = new Lazy<bool>(() => { var containerTrustAvailable = Certificates.Any(c => c.GetCertificateVersion() >= X509Certificate2Extensions.MinimumCertificateVersionSupportingContainerTrust); logger.LogDebug("Container trust for developer certificates is {Status}.", containerTrustAvailable ? "available" : "not available"); return containerTrustAvailable; }); _supportsTlsTermination = new Lazy<bool>(() => { var supportsTlsTermination = Certificates.Any(c => c.HasPrivateKey); logger.LogDebug("Developer certificate HTTPS/TLS termination support: {Available}", supportsTlsTermination); return supportsTlsTermination; }); // By default, only use for server authentication if trust is also enabled (and a developer certificate with a private key is available) UseForHttps = (configuration.GetBool(KnownConfigNames.DeveloperCertificateDefaultHttpsTermination) ?? options.DeveloperCertificateDefaultHttpsTerminationEnabled ?? true) && TrustCertificate && _supportsTlsTermination.Value; } /// <inheritdoc /> public ImmutableList<X509Certificate2> Certificates => _certificates.Value; /// <inheritdoc /> public bool SupportsContainerTrust => _supportsContainerTrust.Value; /// <inheritdoc /> public bool TrustCertificate { get; } /// <inheritdoc /> public bool UseForHttps { get; } /// <summary> /// Gets a value indicating whether a newer ASP.NET Core development certificate was detected /// that is not in the trusted set. This is true when the highest-version/most-recent dev cert /// is not trusted, even though older trusted certs may exist. /// </summary> internal bool LatestCertificateIsUntrusted { get { _ = _certificates.Value; // Ensure certificates have been evaluated return _latestCertificateIsUntrusted; } } /// <summary> /// Finds ASP.NET Core development certificates in the store, filtered by date validity and private key presence. /// </summary> private static IEnumerable<X509Certificate2> FindDevCertificates(X509Store store, DateTimeOffset now) { return store.Certificates .Where(c => c.IsAspNetCoreDevelopmentCertificate()) .Where(c => c.NotBefore <= now && now <= c.NotAfter) .Where(c => c.HasPrivateKey); } }