[Platform] Detect and fix certificates with potentially inaccessible keys on Mac OS (2.1) (#17560)

* [Https] Detects and fixes HTTPS certificates where the key is not guaranteed to be accessible across security partitions

* Fix dotnet dev-certs https --check

* Update logic for detecting missing certs

* Fix security command

* Update warning logic

* Check that the key is accessible in Kestrel

* Add correct link to docs

* Update src/Tools/dotnet-dev-certs/src/Program.cs

Co-Authored-By: Daniel Roth <daroth@microsoft.com>

* Update src/Tools/dotnet-dev-certs/src/Program.cs

Co-Authored-By: Daniel Roth <daroth@microsoft.com>

* Add test for 2.1

* Update src/Tools/dotnet-dev-certs/src/Program.cs

Co-Authored-By: Chris Ross <Tratcher@Outlook.com>

* Address feedback

* Fix non-interctive path

* Fix tests

* Remove a couple of test from an unshipped product

* Check only for certificates considered valid

* Switch the exception being caught, remove invalid test

Co-authored-by: Daniel Roth <daroth@microsoft.com>
Co-authored-by: Chris Ross <Tratcher@Outlook.com>
This commit is contained in:
Javier Calvarro Nelson 2020-01-15 22:54:11 -08:00 committed by Artak
parent 8211a1c313
commit 7f53f7e95b
10 changed files with 234 additions and 119 deletions

View File

@ -518,4 +518,7 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
<data name="ConnectionTimedOutByServer" xml:space="preserve"> <data name="ConnectionTimedOutByServer" xml:space="preserve">
<value>The connection was timed out by the server.</value> <value>The connection was timed out by the server.</value>
</data> </data>
<data name="BadDeveloperCertificateState" xml:space="preserve">
<value>The ASP.NET Core developer certificate is in an invalid state. To fix this issue, run the following commands 'dotnet dev-certs https --clean' and 'dotnet dev-certs https' to remove all existing ASP.NET Core development certificates and create a new untrusted developer certificate. On macOS or Windows, use 'dotnet dev-certs https --trust' to trust the new certificate.</value>
</data>
</root> </root>

View File

@ -9,6 +9,7 @@ using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates; using System.Security.Cryptography.X509Certificates;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Certificates.Generation;
using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core;
@ -177,8 +178,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
EnsureCertificateIsAllowedForServerAuth(serverCert); EnsureCertificateIsAllowedForServerAuth(serverCert);
} }
} }
await sslStream.AuthenticateAsServerAsync(serverCert, certificateRequired, await sslStream.AuthenticateAsServerAsync(serverCert, certificateRequired,
_options.SslProtocols, _options.CheckCertificateRevocation); _options.SslProtocols, _options.CheckCertificateRevocation);
#endif #endif
} }
catch (OperationCanceledException) catch (OperationCanceledException)
@ -187,12 +189,28 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
sslStream.Dispose(); sslStream.Dispose();
return _closedAdaptedConnection; return _closedAdaptedConnection;
} }
catch (Exception ex) when (ex is IOException || ex is AuthenticationException) catch (IOException ex)
{ {
_logger?.LogDebug(1, ex, CoreStrings.AuthenticationFailed); _logger?.LogDebug(1, ex, CoreStrings.AuthenticationFailed);
sslStream.Dispose(); sslStream.Dispose();
return _closedAdaptedConnection; return _closedAdaptedConnection;
} }
catch (AuthenticationException ex)
{
if (_serverCertificate != null &&
CertificateManager.IsHttpsDevelopmentCertificate(_serverCertificate) &&
!CertificateManager.CheckDeveloperCertificateKey(_serverCertificate))
{
_logger?.LogError(3, ex, CoreStrings.BadDeveloperCertificateState);
}
else
{
_logger?.LogDebug(1, ex, CoreStrings.AuthenticationFailed);
}
sslStream.Dispose();
return _closedAdaptedConnection;
}
finally finally
{ {
timeoutFeature.CancelTimeout(); timeoutFeature.CancelTimeout();

View File

@ -1876,6 +1876,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
internal static string FormatConnectionTimedOutByServer() internal static string FormatConnectionTimedOutByServer()
=> GetString("ConnectionTimedOutByServer"); => GetString("ConnectionTimedOutByServer");
/// <summary>
/// The ASP.NET Core developer certificate is in an invalid state. To fix this issue, run the following commands 'dotnet dev-certs https --clean' and 'dotnet dev-certs https' to remove all existing ASP.NET Core development certificates and create a new untrusted developer certificate. On macOS or Windows, use 'dotnet dev-certs https --trust' to trust the new certificate.
/// </summary>
internal static string BadDeveloperCertificateState
{
get => GetString("BadDeveloperCertificateState");
}
/// <summary>
/// The ASP.NET Core developer certificate is in an invalid state. To fix this issue, run the following commands 'dotnet dev-certs https --clean' and 'dotnet dev-certs https' to remove all existing ASP.NET Core development certificates and create a new untrusted developer certificate. On macOS or Windows, use 'dotnet dev-certs https --trust' to trust the new certificate.
/// </summary>
internal static string FormatBadDeveloperCertificateState()
=> GetString("BadDeveloperCertificateState");
private static string GetString(string name, params string[] formatterNames) private static string GetString(string name, params string[] formatterNames)
{ {
var value = _resourceManager.GetString(name); var value = _resourceManager.GetString(name);

View File

@ -22,5 +22,10 @@ namespace Microsoft.AspNetCore.Testing
{ {
return new X509Certificate2(GetCertPath(certName), "testPassword"); return new X509Certificate2(GetCertPath(certName), "testPassword");
} }
public static X509Certificate2 GetTestCertificate(string certName, string password)
{
return new X509Certificate2(GetCertPath(certName), password);
}
} }
} }

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;

View File

@ -7,6 +7,7 @@ using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates; using System.Security.Cryptography.X509Certificates;
using System.Text; using System.Text;
@ -50,6 +51,8 @@ namespace Microsoft.AspNetCore.Certificates.Generation
private static readonly string MacOSTrustCertificateCommandLineArguments = "security add-trusted-cert -d -r trustRoot -k " + MacOSSystemKeyChain + " "; private static readonly string MacOSTrustCertificateCommandLineArguments = "security add-trusted-cert -d -r trustRoot -k " + MacOSSystemKeyChain + " ";
#endif #endif
private const int UserCancelledErrorCode = 1223; private const int UserCancelledErrorCode = 1223;
private const string MacOSSetPartitionKeyPermissionsCommandLine = "sudo";
private static readonly string MacOSSetPartitionKeyPermissionsCommandLineArguments = "security set-key-partition-list -D localhost -S unsigned:,teamid:UBF8T346G9 " + MacOSUserKeyChain;
public IList<X509Certificate2> ListCertificates( public IList<X509Certificate2> ListCertificates(
CertificatePurpose purpose, CertificatePurpose purpose,
@ -147,6 +150,39 @@ namespace Microsoft.AspNetCore.Certificates.Generation
} }
} }
internal static bool IsHttpsDevelopmentCertificate(X509Certificate2 certificate) =>
certificate.Extensions.OfType<X509Extension>()
.Any(e => string.Equals(AspNetHttpsOid, e.Oid.Value, StringComparison.Ordinal));
internal static bool CheckDeveloperCertificateKey(X509Certificate2 candidate)
{
// Tries to use the certificate key to validate it can't access it
try
{
var rsa = candidate.GetRSAPrivateKey();
if (rsa == null)
{
return false;
}
// Encrypting a random value is the ultimate test for a key validity.
// Windows and Mac OS both return HasPrivateKey = true if there is (or there has been) a private key associated
// with the certificate at some point.
var value = new byte[32];
using (var rng = RandomNumberGenerator.Create())
{
rsa.Decrypt(rsa.Encrypt(value, RSAEncryptionPadding.Pkcs1), RSAEncryptionPadding.Pkcs1);
}
// Being able to encrypt and decrypt a payload is the strongest guarantee that the key is valid.
return true;
}
catch (Exception)
{
return false;
}
}
#if NETCOREAPP2_0 || NETCOREAPP2_1 #if NETCOREAPP2_0 || NETCOREAPP2_1
public X509Certificate2 CreateAspNetCoreHttpsDevelopmentCertificate(DateTimeOffset notBefore, DateTimeOffset notAfter, string subjectOverride) public X509Certificate2 CreateAspNetCoreHttpsDevelopmentCertificate(DateTimeOffset notBefore, DateTimeOffset notAfter, string subjectOverride)
@ -192,6 +228,27 @@ namespace Microsoft.AspNetCore.Certificates.Generation
return certificate; return certificate;
} }
internal bool HasValidCertificateWithInnaccessibleKeyAcrossPartitions()
{
var certificates = GetHttpsCertificates();
if (certificates.Count == 0)
{
return false;
}
// We need to check all certificates as a new one might be created that hasn't been correctly setup.
var result = false;
foreach (var certificate in certificates)
{
result = result || !CanAccessCertificateKeyAcrossPartitions(certificate);
}
return result;
}
public IList<X509Certificate2> GetHttpsCertificates() =>
ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: true, requireExportable: true);
public X509Certificate2 CreateApplicationTokenSigningDevelopmentCertificate(DateTimeOffset notBefore, DateTimeOffset notAfter, string subjectOverride) public X509Certificate2 CreateApplicationTokenSigningDevelopmentCertificate(DateTimeOffset notBefore, DateTimeOffset notAfter, string subjectOverride)
{ {
var subject = new X500DistinguishedName(subjectOverride ?? IdentityDistinguishedName); var subject = new X500DistinguishedName(subjectOverride ?? IdentityDistinguishedName);
@ -596,9 +653,10 @@ namespace Microsoft.AspNetCore.Certificates.Generation
bool trust = false, bool trust = false,
bool includePrivateKey = false, bool includePrivateKey = false,
string password = null, string password = null,
string subject = LocalhostHttpsDistinguishedName) string subject = LocalhostHttpsDistinguishedName,
bool isInteractive = true)
{ {
return EnsureValidCertificateExists(notBefore, notAfter, CertificatePurpose.HTTPS, path, trust, includePrivateKey, password, subject); return EnsureValidCertificateExists(notBefore, notAfter, CertificatePurpose.HTTPS, path, trust, includePrivateKey, password, subject, isInteractive);
} }
public EnsureCertificateResult EnsureAspNetCoreApplicationTokensDevelopmentCertificate( public EnsureCertificateResult EnsureAspNetCoreApplicationTokensDevelopmentCertificate(
@ -610,7 +668,7 @@ namespace Microsoft.AspNetCore.Certificates.Generation
string password = null, string password = null,
string subject = IdentityDistinguishedName) string subject = IdentityDistinguishedName)
{ {
return EnsureValidCertificateExists(notBefore, notAfter, CertificatePurpose.Signing, path, trust, includePrivateKey, password, subject); return EnsureValidCertificateExists(notBefore, notAfter, CertificatePurpose.Signing, path, trust, includePrivateKey, password, subject, isInteractive: true);
} }
public EnsureCertificateResult EnsureValidCertificateExists( public EnsureCertificateResult EnsureValidCertificateExists(
@ -621,7 +679,8 @@ namespace Microsoft.AspNetCore.Certificates.Generation
bool trust = false, bool trust = false,
bool includePrivateKey = false, bool includePrivateKey = false,
string password = null, string password = null,
string subjectOverride = null) string subjectOverride = null,
bool isInteractive = true)
{ {
if (purpose == CertificatePurpose.All) if (purpose == CertificatePurpose.All)
{ {
@ -633,6 +692,33 @@ namespace Microsoft.AspNetCore.Certificates.Generation
certificates = subjectOverride == null ? certificates : certificates.Where(c => c.Subject == subjectOverride); certificates = subjectOverride == null ? certificates : certificates.Where(c => c.Subject == subjectOverride);
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
foreach (var cert in certificates)
{
if (!CanAccessCertificateKeyAcrossPartitions(cert))
{
if (!isInteractive)
{
// If the process is not interactive (first run experience) bail out. We will simply create a certificate
// in case there is none or report success during the first run experience.
break;
}
try
{
// The command we run handles making keys for all localhost certificates accessible across partitions. If it can not run the
// command safely (because there are other localhost certificates that were not created by asp.net core, it will throw.
MakeCertificateKeyAccessibleAcrossPartitions(cert);
break;
}
catch (Exception)
{
return EnsureCertificateResult.FailedToMakeKeyAccessible;
}
}
}
}
var result = EnsureCertificateResult.Succeeded; var result = EnsureCertificateResult.Succeeded;
X509Certificate2 certificate = null; X509Certificate2 certificate = null;
@ -672,6 +758,11 @@ namespace Microsoft.AspNetCore.Certificates.Generation
{ {
return EnsureCertificateResult.ErrorSavingTheCertificateIntoTheCurrentUserPersonalStore; return EnsureCertificateResult.ErrorSavingTheCertificateIntoTheCurrentUserPersonalStore;
} }
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && isInteractive)
{
MakeCertificateKeyAccessibleAcrossPartitions(certificate);
}
} }
if (path != null) if (path != null)
{ {
@ -704,6 +795,74 @@ namespace Microsoft.AspNetCore.Certificates.Generation
return result; return result;
} }
private void MakeCertificateKeyAccessibleAcrossPartitions(X509Certificate2 certificate)
{
if (OtherNonAspNetCoreHttpsCertificatesPresent())
{
throw new InvalidOperationException("Unable to make HTTPS ceritificate key trusted across security partitions.");
}
using (var process = Process.Start(MacOSSetPartitionKeyPermissionsCommandLine, MacOSSetPartitionKeyPermissionsCommandLineArguments))
{
process.WaitForExit();
if (process.ExitCode != 0)
{
throw new InvalidOperationException("Error making the key accessible across partitions.");
}
}
var certificateSentinelPath = GetCertificateSentinelPath(certificate);
File.WriteAllText(certificateSentinelPath, "true");
}
private static string GetCertificateSentinelPath(X509Certificate2 certificate) =>
Path.Combine(Environment.GetEnvironmentVariable("HOME"), ".dotnet", $"certificate.{certificate.GetCertHashString(HashAlgorithmName.SHA256)}.sentinel");
private bool OtherNonAspNetCoreHttpsCertificatesPresent()
{
var certificates = new List<X509Certificate2>();
try
{
using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser))
{
store.Open(OpenFlags.ReadOnly);
certificates.AddRange(store.Certificates.OfType<X509Certificate2>());
IEnumerable<X509Certificate2> matchingCertificates = certificates;
// Ensure the certificate hasn't expired, has a private key and its exportable
// (for container/unix scenarios).
var now = DateTimeOffset.Now;
matchingCertificates = matchingCertificates
.Where(c => c.NotBefore <= now &&
now <= c.NotAfter && c.Subject == LocalhostHttpsDistinguishedName);
// We need to enumerate the certificates early to prevent dispoisng issues.
matchingCertificates = matchingCertificates.ToList();
var certificatesToDispose = certificates.Except(matchingCertificates);
DisposeCertificates(certificatesToDispose);
store.Close();
return matchingCertificates.All(c => !HasOid(c, AspNetHttpsOid));
}
}
catch
{
DisposeCertificates(certificates);
certificates.Clear();
return true;
}
bool HasOid(X509Certificate2 certificate, string oid) =>
certificate.Extensions.OfType<X509Extension>()
.Any(e => string.Equals(oid, e.Oid.Value, StringComparison.Ordinal));
}
private bool CanAccessCertificateKeyAcrossPartitions(X509Certificate2 certificate)
{
var certificateSentinelPath = GetCertificateSentinelPath(certificate);
return File.Exists(certificateSentinelPath);
}
private class UserCancelledTrustException : Exception private class UserCancelledTrustException : Exception
{ {
} }
@ -717,4 +876,4 @@ namespace Microsoft.AspNetCore.Certificates.Generation
} }
#endif #endif
} }
} }

View File

@ -13,8 +13,9 @@ namespace Microsoft.AspNetCore.Certificates.Generation
ErrorSavingTheCertificateIntoTheCurrentUserPersonalStore, ErrorSavingTheCertificateIntoTheCurrentUserPersonalStore,
ErrorExportingTheCertificate, ErrorExportingTheCertificate,
FailedToTrustTheCertificate, FailedToTrustTheCertificate,
UserCancelledTrustStep UserCancelledTrustStep,
FailedToMakeKeyAccessible,
} }
} }
#endif #endif

View File

@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.DeveloperCertificates.XPlat
{ {
var manager = new CertificateManager(); var manager = new CertificateManager();
var now = DateTimeOffset.Now; var now = DateTimeOffset.Now;
manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1)); manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), isInteractive: false);
} }
} }
} }

View File

@ -42,7 +42,7 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests
// Act // Act
DateTimeOffset now = DateTimeOffset.UtcNow; DateTimeOffset now = DateTimeOffset.UtcNow;
now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset); now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset);
var result = manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), CertificateName, trust: false, subject: TestCertificateSubject); var result = manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), CertificateName, trust: false, subject: TestCertificateSubject, isInteractive: false);
// Assert // Assert
Assert.Equal(EnsureCertificateResult.Succeeded, result); Assert.Equal(EnsureCertificateResult.Succeeded, result);
@ -140,12 +140,12 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests
DateTimeOffset now = DateTimeOffset.UtcNow; DateTimeOffset now = DateTimeOffset.UtcNow;
now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset); now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset);
manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), path: null, trust: false, subject: TestCertificateSubject); manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), path: null, trust: false, subject: TestCertificateSubject, isInteractive: false);
var httpsCertificate = manager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: false).Single(c => c.Subject == TestCertificateSubject); var httpsCertificate = manager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: false).Single(c => c.Subject == TestCertificateSubject);
// Act // Act
var result = manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), CertificateName, trust: false, includePrivateKey: true, password: certificatePassword, subject: TestCertificateSubject); var result = manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), CertificateName, trust: false, includePrivateKey: true, password: certificatePassword, subject: TestCertificateSubject, isInteractive: false);
// Assert // Assert
Assert.Equal(EnsureCertificateResult.ValidCertificatePresent, result); Assert.Equal(EnsureCertificateResult.ValidCertificatePresent, result);
@ -172,7 +172,7 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests
DateTimeOffset now = DateTimeOffset.UtcNow; DateTimeOffset now = DateTimeOffset.UtcNow;
now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset); now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset);
var trustFailed = manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), path: null, trust: true, subject: TestCertificateSubject); var trustFailed = manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), path: null, trust: true, subject: TestCertificateSubject, isInteractive: false);
Assert.Equal(EnsureCertificateResult.UserCancelledTrustStep, trustFailed); Assert.Equal(EnsureCertificateResult.UserCancelledTrustStep, trustFailed);
} }
@ -184,7 +184,7 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests
DateTimeOffset now = DateTimeOffset.UtcNow; DateTimeOffset now = DateTimeOffset.UtcNow;
now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset); now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset);
manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), path: null, trust: true, subject: TestCertificateSubject); manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), path: null, trust: true, subject: TestCertificateSubject, isInteractive: false);
manager.CleanupHttpsCertificates(TestCertificateSubject); manager.CleanupHttpsCertificates(TestCertificateSubject);
@ -194,107 +194,5 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests
Assert.Empty(manager.ListCertificates(CertificatePurpose.HTTPS, StoreName.Root, StoreLocation.CurrentUser, isValid: false).Where(c => c.Subject == TestCertificateSubject)); Assert.Empty(manager.ListCertificates(CertificatePurpose.HTTPS, StoreName.Root, StoreLocation.CurrentUser, isValid: false).Where(c => c.Subject == TestCertificateSubject));
} }
} }
[Fact]
public void EnsureCreateIdentityTokenSigningCertificate_CreatesACertificate_WhenThereAreNoHttpsCertificates()
{
// Arrange
const string CertificateName = nameof(EnsureCreateIdentityTokenSigningCertificate_CreatesACertificate_WhenThereAreNoHttpsCertificates) + ".cer";
var manager = new CertificateManager();
manager.RemoveAllCertificates(CertificatePurpose.Signing, StoreName.My, StoreLocation.CurrentUser, TestCertificateSubject);
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
manager.RemoveAllCertificates(CertificatePurpose.Signing, StoreName.Root, StoreLocation.CurrentUser, TestCertificateSubject);
}
// Act
DateTimeOffset now = DateTimeOffset.UtcNow;
now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset);
var result = manager.EnsureAspNetCoreApplicationTokensDevelopmentCertificate(now, now.AddYears(1), CertificateName, trust: false, subject: TestCertificateSubject);
// Assert
Assert.Equal(EnsureCertificateResult.Succeeded, result);
Assert.True(File.Exists(CertificateName));
var exportedCertificate = new X509Certificate2(File.ReadAllBytes(CertificateName));
Assert.NotNull(exportedCertificate);
Assert.False(exportedCertificate.HasPrivateKey);
var identityCertificates = manager.ListCertificates(CertificatePurpose.Signing, StoreName.My, StoreLocation.CurrentUser, isValid: false);
var identityCertificate = Assert.Single(identityCertificates, i => i.Subject == TestCertificateSubject);
Assert.True(identityCertificate.HasPrivateKey);
Assert.Equal(TestCertificateSubject, identityCertificate.Subject);
Assert.Equal(TestCertificateSubject, identityCertificate.Issuer);
Assert.Equal("sha256RSA", identityCertificate.SignatureAlgorithm.FriendlyName);
Assert.Equal("1.2.840.113549.1.1.11", identityCertificate.SignatureAlgorithm.Value);
Assert.Equal(now.LocalDateTime, identityCertificate.NotBefore);
Assert.Equal(now.AddYears(1).LocalDateTime, identityCertificate.NotAfter);
Assert.Contains(
identityCertificate.Extensions.OfType<X509Extension>(),
e => e is X509BasicConstraintsExtension basicConstraints &&
basicConstraints.Critical == true &&
basicConstraints.CertificateAuthority == false &&
basicConstraints.HasPathLengthConstraint == false &&
basicConstraints.PathLengthConstraint == 0);
Assert.Contains(
identityCertificate.Extensions.OfType<X509Extension>(),
e => e is X509KeyUsageExtension keyUsage &&
keyUsage.Critical == true &&
keyUsage.KeyUsages == X509KeyUsageFlags.DigitalSignature);
Assert.Contains(
identityCertificate.Extensions.OfType<X509Extension>(),
e => e is X509EnhancedKeyUsageExtension enhancedKeyUsage &&
enhancedKeyUsage.Critical == true &&
enhancedKeyUsage.EnhancedKeyUsages.OfType<Oid>().Single() is Oid keyUsage &&
keyUsage.Value == "1.3.6.1.5.5.7.3.1");
// ASP.NET Core Identity Json Web Token signing development certificate
Assert.Contains(
identityCertificate.Extensions.OfType<X509Extension>(),
e => e.Critical == false &&
e.Oid.Value == "1.3.6.1.4.1.311.84.1.2" &&
Encoding.ASCII.GetString(e.RawData) == "ASP.NET Core Identity Json Web Token signing development certificate");
Assert.Equal(identityCertificate.GetCertHashString(), exportedCertificate.GetCertHashString());
}
[Fact]
public void EnsureCreateIdentityTokenSigningCertificate_DoesNotCreateACertificate_WhenThereIsAnExistingHttpsCertificates()
{
// Arrange
const string CertificateName = nameof(EnsureCreateIdentityTokenSigningCertificate_DoesNotCreateACertificate_WhenThereIsAnExistingHttpsCertificates) + ".pfx";
var certificatePassword = Guid.NewGuid().ToString();
var manager = new CertificateManager();
manager.RemoveAllCertificates(CertificatePurpose.Signing, StoreName.My, StoreLocation.CurrentUser, TestCertificateSubject);
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
manager.RemoveAllCertificates(CertificatePurpose.Signing, StoreName.Root, StoreLocation.CurrentUser, TestCertificateSubject);
}
DateTimeOffset now = DateTimeOffset.UtcNow;
now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset);
manager.EnsureAspNetCoreApplicationTokensDevelopmentCertificate(now, now.AddYears(1), path: null, trust: false, subject: TestCertificateSubject);
var identityTokenSigningCertificates = manager.ListCertificates(CertificatePurpose.Signing, StoreName.My, StoreLocation.CurrentUser, isValid: false).Single(c => c.Subject == TestCertificateSubject);
// Act
var result = manager.EnsureAspNetCoreApplicationTokensDevelopmentCertificate(now, now.AddYears(1), CertificateName, trust: false, includePrivateKey: true, password: certificatePassword, subject: TestCertificateSubject);
// Assert
Assert.Equal(EnsureCertificateResult.ValidCertificatePresent, result);
Assert.True(File.Exists(CertificateName));
var exportedCertificate = new X509Certificate2(File.ReadAllBytes(CertificateName), certificatePassword);
Assert.NotNull(exportedCertificate);
Assert.True(exportedCertificate.HasPrivateKey);
Assert.Equal(identityTokenSigningCertificates.GetCertHashString(), exportedCertificate.GetCertHashString());
}
} }
} }

View File

@ -24,6 +24,7 @@ namespace Microsoft.AspNetCore.DeveloperCertificates.Tools
private const int ErrorNoValidCertificateFound = 6; private const int ErrorNoValidCertificateFound = 6;
private const int ErrorCertificateNotTrusted = 7; private const int ErrorCertificateNotTrusted = 7;
private const int ErrorCleaningUpCertificates = 8; private const int ErrorCleaningUpCertificates = 8;
private const int ErrorMacOsCertificateKeyCouldNotBeAccessible = 9;
public static readonly TimeSpan HttpsCertificateValidity = TimeSpan.FromDays(365); public static readonly TimeSpan HttpsCertificateValidity = TimeSpan.FromDays(365);
@ -157,7 +158,16 @@ namespace Microsoft.AspNetCore.DeveloperCertificates.Tools
} }
else else
{ {
reporter.Verbose("A valid certificate was found."); if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && certificateManager.HasValidCertificateWithInnaccessibleKeyAcrossPartitions())
{
reporter.Warn($"A valid HTTPS certificate was found but it may not be accessible across security partitions. Run dotnet dev-certs https to ensure it will be accessible during development.");
return ErrorMacOsCertificateKeyCouldNotBeAccessible;
}
else
{
reporter.Verbose("A valid certificate was found.");
}
} }
if (trust != null && trust.HasValue()) if (trust != null && trust.HasValue())
@ -184,6 +194,13 @@ namespace Microsoft.AspNetCore.DeveloperCertificates.Tools
var now = DateTimeOffset.Now; var now = DateTimeOffset.Now;
var manager = new CertificateManager(); var manager = new CertificateManager();
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && manager.HasValidCertificateWithInnaccessibleKeyAcrossPartitions() || manager.GetHttpsCertificates().Count == 0)
{
reporter.Warn($"A valid HTTPS certificate with a key accessible across security partitions was not found. The following command will run to fix it:" + Environment.NewLine +
"'sudo security set-key-partition-list -D localhost -S unsigned:,teamid:UBF8T346G9'" + Environment.NewLine +
"This command will make the certificate key accessible across security partitions and might prompt you for your password. For more information see: https://aka.ms/aspnetcore/2.1/troubleshootcertissues");
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && trust?.HasValue() == true) if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && trust?.HasValue() == true)
{ {
reporter.Warn("Trusting the HTTPS development certificate was requested. If the certificate is not " + reporter.Warn("Trusting the HTTPS development certificate was requested. If the certificate is not " +