|
|
@@ -1,11 +1,9 @@
|
|
|
// Licensed to the .NET Foundation under one or more agreements.
|
|
|
// The .NET Foundation licenses this file to you under the MIT license.
|
|
|
|
|
|
-using System;
|
|
|
-using System.Collections.Generic;
|
|
|
using System.Diagnostics;
|
|
|
+using System.Diagnostics.CodeAnalysis;
|
|
|
using System.Globalization;
|
|
|
-using System.IO;
|
|
|
using System.Linq;
|
|
|
using System.Security.Cryptography;
|
|
|
using System.Security.Cryptography.X509Certificates;
|
|
|
@@ -15,24 +13,54 @@ namespace Microsoft.AspNetCore.Certificates.Generation;
|
|
|
|
|
|
internal sealed class MacOSCertificateManager : CertificateManager
|
|
|
{
|
|
|
- private const string CertificateSubjectRegex = "CN=(.*[^,]+).*";
|
|
|
- private static readonly string MacOSUserKeyChain = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + "/Library/Keychains/login.keychain-db";
|
|
|
- private const string MacOSSystemKeyChain = "/Library/Keychains/System.keychain";
|
|
|
- private const string MacOSFindCertificateCommandLine = "security";
|
|
|
- private const string MacOSFindCertificateCommandLineArgumentsFormat = "find-certificate -c {0} -a -Z -p " + MacOSSystemKeyChain;
|
|
|
- private const string MacOSFindCertificateOutputRegex = "SHA-1 hash: ([0-9A-Z]+)";
|
|
|
- private const string MacOSRemoveCertificateTrustCommandLine = "sudo";
|
|
|
- private const string MacOSRemoveCertificateTrustCommandLineArgumentsFormat = "security remove-trusted-cert -d {0}";
|
|
|
+ private static readonly string MacOSUserKeychain = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + "/Library/Keychains/login.keychain-db";
|
|
|
+
|
|
|
+ // System keychain. We no longer store certificates or create trust rules in the system
|
|
|
+ // keychain, but check for their presence here so that we can clean up state left behind
|
|
|
+ // by pre-.NET 7 versions of this tool.
|
|
|
+ private const string MacOSSystemKeychain = "/Library/Keychains/System.keychain";
|
|
|
+
|
|
|
+ // Well-known location on disk where dev-certs are stored.
|
|
|
+ private static readonly string MacOSUserHttpsCertificateLocation = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".aspnet", "dev-certs", "https");
|
|
|
+
|
|
|
+ // Verify the certificate {0} for the SSL and X.509 Basic Policy.
|
|
|
+ private const string MacOSVerifyCertificateCommandLine = "security";
|
|
|
+ private const string MacOSVerifyCertificateCommandLineArgumentsFormat = $"verify-cert -c {{0}} -p basic -p ssl";
|
|
|
+
|
|
|
+ // Delete a certificate with the specified SHA-256 (or SHA-1) hash {0} from keychain {1}.
|
|
|
private const string MacOSDeleteCertificateCommandLine = "sudo";
|
|
|
private const string MacOSDeleteCertificateCommandLineArgumentsFormat = "security delete-certificate -Z {0} {1}";
|
|
|
- private const string MacOSTrustCertificateCommandLine = "sudo";
|
|
|
- private const string MacOSTrustCertificateCommandLineArguments = "security add-trusted-cert -d -r trustRoot -k " + MacOSSystemKeyChain + " ";
|
|
|
|
|
|
+ // Add a certificate to the per-user trust settings in the user keychain. The trust policy
|
|
|
+ // for the certificate will be set to be always trusted for SSL and X.509 Basic Policy.
|
|
|
+ // Note: This operation will require user authentication.
|
|
|
+ private const string MacOSTrustCertificateCommandLine = "security";
|
|
|
+ private static readonly string MacOSTrustCertificateCommandLineArguments = $"add-trusted-cert -p basic -p ssl -k {MacOSUserKeychain} ";
|
|
|
+
|
|
|
+ // Import a pkcs12 certificate into the user keychain using the unwrapping passphrase {1}, and
|
|
|
+ // allow any application to access the imported key without warning.
|
|
|
private const string MacOSAddCertificateToKeyChainCommandLine = "security";
|
|
|
- private static readonly string MacOSAddCertificateToKeyChainCommandLineArgumentsFormat = "import {0} -k " + MacOSUserKeyChain + " -t cert -f pkcs12 -P {1} -A";
|
|
|
+ private static readonly string MacOSAddCertificateToKeyChainCommandLineArgumentsFormat = "import {0} -k " + MacOSUserKeychain + " -t cert -f pkcs12 -P {1} -A";
|
|
|
+
|
|
|
+ // Remove a certificate from the admin trust settings. We no longer add certificates to the
|
|
|
+ // admin trust settings, but need this for cleaning up certs generated by pre-.NET 7 versions
|
|
|
+ // of this tool that used to create trust settings in the system keychain.
|
|
|
+ // Note: This operation will require user authentication.
|
|
|
+ private const string MacOSUntrustLegacyCertificateCommandLine = "sudo";
|
|
|
+ private const string MacOSUntrustLegacyCertificateCommandLineArguments = "security remove-trusted-cert -d {0}";
|
|
|
+
|
|
|
+ // Find all matching certificates on the keychain {1} that have the name {0} and print
|
|
|
+ // print their SHA-256 and SHA-1 hashes.
|
|
|
+ private const string MacOSFindCertificateOnKeychainCommandLine = "security";
|
|
|
+ private const string MacOSFindCertificateOnKeychainCommandLineArgumentsFormat = "find-certificate -c {0} -a -Z -p {1}";
|
|
|
+
|
|
|
+ // Format used by the tool when printing SHA-1 hashes.
|
|
|
+ private const string MacOSFindCertificateOutputRegex = "SHA-1 hash: ([0-9A-Z]+)";
|
|
|
|
|
|
- public const string InvalidCertificateState = "The ASP.NET Core developer certificate is in an invalid state. " +
|
|
|
- "To fix this issue, run the following commands 'dotnet dev-certs https --clean' and 'dotnet dev-certs https' to remove all existing ASP.NET Core development certificates " +
|
|
|
+ public const string InvalidCertificateState =
|
|
|
+ "The ASP.NET Core developer certificate is in an invalid state. " +
|
|
|
+ "To fix this issue, run 'dotnet dev-certs https --clean' and 'dotnet dev-certs https' " +
|
|
|
+ "to remove all existing ASP.NET Core development certificates " +
|
|
|
"and create a new untrusted developer certificate. " +
|
|
|
"On macOS or Windows, use 'dotnet dev-certs https --trust' to trust the new certificate.";
|
|
|
|
|
|
@@ -41,8 +69,6 @@ internal sealed class MacOSCertificateManager : CertificateManager
|
|
|
"A prompt might appear to ask for permission to access the key. " +
|
|
|
"When that happens, select 'Always Allow' to grant 'dotnet' access to the certificate key in the future.";
|
|
|
|
|
|
- private static readonly TimeSpan MaxRegexTimeout = TimeSpan.FromMinutes(1);
|
|
|
-
|
|
|
public MacOSCertificateManager()
|
|
|
{
|
|
|
}
|
|
|
@@ -54,6 +80,12 @@ internal sealed class MacOSCertificateManager : CertificateManager
|
|
|
|
|
|
protected override void TrustCertificateCore(X509Certificate2 publicCertificate)
|
|
|
{
|
|
|
+ if (IsTrusted(publicCertificate))
|
|
|
+ {
|
|
|
+ Log.MacOSCertificateAlreadyTrusted();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
var tmpFile = Path.GetTempFileName();
|
|
|
try
|
|
|
{
|
|
|
@@ -77,10 +109,7 @@ internal sealed class MacOSCertificateManager : CertificateManager
|
|
|
{
|
|
|
try
|
|
|
{
|
|
|
- if (File.Exists(tmpFile))
|
|
|
- {
|
|
|
- File.Delete(tmpFile);
|
|
|
- }
|
|
|
+ File.Delete(tmpFile);
|
|
|
}
|
|
|
catch
|
|
|
{
|
|
|
@@ -91,103 +120,75 @@ internal sealed class MacOSCertificateManager : CertificateManager
|
|
|
|
|
|
internal override CheckCertificateStateResult CheckCertificateState(X509Certificate2 candidate, bool interactive)
|
|
|
{
|
|
|
- var sentinelPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".dotnet", $"certificates.{candidate.GetCertHashString(HashAlgorithmName.SHA256)}.sentinel");
|
|
|
- if (!interactive && !File.Exists(sentinelPath))
|
|
|
- {
|
|
|
- return new CheckCertificateStateResult(false, KeyNotAccessibleWithoutUserInteraction);
|
|
|
- }
|
|
|
+ return File.Exists(GetCertificateFilePath(candidate)) ?
|
|
|
+ new CheckCertificateStateResult(true, null) :
|
|
|
+ new CheckCertificateStateResult(false, InvalidCertificateState);
|
|
|
+ }
|
|
|
|
|
|
- // Tries to use the certificate key to validate it can't access it
|
|
|
+ internal override void CorrectCertificateState(X509Certificate2 candidate)
|
|
|
+ {
|
|
|
try
|
|
|
{
|
|
|
- using var rsa = candidate.GetRSAPrivateKey();
|
|
|
- if (rsa == null)
|
|
|
- {
|
|
|
- return new CheckCertificateStateResult(false, InvalidCertificateState);
|
|
|
- }
|
|
|
-
|
|
|
- // Encrypting a random value is the ultimate test for a key validity.
|
|
|
- // Windows and Mac OS both return HasPrivateKey = true if there is (or there has been) a private key associated
|
|
|
- // with the certificate at some point.
|
|
|
- var value = new byte[32];
|
|
|
- RandomNumberGenerator.Fill(value);
|
|
|
- rsa.Decrypt(rsa.Encrypt(value, RSAEncryptionPadding.Pkcs1), RSAEncryptionPadding.Pkcs1);
|
|
|
+ // Ensure that the directory exists before writing to the file.
|
|
|
+ Directory.CreateDirectory(MacOSUserHttpsCertificateLocation);
|
|
|
|
|
|
- // If we were able to access the key, create a sentinel so that we don't have to show a prompt
|
|
|
- // on every kestrel run.
|
|
|
- if (Directory.Exists(Path.GetDirectoryName(sentinelPath)) && !File.Exists(sentinelPath))
|
|
|
- {
|
|
|
- File.WriteAllText(sentinelPath, "true");
|
|
|
- }
|
|
|
-
|
|
|
- // Being able to encrypt and decrypt a payload is the strongest guarantee that the key is valid.
|
|
|
- return new CheckCertificateStateResult(true, null);
|
|
|
- }
|
|
|
- catch (Exception)
|
|
|
- {
|
|
|
- return new CheckCertificateStateResult(false, InvalidCertificateState);
|
|
|
+ var certificatePath = GetCertificateFilePath(candidate);
|
|
|
+ ExportCertificate(candidate, certificatePath, includePrivateKey: true, null, CertificateKeyExportFormat.Pfx);
|
|
|
}
|
|
|
- }
|
|
|
-
|
|
|
- internal override void CorrectCertificateState(X509Certificate2 candidate)
|
|
|
- {
|
|
|
- var status = CheckCertificateState(candidate, true);
|
|
|
- if (!status.Success)
|
|
|
+ catch (Exception ex)
|
|
|
{
|
|
|
- throw new InvalidOperationException(InvalidCertificateState);
|
|
|
+ Log.MacOSAddCertificateToUserProfileDirError(candidate.Thumbprint, ex.Message);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ // Use verify-cert to verify the certificate for the SSL and X.509 Basic Policy.
|
|
|
public override bool IsTrusted(X509Certificate2 certificate)
|
|
|
{
|
|
|
- var subjectMatch = Regex.Match(certificate.Subject, CertificateSubjectRegex, RegexOptions.Singleline, MaxRegexTimeout);
|
|
|
- if (!subjectMatch.Success)
|
|
|
+ var tmpFile = Path.GetTempFileName();
|
|
|
+ try
|
|
|
{
|
|
|
- throw new InvalidOperationException($"Can't determine the subject for the certificate with subject '{certificate.Subject}'.");
|
|
|
+ ExportCertificate(certificate, tmpFile, includePrivateKey: false, password: null, CertificateKeyExportFormat.Pem);
|
|
|
+
|
|
|
+ using var checkTrustProcess = Process.Start(new ProcessStartInfo(
|
|
|
+ MacOSVerifyCertificateCommandLine,
|
|
|
+ string.Format(CultureInfo.InvariantCulture, MacOSVerifyCertificateCommandLineArgumentsFormat, tmpFile))
|
|
|
+ {
|
|
|
+ RedirectStandardOutput = true,
|
|
|
+ // Do this to avoid showing output to the console when the cert is not trusted. It is trivial to export
|
|
|
+ // the cert and replicate the command to see details.
|
|
|
+ RedirectStandardError = true,
|
|
|
+ });
|
|
|
+ checkTrustProcess!.WaitForExit();
|
|
|
+ return checkTrustProcess.ExitCode == 0;
|
|
|
}
|
|
|
- var subject = subjectMatch.Groups[1].Value;
|
|
|
- using var checkTrustProcess = Process.Start(new ProcessStartInfo(
|
|
|
- MacOSFindCertificateCommandLine,
|
|
|
- string.Format(CultureInfo.InvariantCulture, MacOSFindCertificateCommandLineArgumentsFormat, subject))
|
|
|
+ finally
|
|
|
{
|
|
|
- RedirectStandardOutput = true
|
|
|
- });
|
|
|
- var output = checkTrustProcess!.StandardOutput.ReadToEnd();
|
|
|
- checkTrustProcess.WaitForExit();
|
|
|
- var matches = Regex.Matches(output, MacOSFindCertificateOutputRegex, RegexOptions.Multiline, MaxRegexTimeout);
|
|
|
- var hashes = matches.OfType<Match>().Select(m => m.Groups[1].Value).ToList();
|
|
|
- return hashes.Any(h => string.Equals(h, certificate.Thumbprint, StringComparison.Ordinal));
|
|
|
+ File.Delete(tmpFile);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
protected override void RemoveCertificateFromTrustedRoots(X509Certificate2 certificate)
|
|
|
{
|
|
|
- if (IsTrusted(certificate)) // On OSX this check just ensures its on the system keychain
|
|
|
+ if (IsCertOnKeychain(MacOSSystemKeychain, certificate))
|
|
|
{
|
|
|
- // A trusted certificate in OSX is installed into the system keychain and
|
|
|
- // as a "trust rule" applied to it.
|
|
|
- // To remove the certificate we first need to remove the "trust rule" and then
|
|
|
- // remove the certificate from the keychain.
|
|
|
- // We don't care if we fail to remove the trust rule if
|
|
|
- // for some reason the certificate became untrusted.
|
|
|
- // Trying to remove the certificate from the keychain will fail if the certificate is
|
|
|
- // trusted.
|
|
|
+ // Pre-.NET 7 versions of this tool used to store certs and trust settings on the
|
|
|
+ // system keychain. Check if that's the case for this cert, and if so, remove the
|
|
|
+ // trust rule and the cert from the system keychain.
|
|
|
try
|
|
|
{
|
|
|
- RemoveCertificateTrustRule(certificate);
|
|
|
+ RemoveAdminTrustRule(certificate);
|
|
|
+ RemoveCertificateFromKeychain(MacOSSystemKeychain, certificate);
|
|
|
}
|
|
|
catch
|
|
|
{
|
|
|
}
|
|
|
-
|
|
|
- RemoveCertificateFromKeyChain(MacOSSystemKeyChain, certificate);
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- Log.MacOSCertificateUntrusted(GetDescription(certificate));
|
|
|
}
|
|
|
+
|
|
|
+ RemoveCertificateFromUserStoreCore(certificate);
|
|
|
}
|
|
|
|
|
|
- private static void RemoveCertificateTrustRule(X509Certificate2 certificate)
|
|
|
+ // Remove the certificate from the admin trust settings.
|
|
|
+ private static void RemoveAdminTrustRule(X509Certificate2 certificate)
|
|
|
{
|
|
|
Log.MacOSRemoveCertificateTrustRuleStart(GetDescription(certificate));
|
|
|
var certificatePath = Path.GetTempFileName();
|
|
|
@@ -196,37 +197,37 @@ internal sealed class MacOSCertificateManager : CertificateManager
|
|
|
var certBytes = certificate.Export(X509ContentType.Cert);
|
|
|
File.WriteAllBytes(certificatePath, certBytes);
|
|
|
var processInfo = new ProcessStartInfo(
|
|
|
- MacOSRemoveCertificateTrustCommandLine,
|
|
|
+ MacOSUntrustLegacyCertificateCommandLine,
|
|
|
string.Format(
|
|
|
CultureInfo.InvariantCulture,
|
|
|
- MacOSRemoveCertificateTrustCommandLineArgumentsFormat,
|
|
|
+ MacOSUntrustLegacyCertificateCommandLineArguments,
|
|
|
certificatePath
|
|
|
));
|
|
|
+
|
|
|
using var process = Process.Start(processInfo);
|
|
|
process!.WaitForExit();
|
|
|
+
|
|
|
if (process.ExitCode != 0)
|
|
|
{
|
|
|
Log.MacOSRemoveCertificateTrustRuleError(process.ExitCode);
|
|
|
}
|
|
|
+
|
|
|
Log.MacOSRemoveCertificateTrustRuleEnd();
|
|
|
}
|
|
|
finally
|
|
|
{
|
|
|
try
|
|
|
{
|
|
|
- if (File.Exists(certificatePath))
|
|
|
- {
|
|
|
- File.Delete(certificatePath);
|
|
|
- }
|
|
|
+ File.Delete(certificatePath);
|
|
|
}
|
|
|
catch
|
|
|
{
|
|
|
- // We don't care about failing to do clean-up on a temp file.
|
|
|
+ // We don't care if we can't delete the temp file.
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private static void RemoveCertificateFromKeyChain(string keyChain, X509Certificate2 certificate)
|
|
|
+ private static void RemoveCertificateFromKeychain(string keychain, X509Certificate2 certificate)
|
|
|
{
|
|
|
var processInfo = new ProcessStartInfo(
|
|
|
MacOSDeleteCertificateCommandLine,
|
|
|
@@ -234,7 +235,7 @@ internal sealed class MacOSCertificateManager : CertificateManager
|
|
|
CultureInfo.InvariantCulture,
|
|
|
MacOSDeleteCertificateCommandLineArgumentsFormat,
|
|
|
certificate.Thumbprint.ToUpperInvariant(),
|
|
|
- keyChain
|
|
|
+ keychain
|
|
|
))
|
|
|
{
|
|
|
RedirectStandardOutput = true,
|
|
|
@@ -243,7 +244,7 @@ internal sealed class MacOSCertificateManager : CertificateManager
|
|
|
|
|
|
if (Log.IsEnabled())
|
|
|
{
|
|
|
- Log.MacOSRemoveCertificateFromKeyChainStart(keyChain, GetDescription(certificate));
|
|
|
+ Log.MacOSRemoveCertificateFromKeyChainStart(keychain, GetDescription(certificate));
|
|
|
}
|
|
|
|
|
|
using (var process = Process.Start(processInfo))
|
|
|
@@ -263,12 +264,70 @@ internal sealed class MacOSCertificateManager : CertificateManager
|
|
|
Log.MacOSRemoveCertificateFromKeyChainEnd();
|
|
|
}
|
|
|
|
|
|
- // We don't have a good way of checking on the underlying implementation if ti is exportable, so just return true.
|
|
|
+ private static bool IsCertOnKeychain(string keychain, X509Certificate2 certificate)
|
|
|
+ {
|
|
|
+ TimeSpan MaxRegexTimeout = TimeSpan.FromMinutes(1);
|
|
|
+ const string CertificateSubjectRegex = "CN=(.*[^,]+).*";
|
|
|
+
|
|
|
+ var subjectMatch = Regex.Match(certificate.Subject, CertificateSubjectRegex, RegexOptions.Singleline, MaxRegexTimeout);
|
|
|
+ if (!subjectMatch.Success)
|
|
|
+ {
|
|
|
+ throw new InvalidOperationException($"Can't determine the subject for the certificate with subject '{certificate.Subject}'.");
|
|
|
+ }
|
|
|
+
|
|
|
+ var subject = subjectMatch.Groups[1].Value;
|
|
|
+
|
|
|
+ // Run the find-certificate command, and look for the cert's hash in the output
|
|
|
+ using var findCertificateProcess = Process.Start(new ProcessStartInfo(
|
|
|
+ MacOSFindCertificateOnKeychainCommandLine,
|
|
|
+ string.Format(CultureInfo.InvariantCulture, MacOSFindCertificateOnKeychainCommandLineArgumentsFormat, subject, keychain))
|
|
|
+ {
|
|
|
+ RedirectStandardOutput = true
|
|
|
+ });
|
|
|
+
|
|
|
+ var output = findCertificateProcess!.StandardOutput.ReadToEnd();
|
|
|
+ findCertificateProcess.WaitForExit();
|
|
|
+
|
|
|
+ var matches = Regex.Matches(output, MacOSFindCertificateOutputRegex, RegexOptions.Multiline, MaxRegexTimeout);
|
|
|
+ var hashes = matches.OfType<Match>().Select(m => m.Groups[1].Value).ToList();
|
|
|
+
|
|
|
+ return hashes.Any(h => string.Equals(h, certificate.Thumbprint, StringComparison.Ordinal));
|
|
|
+ }
|
|
|
+
|
|
|
+ // We don't have a good way of checking on the underlying implementation if it is exportable, so just return true.
|
|
|
protected override bool IsExportable(X509Certificate2 c) => true;
|
|
|
|
|
|
protected override X509Certificate2 SaveCertificateCore(X509Certificate2 certificate, StoreName storeName, StoreLocation storeLocation)
|
|
|
{
|
|
|
- // security import https.pfx -k $loginKeyChain -t cert -f pkcs12 -P password -A;
|
|
|
+ SaveCertificateToUserKeychain(certificate);
|
|
|
+
|
|
|
+ try
|
|
|
+ {
|
|
|
+ var certBytes = certificate.Export(X509ContentType.Pfx);
|
|
|
+
|
|
|
+ if (Log.IsEnabled())
|
|
|
+ {
|
|
|
+ Log.MacOSAddCertificateToUserProfileDirStart(MacOSUserKeychain, GetDescription(certificate));
|
|
|
+ }
|
|
|
+
|
|
|
+ // Ensure that the directory exists before writing to the file.
|
|
|
+ Directory.CreateDirectory(MacOSUserHttpsCertificateLocation);
|
|
|
+
|
|
|
+ File.WriteAllBytes(GetCertificateFilePath(certificate), certBytes);
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ Log.MacOSAddCertificateToUserProfileDirError(certificate.Thumbprint, ex.Message);
|
|
|
+ }
|
|
|
+
|
|
|
+ Log.MacOSAddCertificateToKeyChainEnd();
|
|
|
+ Log.MacOSAddCertificateToUserProfileDirEnd();
|
|
|
+
|
|
|
+ return certificate;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static void SaveCertificateToUserKeychain(X509Certificate2 certificate)
|
|
|
+ {
|
|
|
var passwordBytes = new byte[48];
|
|
|
RandomNumberGenerator.Fill(passwordBytes.AsSpan()[0..35]);
|
|
|
var password = Convert.ToBase64String(passwordBytes, 0, 36);
|
|
|
@@ -278,12 +337,7 @@ internal sealed class MacOSCertificateManager : CertificateManager
|
|
|
|
|
|
var processInfo = new ProcessStartInfo(
|
|
|
MacOSAddCertificateToKeyChainCommandLine,
|
|
|
- string.Format(
|
|
|
- CultureInfo.InvariantCulture,
|
|
|
- MacOSAddCertificateToKeyChainCommandLineArgumentsFormat,
|
|
|
- certificatePath,
|
|
|
- password
|
|
|
- ))
|
|
|
+ string.Format(CultureInfo.InvariantCulture, MacOSAddCertificateToKeyChainCommandLineArgumentsFormat, certificatePath, password))
|
|
|
{
|
|
|
RedirectStandardOutput = true,
|
|
|
RedirectStandardError = true
|
|
|
@@ -291,7 +345,7 @@ internal sealed class MacOSCertificateManager : CertificateManager
|
|
|
|
|
|
if (Log.IsEnabled())
|
|
|
{
|
|
|
- Log.MacOSAddCertificateToKeyChainStart(MacOSUserKeyChain, GetDescription(certificate));
|
|
|
+ Log.MacOSAddCertificateToKeyChainStart(MacOSUserKeychain, GetDescription(certificate));
|
|
|
}
|
|
|
|
|
|
using (var process = Process.Start(processInfo))
|
|
|
@@ -301,20 +355,112 @@ internal sealed class MacOSCertificateManager : CertificateManager
|
|
|
|
|
|
if (process.ExitCode != 0)
|
|
|
{
|
|
|
- Log.MacOSAddCertificateToKeyChainError(process.ExitCode);
|
|
|
- throw new InvalidOperationException($@"There was an error importing the certificate into the user key chain '{certificate.Thumbprint}'.
|
|
|
-
|
|
|
-{output}");
|
|
|
+ Log.MacOSAddCertificateToKeyChainError(process.ExitCode, output);
|
|
|
+ throw new InvalidOperationException("Failed to add the certificate to the keychain. Are you running in a non-interactive session perhaps?");
|
|
|
}
|
|
|
}
|
|
|
|
|
|
Log.MacOSAddCertificateToKeyChainEnd();
|
|
|
-
|
|
|
- return certificate;
|
|
|
}
|
|
|
|
|
|
+ private static string GetCertificateFilePath(X509Certificate2 certificate) =>
|
|
|
+ Path.Combine(MacOSUserHttpsCertificateLocation, $"aspnetcore-localhost-{certificate.Thumbprint}.pfx");
|
|
|
+
|
|
|
protected override IList<X509Certificate2> GetCertificatesToRemove(StoreName storeName, StoreLocation storeLocation)
|
|
|
{
|
|
|
return ListCertificates(StoreName.My, StoreLocation.CurrentUser, isValid: false);
|
|
|
}
|
|
|
+
|
|
|
+ protected override void PopulateCertificatesFromStore(X509Store store, List<X509Certificate2> certificates)
|
|
|
+ {
|
|
|
+ if (store.Name! == StoreName.My.ToString() && store.Location == store.Location && Directory.Exists(MacOSUserHttpsCertificateLocation))
|
|
|
+ {
|
|
|
+ var certsFromDisk = GetCertsFromDisk();
|
|
|
+
|
|
|
+ var certsFromStore = new List<X509Certificate2>();
|
|
|
+ base.PopulateCertificatesFromStore(store, certsFromStore);
|
|
|
+
|
|
|
+ // Certs created by pre-.NET 7.
|
|
|
+ var onlyOnKeychain = certsFromStore.Except(certsFromDisk, ThumbprintComparer.Instance);
|
|
|
+
|
|
|
+ // Certs created (or "upgraded") by .NET 7+.
|
|
|
+ // .NET 7+ installs the certificate on disk as well as on the user keychain (for backwards
|
|
|
+ // compatibility with pre-.NET 7).
|
|
|
+ var onDiskAndKeychain = certsFromDisk.Intersect(certsFromStore, ThumbprintComparer.Instance);
|
|
|
+
|
|
|
+ // The only times we can find a certificate on the keychain and a certificate on keychain+disk
|
|
|
+ // are when the certificate on disk and keychain has expired and a pre-.NET 7 SDK has been
|
|
|
+ // used to create a new certificate, or when a pre-.NET 7 certificate has expired and .NET 7+
|
|
|
+ // has been used to create a new certificate. In both cases, the caller filters the invalid
|
|
|
+ // certificates out, so only the valid certificate is selected.
|
|
|
+ certificates.AddRange(onlyOnKeychain);
|
|
|
+ certificates.AddRange(onDiskAndKeychain);
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ base.PopulateCertificatesFromStore(store, certificates);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private sealed class ThumbprintComparer : IEqualityComparer<X509Certificate2>
|
|
|
+ {
|
|
|
+ public static readonly IEqualityComparer<X509Certificate2> Instance = new ThumbprintComparer();
|
|
|
+
|
|
|
+#pragma warning disable CS8769 // Nullability of reference types in type of parameter doesn't match implemented member (possibly because of nullability attributes).
|
|
|
+ bool IEqualityComparer<X509Certificate2>.Equals(X509Certificate2 x, X509Certificate2 y) =>
|
|
|
+ EqualityComparer<string>.Default.Equals(x?.Thumbprint, y?.Thumbprint);
|
|
|
+#pragma warning restore CS8769 // Nullability of reference types in type of parameter doesn't match implemented member (possibly because of nullability attributes).
|
|
|
+
|
|
|
+ int IEqualityComparer<X509Certificate2>.GetHashCode([DisallowNull] X509Certificate2 obj) =>
|
|
|
+ EqualityComparer<string>.Default.GetHashCode(obj.Thumbprint);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static ICollection<X509Certificate2> GetCertsFromDisk()
|
|
|
+ {
|
|
|
+ var certsFromDisk = new List<X509Certificate2>();
|
|
|
+ if (!Directory.Exists(MacOSUserHttpsCertificateLocation))
|
|
|
+ {
|
|
|
+ Log.MacOSDiskStoreDoesNotExist();
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ var certificateFiles = Directory.EnumerateFiles(MacOSUserHttpsCertificateLocation, "aspnetcore-localhost-*.pfx");
|
|
|
+ foreach (var file in certificateFiles)
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ var certificate = new X509Certificate2(file);
|
|
|
+ certsFromDisk.Add(certificate);
|
|
|
+ }
|
|
|
+ catch (Exception)
|
|
|
+ {
|
|
|
+ Log.MacOSFileIsNotAValidCertificate(file);
|
|
|
+ throw;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return certsFromDisk;
|
|
|
+ }
|
|
|
+
|
|
|
+ protected override void RemoveCertificateFromUserStoreCore(X509Certificate2 certificate)
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ var certificatePath = GetCertificateFilePath(certificate);
|
|
|
+ if (File.Exists(certificatePath))
|
|
|
+ {
|
|
|
+ File.Delete(certificatePath);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ Log.MacOSRemoveCertificateFromUserProfileDirError(certificate.Thumbprint, ex.Message);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (IsCertOnKeychain(MacOSUserKeychain, certificate))
|
|
|
+ {
|
|
|
+ RemoveCertificateFromKeychain(MacOSUserKeychain, certificate);
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|