[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">
|
<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>
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,9 @@ namespace Microsoft.AspNetCore.Certificates.Generation
|
||||||
ErrorSavingTheCertificateIntoTheCurrentUserPersonalStore,
|
ErrorSavingTheCertificateIntoTheCurrentUserPersonalStore,
|
||||||
ErrorExportingTheCertificate,
|
ErrorExportingTheCertificate,
|
||||||
FailedToTrustTheCertificate,
|
FailedToTrustTheCertificate,
|
||||||
UserCancelledTrustStep
|
UserCancelledTrustStep,
|
||||||
|
FailedToMakeKeyAccessible,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 " +
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue