[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:
parent
818279f1f5
commit
3c34c3ab0d
|
|
@ -15,6 +15,7 @@ namespace Microsoft.AspNetCore.Certificates.Generation
|
||||||
{
|
{
|
||||||
internal abstract class CertificateManager
|
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 AspNetHttpsOid = "1.3.6.1.4.1.311.84.1.1";
|
||||||
internal const string AspNetHttpsOidFriendlyName = "ASP.NET Core HTTPS development certificate";
|
internal const string AspNetHttpsOidFriendlyName = "ASP.NET Core HTTPS development certificate";
|
||||||
|
|
||||||
|
|
@ -45,7 +46,7 @@ namespace Microsoft.AspNetCore.Certificates.Generation
|
||||||
|
|
||||||
public string Subject { get; }
|
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();
|
Log.CheckCertificatesValidity();
|
||||||
var now = DateTimeOffset.Now;
|
var now = DateTimeOffset.Now;
|
||||||
var validCertificates = matchingCertificates
|
var validCertificates = matchingCertificates
|
||||||
.Where(c => c.NotBefore <= now &&
|
.Where(c => IsValidCertificate(c, now, requireExportable))
|
||||||
now <= c.NotAfter &&
|
.OrderByDescending(c => GetCertificateVersion(c))
|
||||||
(!requireExportable || IsExportable(c))
|
|
||||||
&& MatchesVersion(c))
|
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
var invalidCertificates = matchingCertificates.Except(validCertificates);
|
var invalidCertificates = matchingCertificates.Except(validCertificates);
|
||||||
|
|
@ -123,7 +122,7 @@ namespace Microsoft.AspNetCore.Certificates.Generation
|
||||||
certificate.Extensions.OfType<X509Extension>()
|
certificate.Extensions.OfType<X509Extension>()
|
||||||
.Any(e => string.Equals(oid, e.Oid.Value, StringComparison.Ordinal));
|
.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>()
|
var byteArray = c.Extensions.OfType<X509Extension>()
|
||||||
.Where(e => string.Equals(AspNetHttpsOid, e.Oid.Value, StringComparison.Ordinal))
|
.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)
|
if ((byteArray.Length == AspNetHttpsOidFriendlyName.Length && byteArray[0] == (byte)'A') || byteArray.Length == 0)
|
||||||
{
|
{
|
||||||
// No Version set, default to 0
|
// No Version set, default to 0
|
||||||
return 0 >= AspNetHttpsCertificateVersion;
|
return 0b0;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Version is in the only byte of the byte array.
|
// 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() =>
|
public IList<X509Certificate2> GetHttpsCertificates() =>
|
||||||
|
|
@ -448,7 +453,14 @@ namespace Microsoft.AspNetCore.Certificates.Generation
|
||||||
}
|
}
|
||||||
else
|
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)
|
catch (Exception e)
|
||||||
|
|
|
||||||
|
|
@ -191,6 +191,36 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests
|
||||||
Assert.Equal(httpsCertificate.GetCertHashString(), exportedCertificate.GetCertHashString());
|
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]
|
[ConditionalFact]
|
||||||
[SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6720", Queues = "OSX.1014.Amd64;OSX.1014.Amd64.Open")]
|
[SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6720", Queues = "OSX.1014.Amd64;OSX.1014.Amd64.Open")]
|
||||||
public void EnsureCreateHttpsCertificate_CanImport_ExportedPfx()
|
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);
|
var httpsCertificateList = _manager.ListCertificates(StoreName.My, StoreLocation.CurrentUser, isValid: true);
|
||||||
Assert.NotEmpty(httpsCertificateList);
|
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
|
public class CertFixture : IDisposable
|
||||||
|
|
|
||||||
|
|
@ -98,12 +98,9 @@ namespace Microsoft.AspNetCore.DeveloperCertificates.Tools
|
||||||
CommandOptionType.SingleValue);
|
CommandOptionType.SingleValue);
|
||||||
|
|
||||||
CommandOption trust = null;
|
CommandOption trust = null;
|
||||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
trust = c.Option("-t|--trust",
|
||||||
{
|
"Trust the certificate on the current platform",
|
||||||
trust = c.Option("-t|--trust",
|
CommandOptionType.NoValue);
|
||||||
"Trust the certificate on the current platform",
|
|
||||||
CommandOptionType.NoValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
var verbose = c.Option("-v|--verbose",
|
var verbose = c.Option("-v|--verbose",
|
||||||
"Display more debug information.",
|
"Display more debug information.",
|
||||||
|
|
@ -292,24 +289,32 @@ namespace Microsoft.AspNetCore.DeveloperCertificates.Tools
|
||||||
|
|
||||||
if (trust != null && trust.HasValue())
|
if (trust != null && trust.HasValue())
|
||||||
{
|
{
|
||||||
var store = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? StoreName.My : StoreName.Root;
|
if(!RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||||
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:
|
var store = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? StoreName.My : StoreName.Root;
|
||||||
{string.Join(Environment.NewLine, certificates.Select(c => $"{c.Subject} - {c.Thumbprint}"))}");
|
var trustedCertificates = certificateManager.ListCertificates(store, StoreLocation.CurrentUser, isValid: true);
|
||||||
return ErrorCertificateNotTrusted;
|
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
|
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;
|
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 now = DateTimeOffset.Now;
|
||||||
var manager = CertificateManager.Instance;
|
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 " +
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||||
"already trusted we will run the following command:" + Environment.NewLine +
|
{
|
||||||
"'sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain <<certificate>>'" +
|
reporter.Warn("Trusting the HTTPS development certificate was requested. If the certificate is not " +
|
||||||
Environment.NewLine + "This command might prompt you for your password to install the certificate " +
|
"already trusted we will run the following command:" + Environment.NewLine +
|
||||||
"on the system keychain.");
|
"'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)
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
{
|
{
|
||||||
reporter.Warn("Trusting the HTTPS development certificate was requested. A confirmation prompt will be displayed " +
|
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 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;
|
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;
|
return InvalidKeyExportFormat;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -358,10 +372,10 @@ namespace Microsoft.AspNetCore.DeveloperCertificates.Tools
|
||||||
now,
|
now,
|
||||||
now.Add(HttpsCertificateValidity),
|
now.Add(HttpsCertificateValidity),
|
||||||
exportPath.Value(),
|
exportPath.Value(),
|
||||||
trust == null ? false : trust.HasValue(),
|
trust == null ? false : trust.HasValue() && !RuntimeInformation.IsOSPlatform(OSPlatform.Linux),
|
||||||
password.HasValue() || (noPassword.HasValue() && format == CertificateKeyExportFormat.Pem),
|
password.HasValue() || (noPassword.HasValue() && format == CertificateKeyExportFormat.Pem),
|
||||||
password.Value(),
|
password.Value(),
|
||||||
keyFormat.HasValue() ? format : CertificateKeyExportFormat.Pfx);
|
exportFormat.HasValue() ? format : CertificateKeyExportFormat.Pfx);
|
||||||
|
|
||||||
switch (result)
|
switch (result)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue