Skip to content
Merged
3 changes: 3 additions & 0 deletions src/Servers/Kestrel/Core/src/Internal/LoggerExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,7 @@ internal static partial class LoggerExtensions

[LoggerMessage(8, LogLevel.Warning, "The ASP.NET Core developer certificate is not trusted. For information about trusting the ASP.NET Core developer certificate, see https://aka.ms/aspnet/https-trust-dev-cert", EventName = "DeveloperCertificateNotTrusted")]
public static partial void DeveloperCertificateNotTrusted(this ILogger<KestrelServer> logger);

[LoggerMessage(9, LogLevel.Warning, "The ASP.NET Core developer certificate is only trusted by some clients. For information about trusting the ASP.NET Core developer certificate, see https://aka.ms/aspnet/https-trust-dev-cert", EventName = "DeveloperCertificatePartiallyTrusted")]
public static partial void DeveloperCertificatePartiallyTrusted(this ILogger<KestrelServer> logger);
}
25 changes: 18 additions & 7 deletions src/Servers/Kestrel/Core/src/KestrelServerOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -384,23 +384,34 @@ internal void Serialize(Utf8JsonWriter writer)
return null;
}

var status = CertificateManager.Instance.CheckCertificateState(cert, interactive: false);
var status = CertificateManager.Instance.CheckCertificateState(cert);
if (!status.Success)
{
// Display a warning indicating to the user that a prompt might appear and provide instructions on what to do in that
// case. The underlying implementation of this check is specific to Mac OS and is handled within CheckCertificateState.
// Kestrel must NEVER cause a UI prompt on a production system. We only attempt this here because Mac OS is not supported
// in production.
// Failure is only possible on MacOS and indicates that, if there is a dev cert, it must be from
// a dotnet version prior to 7.0 - newer versions store it in such a way that this check succeeds.
// (Success does not mean that the dev cert has been trusted).
// In practice, success.FailureMessage will always be MacOSCertificateManager.InvalidCertificateState.
// Basically, we're just going to encourage the user to generate and trust the dev cert. We support
// these older certificates not by accepting them as-is, but by modernizing them when dev-certs is run.
// If we detect an issue here, we can avoid a UI prompt below.
Debug.Assert(status.FailureMessage != null, "Status with a failure result must have a message.");
logger.DeveloperCertificateFirstRun(status.FailureMessage);

// Prevent binding to HTTPS if the certificate is not valid (avoid the prompt)
return null;
}

if (!CertificateManager.Instance.IsTrusted(cert))
// On MacOS, this may cause a UI prompt, since it requires accessing the keychain. Kestrel must NEVER
// cause a UI prompt on a production system. We only attempt this here because MacOS is not supported
// in production.
switch (CertificateManager.Instance.GetTrustLevel(cert))
{
logger.DeveloperCertificateNotTrusted();
case CertificateManager.TrustLevel.Partial:
logger.DeveloperCertificatePartiallyTrusted();
break;
case CertificateManager.TrustLevel.None:
logger.DeveloperCertificateNotTrusted();
break;
}

return cert;
Expand Down
186 changes: 175 additions & 11 deletions src/Shared/CertificateGeneration/CertificateManager.cs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ internal enum EnsureCertificateResult
ErrorSavingTheCertificateIntoTheCurrentUserPersonalStore,
ErrorExportingTheCertificate,
FailedToTrustTheCertificate,
PartiallyFailedToTrustTheCertificate,
UserCancelledTrustStep,
FailedToMakeKeyAccessible,
ExistingHttpsCertificateTrusted,
Expand Down
22 changes: 10 additions & 12 deletions src/Shared/CertificateGeneration/MacOSCertificateManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,7 @@ internal sealed class MacOSCertificateManager : CertificateManager
"To fix this issue, run '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.";

public const string KeyNotAccessibleWithoutUserInteraction =
"The application is trying to access the ASP.NET Core developer certificate key. " +
"A prompt might appear to ask for permission to access the key. " +
"When that happens, select 'Always Allow' to grant 'dotnet' access to the certificate key in the future.";
"Use 'dotnet dev-certs https --trust' to trust the new certificate.";

public MacOSCertificateManager()
{
Expand All @@ -85,12 +80,14 @@ internal MacOSCertificateManager(string subject, int version)
{
}

protected override void TrustCertificateCore(X509Certificate2 publicCertificate)
protected override TrustLevel TrustCertificateCore(X509Certificate2 publicCertificate)
{
if (IsTrusted(publicCertificate))
var oldTrustLevel = GetTrustLevel(publicCertificate);
if (oldTrustLevel != TrustLevel.None)
{
Debug.Assert(oldTrustLevel == TrustLevel.Full); // Mac trust is all or nothing
Log.MacOSCertificateAlreadyTrusted();
return;
return oldTrustLevel;
}

var tmpFile = Path.GetTempFileName();
Expand All @@ -111,6 +108,7 @@ protected override void TrustCertificateCore(X509Certificate2 publicCertificate)
}
}
Log.MacOSTrustCommandEnd();
return TrustLevel.Full;
}
finally
{
Expand All @@ -125,7 +123,7 @@ protected override void TrustCertificateCore(X509Certificate2 publicCertificate)
}
}

internal override CheckCertificateStateResult CheckCertificateState(X509Certificate2 candidate, bool interactive)
internal override CheckCertificateStateResult CheckCertificateState(X509Certificate2 candidate)
{
return File.Exists(GetCertificateFilePath(candidate)) ?
new CheckCertificateStateResult(true, null) :
Expand All @@ -149,7 +147,7 @@ internal override void CorrectCertificateState(X509Certificate2 candidate)
}

// Use verify-cert to verify the certificate for the SSL and X.509 Basic Policy.
public override bool IsTrusted(X509Certificate2 certificate)
public override TrustLevel GetTrustLevel(X509Certificate2 certificate)
{
var tmpFile = Path.GetTempFileName();
try
Expand All @@ -166,7 +164,7 @@ public override bool IsTrusted(X509Certificate2 certificate)
RedirectStandardError = true,
});
checkTrustProcess!.WaitForExit();
return checkTrustProcess.ExitCode == 0;
return checkTrustProcess.ExitCode == 0 ? TrustLevel.Full : TrustLevel.None;
}
finally
{
Expand Down
Loading