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 " +