Skip to content

Commit 70f30e1

Browse files
committed
Add support for trusting dev certs on linux (dotnet#56582)
* Add support for trusting dev certs on linux There's no consistent way to do this that works for all clients on all Linux distros, but this approach gives us pretty good coverage. In particular, we aim to support .NET (esp HttpClient), Chromium, and Firefox on Ubuntu- and Fedora-based distros. Certificate trust is applied per-user, which is simpler and preferable for security reasons, but comes with the notable downside that the process can't be completed within the tool - the user has to update an environment variable, probably in their user profile. In particular, OpenSSL consumes the `SSL_CERT_DIR` environment variable to determine where it should look for trusted certificates. We break establishing trust into two categories: OpenSSL, which backs .NET, and NSS databases (henceforth, nssdb), which backs browsers. To establish trust in OpenSSL, we put the certificate in `~/.dotnet/corefx/cryptography/trusted`, run a simplified version of OpenSSL's `c_rehash` tool on the directory, and ask the user to update `SSL_CERT_DIR`. To establish trust in nssdb, we search the home directory for Firefox profiles and `~/.pki/nssdb`. For each one found, we add an entry to the nssdb therein. Each of these locations (the trusted certificate folder and the list of nssdbs) can be overridden with an environment variable. This large number of steps introduces a problem that doesn't exist on Windows or macOS - the dev cert can end up trusted by some clients but not by others. This change introduces a `TrustLevel` concept so that we can produce clearer output when this happens. The only non-bundled tools required to update certificate trust are `openssl` (the CLI) and `certutil`. `sudo` is not required, since all changes are within the user's home directory. * Also trust certificates in the Current User/Root store A belt-and-suspenders approach for dotnet trust (i.e. in addition to OpenSSL trust) that has the notable advantage of not requiring any environment variables. * Clarify the mac-specific comments in GetDevelopmentCertificateFromStore (cherry picked from commit 27ae082)
1 parent 5059927 commit 70f30e1

File tree

9 files changed

+1100
-69
lines changed

9 files changed

+1100
-69
lines changed

src/Servers/Kestrel/Core/src/Internal/LoggerExtensions.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,7 @@ internal static partial class LoggerExtensions
4040

4141
[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")]
4242
public static partial void DeveloperCertificateNotTrusted(this ILogger<KestrelServer> logger);
43+
44+
[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")]
45+
public static partial void DeveloperCertificatePartiallyTrusted(this ILogger<KestrelServer> logger);
4346
}

src/Servers/Kestrel/Core/src/KestrelServerOptions.cs

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -385,23 +385,34 @@ internal void Serialize(Utf8JsonWriter writer)
385385
return null;
386386
}
387387

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

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

402-
if (!CertificateManager.Instance.IsTrusted(cert))
405+
// On MacOS, this may cause a UI prompt, since it requires accessing the keychain. Kestrel must NEVER
406+
// cause a UI prompt on a production system. We only attempt this here because MacOS is not supported
407+
// in production.
408+
switch (CertificateManager.Instance.GetTrustLevel(cert))
403409
{
404-
logger.DeveloperCertificateNotTrusted();
410+
case CertificateManager.TrustLevel.Partial:
411+
logger.DeveloperCertificatePartiallyTrusted();
412+
break;
413+
case CertificateManager.TrustLevel.None:
414+
logger.DeveloperCertificateNotTrusted();
415+
break;
405416
}
406417

407418
return cert;

src/Shared/CertificateGeneration/CertificateManager.cs

Lines changed: 175 additions & 11 deletions
Large diffs are not rendered by default.

src/Shared/CertificateGeneration/EnsureCertificateResult.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ internal enum EnsureCertificateResult
1111
ErrorSavingTheCertificateIntoTheCurrentUserPersonalStore,
1212
ErrorExportingTheCertificate,
1313
FailedToTrustTheCertificate,
14+
PartiallyFailedToTrustTheCertificate,
1415
UserCancelledTrustStep,
1516
FailedToMakeKeyAccessible,
1617
ExistingHttpsCertificateTrusted,

src/Shared/CertificateGeneration/MacOSCertificateManager.cs

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -69,12 +69,7 @@ internal sealed class MacOSCertificateManager : CertificateManager
6969
"To fix this issue, run 'dotnet dev-certs https --clean' and 'dotnet dev-certs https' " +
7070
"to remove all existing ASP.NET Core development certificates " +
7171
"and create a new untrusted developer certificate. " +
72-
"On macOS or Windows, use 'dotnet dev-certs https --trust' to trust the new certificate.";
73-
74-
public const string KeyNotAccessibleWithoutUserInteraction =
75-
"The application is trying to access the ASP.NET Core developer certificate key. " +
76-
"A prompt might appear to ask for permission to access the key. " +
77-
"When that happens, select 'Always Allow' to grant 'dotnet' access to the certificate key in the future.";
72+
"Use 'dotnet dev-certs https --trust' to trust the new certificate.";
7873

7974
public MacOSCertificateManager()
8075
{
@@ -85,12 +80,14 @@ internal MacOSCertificateManager(string subject, int version)
8580
{
8681
}
8782

88-
protected override void TrustCertificateCore(X509Certificate2 publicCertificate)
83+
protected override TrustLevel TrustCertificateCore(X509Certificate2 publicCertificate)
8984
{
90-
if (IsTrusted(publicCertificate))
85+
var oldTrustLevel = GetTrustLevel(publicCertificate);
86+
if (oldTrustLevel != TrustLevel.None)
9187
{
88+
Debug.Assert(oldTrustLevel == TrustLevel.Full); // Mac trust is all or nothing
9289
Log.MacOSCertificateAlreadyTrusted();
93-
return;
90+
return oldTrustLevel;
9491
}
9592

9693
var tmpFile = Path.GetTempFileName();
@@ -111,6 +108,7 @@ protected override void TrustCertificateCore(X509Certificate2 publicCertificate)
111108
}
112109
}
113110
Log.MacOSTrustCommandEnd();
111+
return TrustLevel.Full;
114112
}
115113
finally
116114
{
@@ -125,7 +123,7 @@ protected override void TrustCertificateCore(X509Certificate2 publicCertificate)
125123
}
126124
}
127125

128-
internal override CheckCertificateStateResult CheckCertificateState(X509Certificate2 candidate, bool interactive)
126+
internal override CheckCertificateStateResult CheckCertificateState(X509Certificate2 candidate)
129127
{
130128
return File.Exists(GetCertificateFilePath(candidate)) ?
131129
new CheckCertificateStateResult(true, null) :
@@ -149,7 +147,7 @@ internal override void CorrectCertificateState(X509Certificate2 candidate)
149147
}
150148

151149
// Use verify-cert to verify the certificate for the SSL and X.509 Basic Policy.
152-
public override bool IsTrusted(X509Certificate2 certificate)
150+
public override TrustLevel GetTrustLevel(X509Certificate2 certificate)
153151
{
154152
var tmpFile = Path.GetTempFileName();
155153
try
@@ -166,7 +164,7 @@ public override bool IsTrusted(X509Certificate2 certificate)
166164
RedirectStandardError = true,
167165
});
168166
checkTrustProcess!.WaitForExit();
169-
return checkTrustProcess.ExitCode == 0;
167+
return checkTrustProcess.ExitCode == 0 ? TrustLevel.Full : TrustLevel.None;
170168
}
171169
finally
172170
{

0 commit comments

Comments
 (0)