[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:
parent
c1866c2b61
commit
156023d3f9
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue