diff --git a/src/ProjectTemplates/Shared/DevelopmentCertificate.cs b/src/ProjectTemplates/Shared/DevelopmentCertificate.cs index cc814808e0fe..c37ea2db1916 100644 --- a/src/ProjectTemplates/Shared/DevelopmentCertificate.cs +++ b/src/ProjectTemplates/Shared/DevelopmentCertificate.cs @@ -35,7 +35,7 @@ private static string EnsureDevelopmentCertificates(string certificatePath, stri var manager = CertificateManager.Instance; var certificate = manager.CreateAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1)); var certificateThumbprint = certificate.Thumbprint; - manager.ExportCertificate(certificate, path: certificatePath, includePrivateKey: true, certificatePassword, CertificateKeyExportFormat.Pfx); + CertificateManager.ExportCertificate(certificate, path: certificatePath, includePrivateKey: true, certificatePassword, CertificateKeyExportFormat.Pfx); return certificateThumbprint; } diff --git a/src/Shared/CertificateGeneration/CertificateManager.cs b/src/Shared/CertificateGeneration/CertificateManager.cs index ad5f4f004a50..0ccfb2e50e9f 100644 --- a/src/Shared/CertificateGeneration/CertificateManager.cs +++ b/src/Shared/CertificateGeneration/CertificateManager.cs @@ -435,6 +435,8 @@ public void CleanupHttpsCertificates() public abstract bool IsTrusted(X509Certificate2 certificate); + public virtual bool SupportsTrust => false; + protected abstract X509Certificate2 SaveCertificateCore(X509Certificate2 certificate, StoreName storeName, StoreLocation storeLocation); protected abstract void TrustCertificateCore(X509Certificate2 certificate); @@ -445,7 +447,7 @@ public void CleanupHttpsCertificates() protected abstract IList GetCertificatesToRemove(StoreName storeName, StoreLocation storeLocation); - internal void ExportCertificate(X509Certificate2 certificate, string path, bool includePrivateKey, string? password, CertificateKeyExportFormat format) + internal static void ExportCertificate(X509Certificate2 certificate, string path, bool includePrivateKey, string? password, CertificateKeyExportFormat format) { if (Log.IsEnabled()) { @@ -970,6 +972,12 @@ public class CertificateManagerEventSource : EventSource [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.Informational, Message = "Adding '{0}' to '{1}'.")] + internal void LinuxTrustCertificate(string certificate, string storeName) => WriteEvent(65, certificate, storeName); + + [Event(66, Level = EventLevel.Error, Message = "An error has occurred while running command '{0}' to install certificate. Exit code: {1}. Error: {2}.")] + internal void LinuxCertificateInstallCommandFailed(string command, int exitCode, string stderr) => WriteEvent(66, command, exitCode, stderr); } internal class UserCancelledTrustException : Exception diff --git a/src/Shared/CertificateGeneration/CertificateStore.cs b/src/Shared/CertificateGeneration/CertificateStore.cs new file mode 100644 index 000000000000..8433329aec3a --- /dev/null +++ b/src/Shared/CertificateGeneration/CertificateStore.cs @@ -0,0 +1,29 @@ +using System; +using System.Diagnostics; +using System.Security.Cryptography.X509Certificates; + +#nullable enable + +namespace Microsoft.AspNetCore.Certificates.Generation +{ + internal abstract class CertificateStore + { + public string StoreName { get; } + + protected CertificateStore(string name) + { + StoreName = name; + } + + public abstract bool TryInstallCertificate(X509Certificate2 certificate); + + public abstract void DeleteCertificate(X509Certificate2 certificate); + + public abstract bool HasCertificate(X509Certificate2 certificate); + + protected bool ContainsCertificate(string storeContent, string certificateContent) + { + return false; + } + } +} \ No newline at end of file diff --git a/src/Shared/CertificateGeneration/CertificateStoreFinder.cs b/src/Shared/CertificateGeneration/CertificateStoreFinder.cs new file mode 100644 index 000000000000..f10252f05ff0 --- /dev/null +++ b/src/Shared/CertificateGeneration/CertificateStoreFinder.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; + +#nullable enable + +namespace Microsoft.AspNetCore.Certificates.Generation +{ + internal static class CertificateStoreFinder + { + public static List FindCertificateStores() + { + List stores = new(); + FindChromeEdgeCertificateStores(stores); + FindFirefoxCertificateStores(stores); + FindSystemCertificateStore(stores); + return stores; + } + + private static void FindSystemCertificateStore(List stores) + { + CertificateStore? store = SystemCertificateFolderStore.Instance; + if (store is not null) + { + stores.Add(store); + } + } + + private static void FindChromeEdgeCertificateStores(List stores) + { + string storeFolder = Path.Combine(Paths.Home, ".pki", "nssdb"); + if (Directory.Exists(storeFolder)) + { + stores.Add(new NssCertificateDatabase("Chrome/Edge certificates", storeFolder)); + } + } + + private static void FindFirefoxCertificateStores(List stores) + { + string firefoxFolder = Path.Combine(Paths.Home, ".mozilla", "firefox"); + string profilesIniFileName = Path.Combine(firefoxFolder, "profiles.ini"); + if (File.Exists(profilesIniFileName)) + { + using FileStream profilesIniFile = File.OpenRead(profilesIniFileName); + List sections = ReadIniFile(profilesIniFile); + List profileFolders = new(); + foreach (var section in sections) + { + string? path; + if (section.Name.StartsWith("Install", StringComparison.InvariantCultureIgnoreCase)) + { + if (!section.Properties.TryGetValue("Default", out path)) + { + continue; + } + } + else if (!section.Properties.TryGetValue("Path", out path)) + { + continue; + } + + string profileFolder = Path.Combine(firefoxFolder, path); + if (!profileFolders.Contains(profileFolder)) + { + profileFolders.Add(profileFolder); + if (Directory.Exists(profileFolder)) + { + stores.Add(new NssCertificateDatabase($"Firefox profile certificates ({path})", profileFolder)); + } + } + } + } + } + + private class IniSection + { + public string Name { get; } + + public IniSection(string name) + { + Name = name; + } + + public Dictionary Properties { get; } = new(); + } + + private static List ReadIniFile(Stream stream) + { + // Implementation from https://raw.githubusercontent.com/dotnet/runtime/5a1b8223dab2f7954e7f206095ba937e5d237299/src/libraries/Microsoft.Extensions.Configuration.Ini/src/IniStreamConfigurationProvider.cs. + // Licensed to the .NET Foundation under one or more agreements. + // The .NET Foundation licenses this file to you under the MIT license. + + var sections = new List(); + IniSection? currentSection = null; + + using (var reader = new StreamReader(stream)) + { + string sectionPrefix = string.Empty; + + while (reader.Peek() != -1) + { + string rawLine = reader.ReadLine()!; + string line = rawLine.Trim(); + + // Ignore blank lines + if (string.IsNullOrWhiteSpace(line)) + { + continue; + } + // Ignore comments + if (line[0] == ';' || line[0] == '#' || line[0] == '/') + { + continue; + } + // [Section:header] + if (line[0] == '[' && line[line.Length - 1] == ']') + { + // remove the brackets + string sectionName = line.Substring(1, line.Length - 2); + currentSection = new IniSection(sectionName); + sections.Add(currentSection); + continue; + } + + if (currentSection == null) + { + continue; + } + + // key = value OR "value" + int separator = line.IndexOf('='); + if (separator < 0) + { + throw new FormatException($"Unrecognized line format: '{rawLine}'."); + } + + string key = line.Substring(0, separator).Trim(); + string value = line.Substring(separator + 1).Trim(); + + // Remove quotes + if (value.Length > 1 && value[0] == '"' && value[value.Length - 1] == '"') + { + value = value.Substring(1, value.Length - 2); + } + + if (currentSection.Properties.ContainsKey(key)) + { + throw new FormatException($"A duplicate key '{key}' was found."); + } + + currentSection.Properties.Add(key, value); + } + } + + return sections; + } + } +} \ No newline at end of file diff --git a/src/Shared/CertificateGeneration/MacOSCertificateManager.cs b/src/Shared/CertificateGeneration/MacOSCertificateManager.cs index 15e10b885d97..cc3df20c0a83 100644 --- a/src/Shared/CertificateGeneration/MacOSCertificateManager.cs +++ b/src/Shared/CertificateGeneration/MacOSCertificateManager.cs @@ -159,6 +159,8 @@ public override bool IsTrusted(X509Certificate2 certificate) return hashes.Any(h => string.Equals(h, certificate.Thumbprint, StringComparison.Ordinal)); } + public override bool SupportsTrust => true; + protected override void RemoveCertificateFromTrustedRoots(X509Certificate2 certificate) { if (IsTrusted(certificate)) // On OSX this check just ensures its on the system keychain diff --git a/src/Shared/CertificateGeneration/NssCertificateDatabase.cs b/src/Shared/CertificateGeneration/NssCertificateDatabase.cs new file mode 100644 index 000000000000..65b31ef9f985 --- /dev/null +++ b/src/Shared/CertificateGeneration/NssCertificateDatabase.cs @@ -0,0 +1,98 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Security.Cryptography.X509Certificates; + +#nullable enable + +namespace Microsoft.AspNetCore.Certificates.Generation +{ + internal class NssCertificateDatabase : CertificateStore + { + public string DatabasePath { get; } + + public NssCertificateDatabase(string name, string path) : + base(name) + { + DatabasePath = path; + } + + public override bool TryInstallCertificate(X509Certificate2 certificate) + { + string pemFile = Paths.GetUserTempFile(".pem"); + try + { + CertificateManager.ExportCertificate(certificate, pemFile, includePrivateKey: false, password: null, CertificateKeyExportFormat.Pem); + string name = GetCertificateNickname(certificate); + Process process = Process.Start(new ProcessStartInfo() { + FileName = "certutil", + ArgumentList = { "-d", DatabasePath, "-A", "-t", "C,,", "-n", name, "-i", pemFile }, + RedirectStandardOutput = true, + RedirectStandardError = true })!; + var stderr = process.StandardError.ReadToEnd(); + process.WaitForExit(); + bool success = process.ExitCode == 0; + if (!success) + { + string cmdline = ProcessHelper.GetCommandLine(process.StartInfo); + CertificateManager.Log.LinuxCertificateInstallCommandFailed(cmdline, process.ExitCode, stderr); + } + return success; + } + finally + { + try + { + File.Delete(pemFile); + } + catch + { } + } + } + + public override bool HasCertificate(X509Certificate2 certificate) + { + string name = GetCertificateNickname(certificate); + Process process = Process.Start(new ProcessStartInfo() + { + FileName = "cerutil", + ArgumentList = { "-d", DatabasePath, "-L", "-n", name, "-a" }, + RedirectStandardOutput = true, + RedirectStandardError = true + })!; + string stdout = process.StandardOutput.ReadToEnd(); + process.WaitForExit(); + if (process.ExitCode == 0) + { + stdout = stdout.Replace("\r\n", "\n"); + const string BeginCertificate = "-----BEGIN CERTIFICATE-----"; + var pemCertificates = stdout.Split(BeginCertificate, StringSplitOptions.RemoveEmptyEntries); + foreach (var pem in pemCertificates) + { + X509Certificate2 cert = X509Certificate2.CreateFromPem(BeginCertificate + "\n" + pem); + if (cert.Equals(certificate)) + { + return true; + } + } + } + return false; + } + + public override void DeleteCertificate(X509Certificate2 certificate) + { + string name = GetCertificateNickname(certificate); + var process = Process.Start(new ProcessStartInfo() + { + FileName = "certutil", + ArgumentList = { "-d", DatabasePath, "-D", "-n", name }, + RedirectStandardOutput = true, + RedirectStandardError = true + })!; + process.WaitForExit(); + } + + private string GetCertificateNickname(X509Certificate2 certificate) + => "aspnet-" + certificate.Thumbprint; + } +} \ No newline at end of file diff --git a/src/Shared/CertificateGeneration/Paths.cs b/src/Shared/CertificateGeneration/Paths.cs new file mode 100644 index 000000000000..6f35e824a928 --- /dev/null +++ b/src/Shared/CertificateGeneration/Paths.cs @@ -0,0 +1,23 @@ +using System; +using System.IO; +using static System.Environment; + +#nullable enable + +namespace Microsoft.AspNetCore.Certificates.Generation +{ + internal static class Paths + { + public static string Home => Environment.GetFolderPath(SpecialFolder.MyDocuments); + + public static string GetUserTempFile(string suffix = ".tmp") + { + string directory = Paths.XdgRuntimeDir ?? Home ?? // Should be user folders. + Path.GetTempPath(); // Probably global on Linux. + + return Path.Combine(directory, Guid.NewGuid() + suffix); + } + + private static string? XdgRuntimeDir => Environment.GetEnvironmentVariable("XDG_RUNTIME_DIR"); + } +} \ No newline at end of file diff --git a/src/Shared/CertificateGeneration/ProcessHelper.cs b/src/Shared/CertificateGeneration/ProcessHelper.cs new file mode 100644 index 000000000000..0ea7b3825aba --- /dev/null +++ b/src/Shared/CertificateGeneration/ProcessHelper.cs @@ -0,0 +1,34 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Text; + +#nullable enable + +namespace Microsoft.AspNetCore.Certificates.Generation +{ + internal static class ProcessHelper + { + public static string GetCommandLine(ProcessStartInfo psi) + => $"{psi.FileName} {string.Join(" ", psi.ArgumentList)}"; + + public static bool HasProgram(string program) + { + string path; + string? pathEnvVar = Environment.GetEnvironmentVariable("PATH"); + if (pathEnvVar != null) + { + string[] pathItems = pathEnvVar.Split(':', StringSplitOptions.RemoveEmptyEntries); + foreach (var pathItem in pathItems) + { + path = Path.Combine(pathItem, program); + if (File.Exists(path)) + { + return true; + } + } + } + return false; + } + } +} \ No newline at end of file diff --git a/src/Shared/CertificateGeneration/SystemCertificateFolderStore.cs b/src/Shared/CertificateGeneration/SystemCertificateFolderStore.cs new file mode 100644 index 000000000000..384b6fe1f775 --- /dev/null +++ b/src/Shared/CertificateGeneration/SystemCertificateFolderStore.cs @@ -0,0 +1,173 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Security.Cryptography.X509Certificates; +using Microsoft.AspNetCore.Certificates.Generation; + +#nullable enable + +namespace Microsoft.AspNetCore.Certificates.Generation +{ + internal class SystemCertificateFolderStore : CertificateStore + { + enum LinuxFlavor + { + Fedora, + Debian + } + + private readonly LinuxFlavor _linuxFlavor; + + private string FolderPath => + _linuxFlavor switch + { + LinuxFlavor.Fedora => "/etc/pki/tls/certs", + LinuxFlavor.Debian => "/usr/local/share/ca-certificates", + _ => throw new IndexOutOfRangeException() + }; + + private string Extension => + _linuxFlavor switch + { + LinuxFlavor.Fedora => ".pem", + LinuxFlavor.Debian => ".crt", + _ => throw new IndexOutOfRangeException() + }; + + public static SystemCertificateFolderStore? Instance { get; } = CreateSystemCertificateFolderStore(); + + private static SystemCertificateFolderStore? CreateSystemCertificateFolderStore() + { + if (ProcessHelper.HasProgram("yum")) + { + return new SystemCertificateFolderStore(LinuxFlavor.Fedora); + } + else if (ProcessHelper.HasProgram("apt")) + { + return new SystemCertificateFolderStore(LinuxFlavor.Debian); + } + return null; + } + + private SystemCertificateFolderStore(LinuxFlavor linuxFlavor) : + base("System certificates") + { + _linuxFlavor = linuxFlavor; + } + + public override bool TryInstallCertificate(X509Certificate2 certificate) + { + if (!TryCopyToCertificateFolder(certificate)) + { + return false; + } + + ProcessStartInfo psi = _linuxFlavor switch + { + LinuxFlavor.Fedora => new ProcessStartInfo() + { + FileName = "sudo", + ArgumentList = { "update-ca-trust" } + }, + LinuxFlavor.Debian => new ProcessStartInfo() + { + FileName = "sudo", + ArgumentList = { "update-ca-certificates" } + }, + _ => throw new IndexOutOfRangeException() + }; + psi.RedirectStandardOutput = true; + psi.RedirectStandardError = true; + + Process updateStoreProcess = Process.Start(psi)!; + var stderr = updateStoreProcess.StandardError.ReadToEnd(); + updateStoreProcess.WaitForExit(); + + if (updateStoreProcess.ExitCode != 0) + { + string cmdline = ProcessHelper.GetCommandLine(updateStoreProcess.StartInfo); + CertificateManager.Log.LinuxCertificateInstallCommandFailed(cmdline, updateStoreProcess.ExitCode, stderr); + } + + return updateStoreProcess.ExitCode != 0; + } + + private bool TryCopyToCertificateFolder(X509Certificate2 certificate) + { + string pemFile = Paths.GetUserTempFile(".pem"); + try + { + CertificateManager.ExportCertificate(certificate, pemFile, includePrivateKey: false, password: null, CertificateKeyExportFormat.Pem); + + string sourceFileName = pemFile; + string destFileName = GetCertificatePath(certificate); + + DeleteFile(destFileName); + + var copyProcess = Process.Start(new ProcessStartInfo() + { + FileName = "sudo", + ArgumentList = { "cp", sourceFileName, destFileName }, + RedirectStandardOutput = true, + RedirectStandardError = true, + })!; + var stderr = copyProcess.StandardError.ReadToEnd(); + copyProcess.WaitForExit(); + + if (copyProcess.ExitCode != 0) + { + string cmdline = ProcessHelper.GetCommandLine(copyProcess.StartInfo); + CertificateManager.Log.LinuxCertificateInstallCommandFailed(cmdline, copyProcess.ExitCode, stderr); + return false; + } + + return true; + } + finally + { + try + { + File.Delete(pemFile); + } + catch + { } + } + } + + public override void DeleteCertificate(X509Certificate2 certificate) + { + DeleteFile(GetCertificatePath(certificate)); + } + + public override bool HasCertificate(X509Certificate2 certificate) + { + string certificatePath = GetCertificatePath(certificate); + if (!File.Exists(certificatePath)) + { + return false; + } + X509Certificate2 storeCertificate = X509Certificate2.CreateFromPem(File.ReadAllText(certificatePath)); + return storeCertificate.Equals(certificate); + } + + private string GetCertificatePath(X509Certificate2 certificate) + => Path.Combine(FolderPath, "aspnet-" + certificate.Thumbprint + Extension); + + private void DeleteFile(string path) + { + if (!File.Exists(path)) + { + return; + } + + var process = Process.Start(new ProcessStartInfo() + { + FileName = "sudo", + ArgumentList = { "rm", path }, + RedirectStandardOutput = true, + RedirectStandardError = true, + })!; + process.WaitForExit(); + } + } +} diff --git a/src/Shared/CertificateGeneration/UnixCertificateManager.cs b/src/Shared/CertificateGeneration/UnixCertificateManager.cs index 913a765f12cc..aed39b5ef0f4 100644 --- a/src/Shared/CertificateGeneration/UnixCertificateManager.cs +++ b/src/Shared/CertificateGeneration/UnixCertificateManager.cs @@ -2,10 +2,17 @@ using System.Collections.Generic; using System.Security.Cryptography.X509Certificates; +#nullable enable + namespace Microsoft.AspNetCore.Certificates.Generation { internal class UnixCertificateManager : CertificateManager { + private List? _certificateStores; + + private List CertificateStores + => _certificateStores ??= CertificateStoreFinder.FindCertificateStores(); + public UnixCertificateManager() { } @@ -15,7 +22,22 @@ internal UnixCertificateManager(string subject, int version) { } - public override bool IsTrusted(X509Certificate2 certificate) => false; + public override bool IsTrusted(X509Certificate2 certificate) + { + // TODO: support 'partial' return. + + // Return true when all stores trust the cert. + foreach (var store in CertificateStores) + { + if (!store.HasCertificate(certificate)) + { + return false; + } + } + return CertificateStores.Count > 0; + } + + public override bool SupportsTrust => CertificateStores.Count > 0; protected override X509Certificate2 SaveCertificateCore(X509Certificate2 certificate, StoreName storeName, StoreLocation storeLocation) { @@ -47,12 +69,22 @@ internal override void CorrectCertificateState(X509Certificate2 candidate) 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) + { + foreach (var store in CertificateStores) + { + CertificateManager.Log.LinuxTrustCertificate(CertificateManager.GetDescription(certificate), store.StoreName); + store.TryInstallCertificate(certificate); + // TODO: handle failure. + } + } protected override void RemoveCertificateFromTrustedRoots(X509Certificate2 certificate) { - // No-op here as is benign + foreach (var store in CertificateStores) + { + store.DeleteCertificate(certificate); + } } protected override IList GetCertificatesToRemove(StoreName storeName, StoreLocation storeLocation) diff --git a/src/Shared/CertificateGeneration/WindowsCertificateManager.cs b/src/Shared/CertificateGeneration/WindowsCertificateManager.cs index cefba748dfeb..0bdd76875807 100644 --- a/src/Shared/CertificateGeneration/WindowsCertificateManager.cs +++ b/src/Shared/CertificateGeneration/WindowsCertificateManager.cs @@ -126,6 +126,8 @@ public override bool IsTrusted(X509Certificate2 certificate) .Any(c => c.Thumbprint == certificate.Thumbprint); } + public override bool SupportsTrust => true; + protected override IList GetCertificatesToRemove(StoreName storeName, StoreLocation storeLocation) { return ListCertificates(storeName, storeLocation, isValid: false); diff --git a/src/Tools/dotnet-dev-certs/src/Program.cs b/src/Tools/dotnet-dev-certs/src/Program.cs index f9f67013165d..df6b6d4c5ac7 100644 --- a/src/Tools/dotnet-dev-certs/src/Program.cs +++ b/src/Tools/dotnet-dev-certs/src/Program.cs @@ -254,6 +254,8 @@ private static int CleanHttpsCertificates(IReporter reporter) "require elevated privileges. If that is the case, a prompt for credentials will be displayed."); } + // TODO: check if system tools are available for deleting certificates. + manager.CleanupHttpsCertificates(); reporter.Output("HTTPS development certificates successfully removed from the machine."); return Success; @@ -296,32 +298,21 @@ private static int CheckHttpsCertificate(CommandOption trust, IReporter reporter if (trust != null && trust.HasValue()) { - if(!RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + var trustedCertificates = certificates.Where(c => certificateManager.IsTrusted(c)).ToList(); + if (!trustedCertificates.Any()) { - 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"); - } + 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; @@ -361,25 +352,34 @@ private static int EnsureHttpsCertificate(CommandOption exportPath, CommandOptio if (trust?.HasValue() == true) { - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + if (!manager.SupportsTrust) { - reporter.Warn("Trusting the HTTPS development certificate was requested. If the certificate is not " + - "already trusted we will run the following command:" + Environment.NewLine + - "'sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain <>'" + - Environment.NewLine + "This command might prompt you for your password to install the certificate " + - "on the system keychain."); + reporter.Warn("Trusting the HTTPS development certificate was requested. Trusting the certificate automatically is not supported on this distribition. " + + "For instructions on how to manually trust the certificate, go to https://aka.ms/dev-certs-trust"); } - - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + else { - 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."); - } + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + reporter.Warn("Trusting the HTTPS development certificate was requested. If the certificate is not " + + "already trusted we will run the following command:" + Environment.NewLine + + "'sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain <>'" + + Environment.NewLine + "This command might prompt you for your password to install the certificate " + + "on the system keychain."); + } - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - 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"); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + 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."); + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + reporter.Warn("Trusting the HTTPS development certificate was requested. If the certificate is not " + + "already trusted we will run commands using 'sudo'." + + Environment.NewLine + "This might prompt you for your password."); + } } } @@ -390,11 +390,13 @@ private static int EnsureHttpsCertificate(CommandOption exportPath, CommandOptio return InvalidKeyExportFormat; } + // TODO: check if system tools are available for trusting certificates. + var result = manager.EnsureAspNetCoreHttpsDevelopmentCertificate( now, now.Add(HttpsCertificateValidity), exportPath.Value(), - trust == null ? false : trust.HasValue() && !RuntimeInformation.IsOSPlatform(OSPlatform.Linux), + trust == null ? false : trust.HasValue() && manager.SupportsTrust, password.HasValue() || (noPassword.HasValue() && format == CertificateKeyExportFormat.Pem), password.Value(), exportFormat.HasValue() ? format : CertificateKeyExportFormat.Pfx);