[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 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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue