File: Certificates\CertificateGeneration\CertificateManager.cs
Web Access
Project: src\src\Aspire.Cli\Aspire.Cli.Tool.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.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Net;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
 
#nullable enable
 
namespace Microsoft.AspNetCore.Certificates.Generation;
 
internal abstract class CertificateManager
{
    // This is the version of the ASP.NET Core HTTPS development certificate that will be generated by tooling built with this version of the library.
    // Increment this when making any structural changes to the generated certificate (e.g. changing extensions, key usages, SANs, etc.).
    // Version 6 was introduced in SDK 10.0.102 and runtime 10.0.2.
    internal const int CurrentAspNetCoreCertificateVersion = 6;
    // This is the minimum version of the certificate that will be considered valid by runtime components built using this version of the library.
    // Increment this only when making breaking changes to the certificate or during major runtime version increments. Must always be less than or equal to CurrentAspNetCoreCertificateVersion.
    // This determines the minimum version of the tooling required to generate a certificate that will be considered valid by the runtime.
    // Version 4 was introduced in SDK 10.0.100 and runtime 10.0.0.
    internal const int CurrentMinimumAspNetCoreCertificateVersion = 4;
 
    // OID used for HTTPS certs
    internal const string AspNetHttpsOid = "1.3.6.1.4.1.311.84.1.1";
    internal const string AspNetHttpsOidFriendlyName = "ASP.NET Core HTTPS development certificate";
 
    private const string ServerAuthenticationEnhancedKeyUsageOid = "1.3.6.1.5.5.7.3.1";
    private const string ServerAuthenticationEnhancedKeyUsageOidFriendlyName = "Server Authentication";
 
    internal const string SubjectKeyIdentifierOid = "2.5.29.14";
    internal const string AuthorityKeyIdentifierOid = "2.5.29.35";
 
    // dns names of the host from a container
    private const string LocalhostDockerHttpsDnsName = "host.docker.internal";
    private const string ContainersDockerHttpsDnsName = "host.containers.internal";
 
    // wildcard DNS names
    private const string LocalhostWildcardHttpsDnsName = "*.dev.localhost";
    private const string InternalWildcardHttpsDnsName = "*.dev.internal";
 
    // main cert subject
    private const string LocalhostHttpsDnsName = "localhost";
    internal const string LocalhostHttpsDistinguishedName = "CN=" + LocalhostHttpsDnsName;
 
    public const int RSAMinimumKeySizeInBits = 2048;
 
    public static CertificateManager Create(ILogger logger) => OperatingSystem.IsWindows() ?
#pragma warning disable CA1416 // Validate platform compatibility
            new WindowsCertificateManager(logger) :
#pragma warning restore CA1416 // Validate platform compatibility
            OperatingSystem.IsMacOS() ?
            new MacOSCertificateManager(logger) as CertificateManager :
            new UnixCertificateManager(logger);
 
    protected CertificateManagerLogger Log { get; }
 
    // Setting to 0 means we don't append the version byte,
    // which is what all machines currently have.
    public int AspNetHttpsCertificateVersion
    {
        get;
        // For testing purposes only
        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
        {
            ArgumentOutOfRangeException.ThrowIfGreaterThan(
                value,
                AspNetHttpsCertificateVersion,
                $"{nameof(MinimumAspNetHttpsCertificateVersion)} cannot be greater than {nameof(AspNetHttpsCertificateVersion)}");
            field = value;
        }
    }
 
    public string Subject { get; }
 
    public CertificateManager(ILogger logger) : this(logger, LocalhostHttpsDistinguishedName, CurrentAspNetCoreCertificateVersion, CurrentMinimumAspNetCoreCertificateVersion)
    {
    }
 
    // For testing purposes only
    internal CertificateManager(string subject, int version)
        : this(NullLogger.Instance, subject, version, version)
    {
    }
 
    // For testing purposes only
    internal CertificateManager(ILogger logger, string subject, int generatedVersion, int minimumVersion)
    {
        Log = new CertificateManagerLogger(logger);
        Subject = subject;
        AspNetHttpsCertificateVersion = generatedVersion;
        MinimumAspNetHttpsCertificateVersion = minimumVersion;
    }
 
    /// <remarks>
    /// 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.
    /// </remarks>
    public static bool IsHttpsDevelopmentCertificate(X509Certificate2 certificate)
    {
        foreach (var extension in certificate.Extensions.OfType<X509Extension>())
        {
            if (string.Equals(AspNetHttpsOid, extension.Oid?.Value, StringComparison.Ordinal))
            {
                return true;
            }
        }
        return false;
    }
 
    public IList<X509Certificate2> ListCertificates(
        StoreName storeName,
        StoreLocation location,
        bool isValid,
        bool requireExportable = true)
    {
        Log.ListCertificatesStart(location, storeName);
        var certificates = new List<X509Certificate2>();
        try
        {
            using var store = new X509Store(storeName, location);
            store.Open(OpenFlags.ReadOnly);
            PopulateCertificatesFromStore(store, certificates, requireExportable);
            IEnumerable<X509Certificate2> matchingCertificates = certificates;
            matchingCertificates = matchingCertificates
                .Where(c => HasOid(c, AspNetHttpsOid));
 
            if (Log.IsEnabled())
            {
                Log.DescribeFoundCertificates(ToCertificateDescription(matchingCertificates));
            }
 
            if (isValid)
            {
                // Ensure the certificate hasn't expired, has a private key and its exportable
                // (for container/unix scenarios).
                Log.CheckCertificatesValidity();
                var now = DateTimeOffset.Now;
                var validCertificates = matchingCertificates
                    .Where(c => IsValidCertificate(c, now, requireExportable))
                    .OrderByDescending(GetCertificateVersion)
                    .ToArray();
 
                if (Log.IsEnabled())
                {
                    var invalidCertificates = matchingCertificates.Except(validCertificates);
                    Log.DescribeValidCertificates(ToCertificateDescription(validCertificates));
                    Log.DescribeInvalidCertificates(ToCertificateDescription(invalidCertificates));
                }
 
                // Ensure the certificate meets the minimum version requirement.
                var validMinVersionCertificates = validCertificates
                    .Where(c => GetCertificateVersion(c) >= MinimumAspNetHttpsCertificateVersion)
                    .ToArray();
 
                if (Log.IsEnabled())
                {
                    var belowMinimumVersionCertificates = validCertificates.Except(validMinVersionCertificates);
                    Log.DescribeMinimumVersionCertificates(ToCertificateDescription(validMinVersionCertificates));
                    Log.DescribeBelowMinimumVersionCertificates(ToCertificateDescription(belowMinimumVersionCertificates));
                }
 
                matchingCertificates = validMinVersionCertificates;
            }
 
            // We need to enumerate the certificates early to prevent disposing issues.
            matchingCertificates = matchingCertificates.ToList();
 
            var certificatesToDispose = certificates.Except(matchingCertificates);
            DisposeCertificates(certificatesToDispose);
 
            store.Close();
 
            Log.ListCertificatesEnd();
            return (IList<X509Certificate2>)matchingCertificates;
        }
        catch (Exception e)
        {
            if (Log.IsEnabled())
            {
                Log.ListCertificatesError(e.ToString());
            }
            DisposeCertificates(certificates);
            certificates.Clear();
            return certificates;
        }
 
        bool HasOid(X509Certificate2 certificate, string oid) =>
            certificate.Extensions.OfType<X509Extension>()
                .Any(e => string.Equals(oid, e.Oid?.Value, StringComparison.Ordinal));
    }
 
    /// <summary>
    /// Validate that the certificate is valid at the given date and time (and exportable if required).
    /// </summary>
    /// <param name="certificate">The certificate to validate.</param>
    /// <param name="currentDate">The current date to validate against.</param>
    /// <param name="requireExportable">Whether the certificate must be exportable.</param>
    /// <returns>True if the certificate is valid; otherwise, false.</returns>
    internal bool IsValidCertificate(X509Certificate2 certificate, DateTimeOffset currentDate, bool requireExportable)
    {
        return certificate.NotBefore <= currentDate &&
            currentDate <= certificate.NotAfter &&
            (!requireExportable || IsExportable(certificate));
    }
 
    internal static byte GetCertificateVersion(X509Certificate2 c)
    {
        var byteArray = c.Extensions.OfType<X509Extension>()
            .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<X509Certificate2> certificates, bool requireExportable)
    {
        certificates.AddRange(store.Certificates.OfType<X509Certificate2>());
    }
 
    public IList<X509Certificate2> GetHttpsCertificates() =>
        ListCertificates(StoreName.My, StoreLocation.CurrentUser, isValid: true, requireExportable: true);
 
    /// <summary>
    /// Ensures that a valid ASP.NET Core HTTPS development certificate is present.
    /// </summary>
    /// <param name="notBefore">The date and time before which the certificate is not valid.</param>
    /// <param name="notAfter">The date and time after which the certificate is not valid.</param>
    /// <param name="path">Path to export the certificate (directory must exist).</param>
    /// <param name="trust">Whether to trust the certificate or simply add it to the CurrentUser/My store.</param>
    /// <param name="includePrivateKey">Whether to include the private key in the exported certificate.</param>
    /// <param name="password">Password for the exported certificate.</param>
    /// <param name="keyExportFormat">Format for exporting the certificate key.</param>
    /// <param name="isInteractive">Whether the operation is interactive (dotnet dev-certs tool) or non-interactive (first run experience).</param>
    /// <returns>The result of the ensure operation.</returns>
    /// <exception cref="InvalidOperationException">There was an error ensuring the certificate exists.</exception>
    /// <remarks>
    /// The minimum certificate version checks behave differently based on whether the operation is interactive or not. In interactive mode,
    /// the certificate will only be considered valid if it meets or exceeds the current version of the certificate. In non-interactive mode,
    /// the certificate will be considered valid as long as it meets the minimum supported version requirement. This is to allow first run
    /// to upgrade a certificate if it becomes necessary to bump the minimum version due to security issues, etc. while not leaving users with
    /// a partially valid certificate after a normal first run experience. Interactive scenarios such as the dotnet dev-certs tool should always
    /// ensure the certificate is updated to at least the latest supported version.
    /// </remarks>
    public EnsureCertificateResult EnsureAspNetCoreHttpsDevelopmentCertificate(
        DateTimeOffset notBefore,
        DateTimeOffset notAfter,
        string? path = null,
        bool trust = false,
        bool includePrivateKey = false,
        string? password = null,
        CertificateKeyExportFormat keyExportFormat = CertificateKeyExportFormat.Pfx,
        bool isInteractive = true)
    {
        var result = EnsureCertificateResult.Succeeded;
 
        var allCurrentUserCertificates = ListCertificates(StoreName.My, StoreLocation.CurrentUser, isValid: true, requireExportable: true);
        var allLocalMachineCertificates = ListCertificates(StoreName.My, StoreLocation.LocalMachine, isValid: true, requireExportable: true);
 
        var currentUserCertificates = allCurrentUserCertificates.Where(c => c.Subject == Subject).ToList();
        var localMachineCertificates = allLocalMachineCertificates.Where(c => c.Subject == Subject).ToList();
        var filteredCertificates = currentUserCertificates.Concat(localMachineCertificates).ToList();
 
        if (isInteractive)
        {
            // For purposes of updating the dev cert, only consider certificates with the current version or higher as valid
            // Only applies to interactive scenarios where we want to ensure we're generating the latest certificate
            // For non-interactive scenarios (e.g. first run experience), we want to accept older versions of the certificate as long as they meet the minimum version requirement
            // This will allow us to respond to scenarios where we need to invalidate older certificates due to security issues, etc. but not leave users
            // with a partially valid certificate after their first run experience.
            filteredCertificates = filteredCertificates.Where(c => GetCertificateVersion(c) >= AspNetHttpsCertificateVersion).ToList();
        }
 
        if (Log.IsEnabled())
        {
            var excludedCertificates = allCurrentUserCertificates.Concat(allLocalMachineCertificates).Except(filteredCertificates);
            Log.FilteredCertificates(ToCertificateDescription(filteredCertificates));
            Log.ExcludedCertificates(ToCertificateDescription(excludedCertificates));
        }
 
        // Dispose certificates we're not going to use
        DisposeCertificates(allCurrentUserCertificates.Except(currentUserCertificates));
        DisposeCertificates(allLocalMachineCertificates.Except(localMachineCertificates));
 
        var certificates = filteredCertificates;
 
        X509Certificate2? certificate = null;
        var isNewCertificate = false;
        if (certificates.Any())
        {
            certificate = certificates.First();
            var failedToFixCertificateState = false;
            if (isInteractive)
            {
                // Skip this step if the command is not interactive,
                // as we don't want to prompt on first run experience.
                foreach (var candidate in currentUserCertificates)
                {
                    var status = CheckCertificateState(candidate);
                    if (!status.Success)
                    {
                        try
                        {
                            if (Log.IsEnabled())
                            {
                                Log.CorrectCertificateStateStart(GetDescription(candidate));
                            }
                            CorrectCertificateState(candidate);
                            Log.CorrectCertificateStateEnd();
                        }
                        catch (Exception e)
                        {
                            if (Log.IsEnabled())
                            {
                                Log.CorrectCertificateStateError(e.ToString());
                            }
                            result = EnsureCertificateResult.FailedToMakeKeyAccessible;
                            // We don't return early on this type of failure to allow for tooling to
                            // export or trust the certificate even in this situation, as that enables
                            // exporting the certificate to perform any necessary fix with native tooling.
                            failedToFixCertificateState = true;
                        }
                    }
                }
            }
 
            if (!failedToFixCertificateState)
            {
                if (Log.IsEnabled())
                {
                    Log.ValidCertificatesFound(ToCertificateDescription(certificates));
                }
                certificate = certificates.First();
                if (Log.IsEnabled())
                {
                    Log.SelectedCertificate(GetDescription(certificate));
                }
                result = EnsureCertificateResult.ValidCertificatePresent;
            }
        }
        else
        {
            Log.NoValidCertificatesFound();
            try
            {
                Log.CreateDevelopmentCertificateStart();
                isNewCertificate = true;
                certificate = CreateAspNetCoreHttpsDevelopmentCertificate(notBefore, notAfter);
            }
            catch (Exception e)
            {
                if (Log.IsEnabled())
                {
                    Log.CreateDevelopmentCertificateError(e.ToString());
                }
                result = EnsureCertificateResult.ErrorCreatingTheCertificate;
                DisposeCertificates(certificates);
                return result;
            }
            Log.CreateDevelopmentCertificateEnd();
 
            try
            {
                certificate = SaveCertificate(certificate);
            }
            catch (Exception e)
            {
                Log.SaveCertificateInStoreError(e.ToString());
                if (isNewCertificate)
                {
                    certificate?.Dispose();
                }
                DisposeCertificates(certificates);
                result = EnsureCertificateResult.ErrorSavingTheCertificateIntoTheCurrentUserPersonalStore;
                return result;
            }
 
            if (isInteractive)
            {
                try
                {
                    if (Log.IsEnabled())
                    {
                        Log.CorrectCertificateStateStart(GetDescription(certificate));
                    }
                    CorrectCertificateState(certificate);
                    Log.CorrectCertificateStateEnd();
                }
                catch (Exception e)
                {
                    if (Log.IsEnabled())
                    {
                        Log.CorrectCertificateStateError(e.ToString());
                    }
 
                    // We don't return early on this type of failure to allow for tooling to
                    // export or trust the certificate even in this situation, as that enables
                    // exporting the certificate to perform any necessary fix with native tooling.
                    result = EnsureCertificateResult.FailedToMakeKeyAccessible;
                }
            }
        }
 
        if (path != null)
        {
            try
            {
                // If the user specified a non-existent directory, we don't want to be responsible
                // for setting the permissions appropriately, so we'll bail.
                var exportDir = Path.GetDirectoryName(path);
                if (!string.IsNullOrEmpty(exportDir) && !Directory.Exists(exportDir))
                {
                    result = EnsureCertificateResult.ErrorExportingTheCertificateToNonExistentDirectory;
                    throw new InvalidOperationException($"The directory '{exportDir}' does not exist.  Choose permissions carefully when creating it.");
                }
 
                ExportCertificate(certificate, path, includePrivateKey, password, keyExportFormat);
            }
            catch (Exception e)
            {
                if (Log.IsEnabled())
                {
                    Log.ExportCertificateError(e.ToString());
                }
 
                // We don't want to mask the original source of the error here.
                result = result != EnsureCertificateResult.Succeeded && result != EnsureCertificateResult.ValidCertificatePresent ?
                    result :
                    EnsureCertificateResult.ErrorExportingTheCertificate;
 
                if (isNewCertificate)
                {
                    certificate?.Dispose();
                }
                DisposeCertificates(certificates);
                return result;
            }
        }
 
        if (trust)
        {
            try
            {
                var trustLevel = TrustCertificate(certificate);
                switch (trustLevel)
                {
                    case TrustLevel.Full:
                        // Leave result as-is.
                        break;
                    case TrustLevel.Partial:
                        result = EnsureCertificateResult.PartiallyFailedToTrustTheCertificate;
                        if (isNewCertificate)
                        {
                            certificate?.Dispose();
                        }
                        DisposeCertificates(certificates);
                        return result;
                    case TrustLevel.None:
                    default: // Treat unknown status (should be impossible) as failure
                        result = EnsureCertificateResult.FailedToTrustTheCertificate;
                        if (isNewCertificate)
                        {
                            certificate?.Dispose();
                        }
                        DisposeCertificates(certificates);
                        return result;
                }
            }
            catch (UserCancelledTrustException)
            {
                result = EnsureCertificateResult.UserCancelledTrustStep;
                if (isNewCertificate)
                {
                    certificate?.Dispose();
                }
                DisposeCertificates(certificates);
                return result;
            }
            catch
            {
                result = EnsureCertificateResult.FailedToTrustTheCertificate;
                if (isNewCertificate)
                {
                    certificate?.Dispose();
                }
                DisposeCertificates(certificates);
                return result;
            }
 
            if (result == EnsureCertificateResult.ValidCertificatePresent)
            {
                result = EnsureCertificateResult.ExistingHttpsCertificateTrusted;
            }
            else
            {
                result = EnsureCertificateResult.NewHttpsCertificateTrusted;
            }
        }
 
        DisposeCertificates(!isNewCertificate ? certificates : certificates.Append(certificate));
 
        return result;
    }
 
    internal ImportCertificateResult ImportCertificate(string certificatePath, string password)
    {
        if (!File.Exists(certificatePath))
        {
            Log.ImportCertificateMissingFile(certificatePath);
            return ImportCertificateResult.CertificateFileMissing;
        }
 
        var certificates = ListCertificates(StoreName.My, StoreLocation.CurrentUser, isValid: false, requireExportable: false);
        if (certificates.Any())
        {
            if (Log.IsEnabled())
            {
                Log.ImportCertificateExistingCertificates(ToCertificateDescription(certificates));
            }
            return ImportCertificateResult.ExistingCertificatesPresent;
        }
 
        X509Certificate2? certificate = null;
        try
        {
            try
            {
                Log.LoadCertificateStart(certificatePath);
                certificate = X509CertificateLoader.LoadPkcs12FromFile(certificatePath, password, X509KeyStorageFlags.Exportable | X509KeyStorageFlags.EphemeralKeySet);
                if (Log.IsEnabled())
                {
                    Log.LoadCertificateEnd(GetDescription(certificate));
                }
            }
            catch (Exception e)
            {
                if (Log.IsEnabled())
                {
                    Log.LoadCertificateError(e.ToString());
                }
                return ImportCertificateResult.InvalidCertificate;
            }
 
            // 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())
                {
                    Log.NoHttpsDevelopmentCertificate(GetDescription(certificate));
                }
                return ImportCertificateResult.NoDevelopmentHttpsCertificate;
            }
 
            try
            {
                certificate = SaveCertificate(certificate);
            }
            catch (Exception e)
            {
                if (Log.IsEnabled())
                {
                    Log.SaveCertificateInStoreError(e.ToString());
                }
                return ImportCertificateResult.ErrorSavingTheCertificateIntoTheCurrentUserPersonalStore;
            }
 
            return ImportCertificateResult.Succeeded;
        }
        finally
        {
            certificate?.Dispose();
        }
    }
 
    public void CleanupHttpsCertificates()
    {
        var certificates = ListCertificates(StoreName.My, StoreLocation.CurrentUser, isValid: false);
        var filteredCertificates = certificates.Where(c => c.Subject == Subject);
 
        if (Log.IsEnabled())
        {
            var excludedCertificates = certificates.Except(filteredCertificates);
            Log.FilteredCertificates(ToCertificateDescription(filteredCertificates));
            Log.ExcludedCertificates(ToCertificateDescription(excludedCertificates));
        }
 
        foreach (var certificate in filteredCertificates)
        {
            // RemoveLocations.All will first remove from the trusted roots (e.g. keychain on
            // macOS) and then from the local user store.
            RemoveCertificate(certificate, RemoveLocations.All);
        }
    }
 
    public abstract TrustLevel GetTrustLevel(X509Certificate2 certificate);
 
    protected abstract X509Certificate2 SaveCertificateCore(X509Certificate2 certificate, StoreName storeName, StoreLocation storeLocation);
 
    /// <remarks>Implementations may choose to throw, rather than returning <see cref="TrustLevel.None"/>.</remarks>
    protected abstract TrustLevel TrustCertificateCore(X509Certificate2 certificate);
 
    internal abstract bool IsExportable(X509Certificate2 c);
 
    protected abstract void RemoveCertificateFromTrustedRoots(X509Certificate2 certificate);
 
    protected abstract IList<X509Certificate2> GetCertificatesToRemove(StoreName storeName, StoreLocation storeLocation);
 
    protected abstract void CreateDirectoryWithPermissions(string directoryPath);
 
    /// <remarks>
    /// Will create directories to make it possible to write to <paramref name="path"/>.
    /// If you don't want that, check for existence before calling this method.
    /// </remarks>
    internal void ExportCertificate(X509Certificate2 certificate, string path, bool includePrivateKey, string? password, CertificateKeyExportFormat format)
    {
        if (Log.IsEnabled())
        {
            Log.ExportCertificateStart(GetDescription(certificate), path, includePrivateKey);
        }
 
        if (includePrivateKey && password == null)
        {
            Log.NoPasswordForCertificate();
        }
 
        var targetDirectoryPath = Path.GetDirectoryName(path);
        if (!string.IsNullOrEmpty(targetDirectoryPath))
        {
            Log.CreateExportCertificateDirectory(targetDirectoryPath);
            CreateDirectoryWithPermissions(targetDirectoryPath);
        }
 
        byte[] bytes;
        byte[] keyBytes;
        byte[]? pemEnvelope = null;
        RSA? key = null;
 
        try
        {
            if (includePrivateKey)
            {
                switch (format)
                {
                    case CertificateKeyExportFormat.Pfx:
                        bytes = certificate.Export(X509ContentType.Pkcs12, password);
                        break;
                    case CertificateKeyExportFormat.Pem:
                        key = certificate.GetRSAPrivateKey()!;
 
                        char[] pem;
                        if (password != null)
                        {
                            keyBytes = key.ExportEncryptedPkcs8PrivateKey(password, new PbeParameters(PbeEncryptionAlgorithm.Aes256Cbc, HashAlgorithmName.SHA256, 100000));
                            pem = PemEncoding.Write("ENCRYPTED PRIVATE KEY", keyBytes);
                            pemEnvelope = Encoding.ASCII.GetBytes(pem);
                        }
                        else
                        {
                            // Export the key first to an encrypted PEM to avoid issues with System.Security.Cryptography.Cng indicating that the operation is not supported.
                            // This is likely by design to avoid exporting the key by mistake.
                            // To bypass it, we export the certificate to pem temporarily and then we import it and export it as unprotected PEM.
                            keyBytes = key.ExportEncryptedPkcs8PrivateKey(string.Empty, new PbeParameters(PbeEncryptionAlgorithm.Aes256Cbc, HashAlgorithmName.SHA256, 1));
                            pem = PemEncoding.Write("ENCRYPTED PRIVATE KEY", keyBytes);
                            key.Dispose();
                            key = RSA.Create();
                            key.ImportFromEncryptedPem(pem, string.Empty);
                            Array.Clear(keyBytes, 0, keyBytes.Length);
                            Array.Clear(pem, 0, pem.Length);
                            keyBytes = key.ExportPkcs8PrivateKey();
                            pem = PemEncoding.Write("PRIVATE KEY", keyBytes);
                            pemEnvelope = Encoding.ASCII.GetBytes(pem);
                        }
 
                        Array.Clear(keyBytes, 0, keyBytes.Length);
                        Array.Clear(pem, 0, pem.Length);
 
                        bytes = Encoding.ASCII.GetBytes(PemEncoding.Write("CERTIFICATE", certificate.Export(X509ContentType.Cert)));
                        break;
                    default:
                        throw new InvalidOperationException("Unknown format.");
                }
            }
            else
            {
                if (format == CertificateKeyExportFormat.Pem)
                {
                    bytes = Encoding.ASCII.GetBytes(PemEncoding.Write("CERTIFICATE", certificate.Export(X509ContentType.Cert)));
                }
                else
                {
                    bytes = certificate.Export(X509ContentType.Cert);
                }
            }
        }
        catch (Exception e)
        {
            Log.ExportCertificateError(e.ToString());
            throw;
        }
        finally
        {
            key?.Dispose();
        }
 
        try
        {
            Log.WriteCertificateToDisk(path);
 
            // Create a temp file with the correct Unix file mode before moving it to the expected path.
            if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            {
                var tempFilename = Path.GetTempFileName();
                File.Move(tempFilename, path, overwrite: true);
            }
 
            File.WriteAllBytes(path, bytes);
        }
        catch (Exception ex)
        {
            Log.WriteCertificateToDiskError(ex.ToString());
            throw;
        }
        finally
        {
            Array.Clear(bytes, 0, bytes.Length);
        }
 
        if (includePrivateKey && format == CertificateKeyExportFormat.Pem)
        {
            Debug.Assert(pemEnvelope != null);
 
            try
            {
                var keyPath = Path.ChangeExtension(path, ".key");
                Log.WritePemKeyToDisk(keyPath);
 
                // Create a temp file with the correct Unix file mode before moving it to the expected path.
                if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
                {
                    var tempFilename = Path.GetTempFileName();
                    File.Move(tempFilename, keyPath, overwrite: true);
                }
 
                File.WriteAllBytes(keyPath, pemEnvelope);
            }
            catch (Exception ex)
            {
                Log.WritePemKeyToDiskError(ex.ToString());
                throw;
            }
            finally
            {
                Array.Clear(pemEnvelope, 0, pemEnvelope.Length);
            }
        }
    }
 
    /// <summary>
    /// Creates a new ASP.NET Core HTTPS development certificate.
    /// </summary>
    /// <param name="notBefore">The date and time before which the certificate is not valid.</param>
    /// <param name="notAfter">The date and time after which the certificate is not valid.</param>
    /// <returns>The created X509Certificate2 instance.</returns>
    /// <remarks>
    /// When making changes to the certificate generated by this method, ensure that the <see cref="CurrentAspNetCoreCertificateVersion"/> constant is updated accordingly.
    /// </remarks>
    internal X509Certificate2 CreateAspNetCoreHttpsDevelopmentCertificate(DateTimeOffset notBefore, DateTimeOffset notAfter)
    {
        var subject = new X500DistinguishedName(Subject);
        var extensions = new List<X509Extension>();
        var sanBuilder = new SubjectAlternativeNameBuilder();
        sanBuilder.AddDnsName(LocalhostHttpsDnsName);
        sanBuilder.AddDnsName(LocalhostWildcardHttpsDnsName);
        sanBuilder.AddDnsName(InternalWildcardHttpsDnsName);
        sanBuilder.AddDnsName(LocalhostDockerHttpsDnsName);
        sanBuilder.AddDnsName(ContainersDockerHttpsDnsName);
        sanBuilder.AddIpAddress(IPAddress.Loopback);
        sanBuilder.AddIpAddress(IPAddress.IPv6Loopback);
 
        var keyUsage = new X509KeyUsageExtension(X509KeyUsageFlags.KeyEncipherment | X509KeyUsageFlags.DigitalSignature, critical: true);
        var enhancedKeyUsage = new X509EnhancedKeyUsageExtension(
            new OidCollection() {
                    new Oid(
                        ServerAuthenticationEnhancedKeyUsageOid,
                        ServerAuthenticationEnhancedKeyUsageOidFriendlyName)
            },
            critical: true);
 
        var basicConstraints = new X509BasicConstraintsExtension(
            certificateAuthority: false,
            hasPathLengthConstraint: false,
            pathLengthConstraint: 0,
            critical: true);
 
        byte[] bytePayload;
 
        if (AspNetHttpsCertificateVersion != 0)
        {
            bytePayload = new byte[1];
            bytePayload[0] = (byte)AspNetHttpsCertificateVersion;
        }
        else
        {
            bytePayload = Encoding.ASCII.GetBytes(AspNetHttpsOidFriendlyName);
        }
 
        var aspNetHttpsExtension = new X509Extension(
            new AsnEncodedData(
                new Oid(AspNetHttpsOid, AspNetHttpsOidFriendlyName),
                bytePayload),
            critical: false);
 
        extensions.Add(basicConstraints);
        extensions.Add(keyUsage);
        extensions.Add(enhancedKeyUsage);
        extensions.Add(sanBuilder.Build(critical: true));
        extensions.Add(aspNetHttpsExtension);
 
        var certificate = CreateSelfSignedCertificate(subject, extensions, notBefore, notAfter);
        return certificate;
    }
 
    internal X509Certificate2 SaveCertificate(X509Certificate2 certificate)
    {
        var name = StoreName.My;
        var location = StoreLocation.CurrentUser;
 
        if (Log.IsEnabled())
        {
            Log.SaveCertificateInStoreStart(GetDescription(certificate), name, location);
        }
 
        certificate = SaveCertificateCore(certificate, name, location);
 
        Log.SaveCertificateInStoreEnd();
        return certificate;
    }
 
    internal TrustLevel TrustCertificate(X509Certificate2 certificate)
    {
        try
        {
            if (Log.IsEnabled())
            {
                Log.TrustCertificateStart(GetDescription(certificate));
            }
            var trustLevel = TrustCertificateCore(certificate);
            Log.TrustCertificateEnd();
            return trustLevel;
        }
        catch (Exception ex)
        {
            Log.TrustCertificateError(ex.ToString());
            throw;
        }
    }
 
    // Internal, for testing purposes only.
    internal void RemoveAllCertificates(StoreName storeName, StoreLocation storeLocation)
    {
        var certificates = GetCertificatesToRemove(storeName, storeLocation);
        var certificatesWithName = certificates.Where(c => c.Subject == Subject);
 
        var removeLocation = storeName == StoreName.My ? RemoveLocations.Local : RemoveLocations.Trusted;
 
        foreach (var certificate in certificates)
        {
            RemoveCertificate(certificate, removeLocation);
        }
 
        DisposeCertificates(certificates);
    }
 
    internal void RemoveCertificate(X509Certificate2 certificate, RemoveLocations locations)
    {
        switch (locations)
        {
            case RemoveLocations.Undefined:
                throw new InvalidOperationException($"'{nameof(RemoveLocations.Undefined)}' is not a valid location.");
            case RemoveLocations.Local:
                RemoveCertificateFromUserStore(certificate);
                break;
            case RemoveLocations.Trusted:
                RemoveCertificateFromTrustedRoots(certificate);
                break;
            case RemoveLocations.All:
                RemoveCertificateFromTrustedRoots(certificate);
                RemoveCertificateFromUserStore(certificate);
                break;
            default:
                throw new InvalidOperationException("Invalid location.");
        }
    }
 
    internal abstract CheckCertificateStateResult CheckCertificateState(X509Certificate2 candidate);
 
    internal abstract void CorrectCertificateState(X509Certificate2 candidate);
 
    /// <summary>
    /// Creates a self-signed certificate with the specified subject, extensions, and validity period.
    /// </summary>
    /// <param name="subject">The subject distinguished name for the certificate.</param>
    /// <param name="extensions">The collection of X509 extensions to include in the certificate.</param>
    /// <param name="notBefore">The date and time before which the certificate is not valid.</param>
    /// <param name="notAfter">The date and time after which the certificate is not valid.</param>
    /// <returns>The created X509Certificate2 instance.</returns>
    /// <exception cref="InvalidOperationException">If a key with the specified minimum size cannot be created.</exception>
    /// <remarks>
    /// If making changes to the certificate generated by this method, ensure that the <see cref="CurrentAspNetCoreCertificateVersion"/> constant is updated accordingly.
    /// </remarks>
    internal static X509Certificate2 CreateSelfSignedCertificate(
        X500DistinguishedName subject,
        IEnumerable<X509Extension> extensions,
        DateTimeOffset notBefore,
        DateTimeOffset notAfter)
    {
        using var key = CreateKeyMaterial(RSAMinimumKeySizeInBits);
 
        var request = new CertificateRequest(subject, key, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
        foreach (var extension in extensions)
        {
            request.CertificateExtensions.Add(extension);
        }
 
        // Only add the SKI and AKI extensions if neither is already present.
        // OpenSSL needs these to correctly identify the trust chain for a private key. If multiple certificates don't have a subject key identifier and share the same subject,
        // the wrong certificate can be chosen for the trust chain, leading to validation errors.
        if (!request.CertificateExtensions.Any(ext => ext.Oid?.Value is SubjectKeyIdentifierOid or AuthorityKeyIdentifierOid))
        {
            // RFC 5280 section 4.2.1.2
            var subjectKeyIdentifier = new X509SubjectKeyIdentifierExtension(request.PublicKey, X509SubjectKeyIdentifierHashAlgorithm.Sha256, critical: false);
            // RFC 5280 section 4.2.1.1
            var authorityKeyIdentifier = X509AuthorityKeyIdentifierExtension.CreateFromSubjectKeyIdentifier(subjectKeyIdentifier);
 
            request.CertificateExtensions.Add(subjectKeyIdentifier);
            request.CertificateExtensions.Add(authorityKeyIdentifier);
        }
 
        var result = request.CreateSelfSigned(notBefore, notAfter);
        return result;
 
        static RSA CreateKeyMaterial(int minimumKeySize)
        {
            var rsa = RSA.Create(minimumKeySize);
            if (rsa.KeySize < minimumKeySize)
            {
                throw new InvalidOperationException($"Failed to create a key with a size of {minimumKeySize} bits");
            }
 
            return rsa;
        }
    }
 
    internal static void DisposeCertificates(IEnumerable<X509Certificate2> disposables)
    {
        foreach (var disposable in disposables)
        {
            try
            {
                disposable.Dispose();
            }
            catch
            {
            }
        }
    }
 
    protected void RemoveCertificateFromUserStore(X509Certificate2 certificate)
    {
        try
        {
            if (Log.IsEnabled())
            {
                Log.RemoveCertificateFromUserStoreStart(GetDescription(certificate));
            }
            RemoveCertificateFromUserStoreCore(certificate);
            Log.RemoveCertificateFromUserStoreEnd();
        }
        catch (Exception ex)
        {
            Log.RemoveCertificateFromUserStoreError(ex.ToString());
            throw;
        }
    }
 
    protected virtual void RemoveCertificateFromUserStoreCore(X509Certificate2 certificate)
    {
        using var store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
        store.Open(OpenFlags.ReadWrite);
        var matching = store.Certificates
            .OfType<X509Certificate2>()
            .Single(c => c.SerialNumber == certificate.SerialNumber);
 
        store.Remove(matching);
    }
 
    internal string ToCertificateDescription(IEnumerable<X509Certificate2> certificates)
    {
        var list = certificates.ToList();
        var certificatesDescription = list.Count switch
        {
            0 => "no certificates",
            1 => "1 certificate",
            _ => $"{list.Count} certificates",
        };
        var description = list.OrderBy(c => c.Thumbprint).Select((c, i) => $"    {i + 1}) " + GetDescription(c)).Prepend(certificatesDescription);
        return string.Join(Environment.NewLine, description);
    }
 
    internal string GetDescription(X509Certificate2 c) =>
        $"{c.Thumbprint} - {c.Subject} - Valid from {c.NotBefore:u} to {c.NotAfter:u} - IsHttpsDevelopmentCertificate: {IsHttpsDevelopmentCertificate(c).ToString().ToLowerInvariant()} - IsExportable: {IsExportable(c).ToString().ToLowerInvariant()}";
 
    /// <remarks>
    /// <see cref="X509Certificate.Equals(X509Certificate?)"/> is not adequate for security purposes.
    /// </remarks>
    internal static bool AreCertificatesEqual(X509Certificate2 cert1, X509Certificate2 cert2)
    {
        return cert1.RawDataMemory.Span.SequenceEqual(cert2.RawDataMemory.Span);
    }
 
    /// <summary>
    /// Given a certificate, usually from the <see cref="StoreName.My"/> store, try to find the
    /// corresponding certificate in <paramref name="store"/> (usually the <see cref="StoreName.Root"/> store)."/>
    /// </summary>
    /// <param name="store">An open <see cref="X509Store"/>.</param>
    /// <param name="certificate">A certificate to search for.</param>
    /// <param name="foundCertificate">The certificate, if any, corresponding to <paramref name="certificate"/> in <paramref name="store"/>.</param>
    /// <returns>True if a corresponding certificate was found.</returns>
    /// <remarks><see cref="ListCertificates"/> has richer filtering and a lot of debugging output that's unhelpful here.</remarks>
    internal static bool TryFindCertificateInStore(X509Store store, X509Certificate2 certificate, [NotNullWhen(true)] out X509Certificate2? foundCertificate)
    {
        foundCertificate = null;
 
        // We specifically don't search by thumbprint to avoid being flagged for using a SHA-1 hash.
        var certificatesWithSubjectName = store.Certificates.Find(X509FindType.FindBySerialNumber, certificate.SerialNumber, validOnly: false);
        if (certificatesWithSubjectName.Count == 0)
        {
            return false;
        }
 
        var certificatesToDispose = new List<X509Certificate2>();
        foreach (var candidate in certificatesWithSubjectName.OfType<X509Certificate2>())
        {
            if (foundCertificate is null && AreCertificatesEqual(candidate, certificate))
            {
                foundCertificate = candidate;
            }
            else
            {
                certificatesToDispose.Add(candidate);
            }
        }
        DisposeCertificates(certificatesToDispose);
        return foundCertificate is not null;
    }
 
    internal sealed class CertificateManagerLogger
    {
        private readonly ILogger _logger;
 
        public CertificateManagerLogger() : this(NullLogger.Instance) { }
 
        public CertificateManagerLogger(ILogger logger)
        {
            _logger = logger;
        }
 
        public bool IsEnabled() => _logger.IsEnabled(LogLevel.Debug);
 
        // Event 1 - Verbose
        public void ListCertificatesStart(StoreLocation location, StoreName storeName) =>
            _logger.LogDebug("Listing certificates from {Location}\\{StoreName}", location, storeName);
 
        // Event 2 - Verbose
        public void DescribeFoundCertificates(string matchingCertificates) =>
            _logger.LogDebug("Found certificates: {MatchingCertificates}", matchingCertificates);
 
        // Event 3 - Verbose
        public void CheckCertificatesValidity() =>
            _logger.LogDebug("Checking certificates validity");
 
        // Event 4 - Verbose
        public void DescribeValidCertificates(string validCertificates) =>
            _logger.LogDebug("Valid certificates: {ValidCertificates}", validCertificates);
 
        // Event 5 - Verbose
        public void DescribeInvalidCertificates(string invalidCertificates) =>
            _logger.LogDebug("Invalid certificates: {InvalidCertificates}", invalidCertificates);
 
        // Event 6 - Verbose
        public void ListCertificatesEnd() =>
            _logger.LogDebug("Finished listing certificates.");
 
        // Event 7 - Error
        public void ListCertificatesError(string e) =>
            _logger.LogError("An error occurred while listing the certificates: {Error}", e);
 
        // Event 8 - Verbose
        public void FilteredCertificates(string filteredCertificates) =>
            _logger.LogDebug("Filtered certificates: {FilteredCertificates}", filteredCertificates);
 
        // Event 9 - Verbose
        public void ExcludedCertificates(string excludedCertificates) =>
            _logger.LogDebug("Excluded certificates: {ExcludedCertificates}", excludedCertificates);
 
        // Event 14 - Verbose
        public void ValidCertificatesFound(string certificates) =>
            _logger.LogDebug("Valid certificates: {Certificates}", certificates);
 
        // Event 15 - Verbose
        public void SelectedCertificate(string certificate) =>
            _logger.LogDebug("Selected certificate: {Certificate}", certificate);
 
        // Event 16 - Verbose
        public void NoValidCertificatesFound() =>
            _logger.LogDebug("No valid certificates found.");
 
        // Event 17 - Verbose
        public void CreateDevelopmentCertificateStart() =>
            _logger.LogDebug("Generating HTTPS development certificate.");
 
        // Event 18 - Verbose
        public void CreateDevelopmentCertificateEnd() =>
            _logger.LogDebug("Finished generating HTTPS development certificate.");
 
        // Event 19 - Error
        public void CreateDevelopmentCertificateError(string e) =>
            _logger.LogError("An error has occurred generating the certificate: {Error}.", e);
 
        // Event 20 - Verbose
        public void SaveCertificateInStoreStart(string certificate, StoreName name, StoreLocation location) =>
            _logger.LogDebug("Saving certificate '{Certificate}' to store {Location}\\{StoreName}.", certificate, location, name);
 
        // Event 21 - Verbose
        public void SaveCertificateInStoreEnd() =>
            _logger.LogDebug("Finished saving certificate to the store.");
 
        // Event 22 - Error
        public void SaveCertificateInStoreError(string e) =>
            _logger.LogError("An error has occurred saving the certificate: {Error}.", e);
 
        // Event 23 - Verbose
        public void ExportCertificateStart(string certificate, string path, bool includePrivateKey) =>
            _logger.LogDebug("Saving certificate '{Certificate}' to {Path} {PrivateKey} private key.", certificate, path, includePrivateKey ? "with" : "without");
 
        // Event 24 - Verbose
        public void NoPasswordForCertificate() =>
            _logger.LogDebug("Exporting certificate with private key but no password.");
 
        // Event 25 - Verbose
        public void CreateExportCertificateDirectory(string path) =>
            _logger.LogDebug("Creating directory {Path}.", path);
 
        // Event 26 - Error
        public void ExportCertificateError(string error) =>
            _logger.LogError("An error has occurred while exporting the certificate: {Error}.", error);
 
        // Event 27 - Verbose
        public void WriteCertificateToDisk(string path) =>
            _logger.LogDebug("Writing the certificate to: {Path}.", path);
 
        // Event 28 - Error
        public void WriteCertificateToDiskError(string error) =>
            _logger.LogError("An error has occurred while writing the certificate to disk: {Error}.", error);
 
        // Event 29 - Verbose
        public void TrustCertificateStart(string certificate) =>
            _logger.LogDebug("Trusting the certificate to: {Certificate}.", certificate);
 
        // Event 30 - Verbose
        public void TrustCertificateEnd() =>
            _logger.LogDebug("Finished trusting the certificate.");
 
        // Event 31 - Error
        public void TrustCertificateError(string error) =>
            _logger.LogError("An error has occurred while trusting the certificate: {Error}.", error);
 
        // Event 32 - Verbose
        public void MacOSTrustCommandStart(string command) =>
            _logger.LogDebug("Running the trust command {Command}.", command);
 
        // Event 33 - Verbose
        public void MacOSTrustCommandEnd() =>
            _logger.LogDebug("Finished running the trust command.");
 
        // Event 34 - Warning
        public void MacOSTrustCommandError(int exitCode) =>
            _logger.LogWarning("An error has occurred while running the trust command: {ExitCode}.", exitCode);
 
        // Event 35 - Verbose
        public void MacOSRemoveCertificateTrustRuleStart(string certificate) =>
            _logger.LogDebug("Running the remove trust command for {Certificate}.", certificate);
 
        // Event 36 - Verbose
        public void MacOSRemoveCertificateTrustRuleEnd() =>
            _logger.LogDebug("Finished running the remove trust command.");
 
        // Event 37 - Warning
        public void MacOSRemoveCertificateTrustRuleError(int exitCode) =>
            _logger.LogWarning("An error has occurred while running the remove trust command: {ExitCode}.", exitCode);
 
        // Event 38 - Verbose
        public void MacOSCertificateUntrusted(string certificate) =>
            _logger.LogDebug("The certificate is not trusted: {Certificate}.", certificate);
 
        // Event 39 - Verbose
        public void MacOSRemoveCertificateFromKeyChainStart(string keyChain, string certificate) =>
            _logger.LogDebug("Removing the certificate from the keychain {KeyChain} {Certificate}.", keyChain, certificate);
 
        // Event 40 - Verbose
        public void MacOSRemoveCertificateFromKeyChainEnd() =>
            _logger.LogDebug("Finished removing the certificate from the keychain.");
 
        // Event 41 - Warning
        public void MacOSRemoveCertificateFromKeyChainError(int exitCode) =>
            _logger.LogWarning("An error has occurred while running the remove trust command: {ExitCode}.", exitCode);
 
        // Event 42 - Verbose
        public void RemoveCertificateFromUserStoreStart(string certificate) =>
            _logger.LogDebug("Removing the certificate from the user store {Certificate}.", certificate);
 
        // Event 43 - Verbose
        public void RemoveCertificateFromUserStoreEnd() =>
            _logger.LogDebug("Finished removing the certificate from the user store.");
 
        // Event 44 - Error
        public void RemoveCertificateFromUserStoreError(string error) =>
            _logger.LogError("An error has occurred while removing the certificate from the user store: {Error}.", error);
 
        // Event 45 - Verbose
        public void WindowsAddCertificateToRootStore() =>
            _logger.LogDebug("Adding certificate to the trusted root certification authority store.");
 
        // Event 46 - Verbose
        public void WindowsCertificateAlreadyTrusted() =>
            _logger.LogDebug("The certificate is already trusted.");
 
        // Event 47 - Verbose
        public void WindowsCertificateTrustCanceled() =>
            _logger.LogDebug("Trusting the certificate was cancelled by the user.");
 
        // Event 48 - Verbose
        public void WindowsRemoveCertificateFromRootStoreStart() =>
            _logger.LogDebug("Removing the certificate from the trusted root certification authority store.");
 
        // Event 49 - Verbose
        public void WindowsRemoveCertificateFromRootStoreEnd() =>
            _logger.LogDebug("Finished removing the certificate from the trusted root certification authority store.");
 
        // Event 50 - Verbose
        public void WindowsRemoveCertificateFromRootStoreNotFound() =>
            _logger.LogDebug("The certificate was not trusted.");
 
        // Event 51 - Verbose
        public void CorrectCertificateStateStart(string certificate) =>
            _logger.LogDebug("Correcting the the certificate state for '{Certificate}'.", certificate);
 
        // Event 52 - Verbose
        public void CorrectCertificateStateEnd() =>
            _logger.LogDebug("Finished correcting the certificate state.");
 
        // Event 53 - Error
        public void CorrectCertificateStateError(string error) =>
            _logger.LogError("An error has occurred while correcting the certificate state: {Error}.", error);
 
        // Event 54 - Verbose
        internal void MacOSAddCertificateToKeyChainStart(string keychain, string certificate) =>
            _logger.LogDebug("Importing the certificate {Certificate} to the keychain '{Keychain}'.", certificate, keychain);
 
        // Event 55 - Verbose
        internal void MacOSAddCertificateToKeyChainEnd() =>
            _logger.LogDebug("Finished importing the certificate to the keychain.");
 
        // Event 56 - Error
        internal void MacOSAddCertificateToKeyChainError(int exitCode, string output) =>
            _logger.LogError("An error has occurred while importing the certificate to the keychain: {ExitCode}, {Output}", exitCode, output);
 
        // Event 57 - Verbose
        public void WritePemKeyToDisk(string path) =>
            _logger.LogDebug("Writing the certificate to: {Path}.", path);
 
        // Event 58 - Error
        public void WritePemKeyToDiskError(string error) =>
            _logger.LogError("An error has occurred while writing the certificate to disk: {Error}.", error);
 
        // Event 59 - Error
        internal void ImportCertificateMissingFile(string certificatePath) =>
            _logger.LogError("The file '{CertificatePath}' does not exist.", certificatePath);
 
        // Event 60 - Error
        internal void ImportCertificateExistingCertificates(string certificateDescription) =>
            _logger.LogError("One or more HTTPS certificates exist '{CertificateDescription}'.", certificateDescription);
 
        // Event 61 - Verbose
        internal void LoadCertificateStart(string certificatePath) =>
            _logger.LogDebug("Loading certificate from path '{CertificatePath}'.", certificatePath);
 
        // Event 62 - Verbose
        internal void LoadCertificateEnd(string description) =>
            _logger.LogDebug("The certificate '{Description}' has been loaded successfully.", description);
 
        // Event 63 - Error
        internal void LoadCertificateError(string error) =>
            _logger.LogError("An error has occurred while loading the certificate from disk: {Error}.", error);
 
        // Event 64 - Error
        internal void NoHttpsDevelopmentCertificate(string description) =>
            _logger.LogError("The provided certificate '{Description}' is not a valid ASP.NET Core HTTPS development certificate.", description);
 
        // Event 65 - Verbose
        public void MacOSCertificateAlreadyTrusted() =>
            _logger.LogDebug("The certificate is already trusted.");
 
        // Event 66 - Verbose
        internal void MacOSAddCertificateToUserProfileDirStart(string directory, string certificate) =>
            _logger.LogDebug("Saving the certificate {Certificate} to the user profile folder '{Directory}'.", certificate, directory);
 
        // Event 67 - Verbose
        internal void MacOSAddCertificateToUserProfileDirEnd() =>
            _logger.LogDebug("Finished saving the certificate to the user profile folder.");
 
        // Event 68 - Error
        internal void MacOSAddCertificateToUserProfileDirError(string certificateThumbprint, string errorMessage) =>
            _logger.LogError("An error has occurred while saving certificate '{CertificateThumbprint}' in the user profile folder: {ErrorMessage}.", certificateThumbprint, errorMessage);
 
        // Event 69 - Error
        internal void MacOSRemoveCertificateFromUserProfileDirError(string certificateThumbprint, string errorMessage) =>
            _logger.LogError("An error has occurred while removing certificate '{CertificateThumbprint}' from the user profile folder: {ErrorMessage}.", certificateThumbprint, errorMessage);
 
        // Event 70 - Error
        internal void MacOSFileIsNotAValidCertificate(string path) =>
            _logger.LogError("The file '{Path}' is not a valid certificate.", path);
 
        // Event 71 - Warning
        internal void MacOSDiskStoreDoesNotExist() =>
            _logger.LogWarning("The on-disk store directory was not found.");
 
        // Event 72 - Verbose
        internal void UnixOpenSslCertificateDirectoryOverridePresent(string nssDbOverrideVariableName) =>
            _logger.LogDebug("Reading OpenSSL trusted certificates location from {NssDbOverrideVariableName}.", nssDbOverrideVariableName);
 
        // Event 73 - Verbose
        internal void UnixNssDbOverridePresent(string environmentVariable) =>
            _logger.LogDebug("Reading NSS database locations from {EnvironmentVariable}.", environmentVariable);
 
        // Event 74 - Warning
        internal void UnixNssDbDoesNotExist(string nssDb, string environmentVariable) =>
            _logger.LogWarning("The NSS database '{NssDb}' provided via {EnvironmentVariable} does not exist.", nssDb, environmentVariable);
 
        // Event 75 - Warning
        internal void UnixNotTrustedByDotnet() =>
            _logger.LogWarning("The certificate is not trusted by .NET. This will likely affect System.Net.Http.HttpClient.");
 
        // Event 76 - Warning
        internal void UnixNotTrustedByOpenSsl(string envVarName) =>
            _logger.LogWarning("The certificate is not trusted by OpenSSL.  Ensure that the {EnvVarName} environment variable is set correctly.", envVarName);
 
        // Event 77 - Warning
        internal void UnixNotTrustedByNss(string path, string browser) =>
            _logger.LogWarning("The certificate is not trusted in the NSS database in '{Path}'. This will likely affect the {Browser} family of browsers.", path, browser);
 
        // Event 78 - Verbose
        internal void UnixHomeDirectoryDoesNotExist(string homeDirectory, string username) =>
            _logger.LogDebug("Home directory '{HomeDirectory}' does not exist. Unable to discover NSS databases for user '{Username}'.  This will likely affect browsers.", homeDirectory, username);
 
        // Event 79 - Verbose
        internal void UnixOpenSslVersionParsingFailed() =>
            _logger.LogDebug("OpenSSL reported its directory in an unexpected format.");
 
        // Event 80 - Verbose
        internal void UnixOpenSslVersionFailed() =>
            _logger.LogDebug("Unable to determine the OpenSSL directory.");
 
        // Event 81 - Verbose
        internal void UnixOpenSslVersionException(string exceptionMessage) =>
            _logger.LogDebug("Unable to determine the OpenSSL directory: {ExceptionMessage}.", exceptionMessage);
 
        // Event 82 - Error
        internal void UnixOpenSslHashFailed(string certificatePath) =>
            _logger.LogError("Unable to compute the hash of certificate {CertificatePath}. OpenSSL trust is likely in an inconsistent state.", certificatePath);
 
        // Event 83 - Error
        internal void UnixOpenSslHashException(string certificatePath, string exceptionMessage) =>
            _logger.LogError("Unable to compute the certificate hash: {CertificatePath}. OpenSSL trust is likely in an inconsistent state. {ExceptionMessage}", certificatePath, exceptionMessage);
 
        // Event 84 - Error
        internal void UnixOpenSslRehashTooManyHashes(string fullName, string hash, int maxHashCollisions) =>
            _logger.LogError("Unable to update certificate '{FullName}' in the OpenSSL trusted certificate hash collection - {MaxHashCollisions} certificates have the hash {Hash}.", fullName, maxHashCollisions, hash);
 
        // Event 85 - Error
        internal void UnixOpenSslRehashException(string exceptionMessage) =>
            _logger.LogError("Unable to update the OpenSSL trusted certificate hash collection: {ExceptionMessage}. Manually rehashing may help. See https://aka.ms/dev-certs-trust for more information.", exceptionMessage);
 
        // Event 86 - Warning
        internal void UnixDotnetTrustException(string exceptionMessage) =>
            _logger.LogWarning("Failed to trust the certificate in .NET: {ExceptionMessage}.", exceptionMessage);
 
        // Event 87 - Verbose
        internal void UnixDotnetTrustSucceeded() =>
            _logger.LogDebug("Trusted the certificate in .NET.");
 
        // Event 88 - Warning
        internal void UnixOpenSslTrustFailed() =>
            _logger.LogWarning("Clients that validate certificate trust using OpenSSL will not trust the certificate.");
 
        // Event 89 - Verbose
        internal void UnixOpenSslTrustSucceeded() =>
            _logger.LogDebug("Trusted the certificate in OpenSSL.");
 
        // Event 90 - Warning
        internal void UnixNssDbTrustFailed(string path, string browser) =>
            _logger.LogWarning("Failed to trust the certificate in the NSS database in '{Path}'. This will likely affect the {Browser} family of browsers.", path, browser);
 
        // Event 91 - Verbose
        internal void UnixNssDbTrustSucceeded(string path) =>
            _logger.LogDebug("Trusted the certificate in the NSS database in '{Path}'.", path);
 
        // Event 92 - Warning
        internal void UnixDotnetUntrustException(string exceptionMessage) =>
            _logger.LogWarning("Failed to untrust the certificate in .NET: {ExceptionMessage}.", exceptionMessage);
 
        // Event 93 - Warning
        internal void UnixOpenSslUntrustFailed() =>
            _logger.LogWarning("Failed to untrust the certificate in OpenSSL.");
 
        // Event 94 - Verbose
        internal void UnixOpenSslUntrustSucceeded() =>
            _logger.LogDebug("Untrusted the certificate in OpenSSL.");
 
        // Event 95 - Warning
        internal void UnixNssDbUntrustFailed(string path) =>
            _logger.LogWarning("Failed to remove the certificate from the NSS database in '{Path}'.", path);
 
        // Event 96 - Verbose
        internal void UnixNssDbUntrustSucceeded(string path) =>
            _logger.LogDebug("Removed the certificate from the NSS database in '{Path}'.", path);
 
        // Event 97 - Warning
        internal void UnixTrustPartiallySucceeded() =>
            _logger.LogWarning("The certificate is only partially trusted - some clients will not accept it.");
 
        // Event 98 - Warning
        internal void UnixNssDbCheckException(string path, string exceptionMessage) =>
            _logger.LogWarning("Failed to look up the certificate in the NSS database in '{Path}': {ExceptionMessage}.", path, exceptionMessage);
 
        // Event 99 - Warning
        internal void UnixNssDbAdditionException(string path, string exceptionMessage) =>
            _logger.LogWarning("Failed to add the certificate to the NSS database in '{Path}': {ExceptionMessage}.", path, exceptionMessage);
 
        // Event 100 - Warning
        internal void UnixNssDbRemovalException(string path, string exceptionMessage) =>
            _logger.LogWarning("Failed to remove the certificate from the NSS database in '{Path}': {ExceptionMessage}.", path, exceptionMessage);
 
        // Event 101 - Warning
        internal void UnixFirefoxProfileEnumerationException(string firefoxDirectory, string message) =>
            _logger.LogWarning("Failed to find the Firefox profiles in directory '{FirefoxDirectory}': {Message}.", firefoxDirectory, message);
 
        // Event 102 - Verbose
        internal void UnixNoFirefoxProfilesFound(string firefoxDirectory) =>
            _logger.LogDebug("No Firefox profiles found in directory '{FirefoxDirectory}'.", firefoxDirectory);
 
        // Event 103 - Warning
        internal void UnixNssDbTrustFailedWithProbableConflict(string path, string browser) =>
            _logger.LogWarning("Failed to trust the certificate in the NSS database in '{Path}'. This will likely affect the {Browser} family of browsers. This likely indicates that the database already contains an entry for the certificate under a different name. Please remove it and try again.", path, browser);
 
        // Event 104 - Warning
        internal void UnixOpenSslCertificateDirectoryOverrideIgnored(string openSslCertDirectoryOverrideVariableName) =>
            _logger.LogWarning("The {OpenSslCertDirectoryOverrideVariableName} environment variable is set but will not be consumed while checking trust.", openSslCertDirectoryOverrideVariableName);
 
        // Event 105 - Warning
        internal void UnixMissingOpenSslCommand(string openSslCommand) =>
            _logger.LogWarning("The {OpenSslCommand} command is unavailable.  It is required for updating certificate trust in OpenSSL.", openSslCommand);
 
        // Event 106 - Warning
        internal void UnixMissingCertUtilCommand(string certUtilCommand) =>
            _logger.LogWarning("The {CertUtilCommand} command is unavailable.  It is required for querying and updating NSS databases, which are chiefly used to trust certificates in browsers.", certUtilCommand);
 
        // Event 107 - Verbose
        internal void UnixOpenSslUntrustSkipped(string certPath) =>
            _logger.LogDebug("Untrusting the certificate in OpenSSL was skipped since '{CertPath}' does not exist.", certPath);
 
        // Event 108 - Warning
        internal void UnixCertificateFileDeletionException(string certPath, string exceptionMessage) =>
            _logger.LogWarning("Failed to delete certificate file '{CertPath}': {ExceptionMessage}.", certPath, exceptionMessage);
 
        // Event 109 - Error
        internal void UnixNotOverwritingCertificate(string certPath) =>
            _logger.LogError("Unable to export the certificate since '{CertPath}' already exists. Please remove it.", certPath);
 
        // Event 110 - LogAlways (Info)
        internal void UnixSuggestSettingEnvironmentVariable(string certDir, string openSslDir, string envVarName) =>
            _logger.LogInformation("For OpenSSL trust to take effect, '{CertDir}' must be listed in the {EnvVarName} environment variable. For example, `export {EnvVarName}=\"{CertDir}:{OpenSslDir}\"`. See https://aka.ms/dev-certs-trust for more information.", certDir, envVarName, envVarName, certDir, openSslDir);
 
        // Event 111 - LogAlways (Info)
        internal void UnixSuggestSettingEnvironmentVariableWithoutExample(string certDir, string envVarName) =>
            _logger.LogInformation("For OpenSSL trust to take effect, '{CertDir}' must be listed in the {EnvVarName} environment variable. See https://aka.ms/dev-certs-trust for more information.", certDir, envVarName);
 
        // Event 112 - Warning
        internal void DirectoryPermissionsNotSecure(string directoryPath) =>
            _logger.LogWarning("Directory '{DirectoryPath}' may be readable by other users.", directoryPath);
 
        // Event 113 - Verbose
        internal void UnixOpenSslCertificateDirectoryAlreadyConfigured(string certDir, string envVarName) =>
            _logger.LogDebug("The certificate directory '{CertDir}' is already included in the {EnvVarName} environment variable.", certDir, envVarName);
 
        // Event 114 - LogAlways (Info)
        internal void UnixSuggestAppendingToEnvironmentVariable(string certDir, string envVarName) =>
            _logger.LogInformation("For OpenSSL trust to take effect, '{CertDir}' must be listed in the {EnvVarName} environment variable. For example, `export {EnvVarName}=\"{CertDir}:${EnvVarName}\"`. See https://aka.ms/dev-certs-trust for more information.", certDir, envVarName, envVarName, certDir, envVarName);
 
        // Event 115 - Verbose
        internal void WslWindowsTrustSucceeded() =>
            _logger.LogDebug("Successfully trusted the certificate in the Windows certificate store via WSL.");
 
        // Event 116 - Warning
        internal void WslWindowsTrustFailed() =>
            _logger.LogWarning("Failed to trust the certificate in the Windows certificate store via WSL.");
 
        // Event 117 - Warning
        internal void WslWindowsTrustException(string exceptionMessage) =>
            _logger.LogWarning("Failed to trust the certificate in the Windows certificate store via WSL: {ExceptionMessage}.", exceptionMessage);
 
        // Event 118 - Verbose
        public void DescribeMinimumVersionCertificates(string meetsMinimumVersionCertificates) =>
            _logger.LogDebug("Meets minimum version certificates: {MeetsMinimumVersionCertificates}", meetsMinimumVersionCertificates);
 
        // Event 119 - Verbose
        public void DescribeBelowMinimumVersionCertificates(string belowMinimumVersionCertificates) =>
            _logger.LogDebug("Below minimum version certificates: {BelowMinimumVersionCertificates}", belowMinimumVersionCertificates);
    }
 
    internal sealed class UserCancelledTrustException : Exception
    {
    }
 
    internal readonly struct CheckCertificateStateResult
    {
        public bool Success { get; }
        public string? FailureMessage { get; }
 
        public CheckCertificateStateResult(bool success, string? failureMessage)
        {
            Success = success;
            FailureMessage = failureMessage;
        }
    }
 
    internal enum RemoveLocations
    {
        Undefined,
        Local,
        Trusted,
        All
    }
 
    internal enum TrustLevel
    {
        /// <summary>No trust has been granted.</summary>
        None,
        /// <summary>Trust has been granted in some, but not all, clients.</summary>
        Partial,
        /// <summary>Trust has been granted in all clients.</summary>
        Full,
    }
}