Skip to content

Commit 32e5f4f

Browse files
authored
[8.0] Add support for trusting dev certs on linux (#57108)
* Look up trusted certs consistently on windows (#56701) * Search for trusted certificates consistently on Windows 1. Don't use thumbprint so we don't get flagged for using SHA-1 2. Make TrustCertificateCore and RemoveCertificateFromTrustedRoots consistent * Add a note about our usage of Thumbprint on macOS * Clean up assumptions about root store * FindBySubjectName expects a string * Search by serial number to avoid having to parse subject name * Fix typo Co-authored-by: Martin Costello <martin@martincostello.com> * Call DisposeCertificates more consistently --------- Co-authored-by: Martin Costello <martin@martincostello.com> (cherry picked from commit ed7ea40) * Make dev-certs import consistent with kestrel (#57014) * Make dev-certs import consistent with kestrel Kestrel checks the subject name and our magic extension - import was only checking the extension. They can't easily share a method because import has a test hook. (cherry picked from commit 06155c0) * Add support for trusting dev certs on linux (#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) * Revert 9.0-specific changes * Restrict permissions to the dev cert directory (#56985) * Create directories with secure permissions If we're creating it, make it 700. If it already exists, warn if it's not 700. * Don't create a directory specified by the user (cherry picked from commit 1470e00)
1 parent e9e4cd7 commit 32e5f4f

File tree

11 files changed

+1352
-114
lines changed

11 files changed

+1352
-114
lines changed

src/ProjectTemplates/Shared/DevelopmentCertificate.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ private static string EnsureDevelopmentCertificates(string certificatePath, stri
3535
var manager = CertificateManager.Instance;
3636
var certificate = manager.CreateAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1));
3737
var certificateThumbprint = certificate.Thumbprint;
38-
CertificateManager.ExportCertificate(certificate, path: certificatePath, includePrivateKey: true, certificatePassword, CertificateKeyExportFormat.Pfx);
38+
manager.ExportCertificate(certificate, path: certificatePath, includePrivateKey: true, certificatePassword, CertificateKeyExportFormat.Pfx);
3939

4040
return certificateThumbprint;
4141
}

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/Servers/Kestrel/Core/src/TlsConfigurationLoader.cs

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -176,20 +176,8 @@ public ListenOptions UseHttpsWithSni(
176176

177177
private static bool IsDevelopmentCertificate(X509Certificate2 certificate)
178178
{
179-
if (!string.Equals(certificate.Subject, "CN=localhost", StringComparison.Ordinal))
180-
{
181-
return false;
182-
}
183-
184-
foreach (var ext in certificate.Extensions)
185-
{
186-
if (string.Equals(ext.Oid?.Value, CertificateManager.AspNetHttpsOid, StringComparison.Ordinal))
187-
{
188-
return true;
189-
}
190-
}
191-
192-
return false;
179+
return string.Equals(certificate.Subject, CertificateManager.LocalhostHttpsDistinguishedName, StringComparison.Ordinal) &&
180+
CertificateManager.IsHttpsDevelopmentCertificate(certificate);
193181
}
194182

195183
private static bool TryGetCertificatePath(string applicationName, [NotNullWhen(true)] out string? path)

0 commit comments

Comments
 (0)