Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for trusting dev certs on linux #56582

Merged
merged 12 commits into from
Jul 29, 2024
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
Loading