|
// 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,
}
}
|