File: Certificates\NativeCertificateToolRunner.cs
Web Access
Project: src\src\Aspire.Cli\Aspire.Cli.csproj (aspire)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Microsoft.AspNetCore.Certificates.Generation;
 
namespace Aspire.Cli.Certificates;
 
/// <summary>
/// Certificate tool runner that uses the native CertificateManager directly (no subprocess needed).
/// </summary>
internal sealed class NativeCertificateToolRunner(CertificateManager certificateManager, Func<bool>? isLinux = null) : ICertificateToolRunner
{
    private readonly Func<bool> _isLinux = isLinux ?? OperatingSystem.IsLinux;
 
    public CertificateTrustResult CheckHttpCertificate()
    {
        var availableCertificates = certificateManager.ListCertificates(
            StoreName.My, StoreLocation.CurrentUser, isValid: true);
 
        try
        {
            var now = DateTimeOffset.Now;
            var certInfos = availableCertificates.Select(cert =>
            {
                var status = certificateManager.CheckCertificateState(cert);
                var trustLevel = status.Success
                    ? certificateManager.GetTrustLevel(cert)
                    : CertificateManager.TrustLevel.None;
 
                return new DevCertInfo
                {
                    Thumbprint = cert.Thumbprint,
                    Subject = cert.Subject,
                    SubjectAlternativeNames = GetSanExtension(cert),
                    Version = CertificateManager.GetCertificateVersion(cert),
                    ValidityNotBefore = cert.NotBefore,
                    ValidityNotAfter = cert.NotAfter,
                    IsHttpsDevelopmentCertificate = CertificateManager.IsHttpsDevelopmentCertificate(cert),
                    IsExportable = certificateManager.IsExportable(cert),
                    TrustLevel = trustLevel
                };
            }).ToList();
 
            var validCerts = certInfos
                .Where(c => c.IsHttpsDevelopmentCertificate && c.ValidityNotBefore <= now && now <= c.ValidityNotAfter)
                .OrderByDescending(c => c.Version)
                .ToList();
 
            var highestVersionedCert = validCerts.FirstOrDefault();
 
            return new CertificateTrustResult
            {
                HasCertificates = validCerts.Count > 0,
                TrustLevel = highestVersionedCert?.TrustLevel,
                Certificates = certInfos
            };
        }
        finally
        {
            CertificateManager.DisposeCertificates(availableCertificates);
        }
    }
 
    public EnsureCertificateResult TrustHttpCertificate()
    {
        if (_isLinux())
        {
            var availableCertificates = certificateManager.ListCertificates(
                StoreName.My, StoreLocation.CurrentUser, isValid: true);
 
            try
            {
                return TrustHttpCertificateOnLinux(availableCertificates, DateTimeOffset.Now);
            }
            finally
            {
                CertificateManager.DisposeCertificates(availableCertificates);
            }
        }
 
        var now = DateTimeOffset.Now;
        return certificateManager.EnsureAspNetCoreHttpsDevelopmentCertificate(
            now, now.Add(TimeSpan.FromDays(365)),
            trust: true);
    }
 
    internal EnsureCertificateResult TrustHttpCertificateOnLinux(IEnumerable<X509Certificate2> availableCertificates, DateTimeOffset now)
    {
        X509Certificate2? certificate = null;
        var createdCertificate = false;
 
        try
        {
            certificate = availableCertificates
                .Where(c => c.Subject == certificateManager.Subject && CertificateManager.GetCertificateVersion(c) >= CertificateManager.CurrentAspNetCoreCertificateVersion)
                .OrderByDescending(CertificateManager.GetCertificateVersion)
                .FirstOrDefault();
 
            var successResult = EnsureCertificateResult.ExistingHttpsCertificateTrusted;
 
            if (certificate is null)
            {
                try
                {
                    certificate = certificateManager.CreateAspNetCoreHttpsDevelopmentCertificate(now, now.Add(TimeSpan.FromDays(365)));
                    createdCertificate = true;
                }
                catch
                {
                    return EnsureCertificateResult.ErrorCreatingTheCertificate;
                }
 
                try
                {
                    certificate = certificateManager.SaveCertificate(certificate);
                }
                catch
                {
                    return EnsureCertificateResult.ErrorSavingTheCertificateIntoTheCurrentUserPersonalStore;
                }
 
                successResult = EnsureCertificateResult.NewHttpsCertificateTrusted;
            }
 
            try
            {
                return certificateManager.TrustCertificate(certificate) switch
                {
                    CertificateManager.TrustLevel.Full => successResult,
                    CertificateManager.TrustLevel.Partial => EnsureCertificateResult.PartiallyFailedToTrustTheCertificate,
                    _ => EnsureCertificateResult.FailedToTrustTheCertificate
                };
            }
            catch (CertificateManager.UserCancelledTrustException)
            {
                return EnsureCertificateResult.UserCancelledTrustStep;
            }
            catch
            {
                return EnsureCertificateResult.FailedToTrustTheCertificate;
            }
        }
        finally
        {
            if (createdCertificate)
            {
                certificate?.Dispose();
            }
        }
    }
 
    /// Win32 ERROR_CANCELLED (0x4C7) encoded as an HRESULT (0x800704C7).
    /// Thrown when the user dismisses the Windows certificate-store security dialog.
    private const int UserCancelledHResult = unchecked((int)0x800704C7);
    private const int UserCancelledErrorCode = 1223;
 
    public CertificateCleanResult CleanHttpCertificate()
    {
        try
        {
            certificateManager.CleanupHttpsCertificates();
            return new CertificateCleanResult { Success = true };
        }
        catch (CryptographicException ex) when (ex.HResult == UserCancelledHResult || ex.HResult == UserCancelledErrorCode)
        {
            return new CertificateCleanResult { Success = false, WasCancelled = true, ErrorMessage = ex.Message };
        }
        catch (Exception ex)
        {
            return new CertificateCleanResult { Success = false, ErrorMessage = ex.Message };
        }
    }
 
    private static string[]? GetSanExtension(X509Certificate2 cert)
    {
        var dnsNames = new List<string>();
        foreach (var extension in cert.Extensions)
        {
            if (extension is X509SubjectAlternativeNameExtension sanExtension)
            {
                foreach (var dns in sanExtension.EnumerateDnsNames())
                {
                    dnsNames.Add(dns);
                }
            }
        }
        return dnsNames.Count > 0 ? dnsNames.ToArray() : null;
    }
}