diff --git a/src/Servers/Kestrel/Core/src/CoreStrings.resx b/src/Servers/Kestrel/Core/src/CoreStrings.resx
index ee80a16312..2b0326e1d7 100644
--- a/src/Servers/Kestrel/Core/src/CoreStrings.resx
+++ b/src/Servers/Kestrel/Core/src/CoreStrings.resx
@@ -518,4 +518,7 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
The connection was timed out by the server.
+
+ 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.
+
\ No newline at end of file
diff --git a/src/Servers/Kestrel/Core/src/Internal/HttpsConnectionAdapter.cs b/src/Servers/Kestrel/Core/src/Internal/HttpsConnectionAdapter.cs
index 95ee435b43..53f55a9e4e 100644
--- a/src/Servers/Kestrel/Core/src/Internal/HttpsConnectionAdapter.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/HttpsConnectionAdapter.cs
@@ -9,6 +9,7 @@ using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
+using Microsoft.AspNetCore.Certificates.Generation;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core;
@@ -177,8 +178,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
EnsureCertificateIsAllowedForServerAuth(serverCert);
}
}
+
await sslStream.AuthenticateAsServerAsync(serverCert, certificateRequired,
- _options.SslProtocols, _options.CheckCertificateRevocation);
+ _options.SslProtocols, _options.CheckCertificateRevocation);
#endif
}
catch (OperationCanceledException)
@@ -187,12 +189,28 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
sslStream.Dispose();
return _closedAdaptedConnection;
}
- catch (Exception ex) when (ex is IOException || ex is AuthenticationException)
+ catch (IOException ex)
{
_logger?.LogDebug(1, ex, CoreStrings.AuthenticationFailed);
sslStream.Dispose();
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
{
timeoutFeature.CancelTimeout();
diff --git a/src/Servers/Kestrel/Core/src/Properties/CoreStrings.Designer.cs b/src/Servers/Kestrel/Core/src/Properties/CoreStrings.Designer.cs
index c813873491..164573901f 100644
--- a/src/Servers/Kestrel/Core/src/Properties/CoreStrings.Designer.cs
+++ b/src/Servers/Kestrel/Core/src/Properties/CoreStrings.Designer.cs
@@ -1876,6 +1876,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
internal static string FormatConnectionTimedOutByServer()
=> GetString("ConnectionTimedOutByServer");
+ ///
+ /// 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.
+ ///
+ internal static string BadDeveloperCertificateState
+ {
+ get => GetString("BadDeveloperCertificateState");
+ }
+
+ ///
+ /// 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.
+ ///
+ internal static string FormatBadDeveloperCertificateState()
+ => GetString("BadDeveloperCertificateState");
+
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);
diff --git a/src/Servers/Kestrel/shared/test/TestResources.cs b/src/Servers/Kestrel/shared/test/TestResources.cs
index 3218a1eaca..a5bb04b9de 100644
--- a/src/Servers/Kestrel/shared/test/TestResources.cs
+++ b/src/Servers/Kestrel/shared/test/TestResources.cs
@@ -22,5 +22,10 @@ namespace Microsoft.AspNetCore.Testing
{
return new X509Certificate2(GetCertPath(certName), "testPassword");
}
+
+ public static X509Certificate2 GetTestCertificate(string certName, string password)
+ {
+ return new X509Certificate2(GetCertPath(certName), password);
+ }
}
}
diff --git a/src/Servers/Kestrel/test/FunctionalTests/HttpsTests.cs b/src/Servers/Kestrel/test/FunctionalTests/HttpsTests.cs
index 9de8a29b48..8a538b862f 100644
--- a/src/Servers/Kestrel/test/FunctionalTests/HttpsTests.cs
+++ b/src/Servers/Kestrel/test/FunctionalTests/HttpsTests.cs
@@ -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.
using System;
diff --git a/src/Shared/CertificateGeneration/CertificateManager.cs b/src/Shared/CertificateGeneration/CertificateManager.cs
index 4e2a0a9964..f8b9b112ce 100644
--- a/src/Shared/CertificateGeneration/CertificateManager.cs
+++ b/src/Shared/CertificateGeneration/CertificateManager.cs
@@ -7,6 +7,7 @@ using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
+using System.Runtime.InteropServices.ComTypes;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
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 + " ";
#endif
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 ListCertificates(
CertificatePurpose purpose,
@@ -147,6 +150,39 @@ namespace Microsoft.AspNetCore.Certificates.Generation
}
}
+ internal static bool IsHttpsDevelopmentCertificate(X509Certificate2 certificate) =>
+ certificate.Extensions.OfType()
+ .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
public X509Certificate2 CreateAspNetCoreHttpsDevelopmentCertificate(DateTimeOffset notBefore, DateTimeOffset notAfter, string subjectOverride)
@@ -192,6 +228,27 @@ namespace Microsoft.AspNetCore.Certificates.Generation
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 GetHttpsCertificates() =>
+ ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: true, requireExportable: true);
+
public X509Certificate2 CreateApplicationTokenSigningDevelopmentCertificate(DateTimeOffset notBefore, DateTimeOffset notAfter, string subjectOverride)
{
var subject = new X500DistinguishedName(subjectOverride ?? IdentityDistinguishedName);
@@ -596,9 +653,10 @@ namespace Microsoft.AspNetCore.Certificates.Generation
bool trust = false,
bool includePrivateKey = false,
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(
@@ -610,7 +668,7 @@ namespace Microsoft.AspNetCore.Certificates.Generation
string password = null,
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(
@@ -621,7 +679,8 @@ namespace Microsoft.AspNetCore.Certificates.Generation
bool trust = false,
bool includePrivateKey = false,
string password = null,
- string subjectOverride = null)
+ string subjectOverride = null,
+ bool isInteractive = true)
{
if (purpose == CertificatePurpose.All)
{
@@ -633,6 +692,33 @@ namespace Microsoft.AspNetCore.Certificates.Generation
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;
X509Certificate2 certificate = null;
@@ -672,6 +758,11 @@ namespace Microsoft.AspNetCore.Certificates.Generation
{
return EnsureCertificateResult.ErrorSavingTheCertificateIntoTheCurrentUserPersonalStore;
}
+
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && isInteractive)
+ {
+ MakeCertificateKeyAccessibleAcrossPartitions(certificate);
+ }
}
if (path != null)
{
@@ -704,6 +795,74 @@ namespace Microsoft.AspNetCore.Certificates.Generation
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();
+ try
+ {
+ using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser))
+ {
+ store.Open(OpenFlags.ReadOnly);
+ certificates.AddRange(store.Certificates.OfType());
+ IEnumerable 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()
+ .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
{
}
@@ -717,4 +876,4 @@ namespace Microsoft.AspNetCore.Certificates.Generation
}
#endif
}
-}
\ No newline at end of file
+}
diff --git a/src/Shared/CertificateGeneration/EnsureCertificateResult.cs b/src/Shared/CertificateGeneration/EnsureCertificateResult.cs
index d3c86ce05d..ee2c6976b2 100644
--- a/src/Shared/CertificateGeneration/EnsureCertificateResult.cs
+++ b/src/Shared/CertificateGeneration/EnsureCertificateResult.cs
@@ -13,8 +13,9 @@ namespace Microsoft.AspNetCore.Certificates.Generation
ErrorSavingTheCertificateIntoTheCurrentUserPersonalStore,
ErrorExportingTheCertificate,
FailedToTrustTheCertificate,
- UserCancelledTrustStep
+ UserCancelledTrustStep,
+ FailedToMakeKeyAccessible,
}
}
-#endif
\ No newline at end of file
+#endif
diff --git a/src/Tools/FirstRunCertGenerator/src/CertificateGenerator.cs b/src/Tools/FirstRunCertGenerator/src/CertificateGenerator.cs
index d3f58eae35..d3a94baf2e 100644
--- a/src/Tools/FirstRunCertGenerator/src/CertificateGenerator.cs
+++ b/src/Tools/FirstRunCertGenerator/src/CertificateGenerator.cs
@@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.DeveloperCertificates.XPlat
{
var manager = new CertificateManager();
var now = DateTimeOffset.Now;
- manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1));
+ manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), isInteractive: false);
}
}
}
diff --git a/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs b/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs
index f6503673e5..d786f78336 100644
--- a/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs
+++ b/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs
@@ -42,7 +42,7 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests
// 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.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.Equal(EnsureCertificateResult.Succeeded, result);
@@ -140,12 +140,12 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests
DateTimeOffset now = DateTimeOffset.UtcNow;
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);
// 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.Equal(EnsureCertificateResult.ValidCertificatePresent, result);
@@ -172,7 +172,7 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests
DateTimeOffset now = DateTimeOffset.UtcNow;
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);
}
@@ -184,7 +184,7 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests
DateTimeOffset now = DateTimeOffset.UtcNow;
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);
@@ -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));
}
}
-
- [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(),
- e => e is X509BasicConstraintsExtension basicConstraints &&
- basicConstraints.Critical == true &&
- basicConstraints.CertificateAuthority == false &&
- basicConstraints.HasPathLengthConstraint == false &&
- basicConstraints.PathLengthConstraint == 0);
-
- Assert.Contains(
- identityCertificate.Extensions.OfType(),
- e => e is X509KeyUsageExtension keyUsage &&
- keyUsage.Critical == true &&
- keyUsage.KeyUsages == X509KeyUsageFlags.DigitalSignature);
-
- Assert.Contains(
- identityCertificate.Extensions.OfType(),
- e => e is X509EnhancedKeyUsageExtension enhancedKeyUsage &&
- enhancedKeyUsage.Critical == true &&
- enhancedKeyUsage.EnhancedKeyUsages.OfType().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(),
- 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());
- }
}
}
diff --git a/src/Tools/dotnet-dev-certs/src/Program.cs b/src/Tools/dotnet-dev-certs/src/Program.cs
index 170e11b09d..80f6280ee4 100644
--- a/src/Tools/dotnet-dev-certs/src/Program.cs
+++ b/src/Tools/dotnet-dev-certs/src/Program.cs
@@ -24,6 +24,7 @@ namespace Microsoft.AspNetCore.DeveloperCertificates.Tools
private const int ErrorNoValidCertificateFound = 6;
private const int ErrorCertificateNotTrusted = 7;
private const int ErrorCleaningUpCertificates = 8;
+ private const int ErrorMacOsCertificateKeyCouldNotBeAccessible = 9;
public static readonly TimeSpan HttpsCertificateValidity = TimeSpan.FromDays(365);
@@ -157,7 +158,16 @@ namespace Microsoft.AspNetCore.DeveloperCertificates.Tools
}
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())
@@ -184,6 +194,13 @@ namespace Microsoft.AspNetCore.DeveloperCertificates.Tools
var now = DateTimeOffset.Now;
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)
{
reporter.Warn("Trusting the HTTPS development certificate was requested. If the certificate is not " +