[HTTPS] Update certificate strategy for Mac OS (#20022)
* Fixes and improvements for dotnet-dev-certs: * Revamps the HTTPS developer certificate tool implementation. * It accumulated a lot of cruft during the past few years and that has made making changes harder. * Separated the CertificateManager implementation into different classes per platform. * This centralizes the decision point of choosing a platform in a single place. * Makes clear what the flow is for a given platform. * Isolates changes needed for a given platform in the future. * Moved CertificateManager to a singleton * No more statics! * Updates logging to use EventSource * We didn't have a good way of performing logging as the code is shared and must run in multiple contexts and the set of dependencies need to be kept to a minimum. * Adding ETW allow us to log/monitor the the tool execution and capture the logs with `dotnet trace` without having to invent our own logging. * We can decide to write an EventListener in `dotnet-dev-certs` to write the results to the console output. * Updates the way we handle the dev-cert in Mac OS to use the security tool to import the certificate into the store instead of using the certificate store.
This commit is contained in:
parent
00623928f6
commit
8e1e81ae78
|
|
@ -89,8 +89,8 @@ namespace Templates.Test.Helpers
|
|||
internal void EnsureDevelopmentCertificates()
|
||||
{
|
||||
var now = DateTimeOffset.Now;
|
||||
var manager = new CertificateManager();
|
||||
var certificate = manager.CreateAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), "CN=localhost");
|
||||
var manager = CertificateManager.Instance;
|
||||
var certificate = manager.CreateAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1));
|
||||
manager.ExportCertificate(certificate, path: _certificatePath, includePrivateKey: true, _certificatePassword);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -34,6 +34,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
|
|||
new EventId(3, "FailedToLoadDevelopmentCertificate"),
|
||||
"Failed to load the development https certificate at '{certificatePath}'.");
|
||||
|
||||
private static readonly Action<ILogger, Exception> _badDeveloperCertificateState =
|
||||
LoggerMessage.Define(
|
||||
LogLevel.Error,
|
||||
new EventId(4, "BadDeveloperCertificateState"),
|
||||
CoreStrings.BadDeveloperCertificateState);
|
||||
|
||||
private static readonly Action<ILogger, string, Exception> _developerCertificateFirstRun =
|
||||
LoggerMessage.Define<string>(
|
||||
LogLevel.Warning,
|
||||
new EventId(5, "DeveloperCertificateFirstRun"),
|
||||
"{Message}");
|
||||
|
||||
public static void LocatedDevelopmentCertificate(this ILogger logger, X509Certificate2 certificate) => _locatedDevelopmentCertificate(logger, certificate.Subject, certificate.Thumbprint, null);
|
||||
|
||||
public static void UnableToLocateDevelopmentCertificate(this ILogger logger) => _unableToLocateDevelopmentCertificate(logger, null);
|
||||
|
|
@ -41,5 +53,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
|
|||
public static void FailedToLocateDevelopmentCertificateFile(this ILogger logger, string certificatePath) => _failedToLocateDevelopmentCertificateFile(logger, certificatePath, null);
|
||||
|
||||
public static void FailedToLoadDevelopmentCertificate(this ILogger logger, string certificatePath) => _failedToLoadDevelopmentCertificate(logger, certificatePath, null);
|
||||
|
||||
public static void BadDeveloperCertificateState(this ILogger logger) => _badDeveloperCertificateState(logger, null);
|
||||
|
||||
public static void DeveloperCertificateFirstRun(this ILogger logger, string message) => _developerCertificateFirstRun(logger, message, null);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -162,11 +162,29 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
|
|||
var logger = ApplicationServices.GetRequiredService<ILogger<KestrelServer>>();
|
||||
try
|
||||
{
|
||||
DefaultCertificate = CertificateManager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: true)
|
||||
DefaultCertificate = CertificateManager.Instance.ListCertificates(StoreName.My, StoreLocation.CurrentUser, isValid: true)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (DefaultCertificate != null)
|
||||
{
|
||||
var status = CertificateManager.Instance.CheckCertificateState(DefaultCertificate, interactive: false);
|
||||
if (!status.Result)
|
||||
{
|
||||
// Display a warning indicating to the user that a prompt might appear and provide instructions on what to do in that
|
||||
// case. The underlying implementation of this check is specific to Mac OS and is handled within CheckCertificateState.
|
||||
// Kestrel must NEVER cause a UI prompt on a production system. We only attempt this here because Mac OS is not supported
|
||||
// in production.
|
||||
logger.DeveloperCertificateFirstRun(status.Message);
|
||||
|
||||
// Now that we've displayed a warning in the logs so that the user gets a notification that a prompt might appear, try
|
||||
// and access the certificate key, which might trigger a prompt.
|
||||
status = CertificateManager.Instance.CheckCertificateState(DefaultCertificate, interactive: true);
|
||||
if (!status.Result)
|
||||
{
|
||||
logger.BadDeveloperCertificateState();
|
||||
}
|
||||
}
|
||||
|
||||
logger.LocatedDevelopmentCertificate(DefaultCertificate);
|
||||
}
|
||||
else
|
||||
|
|
|
|||
|
|
@ -220,16 +220,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
|
|||
}
|
||||
catch (AuthenticationException ex)
|
||||
{
|
||||
if (_serverCertificate == null ||
|
||||
!CertificateManager.IsHttpsDevelopmentCertificate(_serverCertificate) ||
|
||||
CertificateManager.CheckDeveloperCertificateKey(_serverCertificate))
|
||||
{
|
||||
_logger.LogDebug(1, ex, CoreStrings.AuthenticationFailed);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogError(3, ex, CoreStrings.BadDeveloperCertificateState);
|
||||
}
|
||||
_logger.LogDebug(1, ex, CoreStrings.AuthenticationFailed);
|
||||
|
||||
await sslStream.DisposeAsync();
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -385,35 +385,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
|||
Assert.Equal(LogLevel.Debug, loggerProvider.FilterLogger.LastLogLevel);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DevCertWithInvalidPrivateKeyProducesCustomWarning()
|
||||
{
|
||||
var loggerProvider = new HandshakeErrorLoggerProvider();
|
||||
LoggerFactory.AddProvider(loggerProvider);
|
||||
|
||||
await using (var server = new TestServer(context => Task.CompletedTask,
|
||||
new TestServiceContext(LoggerFactory),
|
||||
listenOptions =>
|
||||
{
|
||||
listenOptions.UseHttps(TestResources.GetTestCertificate("aspnetdevcert.pfx", "testPassword"));
|
||||
}))
|
||||
{
|
||||
using (var connection = server.CreateConnection())
|
||||
using (var sslStream = new SslStream(connection.Stream, true, (sender, certificate, chain, errors) => true))
|
||||
{
|
||||
// SslProtocols.Tls is TLS 1.0 which isn't supported by Kestrel by default.
|
||||
await Assert.ThrowsAnyAsync<Exception>(() =>
|
||||
sslStream.AuthenticateAsClientAsync("127.0.0.1", clientCertificates: null,
|
||||
enabledSslProtocols: SslProtocols.Tls,
|
||||
checkCertificateRevocation: false));
|
||||
}
|
||||
}
|
||||
|
||||
await loggerProvider.FilterLogger.LogTcs.Task.DefaultTimeout();
|
||||
Assert.Equal(3, loggerProvider.FilterLogger.LastEventId);
|
||||
Assert.Equal(LogLevel.Error, loggerProvider.FilterLogger.LastLogLevel);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OnAuthenticate_SeesOtherSettings()
|
||||
{
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,306 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Microsoft.AspNetCore.Certificates.Generation
|
||||
{
|
||||
internal class MacOSCertificateManager : CertificateManager
|
||||
{
|
||||
private const string CertificateSubjectRegex = "CN=(.*[^,]+).*";
|
||||
private static readonly string MacOSUserKeyChain = Environment.GetEnvironmentVariable("HOME") + "/Library/Keychains/login.keychain-db";
|
||||
private const string MacOSSystemKeyChain = "/Library/Keychains/System.keychain";
|
||||
private const string MacOSFindCertificateCommandLine = "security";
|
||||
private static readonly 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 const string MacOSDeleteCertificateCommandLine = "sudo";
|
||||
private const string MacOSDeleteCertificateCommandLineArgumentsFormat = "security delete-certificate -Z {0} {1}";
|
||||
private const string MacOSTrustCertificateCommandLine = "sudo";
|
||||
private static readonly string MacOSTrustCertificateCommandLineArguments = "security add-trusted-cert -d -r trustRoot -k " + MacOSSystemKeyChain + " ";
|
||||
|
||||
private const string MacOSAddCertificateToKeyChainCommandLine = "security";
|
||||
private static readonly string MacOSAddCertificateToKeyChainCommandLineArgumentsFormat = "import {0} -k " + MacOSUserKeyChain + " -t cert -f pkcs12 -P {1} -A";
|
||||
|
||||
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 " +
|
||||
"and create a new untrusted developer certificate. " +
|
||||
"On macOS or Windows, use 'dotnet dev-certs https --trust' to trust the new certificate.";
|
||||
|
||||
public const string KeyNotAccessibleWithoutUserInteraction =
|
||||
"The application is trying to access the ASP.NET Core developer certificate key. " +
|
||||
"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()
|
||||
{
|
||||
}
|
||||
|
||||
internal MacOSCertificateManager(string subject, int version)
|
||||
: base(subject, version)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void TrustCertificateCore(X509Certificate2 publicCertificate)
|
||||
{
|
||||
var tmpFile = Path.GetTempFileName();
|
||||
try
|
||||
{
|
||||
ExportCertificate(publicCertificate, tmpFile, includePrivateKey: false, password: null);
|
||||
Log.MacOSTrustCommandStart($"{MacOSTrustCertificateCommandLine} {MacOSTrustCertificateCommandLineArguments}{tmpFile}");
|
||||
using (var process = Process.Start(MacOSTrustCertificateCommandLine, MacOSTrustCertificateCommandLineArguments + tmpFile))
|
||||
{
|
||||
process.WaitForExit();
|
||||
if (process.ExitCode != 0)
|
||||
{
|
||||
Log.MacOSTrustCommandError(process.ExitCode);
|
||||
throw new InvalidOperationException("There was an error trusting the certificate.");
|
||||
}
|
||||
}
|
||||
Log.MacOSTrustCommandEnd();
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(tmpFile))
|
||||
{
|
||||
File.Delete(tmpFile);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// We don't care if we can't delete the temp file.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal override CheckCertificateStateResult CheckCertificateState(X509Certificate2 candidate, bool interactive)
|
||||
{
|
||||
var sentinelPath = Path.Combine(Environment.GetEnvironmentVariable("HOME"), ".dotnet", $"certificates.{candidate.GetCertHashString(HashAlgorithmName.SHA256)}.sentinel");
|
||||
if (!interactive && !File.Exists(sentinelPath))
|
||||
{
|
||||
return new CheckCertificateStateResult(false, KeyNotAccessibleWithoutUserInteraction);
|
||||
}
|
||||
|
||||
// Tries to use the certificate key to validate it can't access it
|
||||
try
|
||||
{
|
||||
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);
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
internal override void CorrectCertificateState(X509Certificate2 candidate)
|
||||
{
|
||||
var status = CheckCertificateState(candidate, true);
|
||||
if (!status.Result)
|
||||
{
|
||||
throw new InvalidOperationException(InvalidCertificateState);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public override bool IsTrusted(X509Certificate2 certificate)
|
||||
{
|
||||
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;
|
||||
using var checkTrustProcess = Process.Start(new ProcessStartInfo(
|
||||
MacOSFindCertificateCommandLine,
|
||||
string.Format(MacOSFindCertificateCommandLineArgumentsFormat, subject))
|
||||
{
|
||||
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));
|
||||
}
|
||||
|
||||
protected override void RemoveCertificateFromTrustedRoots(X509Certificate2 certificate)
|
||||
{
|
||||
if (IsTrusted(certificate)) // On OSX this check just ensures its on the system keychain
|
||||
{
|
||||
// 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.
|
||||
try
|
||||
{
|
||||
RemoveCertificateTrustRule(certificate);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
RemoveCertificateFromKeyChain(MacOSSystemKeyChain, certificate);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.MacOSCertificateUntrusted(GetDescription(certificate));
|
||||
}
|
||||
}
|
||||
|
||||
private static void RemoveCertificateTrustRule(X509Certificate2 certificate)
|
||||
{
|
||||
Log.MacOSRemoveCertificateTrustRuleStart(GetDescription(certificate));
|
||||
var certificatePath = Path.GetTempFileName();
|
||||
try
|
||||
{
|
||||
var certBytes = certificate.Export(X509ContentType.Cert);
|
||||
File.WriteAllBytes(certificatePath, certBytes);
|
||||
var processInfo = new ProcessStartInfo(
|
||||
MacOSRemoveCertificateTrustCommandLine,
|
||||
string.Format(
|
||||
MacOSRemoveCertificateTrustCommandLineArgumentsFormat,
|
||||
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);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// We don't care about failing to do clean-up on a temp file.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void RemoveCertificateFromKeyChain(string keyChain, X509Certificate2 certificate)
|
||||
{
|
||||
var processInfo = new ProcessStartInfo(
|
||||
MacOSDeleteCertificateCommandLine,
|
||||
string.Format(
|
||||
MacOSDeleteCertificateCommandLineArgumentsFormat,
|
||||
certificate.Thumbprint.ToUpperInvariant(),
|
||||
keyChain
|
||||
))
|
||||
{
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true
|
||||
};
|
||||
|
||||
Log.MacOSRemoveCertificateFromKeyChainStart(keyChain, GetDescription(certificate));
|
||||
using (var process = Process.Start(processInfo))
|
||||
{
|
||||
var output = process.StandardOutput.ReadToEnd() + process.StandardError.ReadToEnd();
|
||||
process.WaitForExit();
|
||||
|
||||
if (process.ExitCode != 0)
|
||||
{
|
||||
Log.MacOSRemoveCertificateFromKeyChainError(process.ExitCode);
|
||||
throw new InvalidOperationException($@"There was an error removing the certificate with thumbprint '{certificate.Thumbprint}'.
|
||||
|
||||
{output}");
|
||||
}
|
||||
}
|
||||
|
||||
Log.MacOSRemoveCertificateFromKeyChainEnd();
|
||||
}
|
||||
|
||||
// We don't have a good way of checking on the underlying implementation if ti is exportable, so just return true.
|
||||
protected override bool IsExportable(X509Certificate2 c) => true;
|
||||
|
||||
protected override X509Certificate2 SaveCertificateCore(X509Certificate2 certificate)
|
||||
{
|
||||
// security import https.pfx -k $loginKeyChain -t cert -f pkcs12 -P password -A;
|
||||
var passwordBytes = new byte[48];
|
||||
RandomNumberGenerator.Fill(passwordBytes.AsSpan()[0..35]);
|
||||
var password = Convert.ToBase64String(passwordBytes, 0, 36);
|
||||
var certBytes = certificate.Export(X509ContentType.Pfx, password);
|
||||
var certificatePath = Path.GetTempFileName();
|
||||
File.WriteAllBytes(certificatePath, certBytes);
|
||||
|
||||
var processInfo = new ProcessStartInfo(
|
||||
MacOSAddCertificateToKeyChainCommandLine,
|
||||
string.Format(
|
||||
MacOSAddCertificateToKeyChainCommandLineArgumentsFormat,
|
||||
certificatePath,
|
||||
password
|
||||
))
|
||||
{
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true
|
||||
};
|
||||
|
||||
Log.MacOSAddCertificateToKeyChainStart(MacOSUserKeyChain, GetDescription(certificate));
|
||||
using (var process = Process.Start(processInfo))
|
||||
{
|
||||
var output = process.StandardOutput.ReadToEnd() + process.StandardError.ReadToEnd();
|
||||
process.WaitForExit();
|
||||
|
||||
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.MacOSAddCertificateToKeyChainEnd();
|
||||
|
||||
return certificate;
|
||||
}
|
||||
|
||||
protected override IList<X509Certificate2> GetCertificatesToRemove(StoreName storeName, StoreLocation storeLocation)
|
||||
{
|
||||
return ListCertificates(StoreName.My, StoreLocation.CurrentUser, isValid: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
namespace Microsoft.AspNetCore.Certificates.Generation
|
||||
{
|
||||
internal class UnixCertificateManager : CertificateManager
|
||||
{
|
||||
public UnixCertificateManager()
|
||||
{
|
||||
}
|
||||
|
||||
internal UnixCertificateManager(string subject, int version)
|
||||
: base(subject, version)
|
||||
{
|
||||
}
|
||||
|
||||
public override bool IsTrusted(X509Certificate2 certificate) => false;
|
||||
|
||||
protected override X509Certificate2 SaveCertificateCore(X509Certificate2 certificate)
|
||||
{
|
||||
var export = certificate.Export(X509ContentType.Pkcs12, "");
|
||||
certificate = new X509Certificate2(export, "", X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable);
|
||||
Array.Clear(export, 0, export.Length);
|
||||
|
||||
using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser))
|
||||
{
|
||||
store.Open(OpenFlags.ReadWrite);
|
||||
store.Add(certificate);
|
||||
store.Close();
|
||||
};
|
||||
|
||||
return certificate;
|
||||
}
|
||||
|
||||
internal override CheckCertificateStateResult CheckCertificateState(X509Certificate2 candidate, bool interactive)
|
||||
{
|
||||
// Return true as we don't perform any check.
|
||||
return new CheckCertificateStateResult(true, null);
|
||||
}
|
||||
|
||||
internal override void CorrectCertificateState(X509Certificate2 candidate)
|
||||
{
|
||||
// Do nothing since we don't have anything to check here.
|
||||
}
|
||||
|
||||
protected override bool IsExportable(X509Certificate2 c) => true;
|
||||
|
||||
protected override void TrustCertificateCore(X509Certificate2 certificate) =>
|
||||
throw new InvalidOperationException("Trusting the certificate is not supported on linux");
|
||||
|
||||
protected override void RemoveCertificateFromTrustedRoots(X509Certificate2 certificate)
|
||||
{
|
||||
// No-op here as is benign
|
||||
}
|
||||
|
||||
protected override IList<X509Certificate2> GetCertificatesToRemove(StoreName storeName, StoreLocation storeLocation)
|
||||
{
|
||||
return ListCertificates(StoreName.My, StoreLocation.CurrentUser, isValid: false, requireExportable: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
namespace Microsoft.AspNetCore.Certificates.Generation
|
||||
{
|
||||
internal class WindowsCertificateManager : CertificateManager
|
||||
{
|
||||
private const int UserCancelledErrorCode = 1223;
|
||||
|
||||
public WindowsCertificateManager()
|
||||
{
|
||||
}
|
||||
|
||||
// For testing purposes only
|
||||
internal WindowsCertificateManager(string subject, int version)
|
||||
: base(subject, version)
|
||||
{
|
||||
}
|
||||
|
||||
protected override bool IsExportable(X509Certificate2 c)
|
||||
{
|
||||
#if XPLAT
|
||||
// For the first run experience we don't need to know if the certificate can be exported.
|
||||
return true;
|
||||
#else
|
||||
return (c.GetRSAPrivateKey() is RSACryptoServiceProvider rsaPrivateKey &&
|
||||
rsaPrivateKey.CspKeyContainerInfo.Exportable) ||
|
||||
(c.GetRSAPrivateKey() is RSACng cngPrivateKey &&
|
||||
cngPrivateKey.Key.ExportPolicy == CngExportPolicies.AllowExport);
|
||||
#endif
|
||||
}
|
||||
|
||||
internal override CheckCertificateStateResult CheckCertificateState(X509Certificate2 candidate, bool interactive)
|
||||
{
|
||||
// Return true as we don't perform any check.
|
||||
return new CheckCertificateStateResult(true, null);
|
||||
}
|
||||
|
||||
internal override void CorrectCertificateState(X509Certificate2 candidate)
|
||||
{
|
||||
// Do nothing since we don't have anything to check here.
|
||||
}
|
||||
|
||||
protected override X509Certificate2 SaveCertificateCore(X509Certificate2 certificate)
|
||||
{
|
||||
// On non OSX systems we need to export the certificate and import it so that the transient
|
||||
// key that we generated gets persisted.
|
||||
var export = certificate.Export(X509ContentType.Pkcs12, "");
|
||||
certificate = new X509Certificate2(export, "", X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable);
|
||||
Array.Clear(export, 0, export.Length);
|
||||
certificate.FriendlyName = AspNetHttpsOidFriendlyName;
|
||||
|
||||
using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser))
|
||||
{
|
||||
store.Open(OpenFlags.ReadWrite);
|
||||
store.Add(certificate);
|
||||
store.Close();
|
||||
};
|
||||
|
||||
return certificate;
|
||||
}
|
||||
|
||||
protected override void TrustCertificateCore(X509Certificate2 certificate)
|
||||
{
|
||||
var publicCertificate = new X509Certificate2(certificate.Export(X509ContentType.Cert));
|
||||
|
||||
publicCertificate.FriendlyName = certificate.FriendlyName;
|
||||
|
||||
using var store = new X509Store(StoreName.Root, StoreLocation.CurrentUser);
|
||||
|
||||
store.Open(OpenFlags.ReadWrite);
|
||||
var existing = store.Certificates.Find(X509FindType.FindByThumbprint, publicCertificate.Thumbprint, validOnly: false);
|
||||
if (existing.Count > 0)
|
||||
{
|
||||
Log.WindowsCertificateAlreadyTrusted();
|
||||
DisposeCertificates(existing.OfType<X509Certificate2>());
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Log.WindowsAddCertificateToRootStore();
|
||||
store.Add(publicCertificate);
|
||||
store.Close();
|
||||
}
|
||||
catch (CryptographicException exception) when (exception.HResult == UserCancelledErrorCode)
|
||||
{
|
||||
Log.WindowsCertificateTrustCanceled();
|
||||
throw new UserCancelledTrustException();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void RemoveCertificateFromTrustedRoots(X509Certificate2 certificate)
|
||||
{
|
||||
Log.WindowsRemoveCertificateFromRootStoreStart();
|
||||
using var store = new X509Store(StoreName.Root, StoreLocation.CurrentUser);
|
||||
|
||||
store.Open(OpenFlags.ReadWrite);
|
||||
var matching = store.Certificates
|
||||
.OfType<X509Certificate2>()
|
||||
.SingleOrDefault(c => c.SerialNumber == certificate.SerialNumber);
|
||||
|
||||
if (matching != null)
|
||||
{
|
||||
store.Remove(matching);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.WindowsRemoveCertificateFromRootStoreNotFound();
|
||||
}
|
||||
|
||||
store.Close();
|
||||
Log.WindowsRemoveCertificateFromRootStoreEnd();
|
||||
}
|
||||
|
||||
public override bool IsTrusted(X509Certificate2 certificate)
|
||||
{
|
||||
return ListCertificates(StoreName.Root, StoreLocation.CurrentUser, isValid: true, requireExportable: false)
|
||||
.Any(c => c.Thumbprint == certificate.Thumbprint);
|
||||
}
|
||||
|
||||
protected override IList<X509Certificate2> GetCertificatesToRemove(StoreName storeName, StoreLocation storeLocation)
|
||||
{
|
||||
return ListCertificates(storeName, storeLocation, isValid: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@ namespace Microsoft.AspNetCore.DeveloperCertificates.XPlat
|
|||
{
|
||||
public static void GenerateAspNetHttpsCertificate()
|
||||
{
|
||||
var manager = new CertificateManager();
|
||||
var manager = CertificateManager.Instance;
|
||||
var now = DateTimeOffset.Now;
|
||||
manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), isInteractive: false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests
|
|||
{
|
||||
public class CertificateManagerTests : IClassFixture<CertFixture>
|
||||
{
|
||||
private CertFixture _fixture;
|
||||
private readonly CertFixture _fixture;
|
||||
private CertificateManager _manager => _fixture.Manager;
|
||||
|
||||
public CertificateManagerTests(ITestOutputHelper output, CertFixture fixture)
|
||||
|
|
@ -39,20 +39,20 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests
|
|||
|
||||
const string CertificateName = nameof(EnsureCreateHttpsCertificate_CreatesACertificate_WhenThereAreNoHttpsCertificates) + ".cer";
|
||||
|
||||
// Act
|
||||
DateTimeOffset now = DateTimeOffset.UtcNow;
|
||||
// Act
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset);
|
||||
var result = _manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), CertificateName, trust: false, subject: TestCertificateSubject, isInteractive: false);
|
||||
var result = _manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), CertificateName, trust: false, isInteractive: false);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(EnsureCertificateResult.Succeeded, result.ResultCode);
|
||||
Assert.Equal(EnsureCertificateResult.Succeeded, result);
|
||||
Assert.True(File.Exists(CertificateName));
|
||||
|
||||
var exportedCertificate = new X509Certificate2(File.ReadAllBytes(CertificateName));
|
||||
Assert.NotNull(exportedCertificate);
|
||||
Assert.False(exportedCertificate.HasPrivateKey);
|
||||
|
||||
var httpsCertificates = CertificateManager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: false);
|
||||
var httpsCertificates = _manager.ListCertificates(StoreName.My, StoreLocation.CurrentUser, isValid: false);
|
||||
var httpsCertificate = Assert.Single(httpsCertificates, c => c.Subject == TestCertificateSubject);
|
||||
Assert.True(httpsCertificate.HasPrivateKey);
|
||||
Assert.Equal(TestCertificateSubject, httpsCertificate.Subject);
|
||||
|
|
@ -94,7 +94,7 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests
|
|||
httpsCertificate.Extensions.OfType<X509Extension>(),
|
||||
e => e.Critical == false &&
|
||||
e.Oid.Value == "1.3.6.1.4.1.311.84.1.1" &&
|
||||
e.RawData[0] == CertificateManager.AspNetHttpsCertificateVersion);
|
||||
e.RawData[0] == _manager.AspNetHttpsCertificateVersion);
|
||||
|
||||
Assert.Equal(httpsCertificate.GetCertHashString(), exportedCertificate.GetCertHashString());
|
||||
|
||||
|
|
@ -102,12 +102,12 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests
|
|||
catch (Exception e)
|
||||
{
|
||||
Output.WriteLine(e.Message);
|
||||
ListCertificates(Output);
|
||||
ListCertificates();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void ListCertificates(ITestOutputHelper output)
|
||||
private void ListCertificates()
|
||||
{
|
||||
using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser))
|
||||
{
|
||||
|
|
@ -133,17 +133,18 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests
|
|||
|
||||
_fixture.CleanupCertificates();
|
||||
|
||||
DateTimeOffset now = DateTimeOffset.UtcNow;
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset);
|
||||
_manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), path: null, trust: false, subject: TestCertificateSubject, isInteractive: false);
|
||||
|
||||
var httpsCertificate = CertificateManager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: false).Single(c => c.Subject == TestCertificateSubject);
|
||||
var creation = _manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), path: null, trust: false, isInteractive: false);
|
||||
Output.WriteLine(creation.ToString());
|
||||
ListCertificates();
|
||||
|
||||
var httpsCertificate = _manager.ListCertificates(StoreName.My, StoreLocation.CurrentUser, isValid: false).Single(c => c.Subject == TestCertificateSubject);
|
||||
// Act
|
||||
var result = _manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), CertificateName, trust: false, includePrivateKey: true, password: certificatePassword, subject: TestCertificateSubject, isInteractive: false);
|
||||
var result = _manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), CertificateName, trust: false, includePrivateKey: true, password: certificatePassword, isInteractive: false);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(EnsureCertificateResult.ValidCertificatePresent, result.ResultCode);
|
||||
Assert.Equal(EnsureCertificateResult.ValidCertificatePresent, result);
|
||||
Assert.True(File.Exists(CertificateName));
|
||||
|
||||
var exportedCertificate = new X509Certificate2(File.ReadAllBytes(CertificateName), certificatePassword);
|
||||
|
|
@ -159,13 +160,15 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests
|
|||
{
|
||||
_fixture.CleanupCertificates();
|
||||
|
||||
DateTimeOffset now = DateTimeOffset.UtcNow;
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset);
|
||||
_manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), path: null, trust: false, subject: TestCertificateSubject, isInteractive: false);
|
||||
var creation = _manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), path: null, trust: false, isInteractive: false);
|
||||
Output.WriteLine(creation.ToString());
|
||||
ListCertificates();
|
||||
|
||||
CertificateManager.AspNetHttpsCertificateVersion = 2;
|
||||
_manager.AspNetHttpsCertificateVersion = 2;
|
||||
|
||||
var httpsCertificateList = CertificateManager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: true);
|
||||
var httpsCertificateList = _manager.ListCertificates(StoreName.My, StoreLocation.CurrentUser, isValid: true);
|
||||
Assert.Empty(httpsCertificateList);
|
||||
}
|
||||
|
||||
|
|
@ -174,14 +177,16 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests
|
|||
{
|
||||
_fixture.CleanupCertificates();
|
||||
|
||||
DateTimeOffset now = DateTimeOffset.UtcNow;
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset);
|
||||
CertificateManager.AspNetHttpsCertificateVersion = 0;
|
||||
_manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), path: null, trust: false, subject: TestCertificateSubject, isInteractive: false);
|
||||
_manager.AspNetHttpsCertificateVersion = 0;
|
||||
var creation = _manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), path: null, trust: false, isInteractive: false);
|
||||
Output.WriteLine(creation.ToString());
|
||||
ListCertificates();
|
||||
|
||||
CertificateManager.AspNetHttpsCertificateVersion = 1;
|
||||
_manager.AspNetHttpsCertificateVersion = 1;
|
||||
|
||||
var httpsCertificateList = CertificateManager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: true);
|
||||
var httpsCertificateList = _manager.ListCertificates(StoreName.My, StoreLocation.CurrentUser, isValid: true);
|
||||
Assert.Empty(httpsCertificateList);
|
||||
}
|
||||
|
||||
|
|
@ -191,12 +196,14 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests
|
|||
{
|
||||
_fixture.CleanupCertificates();
|
||||
|
||||
DateTimeOffset now = DateTimeOffset.UtcNow;
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset);
|
||||
CertificateManager.AspNetHttpsCertificateVersion = 0;
|
||||
_manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), path: null, trust: false, subject: TestCertificateSubject, isInteractive: false);
|
||||
_manager.AspNetHttpsCertificateVersion = 0;
|
||||
var creation = _manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), path: null, trust: false, isInteractive: false);
|
||||
Output.WriteLine(creation.ToString());
|
||||
ListCertificates();
|
||||
|
||||
var httpsCertificateList = CertificateManager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: true);
|
||||
var httpsCertificateList = _manager.ListCertificates(StoreName.My, StoreLocation.CurrentUser, isValid: true);
|
||||
Assert.NotEmpty(httpsCertificateList);
|
||||
}
|
||||
|
||||
|
|
@ -206,45 +213,17 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests
|
|||
{
|
||||
_fixture.CleanupCertificates();
|
||||
|
||||
DateTimeOffset now = DateTimeOffset.UtcNow;
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset);
|
||||
CertificateManager.AspNetHttpsCertificateVersion = 2;
|
||||
_manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), path: null, trust: false, subject: TestCertificateSubject, isInteractive: false);
|
||||
_manager.AspNetHttpsCertificateVersion = 2;
|
||||
var creation = _manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), path: null, trust: false, isInteractive: false);
|
||||
Output.WriteLine(creation.ToString());
|
||||
ListCertificates();
|
||||
|
||||
CertificateManager.AspNetHttpsCertificateVersion = 1;
|
||||
var httpsCertificateList = CertificateManager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: true);
|
||||
_manager.AspNetHttpsCertificateVersion = 1;
|
||||
var httpsCertificateList = _manager.ListCertificates(StoreName.My, StoreLocation.CurrentUser, isValid: true);
|
||||
Assert.NotEmpty(httpsCertificateList);
|
||||
}
|
||||
|
||||
[Fact(Skip = "Requires user interaction")]
|
||||
public void EnsureAspNetCoreHttpsDevelopmentCertificate_ReturnsCorrectResult_WhenUserCancelsTrustStepOnWindows()
|
||||
{
|
||||
_fixture.CleanupCertificates();
|
||||
|
||||
DateTimeOffset now = DateTimeOffset.UtcNow;
|
||||
now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset);
|
||||
var trustFailed = _manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), path: null, trust: true, subject: TestCertificateSubject, isInteractive: false);
|
||||
|
||||
Assert.Equal(EnsureCertificateResult.UserCancelledTrustStep, trustFailed.ResultCode);
|
||||
}
|
||||
|
||||
[Fact(Skip = "Requires user interaction")]
|
||||
public void EnsureAspNetCoreHttpsDevelopmentCertificate_CanRemoveCertificates()
|
||||
{
|
||||
_fixture.CleanupCertificates();
|
||||
|
||||
DateTimeOffset now = DateTimeOffset.UtcNow;
|
||||
now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset);
|
||||
_manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), path: null, trust: true, subject: TestCertificateSubject, isInteractive: false);
|
||||
|
||||
_manager.CleanupHttpsCertificates(TestCertificateSubject);
|
||||
|
||||
Assert.Empty(CertificateManager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: false).Where(c => c.Subject == TestCertificateSubject));
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
Assert.Empty(CertificateManager.ListCertificates(CertificatePurpose.HTTPS, StoreName.Root, StoreLocation.CurrentUser, isValid: false).Where(c => c.Subject == TestCertificateSubject));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class CertFixture : IDisposable
|
||||
|
|
@ -253,24 +232,25 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests
|
|||
|
||||
public CertFixture()
|
||||
{
|
||||
Manager = new CertificateManager();
|
||||
Manager = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ?
|
||||
new WindowsCertificateManager(TestCertificateSubject, 1) :
|
||||
RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ?
|
||||
new MacOSCertificateManager(TestCertificateSubject, 1) as CertificateManager :
|
||||
new UnixCertificateManager(TestCertificateSubject, 1);
|
||||
|
||||
CleanupCertificates();
|
||||
}
|
||||
|
||||
internal CertificateManager Manager { get; set; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
CleanupCertificates();
|
||||
}
|
||||
public void Dispose() => CleanupCertificates();
|
||||
|
||||
internal void CleanupCertificates()
|
||||
{
|
||||
Manager.RemoveAllCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, TestCertificateSubject);
|
||||
Manager.RemoveAllCertificates(StoreName.My, StoreLocation.CurrentUser);
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
Manager.RemoveAllCertificates(CertificatePurpose.HTTPS, StoreName.Root, StoreLocation.CurrentUser, TestCertificateSubject);
|
||||
Manager.RemoveAllCertificates(StoreName.Root, StoreLocation.CurrentUser);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GetDocumentInsider", "GetDo
|
|||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.ApiDescription.Client.Tests", "Extensions.ApiDescription.Client\test\Microsoft.Extensions.ApiDescription.Client.Tests.csproj", "{2C62584B-EC31-40C8-819B-E46334645AE5}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.DeveloperCertificates.XPlat.Tests", "FirstRunCertGenerator\test\Microsoft.AspNetCore.DeveloperCertificates.XPlat.Tests.csproj", "{88712247-88C1-442B-874D-69D4B302EEBF}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.DeveloperCertificates.XPlat", "FirstRunCertGenerator\src\Microsoft.AspNetCore.DeveloperCertificates.XPlat.csproj", "{28E3181D-FAAA-483C-A924-3AF8D3F274A3}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
|
@ -93,6 +97,14 @@ Global
|
|||
{2C62584B-EC31-40C8-819B-E46334645AE5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{2C62584B-EC31-40C8-819B-E46334645AE5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{2C62584B-EC31-40C8-819B-E46334645AE5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{88712247-88C1-442B-874D-69D4B302EEBF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{88712247-88C1-442B-874D-69D4B302EEBF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{88712247-88C1-442B-874D-69D4B302EEBF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{88712247-88C1-442B-874D-69D4B302EEBF}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{28E3181D-FAAA-483C-A924-3AF8D3F274A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{28E3181D-FAAA-483C-A924-3AF8D3F274A3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{28E3181D-FAAA-483C-A924-3AF8D3F274A3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{28E3181D-FAAA-483C-A924-3AF8D3F274A3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
@ -112,6 +124,8 @@ Global
|
|||
{233119FC-E4C1-421C-89AE-1A445C5A947F} = {003EA860-5DFC-40AE-87C0-9D21BB2C68D7}
|
||||
{EB63AECB-B898-475D-90F7-FE61F9C1CCC6} = {003EA860-5DFC-40AE-87C0-9D21BB2C68D7}
|
||||
{2C62584B-EC31-40C8-819B-E46334645AE5} = {2C485EAF-E4DE-4D14-8AE1-330641E17D44}
|
||||
{88712247-88C1-442B-874D-69D4B302EEBF} = {2C485EAF-E4DE-4D14-8AE1-330641E17D44}
|
||||
{28E3181D-FAAA-483C-A924-3AF8D3F274A3} = {E01EE27B-6CF9-4707-9849-5BA2ABA825F2}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {EC668D8E-97B9-4758-9E5C-2E5DD6B9137B}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
|
|
@ -24,12 +25,22 @@ namespace Microsoft.AspNetCore.DeveloperCertificates.Tools
|
|||
private const int ErrorNoValidCertificateFound = 6;
|
||||
private const int ErrorCertificateNotTrusted = 7;
|
||||
private const int ErrorCleaningUpCertificates = 8;
|
||||
private const int ErrorMacOsCertificateKeyCouldNotBeAccessible = 9;
|
||||
private const int InvalidCertificateState = 9;
|
||||
|
||||
public static readonly TimeSpan HttpsCertificateValidity = TimeSpan.FromDays(365);
|
||||
|
||||
public static int Main(string[] args)
|
||||
{
|
||||
if (args.Contains("--debug"))
|
||||
{
|
||||
// This is so that we can attach `dotnet trace` for debug purposes.
|
||||
Console.WriteLine("Press any key to continue...");
|
||||
_ = Console.ReadKey();
|
||||
var newArgs = new List<string>(args);
|
||||
newArgs.Remove("--debug");
|
||||
args = newArgs.ToArray();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var app = new CommandLineApplication
|
||||
|
|
@ -120,7 +131,7 @@ namespace Microsoft.AspNetCore.DeveloperCertificates.Tools
|
|||
|
||||
private static int CleanHttpsCertificates(IReporter reporter)
|
||||
{
|
||||
var manager = new CertificateManager();
|
||||
var manager = CertificateManager.Instance;
|
||||
try
|
||||
{
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
|
|
@ -138,7 +149,7 @@ namespace Microsoft.AspNetCore.DeveloperCertificates.Tools
|
|||
reporter.Output("HTTPS development certificates successfully removed from the machine.");
|
||||
return Success;
|
||||
}
|
||||
catch(Exception e)
|
||||
catch (Exception e)
|
||||
{
|
||||
reporter.Error("There was an error trying to clean HTTPS development certificates on this machine.");
|
||||
reporter.Error(e.Message);
|
||||
|
|
@ -150,8 +161,8 @@ namespace Microsoft.AspNetCore.DeveloperCertificates.Tools
|
|||
private static int CheckHttpsCertificate(CommandOption trust, IReporter reporter)
|
||||
{
|
||||
var now = DateTimeOffset.Now;
|
||||
var certificateManager = new CertificateManager();
|
||||
var certificates = CertificateManager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: true);
|
||||
var certificateManager = CertificateManager.Instance;
|
||||
var certificates = certificateManager.ListCertificates(StoreName.My, StoreLocation.CurrentUser, isValid: true);
|
||||
if (certificates.Count == 0)
|
||||
{
|
||||
reporter.Output("No valid certificate found.");
|
||||
|
|
@ -159,21 +170,25 @@ namespace Microsoft.AspNetCore.DeveloperCertificates.Tools
|
|||
}
|
||||
else
|
||||
{
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && certificateManager.HasValidCertificateWithInnaccessibleKeyAcrossPartitions())
|
||||
foreach (var certificate in certificates)
|
||||
{
|
||||
reporter.Warn($"A valid HTTPS certificate was found but it may not be accessible across security partitions. Run dotnet dev-certs https to ensure it will be accessible during development.");
|
||||
return ErrorMacOsCertificateKeyCouldNotBeAccessible;
|
||||
}
|
||||
else
|
||||
{
|
||||
reporter.Verbose("A valid certificate was found.");
|
||||
// We never want check to require interaction.
|
||||
// When IDEs run dotnet dev-certs https after calling --check, we will try to access the key and
|
||||
// that will trigger a prompt if necessary.
|
||||
var status = certificateManager.CheckCertificateState(certificate, interactive: false);
|
||||
if (!status.Result)
|
||||
{
|
||||
reporter.Warn(status.Message);
|
||||
return InvalidCertificateState;
|
||||
}
|
||||
}
|
||||
reporter.Verbose("A valid certificate was found.");
|
||||
}
|
||||
|
||||
if (trust != null && trust.HasValue())
|
||||
{
|
||||
var store = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? StoreName.My : StoreName.Root;
|
||||
var trustedCertificates = CertificateManager.ListCertificates(CertificatePurpose.HTTPS, store, StoreLocation.CurrentUser, isValid: true);
|
||||
var trustedCertificates = certificateManager.ListCertificates(store, StoreLocation.CurrentUser, isValid: true);
|
||||
if (!certificates.Any(c => certificateManager.IsTrusted(c)))
|
||||
{
|
||||
reporter.Output($@"The following certificates were found, but none of them is trusted:
|
||||
|
|
@ -192,20 +207,24 @@ namespace Microsoft.AspNetCore.DeveloperCertificates.Tools
|
|||
private static int EnsureHttpsCertificate(CommandOption exportPath, CommandOption password, CommandOption trust, IReporter reporter)
|
||||
{
|
||||
var now = DateTimeOffset.Now;
|
||||
var manager = new CertificateManager();
|
||||
var manager = CertificateManager.Instance;
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && manager.HasValidCertificateWithInnaccessibleKeyAcrossPartitions() || manager.GetHttpsCertificates().Count == 0)
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
{
|
||||
reporter.Warn($"A valid HTTPS certificate with a key accessible across security partitions was not found. The following command will run to fix it:" + Environment.NewLine +
|
||||
"'sudo security set-key-partition-list -D localhost -S unsigned:,teamid:UBF8T346G9'" + Environment.NewLine +
|
||||
"This command will make the certificate key accessible across security partitions and might prompt you for your password. For more information see: https://aka.ms/aspnetcore/2.1/troubleshootcertissues");
|
||||
}
|
||||
var certificates = manager.ListCertificates(StoreName.My, StoreLocation.CurrentUser, isValid: true, exportPath.HasValue());
|
||||
foreach (var certificate in certificates)
|
||||
{
|
||||
var status = manager.CheckCertificateState(certificate, interactive: true);
|
||||
if (!status.Result)
|
||||
{
|
||||
reporter.Warn("One or more certificates might be in an invalid state. We will try to access the certificate key " +
|
||||
"for each certificate and as a result you might be prompted one or more times to enter " +
|
||||
"your password to access the user keychain. " +
|
||||
"When that happens, select 'Always Allow' to grant 'dotnet' access to the certificate key in the future.");
|
||||
}
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && manager.HasValidCertificateWithInnaccessibleKeyAcrossPartitions() || manager.GetHttpsCertificates().Count == 0)
|
||||
{
|
||||
reporter.Warn($"A valid HTTPS certificate with a key accessible across security partitions was not found. The following command will run to fix it:" + Environment.NewLine +
|
||||
"'sudo security set-key-partition-list -D localhost -S unsigned:,teamid:UBF8T346G9'" + Environment.NewLine +
|
||||
"This command will make the certificate key accessible across security partitions and might prompt you for your password. For more information see: https://aka.ms/aspnetcore/3.1/troubleshootcertissues");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && trust?.HasValue() == true)
|
||||
|
|
@ -231,9 +250,7 @@ namespace Microsoft.AspNetCore.DeveloperCertificates.Tools
|
|||
password.HasValue(),
|
||||
password.Value());
|
||||
|
||||
reporter.Verbose(string.Join(Environment.NewLine, result.Diagnostics.Messages));
|
||||
|
||||
switch (result.ResultCode)
|
||||
switch (result)
|
||||
{
|
||||
case EnsureCertificateResult.Succeeded:
|
||||
reporter.Output("The HTTPS developer certificate was generated successfully.");
|
||||
|
|
|
|||
Loading…
Reference in New Issue