Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 8 commits into from
Jul 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/ProjectTemplates/Shared/DevelopmentCertificate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ private static string EnsureDevelopmentCertificates(string certificatePath, stri
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;
}
Expand Down
11 changes: 11 additions & 0 deletions src/Shared/CertificateGeneration/CertificateExportFormat.cs
Original file line number Diff line number Diff line change
@@ -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,
}
}
160 changes: 155 additions & 5 deletions src/Shared/CertificateGeneration/CertificateManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ public EnsureCertificateResult EnsureAspNetCoreHttpsDevelopmentCertificate(
bool trust = false,
bool includePrivateKey = false,
string password = null,
CertificateKeyExportFormat keyExportFormat = CertificateKeyExportFormat.Pfx,
bool isInteractive = true)
{
var result = EnsureCertificateResult.Succeeded;
Expand All @@ -170,6 +171,7 @@ public EnsureCertificateResult EnsureAspNetCoreHttpsDevelopmentCertificate(
certificates = filteredCertificates;

X509Certificate2 certificate = null;
var isNewCertificate = false;
if (certificates.Any())
{
certificate = certificates.First();
Expand Down Expand Up @@ -216,6 +218,7 @@ public EnsureCertificateResult EnsureAspNetCoreHttpsDevelopmentCertificate(
try
{
Log.CreateDevelopmentCertificateStart();
isNewCertificate = true;
certificate = CreateAspNetCoreHttpsDevelopmentCertificate(notBefore, notAfter);
}
catch (Exception e)
Expand Down Expand Up @@ -260,13 +263,13 @@ public EnsureCertificateResult EnsureAspNetCoreHttpsDevelopmentCertificate(
{
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;

Expand All @@ -292,9 +295,58 @@ public EnsureCertificateResult EnsureAspNetCoreHttpsDevelopmentCertificate(
}
}

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
Expand Down Expand Up @@ -329,7 +381,7 @@ public void CleanupHttpsCertificates()

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)
Expand All @@ -345,15 +397,69 @@ internal void ExportCertificate(X509Certificate2 certificate, string path, bool
}

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
{
Expand All @@ -369,6 +475,25 @@ internal void ExportCertificate(X509Certificate2 certificate, string path, bool
{
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)
Expand Down Expand Up @@ -496,7 +621,7 @@ internal X509Certificate2 CreateSelfSignedCertificate(
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)
Expand Down Expand Up @@ -745,6 +870,31 @@ public void ExportCertificateStart(string certificate, string path, bool include

[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
Expand Down
16 changes: 16 additions & 0 deletions src/Shared/CertificateGeneration/ImportCertificateResult.cs
Original file line number Diff line number Diff line change
@@ -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,
}
}

4 changes: 2 additions & 2 deletions src/Shared/CertificateGeneration/MacOSCertificateManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ protected override void TrustCertificateCore(X509Certificate2 publicCertificate)
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))
{
Expand Down Expand Up @@ -94,7 +94,7 @@ internal override CheckCertificateStateResult CheckCertificateState(X509Certific
// 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);
Expand Down
1 change: 1 addition & 0 deletions src/Shared/CertificateGeneration/UnixCertificateManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ internal UnixCertificateManager(string subject, int version)
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);

Expand Down
8 changes: 5 additions & 3 deletions src/Shared/CertificateGeneration/WindowsCertificateManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@ protected override bool IsExportable(X509Certificate2 c)
// 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
}
Expand All @@ -49,6 +50,7 @@ protected override X509Certificate2 SaveCertificateCore(X509Certificate2 certifi
// 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;
Expand All @@ -65,7 +67,7 @@ protected override X509Certificate2 SaveCertificateCore(X509Certificate2 certifi

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;

Expand Down
Loading