[Https] Various improvements to the dev-certs tool (#25037)

* Add support for the trust option on Linux on the command-line tool and print a message when it's used pointing to docs.
* Bump the certificate version to 2 to ensure that the certificate gets updated for 5.0 on Mac OS.
* Ensure we always select the certificate with the highest available version to ensure that when we change the certificate in the future older runtimes pick up the new certificate.
* Support exporting the certificate without key on PEM format.
This commit is contained in:
Javier Calvarro Nelson 2020-08-20 18:55:56 +02:00 committed by GitHub
parent 818279f1f5
commit 3c34c3ab0d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 132 additions and 38 deletions

View File

@ -15,6 +15,7 @@ namespace Microsoft.AspNetCore.Certificates.Generation
{
internal abstract class CertificateManager
{
internal const int CurrentAspNetCoreCertificateVersion = 2;
internal const string AspNetHttpsOid = "1.3.6.1.4.1.311.84.1.1";
internal const string AspNetHttpsOidFriendlyName = "ASP.NET Core HTTPS development certificate";
@ -45,7 +46,7 @@ namespace Microsoft.AspNetCore.Certificates.Generation
public string Subject { get; }
public CertificateManager() : this(LocalhostHttpsDistinguishedName, 1)
public CertificateManager() : this(LocalhostHttpsDistinguishedName, CurrentAspNetCoreCertificateVersion)
{
}
@ -86,10 +87,8 @@ namespace Microsoft.AspNetCore.Certificates.Generation
Log.CheckCertificatesValidity();
var now = DateTimeOffset.Now;
var validCertificates = matchingCertificates
.Where(c => c.NotBefore <= now &&
now <= c.NotAfter &&
(!requireExportable || IsExportable(c))
&& MatchesVersion(c))
.Where(c => IsValidCertificate(c, now, requireExportable))
.OrderByDescending(c => GetCertificateVersion(c))
.ToArray();
var invalidCertificates = matchingCertificates.Except(validCertificates);
@ -123,7 +122,7 @@ namespace Microsoft.AspNetCore.Certificates.Generation
certificate.Extensions.OfType<X509Extension>()
.Any(e => string.Equals(oid, e.Oid.Value, StringComparison.Ordinal));
bool MatchesVersion(X509Certificate2 c)
static byte GetCertificateVersion(X509Certificate2 c)
{
var byteArray = c.Extensions.OfType<X509Extension>()
.Where(e => string.Equals(AspNetHttpsOid, e.Oid.Value, StringComparison.Ordinal))
@ -133,14 +132,20 @@ namespace Microsoft.AspNetCore.Certificates.Generation
if ((byteArray.Length == AspNetHttpsOidFriendlyName.Length && byteArray[0] == (byte)'A') || byteArray.Length == 0)
{
// No Version set, default to 0
return 0 >= AspNetHttpsCertificateVersion;
return 0b0;
}
else
{
// Version is in the only byte of the byte array.
return byteArray[0] >= AspNetHttpsCertificateVersion;
return byteArray[0];
}
}
bool IsValidCertificate(X509Certificate2 certificate, DateTimeOffset currentDate, bool requireExportable) =>
certificate.NotBefore <= currentDate &&
currentDate <= certificate.NotAfter &&
(!requireExportable || IsExportable(certificate)) &&
GetCertificateVersion(certificate) >= AspNetHttpsCertificateVersion;
}
public IList<X509Certificate2> GetHttpsCertificates() =>
@ -448,7 +453,14 @@ namespace Microsoft.AspNetCore.Certificates.Generation
}
else
{
bytes = certificate.Export(X509ContentType.Cert);
if (format == CertificateKeyExportFormat.Pem)
{
bytes = Encoding.ASCII.GetBytes(PemEncoding.Write("CERTIFICATE", certificate.Export(X509ContentType.Cert)));
}
else
{
bytes = certificate.Export(X509ContentType.Cert);
}
}
}
catch (Exception e)

View File

@ -191,6 +191,36 @@ 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_WithoutKey()
{
// Arrange
const string CertificateName = nameof(EnsureCreateHttpsCertificate_DoesNotCreateACertificate_WhenThereIsAnExistingHttpsCertificates) + ".pem";
_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: false, password: null, keyExportFormat: CertificateKeyExportFormat.Pem, isInteractive: false);
// Assert
Assert.Equal(EnsureCertificateResult.ValidCertificatePresent, result);
Assert.True(File.Exists(CertificateName));
var exportedCertificate = new X509Certificate2(CertificateName);
Assert.NotNull(exportedCertificate);
Assert.False(exportedCertificate.HasPrivateKey);
}
[ConditionalFact]
[SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6720", Queues = "OSX.1014.Amd64;OSX.1014.Amd64.Open")]
public void EnsureCreateHttpsCertificate_CanImport_ExportedPfx()
@ -351,6 +381,44 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests
var httpsCertificateList = _manager.ListCertificates(StoreName.My, StoreLocation.CurrentUser, isValid: true);
Assert.NotEmpty(httpsCertificateList);
}
[ConditionalFact]
[SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6720", Queues = "OSX.1014.Amd64;OSX.1014.Amd64.Open")]
public void ListCertificates_AlwaysReturnsTheCertificate_WithHighestVersion()
{
_fixture.CleanupCertificates();
var now = DateTimeOffset.UtcNow;
now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset);
_manager.AspNetHttpsCertificateVersion = 1;
var creation = _manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), path: null, trust: false, isInteractive: false);
Output.WriteLine(creation.ToString());
ListCertificates();
_manager.AspNetHttpsCertificateVersion = 2;
creation = _manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), path: null, trust: false, isInteractive: false);
Output.WriteLine(creation.ToString());
ListCertificates();
_manager.AspNetHttpsCertificateVersion = 1;
var httpsCertificateList = _manager.ListCertificates(StoreName.My, StoreLocation.CurrentUser, isValid: true);
Assert.Equal(2, httpsCertificateList.Count);
var firstCertificate = httpsCertificateList[0];
var secondCertificate = httpsCertificateList[1];
Assert.Contains(
firstCertificate.Extensions.OfType<X509Extension>(),
e => e.Critical == false &&
e.Oid.Value == "1.3.6.1.4.1.311.84.1.1" &&
e.RawData[0] == 2);
Assert.Contains(
secondCertificate.Extensions.OfType<X509Extension>(),
e => e.Critical == false &&
e.Oid.Value == "1.3.6.1.4.1.311.84.1.1" &&
e.RawData[0] == 1);
}
}
public class CertFixture : IDisposable

View File

@ -98,12 +98,9 @@ namespace Microsoft.AspNetCore.DeveloperCertificates.Tools
CommandOptionType.SingleValue);
CommandOption trust = null;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
trust = c.Option("-t|--trust",
"Trust the certificate on the current platform",
CommandOptionType.NoValue);
}
trust = c.Option("-t|--trust",
"Trust the certificate on the current platform",
CommandOptionType.NoValue);
var verbose = c.Option("-v|--verbose",
"Display more debug information.",
@ -292,24 +289,32 @@ namespace Microsoft.AspNetCore.DeveloperCertificates.Tools
if (trust != null && trust.HasValue())
{
var store = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? StoreName.My : StoreName.Root;
var trustedCertificates = certificateManager.ListCertificates(store, StoreLocation.CurrentUser, isValid: true);
if (!certificates.Any(c => certificateManager.IsTrusted(c)))
if(!RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
reporter.Output($@"The following certificates were found, but none of them is trusted:
{string.Join(Environment.NewLine, certificates.Select(c => $"{c.Subject} - {c.Thumbprint}"))}");
return ErrorCertificateNotTrusted;
var store = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? StoreName.My : StoreName.Root;
var trustedCertificates = certificateManager.ListCertificates(store, StoreLocation.CurrentUser, isValid: true);
if (!certificates.Any(c => certificateManager.IsTrusted(c)))
{
reporter.Output($@"The following certificates were found, but none of them is trusted:
{string.Join(Environment.NewLine, certificates.Select(c => $"{c.Subject} - {c.Thumbprint}"))}");
return ErrorCertificateNotTrusted;
}
else
{
reporter.Output("A trusted certificate was found.");
}
}
else
{
reporter.Output("A trusted certificate was found.");
reporter.Warn("Checking the HTTPS development certificate trust status was requested. Checking whether the certificate is trusted or not is not supported on Linux distributions." +
"For instructions on how to manually validate the certificate is trusted on your Linux distribution, go to https://aka.ms/dev-certs-trust");
}
}
return Success;
}
private static int EnsureHttpsCertificate(CommandOption exportPath, CommandOption password, CommandOption noPassword, CommandOption trust, CommandOption keyFormat, IReporter reporter)
private static int EnsureHttpsCertificate(CommandOption exportPath, CommandOption password, CommandOption noPassword, CommandOption trust, CommandOption exportFormat, IReporter reporter)
{
var now = DateTimeOffset.Now;
var manager = CertificateManager.Instance;
@ -332,25 +337,34 @@ namespace Microsoft.AspNetCore.DeveloperCertificates.Tools
}
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && trust?.HasValue() == true)
if (trust?.HasValue() == true)
{
reporter.Warn("Trusting the HTTPS development certificate was requested. If the certificate is not " +
"already trusted we will run the following command:" + Environment.NewLine +
"'sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain <<certificate>>'" +
Environment.NewLine + "This command might prompt you for your password to install the certificate " +
"on the system keychain.");
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
reporter.Warn("Trusting the HTTPS development certificate was requested. If the certificate is not " +
"already trusted we will run the following command:" + Environment.NewLine +
"'sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain <<certificate>>'" +
Environment.NewLine + "This command might prompt you for your password to install the certificate " +
"on the system keychain.");
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && trust?.HasValue() == true)
{
reporter.Warn("Trusting the HTTPS development certificate was requested. A confirmation prompt will be displayed " +
"if the certificate was not previously trusted. Click yes on the prompt to trust the certificate.");
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
reporter.Warn("Trusting the HTTPS development certificate was requested. A confirmation prompt will be displayed " +
"if the certificate was not previously trusted. Click yes on the prompt to trust the certificate.");
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
reporter.Warn("Trusting the HTTPS development certificate was requested. Trusting the certificate on Linux distributions automatically is not supported. " +
"For instructions on how to manually trust the certificate on your Linux distribution, go to https://aka.ms/dev-certs-trust");
}
}
var format = CertificateKeyExportFormat.Pfx;
if (keyFormat.HasValue() && !Enum.TryParse(keyFormat.Value(), ignoreCase: true, out format))
if (exportFormat.HasValue() && !Enum.TryParse(exportFormat.Value(), ignoreCase: true, out format))
{
reporter.Error($"Unknown key format '{keyFormat.Value()}'.");
reporter.Error($"Unknown key format '{exportFormat.Value()}'.");
return InvalidKeyExportFormat;
}
@ -358,10 +372,10 @@ namespace Microsoft.AspNetCore.DeveloperCertificates.Tools
now,
now.Add(HttpsCertificateValidity),
exportPath.Value(),
trust == null ? false : trust.HasValue(),
trust == null ? false : trust.HasValue() && !RuntimeInformation.IsOSPlatform(OSPlatform.Linux),
password.HasValue() || (noPassword.HasValue() && format == CertificateKeyExportFormat.Pem),
password.Value(),
keyFormat.HasValue() ? format : CertificateKeyExportFormat.Pfx);
exportFormat.HasValue() ? format : CertificateKeyExportFormat.Pfx);
switch (result)
{