Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/dotnet/aspnetcore.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorjacalvar <jacalvar@microsoft.com>2022-05-24 12:01:11 +0300
committerjacalvar <jacalvar@microsoft.com>2022-05-30 21:36:48 +0300
commit18b5cc6783f1363127797c992edd37ecb58fe104 (patch)
tree366290a4b1ed9b578814567f53adf41456f6f169
parent0af1274dea32a2103f383933b557c5ed2714d569 (diff)
[dotnet-dev-certs] Adds support for --trust on Linux systemsjaviercn/dev-certs-linux-trust
* Trust requires some tools to be installed in the target distribution: * openssl (greater than 1.1.1k) * c_rehash * certutil * Trust works based on the software you have installed on the machine * If you don't have a browser we support, we don't consider it on our decissions about whether a certificate is trusted or not. * If you later on install a new supported browser, then when you check for trust, the certificate will be reported as untrusted. And when you run the tool again, the appropriate action will be taken just for that software. * We display warnings when some of the prerequisites for trust are not met. * We tailor the warnings based on the software you have installed. * If you don't have a given browser installed, you won't see warnings for it. * We provide environment variables that we check in some cases if there is an issue with one of the prerequisites and we can't detect the correct inputs. * You can provide the Open SSL certificates dir explicitly. * The path to the Firefox profile in use. * The path to the Chrome/Edge certificates database.
-rw-r--r--src/Shared/CertificateGeneration/CertificateManager.cs111
-rw-r--r--src/Shared/CertificateGeneration/CertificateTrustPrerequisite.cs6
-rw-r--r--src/Shared/CertificateGeneration/UnixCertificateManager.cs613
-rw-r--r--src/Tools/dotnet-dev-certs/src/Program.cs99
4 files changed, 800 insertions, 29 deletions
diff --git a/src/Shared/CertificateGeneration/CertificateManager.cs b/src/Shared/CertificateGeneration/CertificateManager.cs
index ec4c1db015..cf66fd2d77 100644
--- a/src/Shared/CertificateGeneration/CertificateManager.cs
+++ b/src/Shared/CertificateGeneration/CertificateManager.cs
@@ -177,7 +177,7 @@ internal abstract class CertificateManager
var result = EnsureCertificateResult.Succeeded;
var currentUserCertificates = ListCertificates(StoreName.My, StoreLocation.CurrentUser, isValid: true, requireExportable: true);
- var trustedCertificates = ListCertificates(StoreName.My, StoreLocation.LocalMachine, isValid: true, requireExportable: true);
+ var trustedCertificates = ListCertificates(StoreName.Root, StoreLocation.LocalMachine, isValid: true, requireExportable: true);
var certificates = currentUserCertificates.Concat(trustedCertificates);
var filteredCertificates = certificates.Where(c => c.Subject == Subject);
@@ -434,6 +434,9 @@ internal abstract class CertificateManager
}
}
+ public virtual IList<CertificateTrustPrerequisite> CheckTrustPrerequisites() =>
+ Array.Empty<CertificateTrustPrerequisite>();
+
public abstract bool IsTrusted(X509Certificate2 certificate);
protected abstract X509Certificate2 SaveCertificateCore(X509Certificate2 certificate, StoreName storeName, StoreLocation storeLocation);
@@ -970,6 +973,112 @@ internal abstract class CertificateManager
[Event(64, Level = EventLevel.Error, Message = "The provided certificate '{0}' is not a valid ASP.NET Core HTTPS development certificate.")]
internal void NoHttpsDevelopmentCertificate(string description) => WriteEvent(64, description);
+
+ [Event(65, Level = EventLevel.Error, Message = "The Open SSL version '{0}' installed is too old. Open SSL 1.1.1k or higher is required.")]
+ internal void OldOpenSSLVersion(string version) => WriteEvent(65, version);
+
+ [Event(66, Level = EventLevel.Error, Message = "Unable to find 'certutil'. Check on what package to install it from in your distribution.")]
+ internal void MissingCertUtil(string error) => WriteEvent(66, error);
+
+ [Event(67, Level = EventLevel.Verbose, Message = "Found a valid Open SSL version '{0}'.")]
+ internal void ValidOpenSSLVersion(string version) => WriteEvent(67, version);
+
+ [Event(68, Level = EventLevel.Verbose, Message = "'certutil' is available.")]
+ internal void FoundCertUtil() => WriteEvent(68);
+
+ [Event(69, Level = EventLevel.Verbose, Message = "Removing the certificate from the Open SSL trust roots.")]
+ public void UnixRemoveCertificateFromRootStoreStart() => WriteEvent(69);
+
+ [Event(70, Level = EventLevel.Verbose, Message = "Finished removing the certificate from the Open SSL trust roots.")]
+ public void UnixRemoveCertificateFromRootStoreEnd() => WriteEvent(70);
+
+ [Event(71, Level = EventLevel.Verbose, Message = "The certificate was not found in the Open SSL trust roots.")]
+ public void UnixRemoveCertificateFromRootStoreNotFound() => WriteEvent(71);
+
+ [Event(72, Level = EventLevel.Error, Message = "Failed to delete file '{0}'. Error: '{1}'")]
+ public void UnixRemoveCertificateFromRootStoreFailedtoDeleteFile(string path, string error) => WriteEvent(72, path, error);
+
+ [Event(73, Level = EventLevel.Error, Message = "Open SSL 'c_rehash' failed: '{0}'.")]
+ public void UnixRemoveCertificateFromRootStoreOpenSSLRehashFailed(string error) => WriteEvent(73, error);
+
+ [Event(74, Level = EventLevel.Verbose, Message = "Firefox profile not found. Search locations: '{0}', '{1}'.")]
+ internal void UnixFirefoxProfileNotFound(string primary, string secondary) => WriteEvent(74, primary, secondary);
+
+ [Event(75, Level = EventLevel.Verbose, Message = "Firefox profile found at: '{0}'.")]
+ internal void UnixFirefoxProfileFound(string firefoxDbPath) => WriteEvent(75, firefoxDbPath);
+
+ [Event(76, Level = EventLevel.Verbose, Message = "Certificate store for Edge and Chrome not found. Search location: '{0}'.")]
+ internal void UnixCommonChromeAndEdgeCertificateDbNotFound(string primary) => WriteEvent(76, primary);
+
+ [Event(77, Level = EventLevel.Verbose, Message = "Certificate store for Edge and Chrome found at: '{0}'.")]
+ internal void UnixCommonChromeAndEdgeCertificateDbFound(string edgeChromeDbPath) => WriteEvent(77, edgeChromeDbPath);
+
+ [Event(78, Level = EventLevel.Error, Message = "An error has occurred while removing the certificate from the Edge and Chrome trust roots: '{0}'.")]
+ internal void UnixRemoveCertificateFromCommonEdgeChromeRootStoreError(string error) => WriteEvent(78, error);
+
+ [Event(79, Level = EventLevel.Error, Message = "An error has occurred while removing the certificate from the Firefox trust roots: '{0}'.")]
+ internal void UnixRemoveCertificateFromFirefoxRootStoreError(string error) => WriteEvent(79, error);
+
+ [Event(80, Level = EventLevel.Error, Message = "Failed to find the Open SSL certificates directory.")]
+ internal void UnixFailedToLocateOpenSSLDirectory(string error) => WriteEvent(80, error);
+
+ [Event(81, Level = EventLevel.Verbose, Message = "Open SSL directory found at: '{0}'.")]
+ internal void UnixOpenSSLDirectoryLocatedAt(string openSSLDirectory) => WriteEvent(81, openSSLDirectory);
+
+ [Event(82, Level = EventLevel.Error, Message = "An error has occurred while copying the ASP.NET Core certificate to the Open SSL certificate store: '{0}'.")]
+ internal void UnixCopyCertificateToOpenSSLCertificateStoreError(string copyError) => WriteEvent(82, copyError);
+
+ [Event(83, Level = EventLevel.Error, Message = "An error has occurred while running 'c_rehash' to trust the certificate in Open SSL.")]
+ internal void UnixTrustCertificateFromRootStoreOpenSSLRehashFailed(string rehashError) => WriteEvent(83, rehashError);
+
+ [Event(84, Level = EventLevel.Error, Message = "An error has occurred while adding the certificate to the Firefox trust roots: '{0}'.")]
+ internal void UnixTrustCertificateFirefoxRootStoreError(string error) => WriteEvent(84, error);
+
+ [Event(85, Level = EventLevel.Error, Message = "An error has occurred while adding the certificate to the Chrome and Edge trust roots: '{0}'.")]
+ internal void UnixTrustCertificateCommonEdgeChromeRootStoreError(string error) => WriteEvent(85, error);
+
+ [Event(86, Level = EventLevel.Verbose, Message = "Open SSL not found in the path.")]
+ internal void MissingOpenSsl() => WriteEvent(86);
+
+ [Event(87, Level = EventLevel.Verbose, Message = "The dotnet to dotnet trust can not be checked because the necessary tools are not available.")]
+ internal void UnixNoDotNetToDotNetTrustCheck() => WriteEvent(87);
+
+ [Event(88, Level = EventLevel.Verbose, Message = "The certificate can not be trusted by dotnet because the necessary tools are not available.")]
+ internal void UnixCannotTrustDotNetToDotNet() => WriteEvent(88);
+
+ [Event(89, Level = EventLevel.Verbose, Message = "Certificate '{0}' already trusted by Open SSL.")]
+ internal void UnixOpensslCertificateAlreadyTrusted(string certificate) => WriteEvent(89, certificate);
+
+ [Event(90, Level = EventLevel.Verbose, Message = "The certificate can not be trusted by Firefox because the necessary tools are not available.")]
+ internal void UnixCannotTrustFirefox() => WriteEvent(90);
+
+ [Event(91, Level = EventLevel.Verbose, Message = "Certificate '{0}' already trusted by Firefox.")]
+ internal void UnixFirefoxCertificateAlreadyTrusted(string certificate) => WriteEvent(91, certificate);
+
+ [Event(92, Level = EventLevel.Verbose, Message = "The certificate can not be trusted by Edge/Chrome because the necessary tools are not available.")]
+ internal void UnixCannotTrustEdgeChrome() => WriteEvent(92);
+
+ [Event(93, Level = EventLevel.Verbose, Message = "Certificate '{0}' already trusted by Edge/Chrome.")]
+ internal void UnixEdgeChromeCertificateAlreadyTrusted(string certificate) => WriteEvent(93, certificate);
+
+ [Event(94, Level = EventLevel.Verbose, Message = "Certificate '{0}' was not trusted by Open SSL.")]
+ internal void UnixOpensslCertificateAlreadyUntrusted(string certificate) => WriteEvent(94, certificate);
+
+ [Event(95, Level = EventLevel.Verbose, Message = "Certificate '{0}' was not trusted by Firefox.")]
+ internal void UnixFirefoxCertificateAlreadyUntrusted(string certificate) => WriteEvent(95, certificate);
+
+ [Event(96, Level = EventLevel.Verbose, Message = "Certificate '{0}' was not trusted by Edge/Chrome.")]
+ internal void UnixEdgeChromeCertificateAlreadyUntrusted(string certificate) => WriteEvent(96, certificate);
+
+ [Event(97, Level = EventLevel.Verbose, Message = "The certificate trust for dotnet can not be removed because the necessary tools are not available.")]
+ internal void UnixCannotUntrustDotNetToDotNet() => WriteEvent(97);
+
+ [Event(98, Level = EventLevel.Verbose, Message = "The certificate trust for Firefox can not be removed because the necessary tools are not available.")]
+ internal void UnixCannotUntrustFirefox() => WriteEvent(98);
+
+ [Event(99, Level = EventLevel.Verbose, Message = "The certificate for Edge/Chrome can not be removed because the necessary tools are not available.")]
+ internal void UnixCannotUntrustEdgeChrome() => WriteEvent(99);
+
}
internal sealed class UserCancelledTrustException : Exception
diff --git a/src/Shared/CertificateGeneration/CertificateTrustPrerequisite.cs b/src/Shared/CertificateGeneration/CertificateTrustPrerequisite.cs
new file mode 100644
index 0000000000..557ec6b7ef
--- /dev/null
+++ b/src/Shared/CertificateGeneration/CertificateTrustPrerequisite.cs
@@ -0,0 +1,6 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.AspNetCore.Certificates.Generation;
+
+internal record struct CertificateTrustPrerequisite(string Tool, bool IsImportant, string Message);
diff --git a/src/Shared/CertificateGeneration/UnixCertificateManager.cs b/src/Shared/CertificateGeneration/UnixCertificateManager.cs
index d322355689..9075cfa5c9 100644
--- a/src/Shared/CertificateGeneration/UnixCertificateManager.cs
+++ b/src/Shared/CertificateGeneration/UnixCertificateManager.cs
@@ -1,14 +1,32 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+#nullable disable
using System;
using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Linq;
using System.Security.Cryptography.X509Certificates;
+using System.Text.RegularExpressions;
namespace Microsoft.AspNetCore.Certificates.Generation;
internal sealed class UnixCertificateManager : CertificateManager
{
+ private List<CertificateTrustPrerequisite> _trustPrerequisites;
+ private string _openSSLPath;
+ private string _openSSLDir;
+ private bool _supportedOpenSSLVersion;
+ private string _cRehashPath;
+ private string _certUtilPath;
+ private string _chromePath;
+ private string _edgePath;
+ private string _firefoxPath;
+ private string _googleChromeAndEdgeNssDbPath;
+ private string _firefoxNssDbPath;
+
public UnixCertificateManager()
{
}
@@ -18,7 +36,188 @@ internal sealed class UnixCertificateManager : CertificateManager
{
}
- public override bool IsTrusted(X509Certificate2 certificate) => false;
+ public override IList<CertificateTrustPrerequisite> CheckTrustPrerequisites()
+ {
+ if (_trustPrerequisites != null)
+ {
+ return _trustPrerequisites;
+ }
+
+ _trustPrerequisites = new List<CertificateTrustPrerequisite>();
+ _openSSLPath = IsInstalled("openssl");
+ if (_openSSLPath == null)
+ {
+ Log.MissingOpenSsl();
+ _trustPrerequisites.Add(new("openssl", true, "'openssl' is not installed. We will not be able to validate that openssl and dotnet trust the certificate."));
+ }
+ else
+ {
+ _supportedOpenSSLVersion = IsSupportedOpenSslVersion(out var version);
+ if (!_supportedOpenSSLVersion)
+ {
+ Log.OldOpenSSLVersion(version);
+ _trustPrerequisites.Add(new("openssl", true, $"The available version '{version}' of Open SSL is too old. Update to a version of Open SSL 1.1.1k or newer."));
+ }
+ else
+ {
+ Log.ValidOpenSSLVersion(version);
+ }
+
+ _openSSLDir = GetOpenSSLDirectory();
+ if (string.IsNullOrEmpty(_openSSLDir))
+ {
+ var openSslDirPrereq = new CertificateTrustPrerequisite(
+ "openssl",
+ true,
+ $"Unable to determine the OPENSSLDIR via 'openssl version -d'. Alternatively, provide the directory manually via the 'SSL_CERT_DIR' environment variable.");
+ _trustPrerequisites.Add(openSslDirPrereq);
+ }
+ }
+ _cRehashPath = IsInstalled("c_rehash");
+ if (_cRehashPath == null)
+ {
+ _trustPrerequisites.Add(new("c_rehash", true, "'c_rehash' is not installed. We will not be able to make openssl and dotnet trust the certificate."));
+ }
+
+ _certUtilPath = IsInstalled("certutil");
+ if (_certUtilPath == null)
+ {
+ _trustPrerequisites.Add(new("certutil", true, "'certutil' is not installed. We will not be able to make Firefox, Edge or Chrome trust the certificate."));
+ Log.MissingCertUtil("certutil is not available on the path.");
+ }
+ else
+ {
+ Log.FoundCertUtil();
+ }
+
+ _chromePath = IsInstalled("google-chrome");
+ _edgePath = IsInstalled("microsoft-edge");
+ _firefoxPath = IsInstalled("firefox");
+
+ if (_chromePath == null)
+ {
+ _trustPrerequisites.Add(new("chrome", false, "'Google Chrome' not detected."));
+ }
+
+ if (_edgePath == null)
+ {
+ _trustPrerequisites.Add(new("edge", false, "'Microsoft Edge' not detected."));
+ }
+
+ if (_firefoxPath == null)
+ {
+ _trustPrerequisites.Add(new("firefox", false, "'Mozilla Firefox' not detected."));
+ }
+
+ _googleChromeAndEdgeNssDbPath = GetEdgeAndChromeDbDirectory();
+
+ if (_certUtilPath != null && (_chromePath != null || _edgePath != null) && _googleChromeAndEdgeNssDbPath == null)
+ {
+ _trustPrerequisites.Add(new("edge", true, $"We could not detect the path of the NSS database used by Edge and Chrome. Alternatively, provide the path manually via " +
+ $"the 'ASPNETCORE_DEV_CERTS_EDGE_CHROME_NSSDB_PATH' environment variable. " +
+ $"Locations searched:" + Environment.NewLine + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".pki/nssdb")));
+ }
+
+ _firefoxNssDbPath = GetFirefoxCertificateDbDirectory();
+
+ if (_certUtilPath != null && _firefoxPath != null && _firefoxNssDbPath == null)
+ {
+ _trustPrerequisites.Add(new("firefox", true, $"We could not detect the Firefox profile. Alternatively, provide the path manually via " +
+ $"the 'ASPNETCORE_DEV_CERTS_FIREFOX_PROFILE_PATH' environment variable. You can locate the Firefox profile by visiting about:profiles in Firefox. " +
+ $"Locations searched:" + Environment.NewLine + string.Join($" {Environment.NewLine}", GetFirefoxProfileLocations())));
+ }
+
+ return _trustPrerequisites;
+
+ static IEnumerable<string> GetFirefoxProfileLocations()
+ {
+ yield return "~/.mozilla/firefox/*.default-release";
+ yield return "~/snap/firefox/common/.mozilla/firefox/*.default-release";
+ yield return "~/snap/firefox/common/.mozilla/firefox/*.default";
+ }
+ }
+
+ private static string IsInstalled(string tool)
+ {
+ using var process = Process.Start(new ProcessStartInfo("which", tool) { RedirectStandardError = true, RedirectStandardOutput = true });
+ process.WaitForExit();
+ return process.ExitCode == 0 ? process.StandardOutput.ReadToEnd() : null;
+ }
+
+ public override bool IsTrusted(X509Certificate2 certificate)
+ {
+ var trustChecks = CalculateTrustDetails(certificate);
+ return trustChecks.IsTrusted();
+ }
+
+ private TrustChecks CalculateTrustDetails(X509Certificate2 certificate)
+ {
+ var tempCertificate = Path.Combine(Path.GetTempPath(), $"aspnetcore-localhost-{certificate.Thumbprint}.crt");
+ bool? trustedByOpenSSL = null;
+ bool? trustedByFirefox = null;
+ bool? trustedByEdgeChrome = null;
+
+ try
+ {
+ File.WriteAllText(tempCertificate, certificate.ExportCertificatePem());
+ if (!_trustPrerequisites.Any(p => p.Tool == "openssl"))
+ {
+ var program = RunScriptAndCaptureOutput("openssl", $"verify {tempCertificate}");
+ trustedByOpenSSL = program.ExitCode == 0;
+ }
+ else
+ {
+ Log.UnixNoDotNetToDotNetTrustCheck();
+ }
+ }
+ finally
+ {
+ if (File.Exists(tempCertificate))
+ {
+ File.Delete(tempCertificate);
+ }
+ }
+
+ if (_certUtilPath != null && _firefoxNssDbPath != null)
+ {
+ trustedByFirefox = IsTrustedInNssDb(_firefoxNssDbPath, certificate);
+ }
+
+ if (_certUtilPath != null && _googleChromeAndEdgeNssDbPath != null)
+ {
+ trustedByEdgeChrome = IsTrustedInNssDb(_googleChromeAndEdgeNssDbPath, certificate);
+ }
+
+ return new(trustedByOpenSSL, trustedByFirefox, trustedByEdgeChrome);
+ }
+
+ private static string GetEdgeAndChromeDbDirectory()
+ {
+ var pathFromEnvironment = Environment.GetEnvironmentVariable("ASPNETCORE_DEV_CERTS_EDGE_CHROME_NSSDB_PATH");
+ if (!string.IsNullOrEmpty(pathFromEnvironment))
+ {
+ return pathFromEnvironment;
+ }
+
+ var directory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".pki/nssdb");
+ return Directory.Exists(directory) ? directory : null;
+ }
+
+ private static bool IsTrustedInNssDb(string dbPath, X509Certificate2 certificate)
+ {
+ if (dbPath != null)
+ {
+ // The fact that the certificate is present in this database is enough to
+ // consider it trusted. Otherwise, it would be a bug in our code.
+ var (exitCode, _, _) = RunScriptAndCaptureOutput(
+ "certutil",
+ $"-L -d sql:{dbPath} -n aspnetcore-localhost-{certificate.Thumbprint[0..6]}");
+
+ return exitCode == 0;
+ }
+
+ return false;
+ }
protected override X509Certificate2 SaveCertificateCore(X509Certificate2 certificate, StoreName storeName, StoreLocation storeLocation)
{
@@ -50,16 +249,422 @@ internal sealed class UnixCertificateManager : CertificateManager
protected override bool IsExportable(X509Certificate2 c) => true;
- protected override void TrustCertificateCore(X509Certificate2 certificate) =>
- throw new InvalidOperationException("Trusting the certificate is not supported on linux");
+ protected override void TrustCertificateCore(X509Certificate2 certificate)
+ {
+ // We already did all the needed checks and this method tells us the information we need to determine if
+ // we need to attempt trusting the certificate in any of the components we support.
+ var trustDetails = CalculateTrustDetails(certificate);
+
+ var exceptions = new List<Exception>();
+ var hasErrors = false;
+
+ var certificateName = $"aspnetcore-localhost-{certificate.Thumbprint}.crt";
+ var tempCertificate = Path.Combine(Path.GetTempPath(), certificateName);
+ File.WriteAllText(tempCertificate, certificate.ExportCertificatePem());
+
+ if (trustDetails.OpenSSL == false && !_trustPrerequisites.Any(p => p.Tool == "c_rehash"))
+ {
+ try
+ {
+ // Copy
+ var (copyExitCode, _, copyError) = RunScriptAndCaptureOutput("sudo", $"cp {tempCertificate} {_openSSLDir}");
+ if (copyExitCode != 0)
+ {
+ Log.UnixCopyCertificateToOpenSSLCertificateStoreError(copyError);
+ hasErrors = true;
+ }
+
+ // Rehash
+ try
+ {
+ var (exitCode, _, rehashError) = RunScriptAndCaptureOutput("sudo", "c_rehash");
+ if (exitCode != 0)
+ {
+ Log.UnixTrustCertificateFromRootStoreOpenSSLRehashFailed(rehashError);
+ hasErrors = true;
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.UnixTrustCertificateFromRootStoreOpenSSLRehashFailed(ex.Message);
+ exceptions.Add(ex);
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.UnixCopyCertificateToOpenSSLCertificateStoreError(ex.Message);
+ exceptions.Add(ex);
+ }
+ }
+ else
+ {
+ if (trustDetails.OpenSSL == true)
+ {
+ Log.UnixOpensslCertificateAlreadyTrusted(GetDescription(certificate));
+ }
+ else
+ {
+ Log.UnixCannotTrustDotNetToDotNet();
+ }
+ }
+
+ if (trustDetails.Firefox == false)
+ {
+ try
+ {
+ if (!TryTrustCertificateInNssDb(_firefoxNssDbPath, certificate, tempCertificate, out var command, out var errorMessage))
+ {
+ Log.UnixTrustCertificateFirefoxRootStoreError($"Failed to run the command '{command}'.{Environment.NewLine}{errorMessage}");
+ hasErrors = true;
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.UnixTrustCertificateFirefoxRootStoreError(ex.Message);
+ exceptions.Add(ex);
+ }
+ }
+ else
+ {
+ if (trustDetails.Firefox == true)
+ {
+ Log.UnixFirefoxCertificateAlreadyTrusted(GetDescription(certificate));
+ }
+ else
+ {
+ Log.UnixCannotTrustFirefox();
+ }
+ }
+
+ if (trustDetails.EdgeChrome == false)
+ {
+ try
+ {
+ if (!TryTrustCertificateInNssDb(_googleChromeAndEdgeNssDbPath, certificate, tempCertificate, out var command, out var errorMessage))
+ {
+ Log.UnixTrustCertificateCommonEdgeChromeRootStoreError($"Failed to run the command '{command}'.{Environment.NewLine}{errorMessage}");
+ hasErrors = true;
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.UnixTrustCertificateCommonEdgeChromeRootStoreError(ex.Message);
+ exceptions.Add(ex);
+ }
+ }
+ else
+ {
+ if (trustDetails.EdgeChrome == true)
+ {
+ Log.UnixEdgeChromeCertificateAlreadyTrusted(GetDescription(certificate));
+ }
+ else
+ {
+ Log.UnixCannotTrustEdgeChrome();
+ }
+
+ }
+
+ if (hasErrors || exceptions.Any())
+ {
+ if (hasErrors)
+ {
+ throw new InvalidOperationException("There were some errors trusting the certificate. Use --verbose to get more details.");
+ }
+ else
+ {
+ throw exceptions.Count == 1 ? exceptions[0] : new AggregateException("There were some errors trusting the certificate. Use --verbose to get more details.", exceptions);
+ }
+ }
+ }
+
+ private static bool TryTrustCertificateInNssDb(string dbPath, X509Certificate2 certificate, string certificatePath, out string command, out string error)
+ {
+ command = null;
+ error = null;
+ if (dbPath != null)
+ {
+ var result = RunScriptAndCaptureOutput(
+ "certutil",
+ $"-A -d sql:{dbPath} -t \"C,,\" -n aspnetcore-localhost-{certificate.Thumbprint[0..6]} -i {certificatePath}");
+ if (result.ExitCode != 0)
+ {
+ command = $"certutil -A -d sql:{dbPath} -t \"C,,\" -n aspnetcore-localhost-{certificate.Thumbprint[0..6]} -i {certificatePath}";
+ error = result.Output + Environment.NewLine + result.Error;
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private static string GetFirefoxCertificateDbDirectory()
+ {
+ var fromEnv = Environment.GetEnvironmentVariable("ASPNETCORE_DEV_CERTS_FIREFOX_PROFILE_PATH");
+ if (!string.IsNullOrEmpty(fromEnv))
+ {
+ return fromEnv;
+ }
+
+ return EnumerateIfExistsInUserProfile(".mozilla/firefox/", "*.default-release") ??
+ EnumerateIfExistsInUserProfile("snap/firefox/common/.mozilla/firefox/", "*.default-release") ??
+ EnumerateIfExistsInUserProfile("snap/firefox/common/.mozilla/firefox/", "*.default");
+ }
+
+ private static string EnumerateIfExistsInUserProfile(string subpath, string pattern)
+ {
+ var directory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), subpath);
+ if (!Directory.Exists(directory))
+ {
+ return null;
+ }
+
+ return Directory.EnumerateDirectories(directory, pattern).SingleOrDefault();
+ }
+
+ private static string GetOpenSSLDirectory()
+ {
+ var fromEnvironment = Environment.GetEnvironmentVariable("SSL_CERT_DIR");
+ if (!string.IsNullOrEmpty(fromEnvironment))
+ {
+ Log.UnixOpenSSLDirectoryLocatedAt(fromEnvironment);
+ return fromEnvironment;
+ }
+
+ var (directoryExitCode, openSSLDirectory, directoryError) = RunScriptAndCaptureOutput(
+ "openssl",
+ "version -d",
+ "OPENSSLDIR: \"(?<libpath>.+?)\"",
+ "libpath");
+
+ if (directoryExitCode != 0 || string.IsNullOrEmpty(openSSLDirectory))
+ {
+ Log.UnixFailedToLocateOpenSSLDirectory(directoryError);
+ return null;
+ }
+ else
+ {
+ Log.UnixOpenSSLDirectoryLocatedAt(openSSLDirectory);
+ }
+
+ return Path.Combine(openSSLDirectory, "certs");
+ }
+
+ private static bool IsSupportedOpenSslVersion(out string output)
+ {
+ (var exitCode, output, _) = RunScriptAndCaptureOutput(
+ "openssl",
+ "version",
+ @"OpenSSL (?<version>\d\.\d.\d(\.\d\w)?)",
+ "version");
+
+ if (exitCode != 0 || string.IsNullOrEmpty(output))
+ {
+ return false;
+ }
+
+ var version = output.Split('.');
+ var major = version[0];
+ var letter = version.Length > 3 ? version[3][1] : 'a';
+ return int.Parse(major, CultureInfo.InvariantCulture) >= 3 || letter >= 'k';
+ }
+
+ private static ProgramOutput RunScriptAndCaptureOutput(string name, string arguments, [StringSyntax("Regex")] string regex = null, string captureName = null)
+ {
+ var processInfo = new ProcessStartInfo(name, arguments)
+ {
+ RedirectStandardOutput = true,
+ RedirectStandardError = true
+ };
+ using var process = Process.Start(processInfo);
+ process.WaitForExit();
+ var output = process.StandardOutput.ReadToEnd();
+ var error = process.StandardError.ReadToEnd();
+ if (process.ExitCode == -1)
+ {
+ return new(process.ExitCode, null, error);
+ }
+
+ if (regex == null || captureName == null)
+ {
+ return new(process.ExitCode, output, null);
+ }
+
+ var versionMatch = Regex.Match(output, regex);
+ if (!versionMatch.Success)
+ {
+ return new(process.ExitCode, null, null);
+ }
+
+ return new(process.ExitCode, versionMatch.Groups[captureName].Value, null);
+ }
protected override void RemoveCertificateFromTrustedRoots(X509Certificate2 certificate)
{
- // No-op here as is benign
+ // We already did all the needed checks and this method tells us the information we need to determine if
+ // we need to attempt trusting the certificate in any of the components we support.
+ var trustDetails = CalculateTrustDetails(certificate);
+
+ var exceptions = new List<Exception>();
+ var hasErrors = false;
+
+ if (trustDetails.OpenSSL == true && !_trustPrerequisites.Any(p => p.Tool == "c_rehash"))
+ {
+ var installedCertificate = Path.Combine(_openSSLDir, "certs", $"aspnetcore-localhost-{certificate.Thumbprint}.crt");
+ try
+ {
+ Log.UnixRemoveCertificateFromRootStoreStart();
+ if (!File.Exists(installedCertificate))
+ {
+ Log.UnixRemoveCertificateFromRootStoreNotFound();
+ }
+ else
+ {
+ var rmResult = RunScriptAndCaptureOutput("sudo", $"rm {installedCertificate}");
+ if (rmResult.ExitCode != 0)
+ {
+ Log.UnixRemoveCertificateFromRootStoreFailedtoDeleteFile(installedCertificate, rmResult.Error);
+ hasErrors = true;
+ }
+ }
+
+ if (!hasErrors)
+ {
+ try
+ {
+ var reHashResult = RunScriptAndCaptureOutput("sudo", "c_rehash");
+ if (reHashResult.ExitCode != 0)
+ {
+ Log.UnixRemoveCertificateFromRootStoreOpenSSLRehashFailed(reHashResult.Error);
+ hasErrors = true;
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.UnixRemoveCertificateFromRootStoreOpenSSLRehashFailed(ex.Message);
+ exceptions.Add(ex);
+ }
+ }
+ Log.UnixRemoveCertificateFromRootStoreEnd();
+ }
+ catch (Exception ex)
+ {
+ Log.UnixRemoveCertificateFromRootStoreFailedtoDeleteFile(installedCertificate, ex.Message);
+ exceptions.Add(ex);
+ }
+ }
+ else
+ {
+ if (trustDetails.OpenSSL == false)
+ {
+ Log.UnixOpensslCertificateAlreadyUntrusted(GetDescription(certificate));
+ }
+ else
+ {
+ Log.UnixCannotUntrustDotNetToDotNet();
+ }
+ }
+
+ if (trustDetails.Firefox == true)
+ {
+ try
+ {
+ if (!TryRemoveCertificateFromNssDb(_firefoxNssDbPath, certificate, out var command, out var error))
+ {
+ Log.UnixRemoveCertificateFromFirefoxRootStoreError($"Failed to run the command '{command}'.{Environment.NewLine}{error}");
+ hasErrors = true;
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.UnixRemoveCertificateFromFirefoxRootStoreError(ex.Message);
+ exceptions.Add(ex);
+ }
+ }
+ else
+ {
+ if (trustDetails.Firefox == false)
+ {
+ Log.UnixFirefoxCertificateAlreadyUntrusted(GetDescription(certificate));
+ }
+ else
+ {
+ Log.UnixCannotUntrustFirefox();
+ }
+ }
+
+ if (trustDetails.EdgeChrome == false)
+ {
+ try
+ {
+ if (!TryRemoveCertificateFromNssDb(_googleChromeAndEdgeNssDbPath, certificate, out var command, out var error))
+ {
+ Log.UnixRemoveCertificateFromFirefoxRootStoreError($"Failed to run the command '{command}'.{Environment.NewLine}{error}");
+ hasErrors = true;
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.UnixRemoveCertificateFromCommonEdgeChromeRootStoreError(ex.Message);
+ exceptions.Add(ex);
+ }
+ }
+ else
+ {
+ if (trustDetails.Firefox == false)
+ {
+ Log.UnixEdgeChromeCertificateAlreadyUntrusted(GetDescription(certificate));
+ }
+ else
+ {
+ Log.UnixCannotUntrustEdgeChrome();
+ }
+ }
+
+ if (hasErrors || exceptions.Any())
+ {
+ if (hasErrors)
+ {
+ throw new InvalidOperationException("There were some errors trusting the certificate. Use --verbose to get more details.");
+ }
+ else
+ {
+ throw exceptions.Count == 1 ? exceptions[0] : new AggregateException("There were some removing the certificate trust. Use --verbose to get more details.", exceptions);
+ }
+ }
+
+ Log.UnixRemoveCertificateFromRootStoreEnd();
+ }
+
+ private static bool TryRemoveCertificateFromNssDb(string dbPath, X509Certificate2 certificate, out string command, out string error)
+ {
+ command = null;
+ error = null;
+ if (dbPath != null)
+ {
+ var result = RunScriptAndCaptureOutput(
+ "certutil",
+ $"-D -d sql:{dbPath} -n aspnetcore-localhost-{certificate.Thumbprint[0..6]}");
+ if (result.ExitCode != 0)
+ {
+ command = "certutil " + $"-D -d sql:{dbPath} -n aspnetcore-localhost-{certificate.Thumbprint[0..6]}";
+ error = result.Output + Environment.NewLine + result.Error;
+ return false;
+ }
+ }
+
+ return true;
}
protected override IList<X509Certificate2> GetCertificatesToRemove(StoreName storeName, StoreLocation storeLocation)
{
return ListCertificates(StoreName.My, StoreLocation.CurrentUser, isValid: false, requireExportable: false);
}
+
+ private record struct ProgramOutput(int ExitCode, string Output, string Error);
+
+ private record struct TrustChecks(bool? OpenSSL, bool? Firefox, bool? EdgeChrome)
+ {
+ internal bool IsTrusted() => (OpenSSL ?? true) && (Firefox ?? true) && (EdgeChrome ?? true);
+ }
}
diff --git a/src/Tools/dotnet-dev-certs/src/Program.cs b/src/Tools/dotnet-dev-certs/src/Program.cs
index 06cd156aa9..431dd5a772 100644
--- a/src/Tools/dotnet-dev-certs/src/Program.cs
+++ b/src/Tools/dotnet-dev-certs/src/Program.cs
@@ -253,6 +253,13 @@ internal sealed class Program
reporter.Output("Cleaning HTTPS development certificates from the machine. This operation might " +
"require elevated privileges. If that is the case, a prompt for credentials will be displayed.");
}
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+ {
+ ReportLinuxPrerequisitesCheck(reporter, manager);
+
+ reporter.Output("Cleaning HTTPS development certificates from the machine. This operation might " +
+ "require elevated privileges. If that is the case you will be prompted for your password.");
+ }
manager.CleanupHttpsCertificates();
reporter.Output("HTTPS development certificates successfully removed from the machine.");
@@ -296,32 +303,26 @@ internal sealed class Program
if (trust != null && trust.HasValue())
{
- if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
- var trustedCertificates = certificates.Where(c => certificateManager.IsTrusted(c)).ToList();
- if (!trustedCertificates.Any())
- {
- reporter.Output($@"The following certificates were found, but none of them is trusted: {CertificateManager.ToCertificateDescription(certificates)}");
- return ErrorCertificateNotTrusted;
- }
- else
- {
- ReportCertificates(reporter, trustedCertificates, "trusted");
- }
+ ReportLinuxPrerequisitesCheck(reporter, certificateManager);
+ }
+
+ var trustedCertificates = certificates.Where(c => certificateManager.IsTrusted(c)).ToList();
+ if (!trustedCertificates.Any())
+ {
+ reporter.Output($@"The following certificates were found, but none of them is trusted: {CertificateManager.ToCertificateDescription(certificates)}");
+ return ErrorCertificateNotTrusted;
}
else
{
- reporter.Warn("Checking the HTTPS development certificate trust status was requested. Checking whether the certificate is trusted or not is not supported on Linux distributions." +
- "For instructions on how to manually validate the certificate is trusted on your Linux distribution, go to https://aka.ms/dev-certs-trust");
+ ReportCertificates(reporter, trustedCertificates, "trusted");
}
}
else
{
ReportCertificates(reporter, validCertificates, "valid");
- if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
- {
- reporter.Output("Run the command with both --check and --trust options to ensure that the certificate is not only valid but also trusted.");
- }
+ reporter.Output("Run the command with both --check and --trust options to ensure that the certificate is not only valid but also trusted.");
}
return Success;
@@ -370,16 +371,23 @@ internal sealed class Program
"on the system keychain. To undo these changes: 'sudo security remove-trusted-cert -d <<certificate>>'");
}
- if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
- reporter.Warn("Trusting the HTTPS development certificate was requested. A confirmation prompt will be displayed " +
- "if the certificate was not previously trusted. Click yes on the prompt to trust the certificate.");
+ ReportLinuxPrerequisitesCheck(reporter, manager);
+
+ reporter.Warn("Trusting the HTTPS development certificate was requested. If the certificate is not " +
+ "already trusted we will run the following commands:" + Environment.NewLine +
+ "'sudo cp <<certificate>> <<openssl-dir>>/certs/aspnetcore-localhost-<<certificate-thumbprint>>.crt'" + Environment.NewLine +
+ "'sudo c_rehash'" + Environment.NewLine +
+ "'certutil -A -d sql:<<firefox-profile-certificate-db>> -t \"C,,\", -n \"aspnetcore-localhost-<<certificate-thumbprint[0..6]>>'" + Environment.NewLine +
+ "'certutil -A -d sql:~/.pki/nssdb -t \"C,,\", -n \"aspnetcore-localhost-<<certificate-thumbprint[0..6]>>'" + Environment.NewLine +
+ "These commands might prompt you for your password to trust the certificate");
}
- if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
- reporter.Warn("Trusting the HTTPS development certificate was requested. Trusting the certificate on Linux distributions automatically is not supported. " +
- "For instructions on how to manually trust the certificate on your Linux distribution, go to https://aka.ms/dev-certs-trust");
+ reporter.Warn("Trusting the HTTPS development certificate was requested. A confirmation prompt will be displayed " +
+ "if the certificate was not previously trusted. Click yes on the prompt to trust the certificate.");
}
}
@@ -394,7 +402,7 @@ internal sealed class Program
now,
now.Add(HttpsCertificateValidity),
exportPath.Value(),
- trust == null ? false : trust.HasValue() && !RuntimeInformation.IsOSPlatform(OSPlatform.Linux),
+ trust?.HasValue() == true,
password.HasValue() || (noPassword.HasValue() && format == CertificateKeyExportFormat.Pem),
password.Value(),
exportFormat.HasValue() ? format : CertificateKeyExportFormat.Pfx);
@@ -435,4 +443,47 @@ internal sealed class Program
return CriticalError;
}
}
+
+ private static void ReportLinuxPrerequisitesCheck(IReporter reporter, CertificateManager manager)
+ {
+ IList<CertificateTrustPrerequisite> prerequisites = Array.Empty<CertificateTrustPrerequisite>();
+ try
+ {
+ prerequisites = manager.CheckTrustPrerequisites();
+ }
+ catch (Exception ex)
+ {
+ reporter.Error($"There was an error checking the prerequisites: {ex.Message}");
+ }
+ if (prerequisites.Where(p => p.IsImportant).Any())
+ {
+ reporter.Warn("Some required tools are missing in the path. Check your distribution instructions on how to " +
+ "install each missing dependency. In most distros, trying to run the tool will indicate the missing package " +
+ "to install it. We will continue to trust the certificate where possible but you might need to run the tool again " +
+ "if you decide to install some additional tools or browsers afterwards.");
+
+ foreach (var prerequisite in prerequisites)
+ {
+ if (prerequisite.IsImportant)
+ {
+ reporter.Warn($" '{prerequisite.Tool}': {prerequisite.Message}.");
+ }
+ }
+ reporter.Output(Environment.NewLine);
+ }
+
+ if (prerequisites.Where(p => !p.IsImportant).Any())
+ {
+ reporter.Output("Some optional tools are missing in the path. If you install them afterwards you might need to run the tool again. Use --verbose for more details.");
+ foreach (var prerequisite in prerequisites)
+ {
+ if (!prerequisite.IsImportant)
+ {
+ reporter.Verbose($" '{prerequisite.Tool}': {prerequisite.Message}.");
+ }
+ }
+
+ reporter.Output(Environment.NewLine);
+ }
+ }
}