[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:
parent
8211a1c313
commit
7f53f7e95b
|
|
@ -518,4 +518,7 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
|
|||
<data name="ConnectionTimedOutByServer" xml:space="preserve">
|
||||
<value>The connection was timed out by the server.</value>
|
||||
</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>
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -1876,6 +1876,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
|
|||
internal static string FormatConnectionTimedOutByServer()
|
||||
=> 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)
|
||||
{
|
||||
var value = _resourceManager.GetString(name);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<X509Certificate2> ListCertificates(
|
||||
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
|
||||
|
||||
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<X509Certificate2> 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<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
|
||||
{
|
||||
}
|
||||
|
|
@ -717,4 +876,4 @@ namespace Microsoft.AspNetCore.Certificates.Generation
|
|||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,8 +13,9 @@ namespace Microsoft.AspNetCore.Certificates.Generation
|
|||
ErrorSavingTheCertificateIntoTheCurrentUserPersonalStore,
|
||||
ErrorExportingTheCertificate,
|
||||
FailedToTrustTheCertificate,
|
||||
UserCancelledTrustStep
|
||||
UserCancelledTrustStep,
|
||||
FailedToMakeKeyAccessible,
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 " +
|
||||
|
|
|
|||
Loading…
Reference in New Issue