[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()
|
internal void EnsureDevelopmentCertificates()
|
||||||
{
|
{
|
||||||
var now = DateTimeOffset.Now;
|
var now = DateTimeOffset.Now;
|
||||||
var manager = new CertificateManager();
|
var manager = CertificateManager.Instance;
|
||||||
var certificate = manager.CreateAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), "CN=localhost");
|
var certificate = manager.CreateAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1));
|
||||||
manager.ExportCertificate(certificate, path: _certificatePath, includePrivateKey: true, _certificatePassword);
|
manager.ExportCertificate(certificate, path: _certificatePath, includePrivateKey: true, _certificatePassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
|
||||||
new EventId(3, "FailedToLoadDevelopmentCertificate"),
|
new EventId(3, "FailedToLoadDevelopmentCertificate"),
|
||||||
"Failed to load the development https certificate at '{certificatePath}'.");
|
"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 LocatedDevelopmentCertificate(this ILogger logger, X509Certificate2 certificate) => _locatedDevelopmentCertificate(logger, certificate.Subject, certificate.Thumbprint, null);
|
||||||
|
|
||||||
public static void UnableToLocateDevelopmentCertificate(this ILogger logger) => _unableToLocateDevelopmentCertificate(logger, 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 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 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>>();
|
var logger = ApplicationServices.GetRequiredService<ILogger<KestrelServer>>();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
DefaultCertificate = CertificateManager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: true)
|
DefaultCertificate = CertificateManager.Instance.ListCertificates(StoreName.My, StoreLocation.CurrentUser, isValid: true)
|
||||||
.FirstOrDefault();
|
.FirstOrDefault();
|
||||||
|
|
||||||
if (DefaultCertificate != null)
|
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);
|
logger.LocatedDevelopmentCertificate(DefaultCertificate);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
|
||||||
|
|
@ -220,16 +220,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
|
||||||
}
|
}
|
||||||
catch (AuthenticationException ex)
|
catch (AuthenticationException ex)
|
||||||
{
|
{
|
||||||
if (_serverCertificate == null ||
|
_logger.LogDebug(1, ex, CoreStrings.AuthenticationFailed);
|
||||||
!CertificateManager.IsHttpsDevelopmentCertificate(_serverCertificate) ||
|
|
||||||
CertificateManager.CheckDeveloperCertificateKey(_serverCertificate))
|
|
||||||
{
|
|
||||||
_logger.LogDebug(1, ex, CoreStrings.AuthenticationFailed);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_logger.LogError(3, ex, CoreStrings.BadDeveloperCertificateState);
|
|
||||||
}
|
|
||||||
|
|
||||||
await sslStream.DisposeAsync();
|
await sslStream.DisposeAsync();
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -385,35 +385,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
||||||
Assert.Equal(LogLevel.Debug, loggerProvider.FilterLogger.LastLogLevel);
|
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]
|
[Fact]
|
||||||
public async Task OnAuthenticate_SeesOtherSettings()
|
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()
|
public static void GenerateAspNetHttpsCertificate()
|
||||||
{
|
{
|
||||||
var manager = new CertificateManager();
|
var manager = CertificateManager.Instance;
|
||||||
var now = DateTimeOffset.Now;
|
var now = DateTimeOffset.Now;
|
||||||
manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), isInteractive: false);
|
manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), isInteractive: false);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests
|
||||||
{
|
{
|
||||||
public class CertificateManagerTests : IClassFixture<CertFixture>
|
public class CertificateManagerTests : IClassFixture<CertFixture>
|
||||||
{
|
{
|
||||||
private CertFixture _fixture;
|
private readonly CertFixture _fixture;
|
||||||
private CertificateManager _manager => _fixture.Manager;
|
private CertificateManager _manager => _fixture.Manager;
|
||||||
|
|
||||||
public CertificateManagerTests(ITestOutputHelper output, CertFixture fixture)
|
public CertificateManagerTests(ITestOutputHelper output, CertFixture fixture)
|
||||||
|
|
@ -39,20 +39,20 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests
|
||||||
|
|
||||||
const string CertificateName = nameof(EnsureCreateHttpsCertificate_CreatesACertificate_WhenThereAreNoHttpsCertificates) + ".cer";
|
const string CertificateName = nameof(EnsureCreateHttpsCertificate_CreatesACertificate_WhenThereAreNoHttpsCertificates) + ".cer";
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
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);
|
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
|
||||||
Assert.Equal(EnsureCertificateResult.Succeeded, result.ResultCode);
|
Assert.Equal(EnsureCertificateResult.Succeeded, result);
|
||||||
Assert.True(File.Exists(CertificateName));
|
Assert.True(File.Exists(CertificateName));
|
||||||
|
|
||||||
var exportedCertificate = new X509Certificate2(File.ReadAllBytes(CertificateName));
|
var exportedCertificate = new X509Certificate2(File.ReadAllBytes(CertificateName));
|
||||||
Assert.NotNull(exportedCertificate);
|
Assert.NotNull(exportedCertificate);
|
||||||
Assert.False(exportedCertificate.HasPrivateKey);
|
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);
|
var httpsCertificate = Assert.Single(httpsCertificates, c => c.Subject == TestCertificateSubject);
|
||||||
Assert.True(httpsCertificate.HasPrivateKey);
|
Assert.True(httpsCertificate.HasPrivateKey);
|
||||||
Assert.Equal(TestCertificateSubject, httpsCertificate.Subject);
|
Assert.Equal(TestCertificateSubject, httpsCertificate.Subject);
|
||||||
|
|
@ -94,7 +94,7 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests
|
||||||
httpsCertificate.Extensions.OfType<X509Extension>(),
|
httpsCertificate.Extensions.OfType<X509Extension>(),
|
||||||
e => e.Critical == false &&
|
e => e.Critical == false &&
|
||||||
e.Oid.Value == "1.3.6.1.4.1.311.84.1.1" &&
|
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());
|
Assert.Equal(httpsCertificate.GetCertHashString(), exportedCertificate.GetCertHashString());
|
||||||
|
|
||||||
|
|
@ -102,12 +102,12 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Output.WriteLine(e.Message);
|
Output.WriteLine(e.Message);
|
||||||
ListCertificates(Output);
|
ListCertificates();
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ListCertificates(ITestOutputHelper output)
|
private void ListCertificates()
|
||||||
{
|
{
|
||||||
using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser))
|
using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser))
|
||||||
{
|
{
|
||||||
|
|
@ -133,17 +133,18 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests
|
||||||
|
|
||||||
_fixture.CleanupCertificates();
|
_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);
|
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());
|
||||||
var httpsCertificate = CertificateManager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: false).Single(c => c.Subject == TestCertificateSubject);
|
ListCertificates();
|
||||||
|
|
||||||
|
var httpsCertificate = _manager.ListCertificates(StoreName.My, StoreLocation.CurrentUser, isValid: false).Single(c => c.Subject == TestCertificateSubject);
|
||||||
// Act
|
// 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
|
||||||
Assert.Equal(EnsureCertificateResult.ValidCertificatePresent, result.ResultCode);
|
Assert.Equal(EnsureCertificateResult.ValidCertificatePresent, result);
|
||||||
Assert.True(File.Exists(CertificateName));
|
Assert.True(File.Exists(CertificateName));
|
||||||
|
|
||||||
var exportedCertificate = new X509Certificate2(File.ReadAllBytes(CertificateName), certificatePassword);
|
var exportedCertificate = new X509Certificate2(File.ReadAllBytes(CertificateName), certificatePassword);
|
||||||
|
|
@ -159,13 +160,15 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests
|
||||||
{
|
{
|
||||||
_fixture.CleanupCertificates();
|
_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);
|
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);
|
Assert.Empty(httpsCertificateList);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -174,14 +177,16 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests
|
||||||
{
|
{
|
||||||
_fixture.CleanupCertificates();
|
_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);
|
now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset);
|
||||||
CertificateManager.AspNetHttpsCertificateVersion = 0;
|
_manager.AspNetHttpsCertificateVersion = 0;
|
||||||
_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 = 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);
|
Assert.Empty(httpsCertificateList);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -191,12 +196,14 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests
|
||||||
{
|
{
|
||||||
_fixture.CleanupCertificates();
|
_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);
|
now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset);
|
||||||
CertificateManager.AspNetHttpsCertificateVersion = 0;
|
_manager.AspNetHttpsCertificateVersion = 0;
|
||||||
_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();
|
||||||
|
|
||||||
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);
|
Assert.NotEmpty(httpsCertificateList);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -206,45 +213,17 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests
|
||||||
{
|
{
|
||||||
_fixture.CleanupCertificates();
|
_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);
|
now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset);
|
||||||
CertificateManager.AspNetHttpsCertificateVersion = 2;
|
_manager.AspNetHttpsCertificateVersion = 2;
|
||||||
_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 = 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.NotEmpty(httpsCertificateList);
|
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
|
public class CertFixture : IDisposable
|
||||||
|
|
@ -253,24 +232,25 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests
|
||||||
|
|
||||||
public CertFixture()
|
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();
|
CleanupCertificates();
|
||||||
}
|
}
|
||||||
|
|
||||||
internal CertificateManager Manager { get; set; }
|
internal CertificateManager Manager { get; set; }
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose() => CleanupCertificates();
|
||||||
{
|
|
||||||
CleanupCertificates();
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void CleanupCertificates()
|
internal void CleanupCertificates()
|
||||||
{
|
{
|
||||||
Manager.RemoveAllCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, TestCertificateSubject);
|
Manager.RemoveAllCertificates(StoreName.My, StoreLocation.CurrentUser);
|
||||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
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
|
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}"
|
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
|
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
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
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}.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.ActiveCfg = Release|Any CPU
|
||||||
{2C62584B-EC31-40C8-819B-E46334645AE5}.Release|Any CPU.Build.0 = 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
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|
@ -112,6 +124,8 @@ Global
|
||||||
{233119FC-E4C1-421C-89AE-1A445C5A947F} = {003EA860-5DFC-40AE-87C0-9D21BB2C68D7}
|
{233119FC-E4C1-421C-89AE-1A445C5A947F} = {003EA860-5DFC-40AE-87C0-9D21BB2C68D7}
|
||||||
{EB63AECB-B898-475D-90F7-FE61F9C1CCC6} = {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}
|
{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
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {EC668D8E-97B9-4758-9E5C-2E5DD6B9137B}
|
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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
@ -24,12 +25,22 @@ namespace Microsoft.AspNetCore.DeveloperCertificates.Tools
|
||||||
private const int ErrorNoValidCertificateFound = 6;
|
private const int ErrorNoValidCertificateFound = 6;
|
||||||
private const int ErrorCertificateNotTrusted = 7;
|
private const int ErrorCertificateNotTrusted = 7;
|
||||||
private const int ErrorCleaningUpCertificates = 8;
|
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 readonly TimeSpan HttpsCertificateValidity = TimeSpan.FromDays(365);
|
||||||
|
|
||||||
public static int Main(string[] args)
|
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
|
try
|
||||||
{
|
{
|
||||||
var app = new CommandLineApplication
|
var app = new CommandLineApplication
|
||||||
|
|
@ -120,7 +131,7 @@ namespace Microsoft.AspNetCore.DeveloperCertificates.Tools
|
||||||
|
|
||||||
private static int CleanHttpsCertificates(IReporter reporter)
|
private static int CleanHttpsCertificates(IReporter reporter)
|
||||||
{
|
{
|
||||||
var manager = new CertificateManager();
|
var manager = CertificateManager.Instance;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
|
|
@ -138,7 +149,7 @@ namespace Microsoft.AspNetCore.DeveloperCertificates.Tools
|
||||||
reporter.Output("HTTPS development certificates successfully removed from the machine.");
|
reporter.Output("HTTPS development certificates successfully removed from the machine.");
|
||||||
return Success;
|
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("There was an error trying to clean HTTPS development certificates on this machine.");
|
||||||
reporter.Error(e.Message);
|
reporter.Error(e.Message);
|
||||||
|
|
@ -150,8 +161,8 @@ namespace Microsoft.AspNetCore.DeveloperCertificates.Tools
|
||||||
private static int CheckHttpsCertificate(CommandOption trust, IReporter reporter)
|
private static int CheckHttpsCertificate(CommandOption trust, IReporter reporter)
|
||||||
{
|
{
|
||||||
var now = DateTimeOffset.Now;
|
var now = DateTimeOffset.Now;
|
||||||
var certificateManager = new CertificateManager();
|
var certificateManager = CertificateManager.Instance;
|
||||||
var certificates = CertificateManager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: true);
|
var certificates = certificateManager.ListCertificates(StoreName.My, StoreLocation.CurrentUser, isValid: true);
|
||||||
if (certificates.Count == 0)
|
if (certificates.Count == 0)
|
||||||
{
|
{
|
||||||
reporter.Output("No valid certificate found.");
|
reporter.Output("No valid certificate found.");
|
||||||
|
|
@ -159,21 +170,25 @@ namespace Microsoft.AspNetCore.DeveloperCertificates.Tools
|
||||||
}
|
}
|
||||||
else
|
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.");
|
// We never want check to require interaction.
|
||||||
return ErrorMacOsCertificateKeyCouldNotBeAccessible;
|
// 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.
|
||||||
else
|
var status = certificateManager.CheckCertificateState(certificate, interactive: false);
|
||||||
{
|
if (!status.Result)
|
||||||
reporter.Verbose("A valid certificate was found.");
|
{
|
||||||
|
reporter.Warn(status.Message);
|
||||||
|
return InvalidCertificateState;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
reporter.Verbose("A valid certificate was found.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (trust != null && trust.HasValue())
|
if (trust != null && trust.HasValue())
|
||||||
{
|
{
|
||||||
var store = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? StoreName.My : StoreName.Root;
|
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)))
|
if (!certificates.Any(c => certificateManager.IsTrusted(c)))
|
||||||
{
|
{
|
||||||
reporter.Output($@"The following certificates were found, but none of them is trusted:
|
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)
|
private static int EnsureHttpsCertificate(CommandOption exportPath, CommandOption password, CommandOption trust, IReporter reporter)
|
||||||
{
|
{
|
||||||
var now = DateTimeOffset.Now;
|
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 +
|
var certificates = manager.ListCertificates(StoreName.My, StoreLocation.CurrentUser, isValid: true, exportPath.HasValue());
|
||||||
"'sudo security set-key-partition-list -D localhost -S unsigned:,teamid:UBF8T346G9'" + Environment.NewLine +
|
foreach (var certificate in certificates)
|
||||||
"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 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)
|
break;
|
||||||
{
|
}
|
||||||
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");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && trust?.HasValue() == true)
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && trust?.HasValue() == true)
|
||||||
|
|
@ -231,9 +250,7 @@ namespace Microsoft.AspNetCore.DeveloperCertificates.Tools
|
||||||
password.HasValue(),
|
password.HasValue(),
|
||||||
password.Value());
|
password.Value());
|
||||||
|
|
||||||
reporter.Verbose(string.Join(Environment.NewLine, result.Diagnostics.Messages));
|
switch (result)
|
||||||
|
|
||||||
switch (result.ResultCode)
|
|
||||||
{
|
{
|
||||||
case EnsureCertificateResult.Succeeded:
|
case EnsureCertificateResult.Succeeded:
|
||||||
reporter.Output("The HTTPS developer certificate was generated successfully.");
|
reporter.Output("The HTTPS developer certificate was generated successfully.");
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue