[HTTPS] Support exporting the dev-cert in PEM format and support importing an existing dev-cert in PFX (#23567)

* Support exporting the certificate key into PEM format
* Support importing an existing https dev certificate into the certificate store
This commit is contained in:
Javier Calvarro Nelson 2020-07-07 17:26:08 +02:00 committed by GitHub
parent c1866c2b61
commit 156023d3f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 451 additions and 25 deletions

View File

@ -35,7 +35,7 @@ namespace Templates.Test.Helpers
var manager = CertificateManager.Instance;
var certificate = manager.CreateAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1));
var certificateThumbprint = certificate.Thumbprint;
manager.ExportCertificate(certificate, path: certificatePath, includePrivateKey: true, certificatePassword);
manager.ExportCertificate(certificate, path: certificatePath, includePrivateKey: true, certificatePassword, CertificateKeyExportFormat.Pfx);
return certificateThumbprint;
}

View File

@ -0,0 +1,11 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Certificates.Generation
{
internal enum CertificateKeyExportFormat
{
Pfx,
Pem,
}
}

View File

@ -153,6 +153,7 @@ namespace Microsoft.AspNetCore.Certificates.Generation
bool trust = false,
bool includePrivateKey = false,
string password = null,
CertificateKeyExportFormat keyExportFormat = CertificateKeyExportFormat.Pfx,
bool isInteractive = true)
{
var result = EnsureCertificateResult.Succeeded;
@ -170,6 +171,7 @@ namespace Microsoft.AspNetCore.Certificates.Generation
certificates = filteredCertificates;
X509Certificate2 certificate = null;
var isNewCertificate = false;
if (certificates.Any())
{
certificate = certificates.First();
@ -216,6 +218,7 @@ namespace Microsoft.AspNetCore.Certificates.Generation
try
{
Log.CreateDevelopmentCertificateStart();
isNewCertificate = true;
certificate = CreateAspNetCoreHttpsDevelopmentCertificate(notBefore, notAfter);
}
catch (Exception e)
@ -260,13 +263,13 @@ namespace Microsoft.AspNetCore.Certificates.Generation
{
try
{
ExportCertificate(certificate, path, includePrivateKey, password);
ExportCertificate(certificate, path, includePrivateKey, password, keyExportFormat);
}
catch (Exception e)
{
Log.ExportCertificateError(e.ToString());
// We don't want to mask the original source of the error here.
result = result != EnsureCertificateResult.Succeeded || result != EnsureCertificateResult.ValidCertificatePresent ?
result = result != EnsureCertificateResult.Succeeded && result != EnsureCertificateResult.ValidCertificatePresent ?
result :
EnsureCertificateResult.ErrorExportingTheCertificate;
@ -292,9 +295,58 @@ namespace Microsoft.AspNetCore.Certificates.Generation
}
}
DisposeCertificates(!isNewCertificate ? certificates : certificates.Append(certificate));
return result;
}
internal ImportCertificateResult ImportCertificate(string certificatePath, string password)
{
if (!File.Exists(certificatePath))
{
Log.ImportCertificateMissingFile(certificatePath);
return ImportCertificateResult.CertificateFileMissing;
}
var certificates = ListCertificates(StoreName.My, StoreLocation.CurrentUser, isValid: false, requireExportable: false);
if (certificates.Any())
{
Log.ImportCertificateExistingCertificates(ToCertificateDescription(certificates));
return ImportCertificateResult.ExistingCertificatesPresent;
}
X509Certificate2 certificate;
try
{
Log.LoadCertificateStart(certificatePath);
certificate = new X509Certificate2(certificatePath, password, X509KeyStorageFlags.Exportable | X509KeyStorageFlags.EphemeralKeySet);
Log.LoadCertificateEnd(GetDescription(certificate));
}
catch (Exception e)
{
Log.LoadCertificateError(e.ToString());
return ImportCertificateResult.InvalidCertificate;
}
if (!IsHttpsDevelopmentCertificate(certificate))
{
Log.NoHttpsDevelopmentCertificate(GetDescription(certificate));
return ImportCertificateResult.NoDevelopmentHttpsCertificate;
}
try
{
SaveCertificate(certificate);
}
catch (Exception e)
{
Log.SaveCertificateInStoreError(e.ToString());
return ImportCertificateResult.ErrorSavingTheCertificateIntoTheCurrentUserPersonalStore;
}
return ImportCertificateResult.Succeeded;
}
public void CleanupHttpsCertificates()
{
// On OS X we don't have a good way to manage trusted certificates in the system keychain
@ -329,7 +381,7 @@ namespace Microsoft.AspNetCore.Certificates.Generation
protected abstract IList<X509Certificate2> GetCertificatesToRemove(StoreName storeName, StoreLocation storeLocation);
internal void ExportCertificate(X509Certificate2 certificate, string path, bool includePrivateKey, string password)
internal void ExportCertificate(X509Certificate2 certificate, string path, bool includePrivateKey, string password, CertificateKeyExportFormat format)
{
Log.ExportCertificateStart(GetDescription(certificate), path, includePrivateKey);
if (includePrivateKey && password == null)
@ -345,15 +397,69 @@ namespace Microsoft.AspNetCore.Certificates.Generation
}
byte[] bytes;
byte[] keyBytes;
byte[] pemEnvelope = null;
RSA key = null;
try
{
bytes = includePrivateKey ? certificate.Export(X509ContentType.Pkcs12, password) : certificate.Export(X509ContentType.Cert);
if (includePrivateKey)
{
switch (format)
{
case CertificateKeyExportFormat.Pfx:
bytes = certificate.Export(X509ContentType.Pkcs12, password);
break;
case CertificateKeyExportFormat.Pem:
key = certificate.GetRSAPrivateKey();
char[] pem;
if (password != null)
{
keyBytes = key.ExportEncryptedPkcs8PrivateKey(password, new PbeParameters(PbeEncryptionAlgorithm.Aes256Cbc, HashAlgorithmName.SHA256, 100000));
pem = PemEncoding.Write("ENCRYPTED PRIVATE KEY", keyBytes);
pemEnvelope = Encoding.ASCII.GetBytes(pem);
}
else
{
// Export the key first to an encrypted PEM to avoid issues with System.Security.Cryptography.Cng indicating that the operation is not supported.
// This is likely by design to avoid exporting the key by mistake.
// To bypass it, we export the certificate to pem temporarily and then we import it and export it as unprotected PEM.
keyBytes = key.ExportEncryptedPkcs8PrivateKey("", new PbeParameters(PbeEncryptionAlgorithm.Aes256Cbc, HashAlgorithmName.SHA256, 1));
pem = PemEncoding.Write("ENCRYPTED PRIVATE KEY", keyBytes);
key.Dispose();
key = RSA.Create();
key.ImportFromEncryptedPem(pem, "");
Array.Clear(keyBytes, 0, keyBytes.Length);
Array.Clear(pem, 0, pem.Length);
keyBytes = key.ExportPkcs8PrivateKey();
pem = PemEncoding.Write("PRIVATE KEY", keyBytes);
pemEnvelope = Encoding.ASCII.GetBytes(pem);
}
Array.Clear(keyBytes, 0, keyBytes.Length);
Array.Clear(pem, 0, pem.Length);
bytes = certificate.Export(X509ContentType.Cert);
break;
default:
throw new InvalidOperationException("Unknown format.");
}
}
else
{
bytes = certificate.Export(X509ContentType.Cert);
}
}
catch (Exception e)
{
Log.ExportCertificateError(e.ToString());
throw;
}
finally
{
key?.Dispose();
}
try
{
@ -369,6 +475,25 @@ namespace Microsoft.AspNetCore.Certificates.Generation
{
Array.Clear(bytes, 0, bytes.Length);
}
if (includePrivateKey && format == CertificateKeyExportFormat.Pem)
{
try
{
var keyPath = Path.ChangeExtension(path, ".key");
Log.WritePemKeyToDisk(keyPath);
File.WriteAllBytes(keyPath, pemEnvelope);
}
catch (Exception ex)
{
Log.WritePemKeyToDiskError(ex.ToString());
throw;
}
finally
{
Array.Clear(pemEnvelope, 0, pemEnvelope.Length);
}
}
}
internal X509Certificate2 CreateAspNetCoreHttpsDevelopmentCertificate(DateTimeOffset notBefore, DateTimeOffset notAfter)
@ -496,7 +621,7 @@ namespace Microsoft.AspNetCore.Certificates.Generation
DateTimeOffset notBefore,
DateTimeOffset notAfter)
{
var key = CreateKeyMaterial(RSAMinimumKeySizeInBits);
using var key = CreateKeyMaterial(RSAMinimumKeySizeInBits);
var request = new CertificateRequest(subject, key, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
foreach (var extension in extensions)
@ -745,6 +870,31 @@ namespace Microsoft.AspNetCore.Certificates.Generation
[Event(56, Level = EventLevel.Error)]
internal void MacOSAddCertificateToKeyChainError(int exitCode) => WriteEvent(56, $"An error has ocurred while importing the certificate to the keychain: {exitCode}.");
[Event(57, Level = EventLevel.Verbose)]
public void WritePemKeyToDisk(string path) => WriteEvent(57, $"Writing the certificate to: {path}.");
[Event(58, Level = EventLevel.Error)]
public void WritePemKeyToDiskError(string ex) => WriteEvent(58, $"An error has ocurred while writing the certificate to disk: {ex}.");
[Event(59, Level = EventLevel.Error)]
internal void ImportCertificateMissingFile(string certificatePath) => WriteEvent(59, $"The file '{certificatePath}' does not exist.");
[Event(60, Level = EventLevel.Error)]
internal void ImportCertificateExistingCertificates(string certificateDescription) => WriteEvent(60, $"One or more HTTPS certificates exist '{certificateDescription}'.");
[Event(61, Level = EventLevel.Verbose)]
internal void LoadCertificateStart(string certificatePath) => WriteEvent(61, $"Loading certificate from path '{certificatePath}'.");
[Event(62, Level = EventLevel.Verbose)]
internal void LoadCertificateEnd(string description) => WriteEvent(62, $"The certificate '{description}' has been loaded successfully.");
[Event(63, Level = EventLevel.Error)]
internal void LoadCertificateError(string ex) => WriteEvent(63, $"An error has ocurred while loading the certificate from disk: {ex}.");
[Event(64, Level = EventLevel.Error)]
internal void NoHttpsDevelopmentCertificate(string description) => WriteEvent(64, $"The provided certificate '{description}' is not a valid ASP.NET Core HTTPS development certificate.");
}
internal class UserCancelledTrustException : Exception

View File

@ -0,0 +1,16 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Certificates.Generation
{
internal enum ImportCertificateResult
{
Succeeded = 1,
CertificateFileMissing,
InvalidCertificate,
NoDevelopmentHttpsCertificate,
ExistingCertificatesPresent,
ErrorSavingTheCertificateIntoTheCurrentUserPersonalStore,
}
}

View File

@ -54,7 +54,7 @@ namespace Microsoft.AspNetCore.Certificates.Generation
var tmpFile = Path.GetTempFileName();
try
{
ExportCertificate(publicCertificate, tmpFile, includePrivateKey: false, password: null);
ExportCertificate(publicCertificate, tmpFile, includePrivateKey: false, password: null, CertificateKeyExportFormat.Pfx);
Log.MacOSTrustCommandStart($"{MacOSTrustCertificateCommandLine} {MacOSTrustCertificateCommandLineArguments}{tmpFile}");
using (var process = Process.Start(MacOSTrustCertificateCommandLine, MacOSTrustCertificateCommandLineArguments + tmpFile))
{
@ -94,7 +94,7 @@ namespace Microsoft.AspNetCore.Certificates.Generation
// Tries to use the certificate key to validate it can't access it
try
{
var rsa = candidate.GetRSAPrivateKey();
using var rsa = candidate.GetRSAPrivateKey();
if (rsa == null)
{
return new CheckCertificateStateResult(false, InvalidCertificateState);

View File

@ -20,6 +20,7 @@ namespace Microsoft.AspNetCore.Certificates.Generation
protected override X509Certificate2 SaveCertificateCore(X509Certificate2 certificate)
{
var export = certificate.Export(X509ContentType.Pkcs12, "");
certificate.Dispose();
certificate = new X509Certificate2(export, "", X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable);
Array.Clear(export, 0, export.Length);

View File

@ -26,9 +26,10 @@ namespace Microsoft.AspNetCore.Certificates.Generation
// 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 &&
using var key = c.GetRSAPrivateKey();
return (key is RSACryptoServiceProvider rsaPrivateKey &&
rsaPrivateKey.CspKeyContainerInfo.Exportable) ||
(c.GetRSAPrivateKey() is RSACng cngPrivateKey &&
(key is RSACng cngPrivateKey &&
cngPrivateKey.Key.ExportPolicy == CngExportPolicies.AllowExport);
#endif
}
@ -49,6 +50,7 @@ namespace Microsoft.AspNetCore.Certificates.Generation
// 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.Dispose();
certificate = new X509Certificate2(export, "", X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable);
Array.Clear(export, 0, export.Length);
certificate.FriendlyName = AspNetHttpsOidFriendlyName;
@ -65,7 +67,7 @@ namespace Microsoft.AspNetCore.Certificates.Generation
protected override void TrustCertificateCore(X509Certificate2 certificate)
{
var publicCertificate = new X509Certificate2(certificate.Export(X509ContentType.Cert));
using var publicCertificate = new X509Certificate2(certificate.Export(X509ContentType.Cert));
publicCertificate.FriendlyName = certificate.FriendlyName;

View File

@ -7,6 +7,7 @@ using System.Linq;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using Microsoft.AspNetCore.Testing;
using Xunit;
using Xunit.Abstractions;
@ -155,6 +156,138 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests
Assert.Equal(httpsCertificate.GetCertHashString(), exportedCertificate.GetCertHashString());
}
[ConditionalFact]
[SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6720", Queues = "OSX.1014.Amd64;OSX.1014.Amd64.Open")]
public void EnsureCreateHttpsCertificate_CanExportTheCertInPemFormat()
{
// Arrange
var message = "plaintext";
const string CertificateName = nameof(EnsureCreateHttpsCertificate_DoesNotCreateACertificate_WhenThereIsAnExistingHttpsCertificates) + ".pfx";
var certificatePassword = Guid.NewGuid().ToString();
_fixture.CleanupCertificates();
var now = DateTimeOffset.UtcNow;
now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset);
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, keyExportFormat: CertificateKeyExportFormat.Pem, isInteractive: false);
// Assert
Assert.Equal(EnsureCertificateResult.ValidCertificatePresent, result);
Assert.True(File.Exists(CertificateName));
var key = RSA.Create();
key.ImportFromEncryptedPem(File.ReadAllText(Path.ChangeExtension(CertificateName, "key")), certificatePassword);
var exportedCertificate = new X509Certificate2(File.ReadAllBytes(CertificateName));
exportedCertificate = exportedCertificate.CopyWithPrivateKey(key);
Assert.NotNull(exportedCertificate);
Assert.True(exportedCertificate.HasPrivateKey);
Assert.Equal("plaintext", Encoding.ASCII.GetString(exportedCertificate.GetRSAPrivateKey().Decrypt(exportedCertificate.GetRSAPrivateKey().Encrypt(Encoding.ASCII.GetBytes(message), RSAEncryptionPadding.OaepSHA256), RSAEncryptionPadding.OaepSHA256)));
Assert.Equal(httpsCertificate.GetCertHashString(), exportedCertificate.GetCertHashString());
}
[ConditionalFact]
[SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6720", Queues = "OSX.1014.Amd64;OSX.1014.Amd64.Open")]
public void EnsureCreateHttpsCertificate_CanImport_ExportedPfx()
{
// Arrange
const string CertificateName = nameof(EnsureCreateHttpsCertificate_DoesNotCreateACertificate_WhenThereIsAnExistingHttpsCertificates) + ".pfx";
var certificatePassword = Guid.NewGuid().ToString();
_fixture.CleanupCertificates();
var now = DateTimeOffset.UtcNow;
now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset);
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);
_manager
.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), CertificateName, trust: false, includePrivateKey: true, password: certificatePassword, isInteractive: false);
_manager.CleanupHttpsCertificates();
// Act
var result = _manager.ImportCertificate(CertificateName, certificatePassword);
// Assert
Assert.Equal(ImportCertificateResult.Succeeded, result);
var importedCertificate = Assert.Single(_manager.ListCertificates(StoreName.My, StoreLocation.CurrentUser, isValid: false));
Assert.Equal(httpsCertificate.GetCertHashString(), importedCertificate.GetCertHashString());
}
[ConditionalFact]
[SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6720", Queues = "OSX.1014.Amd64;OSX.1014.Amd64.Open")]
public void EnsureCreateHttpsCertificate_CanImport_ExportedPfx_FailsIfThereAreCertificatesPresent()
{
// Arrange
const string CertificateName = nameof(EnsureCreateHttpsCertificate_DoesNotCreateACertificate_WhenThereIsAnExistingHttpsCertificates) + ".pfx";
var certificatePassword = Guid.NewGuid().ToString();
_fixture.CleanupCertificates();
var now = DateTimeOffset.UtcNow;
now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset);
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);
_manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), CertificateName, trust: false, includePrivateKey: true, password: certificatePassword, isInteractive: false);
// Act
var result = _manager.ImportCertificate(CertificateName, certificatePassword);
// Assert
Assert.Equal(ImportCertificateResult.ExistingCertificatesPresent, result);
}
[ConditionalFact]
[SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6720", Queues = "OSX.1014.Amd64;OSX.1014.Amd64.Open")]
public void EnsureCreateHttpsCertificate_CanExportTheCertInPemFormat_WithoutPassword()
{
// Arrange
var message = "plaintext";
const string CertificateName = nameof(EnsureCreateHttpsCertificate_DoesNotCreateACertificate_WhenThereIsAnExistingHttpsCertificates) + ".pfx";
_fixture.CleanupCertificates();
var now = DateTimeOffset.UtcNow;
now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset);
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: null, keyExportFormat: CertificateKeyExportFormat.Pem, isInteractive: false);
// Assert
Assert.Equal(EnsureCertificateResult.ValidCertificatePresent, result);
Assert.True(File.Exists(CertificateName));
var key = RSA.Create();
key.ImportFromPem(File.ReadAllText(Path.ChangeExtension(CertificateName, "key")));
var exportedCertificate = new X509Certificate2(File.ReadAllBytes(CertificateName));
exportedCertificate = exportedCertificate.CopyWithPrivateKey(key);
Assert.NotNull(exportedCertificate);
Assert.True(exportedCertificate.HasPrivateKey);
Assert.Equal("plaintext", Encoding.ASCII.GetString(exportedCertificate.GetRSAPrivateKey().Decrypt(exportedCertificate.GetRSAPrivateKey().Encrypt(Encoding.ASCII.GetBytes(message), RSAEncryptionPadding.OaepSHA256), RSAEncryptionPadding.OaepSHA256)));
Assert.Equal(httpsCertificate.GetCertHashString(), exportedCertificate.GetCertHashString());
}
[Fact]
public void EnsureCreateHttpsCertificate_ReturnsExpiredCertificateIfVersionIsIncorrect()
{

View File

@ -26,6 +26,21 @@ namespace Microsoft.AspNetCore.DeveloperCertificates.Tools
private const int ErrorCertificateNotTrusted = 7;
private const int ErrorCleaningUpCertificates = 8;
private const int InvalidCertificateState = 9;
private const int InvalidKeyExportFormat = 10;
private const int ErrorImportingCertificate = 11;
private const int MissingCertificateFile = 12;
private const int FailedToLoadCertificate = 13;
private const int NoDevelopmentHttpsCertificate = 14;
private const int ExistingCertificatesPresent = 15;
private const string InvalidUsageErrorMessage = @"Incompatible set of flags. Sample usages
'dotnet dev-certs https'
'dotnet dev-certs https --clean'
'dotnet dev-certs https --clean --import ./certificate.pfx -p password'
'dotnet dev-certs https --check --trust'
'dotnet dev-certs https -ep ./certificate.pfx -p password --trust'
'dotnet dev-certs https -ep ./certificate.crt --trust --key-format Pem'
'dotnet dev-certs https -ep ./certificate.crt -p password --trust --key-format Pem'";
public static readonly TimeSpan HttpsCertificateValidity = TimeSpan.FromDays(365);
@ -33,7 +48,6 @@ namespace Microsoft.AspNetCore.DeveloperCertificates.Tools
{
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);
@ -55,9 +69,14 @@ namespace Microsoft.AspNetCore.DeveloperCertificates.Tools
CommandOptionType.SingleValue);
var password = c.Option("-p|--password",
"Password to use when exporting the certificate with the private key into a pfx file",
"Password to use when exporting the certificate with the private key into a pfx file or to encrypt the Pem exported key",
CommandOptionType.SingleValue);
// We want to force generating a key without a password to not be an accident.
var noPassword = c.Option("-np|--no-password",
"Explicitly request that you don't use a password for the key when exporting a certificate to a PEM format",
CommandOptionType.NoValue);
var check = c.Option(
"-c|--check",
"Check for the existence of the certificate but do not perform any action",
@ -68,6 +87,16 @@ namespace Microsoft.AspNetCore.DeveloperCertificates.Tools
"Cleans all HTTPS development certificates from the machine.",
CommandOptionType.NoValue);
var import = c.Option(
"-i|--import",
"Imports the provided HTTPS development certificate into the machine. All other HTTPS developer certificates will be cleared out",
CommandOptionType.SingleValue);
var keyFormat = c.Option(
"--key-format",
"Export the certificate key in the given format. Valid values are Pfx and Pem. Pfx is the default.",
CommandOptionType.SingleValue);
CommandOption trust = null;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
@ -89,14 +118,46 @@ namespace Microsoft.AspNetCore.DeveloperCertificates.Tools
c.OnExecute(() =>
{
var reporter = new ConsoleReporter(PhysicalConsole.Singleton, verbose.HasValue(), quiet.HasValue());
if ((clean.HasValue() && (exportPath.HasValue() || password.HasValue() || trust?.HasValue() == true)) ||
(check.HasValue() && (exportPath.HasValue() || password.HasValue() || clean.HasValue())))
if (clean.HasValue())
{
reporter.Error(@"Incompatible set of flags. Sample usages
'dotnet dev-certs https'
'dotnet dev-certs https --clean'
'dotnet dev-certs https --check --trust'
'dotnet dev-certs https -ep ./certificate.pfx -p password --trust'");
if (exportPath.HasValue() || trust?.HasValue() == true || keyFormat.HasValue() || noPassword.HasValue() || check.HasValue() ||
(!import.HasValue() && password.HasValue()) ||
(import.HasValue() && !password.HasValue()))
{
reporter.Error(InvalidUsageErrorMessage);
return CriticalError;
}
}
if (check.HasValue())
{
if (exportPath.HasValue() || password.HasValue() || noPassword.HasValue() || clean.HasValue() || keyFormat.HasValue() || import.HasValue())
{
reporter.Error(InvalidUsageErrorMessage);
return CriticalError;
}
}
if (!clean.HasValue() && !check.HasValue())
{
if (password.HasValue() && noPassword.HasValue())
{
reporter.Error(InvalidUsageErrorMessage);
return CriticalError;
}
if (noPassword.HasValue() && !(keyFormat.HasValue() && string.Equals(keyFormat.Value(), "PEM", StringComparison.OrdinalIgnoreCase)))
{
reporter.Error(InvalidUsageErrorMessage);
return CriticalError;
}
if (import.HasValue())
{
reporter.Error(InvalidUsageErrorMessage);
return CriticalError;
}
}
if (check.HasValue())
@ -106,10 +167,16 @@ namespace Microsoft.AspNetCore.DeveloperCertificates.Tools
if (clean.HasValue())
{
return CleanHttpsCertificates(reporter);
var clean = CleanHttpsCertificates(reporter);
if (clean != Success || !import.HasValue())
{
return clean;
}
return ImportCertificate(import, password, reporter);
}
return EnsureHttpsCertificate(exportPath, password, trust, reporter);
return EnsureHttpsCertificate(exportPath, password, noPassword, trust, keyFormat, reporter);
});
});
@ -129,6 +196,44 @@ namespace Microsoft.AspNetCore.DeveloperCertificates.Tools
}
}
private static int ImportCertificate(CommandOption import, CommandOption password, ConsoleReporter reporter)
{
var manager = CertificateManager.Instance;
try
{
var result = manager.ImportCertificate(import.Value(), password.Value());
switch (result)
{
case ImportCertificateResult.Succeeded:
reporter.Output("The certificate was successfully imported.");
break;
case ImportCertificateResult.CertificateFileMissing:
reporter.Error($"The certificate file '{import.Value()}' does not exist.");
return MissingCertificateFile;
case ImportCertificateResult.InvalidCertificate:
reporter.Error($"The provided certificate file '{import.Value()}' is not a valid PFX file or the password is incorrect.");
return FailedToLoadCertificate;
case ImportCertificateResult.NoDevelopmentHttpsCertificate:
reporter.Error($"The certificate at '{import.Value()}' is not a valid ASP.NET Core HTTPS development certificate.");
return NoDevelopmentHttpsCertificate;
case ImportCertificateResult.ExistingCertificatesPresent:
reporter.Error($"There are one or more ASP.NET Core HTTPS development certificates present in the environment. Remove them before importing the given certificate.");
return ExistingCertificatesPresent;
case ImportCertificateResult.ErrorSavingTheCertificateIntoTheCurrentUserPersonalStore:
reporter.Error("There was an error saving the HTTPS developer certificate to the current user personal certificate store.");
return ErrorSavingTheCertificate;
default:
break;
}
}
catch (Exception)
{
return ErrorImportingCertificate;
}
return Success;
}
private static int CleanHttpsCertificates(IReporter reporter)
{
var manager = CertificateManager.Instance;
@ -204,7 +309,7 @@ namespace Microsoft.AspNetCore.DeveloperCertificates.Tools
return Success;
}
private static int EnsureHttpsCertificate(CommandOption exportPath, CommandOption password, CommandOption trust, IReporter reporter)
private static int EnsureHttpsCertificate(CommandOption exportPath, CommandOption password, CommandOption noPassword, CommandOption trust, CommandOption keyFormat, IReporter reporter)
{
var now = DateTimeOffset.Now;
var manager = CertificateManager.Instance;
@ -242,13 +347,21 @@ namespace Microsoft.AspNetCore.DeveloperCertificates.Tools
"if the certificate was not previously trusted. Click yes on the prompt to trust the certificate.");
}
var format = CertificateKeyExportFormat.Pfx;
if (keyFormat.HasValue() && !Enum.TryParse(keyFormat.Value(), ignoreCase: true, out format))
{
reporter.Error($"Unknown key format '{keyFormat.Value()}'.");
return InvalidKeyExportFormat;
}
var result = manager.EnsureAspNetCoreHttpsDevelopmentCertificate(
now,
now.Add(HttpsCertificateValidity),
exportPath.Value(),
trust == null ? false : trust.HasValue(),
password.HasValue(),
password.Value());
password.HasValue() || (noPassword.HasValue() && format == CertificateKeyExportFormat.Pem),
password.Value(),
keyFormat.HasValue() ? format : CertificateKeyExportFormat.Pfx);
switch (result)
{