From bb1364d43a9902e43089e1a06f23ec8eab5155f5 Mon Sep 17 00:00:00 2001 From: Tom Deseyn Date: Fri, 4 Jun 2021 12:07:32 +0200 Subject: [PATCH 01/11] linux-dev-certs: prototype Linux trust support --- .../CertificateFolderStore.cs | 107 ++++++++++++ .../CertificateManager.cs | 26 +-- .../CertificateGeneration/CertificateStore.cs | 47 +++++ .../CertificateStoreFinder.cs | 162 ++++++++++++++++++ .../MacOSCertificateManager.cs | 5 +- .../NssCertificateDatabase.cs | 71 ++++++++ src/Shared/CertificateGeneration/Paths.cs | 13 ++ .../PemCertificateFile.cs | 34 ++++ .../ProcessRunOptions.cs | 17 ++ .../CertificateGeneration/ProcessRunResult.cs | 15 ++ .../CertificateGeneration/ProcessRunner.cs | 106 ++++++++++++ .../UnixCertificateManager.cs | 41 ++++- .../WindowsCertificateManager.cs | 5 +- ...NetCore.DeveloperCertificates.XPlat.csproj | 1 + src/Tools/dotnet-dev-certs/src/Program.cs | 45 ++--- 15 files changed, 648 insertions(+), 47 deletions(-) create mode 100644 src/Shared/CertificateGeneration/CertificateFolderStore.cs create mode 100644 src/Shared/CertificateGeneration/CertificateStore.cs create mode 100644 src/Shared/CertificateGeneration/CertificateStoreFinder.cs create mode 100644 src/Shared/CertificateGeneration/NssCertificateDatabase.cs create mode 100644 src/Shared/CertificateGeneration/Paths.cs create mode 100644 src/Shared/CertificateGeneration/PemCertificateFile.cs create mode 100644 src/Shared/CertificateGeneration/ProcessRunOptions.cs create mode 100644 src/Shared/CertificateGeneration/ProcessRunResult.cs create mode 100644 src/Shared/CertificateGeneration/ProcessRunner.cs diff --git a/src/Shared/CertificateGeneration/CertificateFolderStore.cs b/src/Shared/CertificateGeneration/CertificateFolderStore.cs new file mode 100644 index 000000000000..e3607e70f201 --- /dev/null +++ b/src/Shared/CertificateGeneration/CertificateFolderStore.cs @@ -0,0 +1,107 @@ +using System.IO; +using System.Security.Cryptography.X509Certificates; +using Microsoft.AspNetCore.Certificates.Generation; +using Microsoft.Extensions.Tools.Internal; + +#nullable enable + +namespace Microsoft.AspNetCore.Certificates.Generation +{ + internal class CertificateFolderStore : CertificateStore + { + public string FolderPath { get; } + + private readonly ProcessRunOptions _updateStore; + + private bool Elevate => _updateStore.Elevate; + + public CertificateFolderStore(string name, string path, ProcessRunOptions updateStore) : + base(name) + { + FolderPath = path; + _updateStore = updateStore; + } + + private void ReportElevationNeeded(IReporter? reporter) + { + if (Elevate) + { + reporter?.Output($"Changing '{StoreName}' requires root priviledges. You may be prompted for your password."); + } + } + + public override bool CheckDependencies(IReporter? reporter) + { + return CheckProgramDependency(_updateStore, reporter); + } + + public override bool TryInstallCertificate(string name, PemCertificateFile pemFile, IReporter? reporter, bool isInteractive) + { + ReportElevationNeeded(reporter); + CopyFile(pemFile.FilePath, GetCertificatePath(name), isInteractive); + ProcessRunner.Run(_updateStore with { IsInteractive = isInteractive }); + return true; + } + + public override void DeleteCertificate(string name, IReporter? reporter, bool isInteractive) + { + ReportElevationNeeded(reporter); + DeleteFile(GetCertificatePath(name), isInteractive); + } + + public override bool HasCertificate(string name, X509Certificate2 certificate) + { + string certificatePath = GetCertificatePath(name); + if (!File.Exists(certificatePath)) + { + return false; + } + X509Certificate2 storeCertificate = X509Certificate2.CreateFromPem(File.ReadAllText(certificatePath)); + return storeCertificate.Equals(certificate); + } + + private string GetCertificatePath(string name) + => Path.Combine(FolderPath, name + ".pem"); + + private void DeleteFile(string path, bool isInteractive) + { + if (!File.Exists(path)) + { + return; + } + + if (Elevate) + { + ProcessRunner.Run(new() + { + Command = { "rm", path }, + Elevate = true, + IsInteractive = isInteractive + }); + } + else + { + File.Delete(path); + } + } + + private void CopyFile(string sourceFileName, string destFileName, bool isInteractive) + { + DeleteFile(destFileName, isInteractive); + + if (Elevate) + { + ProcessRunner.Run(new() + { + Command = { "cp", sourceFileName, destFileName }, + Elevate = true, + IsInteractive = isInteractive + }); + } + else + { + File.Copy(sourceFileName, destFileName); + } + } + } +} diff --git a/src/Shared/CertificateGeneration/CertificateManager.cs b/src/Shared/CertificateGeneration/CertificateManager.cs index ad5f4f004a50..b54d9236e7fe 100644 --- a/src/Shared/CertificateGeneration/CertificateManager.cs +++ b/src/Shared/CertificateGeneration/CertificateManager.cs @@ -10,6 +10,7 @@ using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; +using Microsoft.Extensions.Tools.Internal; #nullable enable @@ -171,6 +172,7 @@ public EnsureCertificateResult EnsureAspNetCoreHttpsDevelopmentCertificate( bool includePrivateKey = false, string? password = null, CertificateKeyExportFormat keyExportFormat = CertificateKeyExportFormat.Pfx, + IReporter? reporter = null, bool isInteractive = true) { var result = EnsureCertificateResult.Succeeded; @@ -327,7 +329,7 @@ public EnsureCertificateResult EnsureAspNetCoreHttpsDevelopmentCertificate( { try { - TrustCertificate(certificate); + TrustCertificate(certificate, reporter, isInteractive); } catch (UserCancelledTrustException) { @@ -408,7 +410,7 @@ internal ImportCertificateResult ImportCertificate(string certificatePath, strin return ImportCertificateResult.Succeeded; } - public void CleanupHttpsCertificates() + public void CleanupHttpsCertificates(IReporter? reporter = null, bool isInteractive = false) { // On OS X we don't have a good way to manage trusted certificates in the system keychain // so we do everything by invoking the native toolchain. @@ -429,7 +431,7 @@ public void CleanupHttpsCertificates() foreach (var certificate in filteredCertificates) { - RemoveCertificate(certificate, RemoveLocations.All); + RemoveCertificate(certificate, RemoveLocations.All, reporter, isInteractive); } } @@ -437,11 +439,11 @@ public void CleanupHttpsCertificates() protected abstract X509Certificate2 SaveCertificateCore(X509Certificate2 certificate, StoreName storeName, StoreLocation storeLocation); - protected abstract void TrustCertificateCore(X509Certificate2 certificate); + protected abstract void TrustCertificateCore(X509Certificate2 certificate, IReporter? reporter, bool isInteractive); protected abstract bool IsExportable(X509Certificate2 c); - protected abstract void RemoveCertificateFromTrustedRoots(X509Certificate2 certificate); + protected abstract void RemoveCertificateFromTrustedRoots(X509Certificate2 certificate, IReporter? reporter, bool isInteractive); protected abstract IList GetCertificatesToRemove(StoreName storeName, StoreLocation storeLocation); @@ -639,7 +641,7 @@ internal X509Certificate2 SaveCertificate(X509Certificate2 certificate) return certificate; } - internal void TrustCertificate(X509Certificate2 certificate) + internal void TrustCertificate(X509Certificate2 certificate, IReporter? reporter, bool isInteractive) { try { @@ -647,7 +649,7 @@ internal void TrustCertificate(X509Certificate2 certificate) { Log.TrustCertificateStart(GetDescription(certificate)); } - TrustCertificateCore(certificate); + TrustCertificateCore(certificate, reporter, isInteractive); Log.TrustCertificateEnd(); } catch (Exception ex) when (Log.IsEnabled()) @@ -658,7 +660,7 @@ internal void TrustCertificate(X509Certificate2 certificate) } // Internal, for testing purposes only. - internal void RemoveAllCertificates(StoreName storeName, StoreLocation storeLocation) + internal void RemoveAllCertificates(StoreName storeName, StoreLocation storeLocation, IReporter? reporter = null, bool isInteractive = false) { var certificates = GetCertificatesToRemove(storeName, storeLocation); var certificatesWithName = certificates.Where(c => c.Subject == Subject); @@ -667,13 +669,13 @@ internal void RemoveAllCertificates(StoreName storeName, StoreLocation storeLoca foreach (var certificate in certificates) { - RemoveCertificate(certificate, removeLocation); + RemoveCertificate(certificate, removeLocation, reporter, isInteractive); } DisposeCertificates(certificates); } - internal void RemoveCertificate(X509Certificate2 certificate, RemoveLocations locations) + internal void RemoveCertificate(X509Certificate2 certificate, RemoveLocations locations, IReporter? reporter, bool isInteractive) { switch (locations) { @@ -683,10 +685,10 @@ internal void RemoveCertificate(X509Certificate2 certificate, RemoveLocations lo RemoveCertificateFromUserStore(certificate); break; case RemoveLocations.Trusted: - RemoveCertificateFromTrustedRoots(certificate); + RemoveCertificateFromTrustedRoots(certificate, reporter, isInteractive); break; case RemoveLocations.All: - RemoveCertificateFromTrustedRoots(certificate); + RemoveCertificateFromTrustedRoots(certificate, reporter, isInteractive); RemoveCertificateFromUserStore(certificate); break; default: diff --git a/src/Shared/CertificateGeneration/CertificateStore.cs b/src/Shared/CertificateGeneration/CertificateStore.cs new file mode 100644 index 000000000000..cd5a53181a02 --- /dev/null +++ b/src/Shared/CertificateGeneration/CertificateStore.cs @@ -0,0 +1,47 @@ +using System; +using System.Security.Cryptography.X509Certificates; +using Microsoft.Extensions.Tools.Internal; + +#nullable enable + +namespace Microsoft.AspNetCore.Certificates.Generation +{ + internal abstract class CertificateStore + { + public string StoreName { get; } + + protected CertificateStore(string name) + { + StoreName = name; + } + + public abstract bool CheckDependencies(IReporter? reporter); + + public abstract bool TryInstallCertificate(string name, PemCertificateFile pemFile, IReporter? reporter, bool isInteractive); + + public abstract void DeleteCertificate(string name, IReporter? reporter, bool isInteractive); + + public abstract bool HasCertificate(string name, X509Certificate2 pemContent); + + protected bool CheckProgramDependency(string program, IReporter? reporter) + { + if (!ProcessRunner.HasProgram(program)) + { + reporter?.Warn($"Cannot use '{StoreName}' because '{program}' is not installed."); + return false; + } + return true; + } + + protected bool CheckProgramDependency(ProcessRunOptions runOptions, IReporter? reporter) + { + return CheckProgramDependency(runOptions.Command[0], reporter) + & (!runOptions.Elevate || CheckProgramDependency("sudo", reporter)); + } + + 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..e090c5c83739 --- /dev/null +++ b/src/Shared/CertificateGeneration/CertificateStoreFinder.cs @@ -0,0 +1,162 @@ +using System; +using System.Collections.Generic; +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) + { + const string StoreName = "System certificates"; + if (ProcessRunner.HasProgram("yum")) + { + stores.Add(new CertificateFolderStore(StoreName, "/etc/pki/tls/certs", new() + { + Command = { "update-ca-trust" }, + Elevate = true + })); + } + } + + 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..4c96c10e4f7d 100644 --- a/src/Shared/CertificateGeneration/MacOSCertificateManager.cs +++ b/src/Shared/CertificateGeneration/MacOSCertificateManager.cs @@ -8,6 +8,7 @@ using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text.RegularExpressions; +using Microsoft.Extensions.Tools.Internal; namespace Microsoft.AspNetCore.Certificates.Generation { @@ -50,7 +51,7 @@ internal MacOSCertificateManager(string subject, int version) { } - protected override void TrustCertificateCore(X509Certificate2 publicCertificate) + protected override void TrustCertificateCore(X509Certificate2 publicCertificate, IReporter reporter, bool isInteractive) { var tmpFile = Path.GetTempFileName(); try @@ -159,7 +160,7 @@ public override bool IsTrusted(X509Certificate2 certificate) return hashes.Any(h => string.Equals(h, certificate.Thumbprint, StringComparison.Ordinal)); } - protected override void RemoveCertificateFromTrustedRoots(X509Certificate2 certificate) + protected override void RemoveCertificateFromTrustedRoots(X509Certificate2 certificate, IReporter reporter, bool isInteractive) { 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..655c9bef12b9 --- /dev/null +++ b/src/Shared/CertificateGeneration/NssCertificateDatabase.cs @@ -0,0 +1,71 @@ +using System; +using System.Security.Cryptography.X509Certificates; +using Microsoft.Extensions.Tools.Internal; + +#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 CheckDependencies(IReporter? reporter) + { + return CheckProgramDependency("certutil", reporter); + } + + public override bool TryInstallCertificate(string name, PemCertificateFile pemFile, IReporter? reporter, bool isInteractive) + { + var result = ProcessRunner.Run(new() { + Command = { "certutil", "-d", DatabasePath, "-A", "-t", "C,,", "-n", name, "-i", pemFile.FilePath }, + ThrowOnFailure = false }); + if (!result.IsSuccess) + { + reporter?.Error($"Failed to install certificate using command '{result.CommandLine}': {result.StandardError}"); + } + return result.IsSuccess; + } + + public override bool HasCertificate(string name, X509Certificate2 certificate) + { + ProcessRunResult runResult = ProcessRunner.Run(new() + { + Command = { "certutil", "-d", DatabasePath, "-L", "-n", name, "-a" }, + ReadStandardOutput = true, + ThrowOnFailure = false + }); + if (runResult.IsSuccess) + { + runResult.StandardOutput!.Replace("\r\n", "\n"); + const string BeginCertificate = "-----BEGIN CERTIFICATE-----"; + var pemCertificates = runResult.StandardOutput.ToString() + .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(string name, IReporter? reporter, bool isInteractive) + { + ProcessRunner.Run(new() + { + Command = { "certutil", "-d", DatabasePath, "-D", "-n", name }, + ThrowOnFailure = false + }); + } + } +} \ 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..379567d2b847 --- /dev/null +++ b/src/Shared/CertificateGeneration/Paths.cs @@ -0,0 +1,13 @@ +using System; +using static System.Environment; + +#nullable enable + +namespace Microsoft.AspNetCore.Certificates.Generation +{ + internal static class Paths + { + public static string? XdgRuntimeDir => Environment.GetEnvironmentVariable("XDG_RUNTIME_DIR"); + public static string Home => Environment.GetFolderPath(SpecialFolder.MyDocuments, SpecialFolderOption.DoNotVerify); + } +} \ No newline at end of file diff --git a/src/Shared/CertificateGeneration/PemCertificateFile.cs b/src/Shared/CertificateGeneration/PemCertificateFile.cs new file mode 100644 index 000000000000..344079ed112e --- /dev/null +++ b/src/Shared/CertificateGeneration/PemCertificateFile.cs @@ -0,0 +1,34 @@ +using System; +using System.IO; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; + +#nullable enable + +namespace Microsoft.AspNetCore.Certificates.Generation +{ + internal class PemCertificateFile : IDisposable + { + public string FilePath { get; } + public X509Certificate2 Certificate { get; } + + public PemCertificateFile(X509Certificate2 certificate) + { + Certificate = certificate; + string directory = Paths.XdgRuntimeDir ?? Paths.Home ?? Path.GetTempPath(); + FilePath = Path.Combine(directory, Guid.NewGuid() + ".pem"); + string pem = new string(PemEncoding.Write("CERTIFICATE", certificate.Export(X509ContentType.Cert))); + File.WriteAllText(FilePath, pem); + } + + public void Dispose() + { + try + { + File.Delete(FilePath); + } + finally + { } + } + } +} \ No newline at end of file diff --git a/src/Shared/CertificateGeneration/ProcessRunOptions.cs b/src/Shared/CertificateGeneration/ProcessRunOptions.cs new file mode 100644 index 000000000000..2f325a8519b3 --- /dev/null +++ b/src/Shared/CertificateGeneration/ProcessRunOptions.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +#nullable enable + +namespace Microsoft.AspNetCore.Certificates.Generation +{ + internal record ProcessRunOptions + { + public List Command { get; } = new(); + public bool ThrowOnFailure { get; init; } = true; + public bool ReadStandardOutput { get; init; } + public bool ReadStandardError { get; init; } = true; + public bool Elevate { get; init; } + public bool IsInteractive { get; init; } + public string? StandardInput { get; init; } + } +} \ No newline at end of file diff --git a/src/Shared/CertificateGeneration/ProcessRunResult.cs b/src/Shared/CertificateGeneration/ProcessRunResult.cs new file mode 100644 index 000000000000..211221197ba5 --- /dev/null +++ b/src/Shared/CertificateGeneration/ProcessRunResult.cs @@ -0,0 +1,15 @@ +using System.Text; + +#nullable enable + +namespace Microsoft.AspNetCore.Certificates.Generation +{ + internal record ProcessRunResult + { + public bool IsSuccess => ExitCode == 0; + public string CommandLine { get; init; } = ""; + public int ExitCode { get; init; } + public StringBuilder? StandardOutput { get; init; } + public StringBuilder? StandardError { get; init; } + } +} \ No newline at end of file diff --git a/src/Shared/CertificateGeneration/ProcessRunner.cs b/src/Shared/CertificateGeneration/ProcessRunner.cs new file mode 100644 index 000000000000..603d2b72318c --- /dev/null +++ b/src/Shared/CertificateGeneration/ProcessRunner.cs @@ -0,0 +1,106 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Text; + +#nullable enable + +namespace Microsoft.AspNetCore.Certificates.Generation +{ + internal class ProcessRunner + { + public static ProcessRunResult Run(ProcessRunOptions options) + { + Process process = new Process() + { + StartInfo = + { + RedirectStandardError = true, + RedirectStandardOutput = true, + RedirectStandardInput = true + } + }; + int i = 0; + if (options.Elevate) + { + process.StartInfo.FileName = "sudo"; + if (!options.IsInteractive) + { + process.StartInfo.ArgumentList.Add("--non-interactive"); + } + } + else + { + process.StartInfo.FileName = options.Command[i++]; + } + for (; i < options.Command.Count; i++) + { + process.StartInfo.ArgumentList.Add(options.Command[i]); + } + string commandLine = $"{process.StartInfo.FileName} {string.Join(" ", process.StartInfo.ArgumentList)}"; + bool readStdErr = options.ThrowOnFailure || options.ReadStandardError; + StringBuilder? stdErr = null; + if (readStdErr) + { + stdErr = new StringBuilder(); + process.ErrorDataReceived += (o, e) => { + if (e.Data != null) + { + stdErr.AppendLine(e.Data); + } + }; + } + StringBuilder? stdOut = null; + bool readStdOut = options.ReadStandardOutput; + if (readStdOut) + { + stdOut = new StringBuilder(); + process.OutputDataReceived += (o, e) => { + if (e.Data != null) + { + stdOut.AppendLine(e.Data); + } + }; + } + process.Start(); + process.BeginErrorReadLine(); + process.BeginOutputReadLine(); + if (!string.IsNullOrEmpty(options.StandardInput)) + { + process.StandardInput.Write(options.StandardInput); + } + process.StandardInput.Close(); + process.WaitForExit(); + if (options.ThrowOnFailure && process.ExitCode != 0) + { + throw new Exception($"Command '{commandLine}' failed with {process.ExitCode}: {stdErr}."); + } + return new ProcessRunResult + { + ExitCode = process.ExitCode, + CommandLine = commandLine, + StandardError = stdErr, + StandardOutput = stdOut + }; + } + + 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/UnixCertificateManager.cs b/src/Shared/CertificateGeneration/UnixCertificateManager.cs index 913a765f12cc..15f26dce76d4 100644 --- a/src/Shared/CertificateGeneration/UnixCertificateManager.cs +++ b/src/Shared/CertificateGeneration/UnixCertificateManager.cs @@ -1,11 +1,19 @@ using System; using System.Collections.Generic; using System.Security.Cryptography.X509Certificates; +using Microsoft.Extensions.Tools.Internal; namespace Microsoft.AspNetCore.Certificates.Generation { internal class UnixCertificateManager : CertificateManager { + private string CertificateStoreKey => $"aspnet-{Environment.UserName}"; + + private List _certificateStores; + + private List CertificateStores + => _certificateStores ??= CertificateStoreFinder.FindCertificateStores(); + public UnixCertificateManager() { } @@ -15,7 +23,19 @@ internal UnixCertificateManager(string subject, int version) { } - public override bool IsTrusted(X509Certificate2 certificate) => false; + public override bool IsTrusted(X509Certificate2 certificate) + { + // Return true when all stores trust the cert. + using var pemCertificateFile = new PemCertificateFile(certificate); + foreach (var store in CertificateStores) + { + if (!store.HasCertificate(CertificateStoreKey, certificate)) + { + return false; + } + } + return CertificateStores.Count > 0; + } protected override X509Certificate2 SaveCertificateCore(X509Certificate2 certificate, StoreName storeName, StoreLocation storeLocation) { @@ -47,12 +67,23 @@ 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, IReporter reporter, bool isInteractive) + { + using var pemFile = new PemCertificateFile(certificate); + foreach (var store in CertificateStores) + { + reporter.Output($"Installing into {store.StoreName}"); + store.TryInstallCertificate(CertificateStoreKey, pemFile, reporter, isInteractive); + // TODO: handle failure. + } + } - protected override void RemoveCertificateFromTrustedRoots(X509Certificate2 certificate) + protected override void RemoveCertificateFromTrustedRoots(X509Certificate2 certificate, IReporter reporter, bool isInteractive) { - // No-op here as is benign + foreach (var store in CertificateStores) + { + store.DeleteCertificate(CertificateStoreKey, reporter, isInteractive); + } } protected override IList GetCertificatesToRemove(StoreName storeName, StoreLocation storeLocation) diff --git a/src/Shared/CertificateGeneration/WindowsCertificateManager.cs b/src/Shared/CertificateGeneration/WindowsCertificateManager.cs index cefba748dfeb..81e046a55dad 100644 --- a/src/Shared/CertificateGeneration/WindowsCertificateManager.cs +++ b/src/Shared/CertificateGeneration/WindowsCertificateManager.cs @@ -4,6 +4,7 @@ using System.Runtime.Versioning; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; +using Microsoft.Extensions.Tools.Internal; namespace Microsoft.AspNetCore.Certificates.Generation { @@ -67,7 +68,7 @@ protected override X509Certificate2 SaveCertificateCore(X509Certificate2 certifi return certificate; } - protected override void TrustCertificateCore(X509Certificate2 certificate) + protected override void TrustCertificateCore(X509Certificate2 certificate, IReporter reporter, bool isInteractive) { using var publicCertificate = new X509Certificate2(certificate.Export(X509ContentType.Cert)); @@ -97,7 +98,7 @@ protected override void TrustCertificateCore(X509Certificate2 certificate) } } - protected override void RemoveCertificateFromTrustedRoots(X509Certificate2 certificate) + protected override void RemoveCertificateFromTrustedRoots(X509Certificate2 certificate, IReporter reporter, bool isInteractive) { Log.WindowsRemoveCertificateFromRootStoreStart(); using var store = new X509Store(StoreName.Root, StoreLocation.CurrentUser); diff --git a/src/Tools/FirstRunCertGenerator/src/Microsoft.AspNetCore.DeveloperCertificates.XPlat.csproj b/src/Tools/FirstRunCertGenerator/src/Microsoft.AspNetCore.DeveloperCertificates.XPlat.csproj index 0f373fe9274d..e75f8eac0fc6 100644 --- a/src/Tools/FirstRunCertGenerator/src/Microsoft.AspNetCore.DeveloperCertificates.XPlat.csproj +++ b/src/Tools/FirstRunCertGenerator/src/Microsoft.AspNetCore.DeveloperCertificates.XPlat.csproj @@ -14,6 +14,7 @@ + diff --git a/src/Tools/dotnet-dev-certs/src/Program.cs b/src/Tools/dotnet-dev-certs/src/Program.cs index f9f67013165d..826813db9f01 100644 --- a/src/Tools/dotnet-dev-certs/src/Program.cs +++ b/src/Tools/dotnet-dev-certs/src/Program.cs @@ -115,6 +115,7 @@ public static int Main(string[] args) c.OnExecute(() => { var reporter = new ConsoleReporter(PhysicalConsole.Singleton, verbose.HasValue(), quiet.HasValue()); + bool isInteractive = !Console.IsInputRedirected; if (verbose.HasValue()) { @@ -170,7 +171,7 @@ public static int Main(string[] args) if (clean.HasValue()) { - var clean = CleanHttpsCertificates(reporter); + var clean = CleanHttpsCertificates(reporter, isInteractive); if (clean != Success || !import.HasValue()) { return clean; @@ -179,7 +180,7 @@ public static int Main(string[] args) return ImportCertificate(import, password, reporter); } - return EnsureHttpsCertificate(exportPath, password, noPassword, trust, format, reporter); + return EnsureHttpsCertificate(exportPath, password, noPassword, trust, format, reporter, isInteractive); }); }); @@ -238,7 +239,7 @@ private static int ImportCertificate(CommandOption import, CommandOption passwor return Success; } - private static int CleanHttpsCertificates(IReporter reporter) + private static int CleanHttpsCertificates(IReporter reporter, bool isInteractive) { var manager = CertificateManager.Instance; try @@ -254,7 +255,7 @@ private static int CleanHttpsCertificates(IReporter reporter) "require elevated privileges. If that is the case, a prompt for credentials will be displayed."); } - manager.CleanupHttpsCertificates(); + manager.CleanupHttpsCertificates(reporter, isInteractive); reporter.Output("HTTPS development certificates successfully removed from the machine."); return Success; } @@ -296,32 +297,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; @@ -336,7 +326,7 @@ private static void ReportCertificates(IReporter reporter, IReadOnlyList Date: Mon, 7 Jun 2021 08:11:05 +0200 Subject: [PATCH 02/11] Remove isInteractive/reporter use --- .../CertificateFolderStore.cs | 37 ++++++------------- .../CertificateManager.cs | 32 +++++++++------- .../CertificateGeneration/CertificateStore.cs | 17 ++++----- .../MacOSCertificateManager.cs | 5 +-- .../NssCertificateDatabase.cs | 11 +++--- .../ProcessRunOptions.cs | 2 - .../CertificateGeneration/ProcessRunner.cs | 8 ---- .../UnixCertificateManager.cs | 11 +++--- .../WindowsCertificateManager.cs | 5 +-- ...NetCore.DeveloperCertificates.XPlat.csproj | 1 - src/Tools/dotnet-dev-certs/src/Program.cs | 15 +++----- 11 files changed, 58 insertions(+), 86 deletions(-) diff --git a/src/Shared/CertificateGeneration/CertificateFolderStore.cs b/src/Shared/CertificateGeneration/CertificateFolderStore.cs index e3607e70f201..55c00d5a79b9 100644 --- a/src/Shared/CertificateGeneration/CertificateFolderStore.cs +++ b/src/Shared/CertificateGeneration/CertificateFolderStore.cs @@ -1,7 +1,6 @@ using System.IO; using System.Security.Cryptography.X509Certificates; using Microsoft.AspNetCore.Certificates.Generation; -using Microsoft.Extensions.Tools.Internal; #nullable enable @@ -22,31 +21,21 @@ public CertificateFolderStore(string name, string path, ProcessRunOptions update _updateStore = updateStore; } - private void ReportElevationNeeded(IReporter? reporter) + public override bool CheckDependencies() { - if (Elevate) - { - reporter?.Output($"Changing '{StoreName}' requires root priviledges. You may be prompted for your password."); - } - } - - public override bool CheckDependencies(IReporter? reporter) - { - return CheckProgramDependency(_updateStore, reporter); + return CheckProgramDependency(_updateStore); } - public override bool TryInstallCertificate(string name, PemCertificateFile pemFile, IReporter? reporter, bool isInteractive) + public override bool TryInstallCertificate(string name, PemCertificateFile pemFile) { - ReportElevationNeeded(reporter); - CopyFile(pemFile.FilePath, GetCertificatePath(name), isInteractive); - ProcessRunner.Run(_updateStore with { IsInteractive = isInteractive }); + CopyFile(pemFile.FilePath, GetCertificatePath(name)); + ProcessRunner.Run(_updateStore); return true; } - public override void DeleteCertificate(string name, IReporter? reporter, bool isInteractive) + public override void DeleteCertificate(string name) { - ReportElevationNeeded(reporter); - DeleteFile(GetCertificatePath(name), isInteractive); + DeleteFile(GetCertificatePath(name)); } public override bool HasCertificate(string name, X509Certificate2 certificate) @@ -63,7 +52,7 @@ public override bool HasCertificate(string name, X509Certificate2 certificate) private string GetCertificatePath(string name) => Path.Combine(FolderPath, name + ".pem"); - private void DeleteFile(string path, bool isInteractive) + private void DeleteFile(string path) { if (!File.Exists(path)) { @@ -75,8 +64,7 @@ private void DeleteFile(string path, bool isInteractive) ProcessRunner.Run(new() { Command = { "rm", path }, - Elevate = true, - IsInteractive = isInteractive + Elevate = true }); } else @@ -85,17 +73,16 @@ private void DeleteFile(string path, bool isInteractive) } } - private void CopyFile(string sourceFileName, string destFileName, bool isInteractive) + private void CopyFile(string sourceFileName, string destFileName) { - DeleteFile(destFileName, isInteractive); + DeleteFile(destFileName); if (Elevate) { ProcessRunner.Run(new() { Command = { "cp", sourceFileName, destFileName }, - Elevate = true, - IsInteractive = isInteractive + Elevate = true }); } else diff --git a/src/Shared/CertificateGeneration/CertificateManager.cs b/src/Shared/CertificateGeneration/CertificateManager.cs index b54d9236e7fe..e76787d06204 100644 --- a/src/Shared/CertificateGeneration/CertificateManager.cs +++ b/src/Shared/CertificateGeneration/CertificateManager.cs @@ -10,7 +10,6 @@ using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; -using Microsoft.Extensions.Tools.Internal; #nullable enable @@ -172,7 +171,6 @@ public EnsureCertificateResult EnsureAspNetCoreHttpsDevelopmentCertificate( bool includePrivateKey = false, string? password = null, CertificateKeyExportFormat keyExportFormat = CertificateKeyExportFormat.Pfx, - IReporter? reporter = null, bool isInteractive = true) { var result = EnsureCertificateResult.Succeeded; @@ -329,7 +327,7 @@ public EnsureCertificateResult EnsureAspNetCoreHttpsDevelopmentCertificate( { try { - TrustCertificate(certificate, reporter, isInteractive); + TrustCertificate(certificate); } catch (UserCancelledTrustException) { @@ -410,7 +408,7 @@ internal ImportCertificateResult ImportCertificate(string certificatePath, strin return ImportCertificateResult.Succeeded; } - public void CleanupHttpsCertificates(IReporter? reporter = null, bool isInteractive = false) + public void CleanupHttpsCertificates() { // On OS X we don't have a good way to manage trusted certificates in the system keychain // so we do everything by invoking the native toolchain. @@ -431,7 +429,7 @@ public void CleanupHttpsCertificates(IReporter? reporter = null, bool isInteract foreach (var certificate in filteredCertificates) { - RemoveCertificate(certificate, RemoveLocations.All, reporter, isInteractive); + RemoveCertificate(certificate, RemoveLocations.All); } } @@ -439,11 +437,11 @@ public void CleanupHttpsCertificates(IReporter? reporter = null, bool isInteract protected abstract X509Certificate2 SaveCertificateCore(X509Certificate2 certificate, StoreName storeName, StoreLocation storeLocation); - protected abstract void TrustCertificateCore(X509Certificate2 certificate, IReporter? reporter, bool isInteractive); + protected abstract void TrustCertificateCore(X509Certificate2 certificate); protected abstract bool IsExportable(X509Certificate2 c); - protected abstract void RemoveCertificateFromTrustedRoots(X509Certificate2 certificate, IReporter? reporter, bool isInteractive); + protected abstract void RemoveCertificateFromTrustedRoots(X509Certificate2 certificate); protected abstract IList GetCertificatesToRemove(StoreName storeName, StoreLocation storeLocation); @@ -641,7 +639,7 @@ internal X509Certificate2 SaveCertificate(X509Certificate2 certificate) return certificate; } - internal void TrustCertificate(X509Certificate2 certificate, IReporter? reporter, bool isInteractive) + internal void TrustCertificate(X509Certificate2 certificate) { try { @@ -649,7 +647,7 @@ internal void TrustCertificate(X509Certificate2 certificate, IReporter? reporter { Log.TrustCertificateStart(GetDescription(certificate)); } - TrustCertificateCore(certificate, reporter, isInteractive); + TrustCertificateCore(certificate); Log.TrustCertificateEnd(); } catch (Exception ex) when (Log.IsEnabled()) @@ -660,7 +658,7 @@ internal void TrustCertificate(X509Certificate2 certificate, IReporter? reporter } // Internal, for testing purposes only. - internal void RemoveAllCertificates(StoreName storeName, StoreLocation storeLocation, IReporter? reporter = null, bool isInteractive = false) + internal void RemoveAllCertificates(StoreName storeName, StoreLocation storeLocation) { var certificates = GetCertificatesToRemove(storeName, storeLocation); var certificatesWithName = certificates.Where(c => c.Subject == Subject); @@ -669,13 +667,13 @@ internal void RemoveAllCertificates(StoreName storeName, StoreLocation storeLoca foreach (var certificate in certificates) { - RemoveCertificate(certificate, removeLocation, reporter, isInteractive); + RemoveCertificate(certificate, removeLocation); } DisposeCertificates(certificates); } - internal void RemoveCertificate(X509Certificate2 certificate, RemoveLocations locations, IReporter? reporter, bool isInteractive) + internal void RemoveCertificate(X509Certificate2 certificate, RemoveLocations locations) { switch (locations) { @@ -685,10 +683,10 @@ internal void RemoveCertificate(X509Certificate2 certificate, RemoveLocations lo RemoveCertificateFromUserStore(certificate); break; case RemoveLocations.Trusted: - RemoveCertificateFromTrustedRoots(certificate, reporter, isInteractive); + RemoveCertificateFromTrustedRoots(certificate); break; case RemoveLocations.All: - RemoveCertificateFromTrustedRoots(certificate, reporter, isInteractive); + RemoveCertificateFromTrustedRoots(certificate); RemoveCertificateFromUserStore(certificate); break; default: @@ -972,6 +970,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 LinuxCertutilInstallFailed(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 index cd5a53181a02..5b04ea274ccf 100644 --- a/src/Shared/CertificateGeneration/CertificateStore.cs +++ b/src/Shared/CertificateGeneration/CertificateStore.cs @@ -1,6 +1,5 @@ using System; using System.Security.Cryptography.X509Certificates; -using Microsoft.Extensions.Tools.Internal; #nullable enable @@ -15,28 +14,28 @@ protected CertificateStore(string name) StoreName = name; } - public abstract bool CheckDependencies(IReporter? reporter); + public abstract bool CheckDependencies(); - public abstract bool TryInstallCertificate(string name, PemCertificateFile pemFile, IReporter? reporter, bool isInteractive); + public abstract bool TryInstallCertificate(string name, PemCertificateFile pemFile); - public abstract void DeleteCertificate(string name, IReporter? reporter, bool isInteractive); + public abstract void DeleteCertificate(string name); public abstract bool HasCertificate(string name, X509Certificate2 pemContent); - protected bool CheckProgramDependency(string program, IReporter? reporter) + protected bool CheckProgramDependency(string program) { if (!ProcessRunner.HasProgram(program)) { - reporter?.Warn($"Cannot use '{StoreName}' because '{program}' is not installed."); + // TODO reporter?.Warn($"Cannot use '{StoreName}' because '{program}' is not installed."); return false; } return true; } - protected bool CheckProgramDependency(ProcessRunOptions runOptions, IReporter? reporter) + protected bool CheckProgramDependency(ProcessRunOptions runOptions) { - return CheckProgramDependency(runOptions.Command[0], reporter) - & (!runOptions.Elevate || CheckProgramDependency("sudo", reporter)); + return CheckProgramDependency(runOptions.Command[0]) + & (!runOptions.Elevate || CheckProgramDependency("sudo")); } protected bool ContainsCertificate(string storeContent, string certificateContent) diff --git a/src/Shared/CertificateGeneration/MacOSCertificateManager.cs b/src/Shared/CertificateGeneration/MacOSCertificateManager.cs index 4c96c10e4f7d..15e10b885d97 100644 --- a/src/Shared/CertificateGeneration/MacOSCertificateManager.cs +++ b/src/Shared/CertificateGeneration/MacOSCertificateManager.cs @@ -8,7 +8,6 @@ using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text.RegularExpressions; -using Microsoft.Extensions.Tools.Internal; namespace Microsoft.AspNetCore.Certificates.Generation { @@ -51,7 +50,7 @@ internal MacOSCertificateManager(string subject, int version) { } - protected override void TrustCertificateCore(X509Certificate2 publicCertificate, IReporter reporter, bool isInteractive) + protected override void TrustCertificateCore(X509Certificate2 publicCertificate) { var tmpFile = Path.GetTempFileName(); try @@ -160,7 +159,7 @@ public override bool IsTrusted(X509Certificate2 certificate) return hashes.Any(h => string.Equals(h, certificate.Thumbprint, StringComparison.Ordinal)); } - protected override void RemoveCertificateFromTrustedRoots(X509Certificate2 certificate, IReporter reporter, bool isInteractive) + 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 index 655c9bef12b9..8b6c7cfcab1a 100644 --- a/src/Shared/CertificateGeneration/NssCertificateDatabase.cs +++ b/src/Shared/CertificateGeneration/NssCertificateDatabase.cs @@ -1,6 +1,5 @@ using System; using System.Security.Cryptography.X509Certificates; -using Microsoft.Extensions.Tools.Internal; #nullable enable @@ -16,19 +15,19 @@ public NssCertificateDatabase(string name, string path) : DatabasePath = path; } - public override bool CheckDependencies(IReporter? reporter) + public override bool CheckDependencies() { - return CheckProgramDependency("certutil", reporter); + return CheckProgramDependency("certutil"); } - public override bool TryInstallCertificate(string name, PemCertificateFile pemFile, IReporter? reporter, bool isInteractive) + public override bool TryInstallCertificate(string name, PemCertificateFile pemFile) { var result = ProcessRunner.Run(new() { Command = { "certutil", "-d", DatabasePath, "-A", "-t", "C,,", "-n", name, "-i", pemFile.FilePath }, ThrowOnFailure = false }); if (!result.IsSuccess) { - reporter?.Error($"Failed to install certificate using command '{result.CommandLine}': {result.StandardError}"); + CertificateManager.Log.LinuxCertutilInstallFailed(result.CommandLine, result.ExitCode, result.StandardError!.ToString()); } return result.IsSuccess; } @@ -59,7 +58,7 @@ public override bool HasCertificate(string name, X509Certificate2 certificate) return false; } - public override void DeleteCertificate(string name, IReporter? reporter, bool isInteractive) + public override void DeleteCertificate(string name) { ProcessRunner.Run(new() { diff --git a/src/Shared/CertificateGeneration/ProcessRunOptions.cs b/src/Shared/CertificateGeneration/ProcessRunOptions.cs index 2f325a8519b3..4e1607eed217 100644 --- a/src/Shared/CertificateGeneration/ProcessRunOptions.cs +++ b/src/Shared/CertificateGeneration/ProcessRunOptions.cs @@ -11,7 +11,5 @@ internal record ProcessRunOptions public bool ReadStandardOutput { get; init; } public bool ReadStandardError { get; init; } = true; public bool Elevate { get; init; } - public bool IsInteractive { get; init; } - public string? StandardInput { get; init; } } } \ No newline at end of file diff --git a/src/Shared/CertificateGeneration/ProcessRunner.cs b/src/Shared/CertificateGeneration/ProcessRunner.cs index 603d2b72318c..3e9057c18dfd 100644 --- a/src/Shared/CertificateGeneration/ProcessRunner.cs +++ b/src/Shared/CertificateGeneration/ProcessRunner.cs @@ -24,10 +24,6 @@ public static ProcessRunResult Run(ProcessRunOptions options) if (options.Elevate) { process.StartInfo.FileName = "sudo"; - if (!options.IsInteractive) - { - process.StartInfo.ArgumentList.Add("--non-interactive"); - } } else { @@ -65,10 +61,6 @@ public static ProcessRunResult Run(ProcessRunOptions options) process.Start(); process.BeginErrorReadLine(); process.BeginOutputReadLine(); - if (!string.IsNullOrEmpty(options.StandardInput)) - { - process.StandardInput.Write(options.StandardInput); - } process.StandardInput.Close(); process.WaitForExit(); if (options.ThrowOnFailure && process.ExitCode != 0) diff --git a/src/Shared/CertificateGeneration/UnixCertificateManager.cs b/src/Shared/CertificateGeneration/UnixCertificateManager.cs index 15f26dce76d4..a8392f598c7e 100644 --- a/src/Shared/CertificateGeneration/UnixCertificateManager.cs +++ b/src/Shared/CertificateGeneration/UnixCertificateManager.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Security.Cryptography.X509Certificates; -using Microsoft.Extensions.Tools.Internal; namespace Microsoft.AspNetCore.Certificates.Generation { @@ -67,22 +66,22 @@ internal override void CorrectCertificateState(X509Certificate2 candidate) protected override bool IsExportable(X509Certificate2 c) => true; - protected override void TrustCertificateCore(X509Certificate2 certificate, IReporter reporter, bool isInteractive) + protected override void TrustCertificateCore(X509Certificate2 certificate) { using var pemFile = new PemCertificateFile(certificate); foreach (var store in CertificateStores) { - reporter.Output($"Installing into {store.StoreName}"); - store.TryInstallCertificate(CertificateStoreKey, pemFile, reporter, isInteractive); + CertificateManager.Log.LinuxTrustCertificate(CertificateManager.GetDescription(certificate), store.StoreName); + store.TryInstallCertificate(CertificateStoreKey, pemFile); // TODO: handle failure. } } - protected override void RemoveCertificateFromTrustedRoots(X509Certificate2 certificate, IReporter reporter, bool isInteractive) + protected override void RemoveCertificateFromTrustedRoots(X509Certificate2 certificate) { foreach (var store in CertificateStores) { - store.DeleteCertificate(CertificateStoreKey, reporter, isInteractive); + store.DeleteCertificate(CertificateStoreKey); } } diff --git a/src/Shared/CertificateGeneration/WindowsCertificateManager.cs b/src/Shared/CertificateGeneration/WindowsCertificateManager.cs index 81e046a55dad..cefba748dfeb 100644 --- a/src/Shared/CertificateGeneration/WindowsCertificateManager.cs +++ b/src/Shared/CertificateGeneration/WindowsCertificateManager.cs @@ -4,7 +4,6 @@ using System.Runtime.Versioning; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; -using Microsoft.Extensions.Tools.Internal; namespace Microsoft.AspNetCore.Certificates.Generation { @@ -68,7 +67,7 @@ protected override X509Certificate2 SaveCertificateCore(X509Certificate2 certifi return certificate; } - protected override void TrustCertificateCore(X509Certificate2 certificate, IReporter reporter, bool isInteractive) + protected override void TrustCertificateCore(X509Certificate2 certificate) { using var publicCertificate = new X509Certificate2(certificate.Export(X509ContentType.Cert)); @@ -98,7 +97,7 @@ protected override void TrustCertificateCore(X509Certificate2 certificate, IRepo } } - protected override void RemoveCertificateFromTrustedRoots(X509Certificate2 certificate, IReporter reporter, bool isInteractive) + protected override void RemoveCertificateFromTrustedRoots(X509Certificate2 certificate) { Log.WindowsRemoveCertificateFromRootStoreStart(); using var store = new X509Store(StoreName.Root, StoreLocation.CurrentUser); diff --git a/src/Tools/FirstRunCertGenerator/src/Microsoft.AspNetCore.DeveloperCertificates.XPlat.csproj b/src/Tools/FirstRunCertGenerator/src/Microsoft.AspNetCore.DeveloperCertificates.XPlat.csproj index e75f8eac0fc6..0f373fe9274d 100644 --- a/src/Tools/FirstRunCertGenerator/src/Microsoft.AspNetCore.DeveloperCertificates.XPlat.csproj +++ b/src/Tools/FirstRunCertGenerator/src/Microsoft.AspNetCore.DeveloperCertificates.XPlat.csproj @@ -14,7 +14,6 @@ - diff --git a/src/Tools/dotnet-dev-certs/src/Program.cs b/src/Tools/dotnet-dev-certs/src/Program.cs index 826813db9f01..5ef0ba02d0eb 100644 --- a/src/Tools/dotnet-dev-certs/src/Program.cs +++ b/src/Tools/dotnet-dev-certs/src/Program.cs @@ -115,7 +115,6 @@ public static int Main(string[] args) c.OnExecute(() => { var reporter = new ConsoleReporter(PhysicalConsole.Singleton, verbose.HasValue(), quiet.HasValue()); - bool isInteractive = !Console.IsInputRedirected; if (verbose.HasValue()) { @@ -171,7 +170,7 @@ public static int Main(string[] args) if (clean.HasValue()) { - var clean = CleanHttpsCertificates(reporter, isInteractive); + var clean = CleanHttpsCertificates(reporter); if (clean != Success || !import.HasValue()) { return clean; @@ -180,7 +179,7 @@ public static int Main(string[] args) return ImportCertificate(import, password, reporter); } - return EnsureHttpsCertificate(exportPath, password, noPassword, trust, format, reporter, isInteractive); + return EnsureHttpsCertificate(exportPath, password, noPassword, trust, format, reporter); }); }); @@ -239,7 +238,7 @@ private static int ImportCertificate(CommandOption import, CommandOption passwor return Success; } - private static int CleanHttpsCertificates(IReporter reporter, bool isInteractive) + private static int CleanHttpsCertificates(IReporter reporter) { var manager = CertificateManager.Instance; try @@ -255,7 +254,7 @@ private static int CleanHttpsCertificates(IReporter reporter, bool isInteractive "require elevated privileges. If that is the case, a prompt for credentials will be displayed."); } - manager.CleanupHttpsCertificates(reporter, isInteractive); + manager.CleanupHttpsCertificates(); reporter.Output("HTTPS development certificates successfully removed from the machine."); return Success; } @@ -326,7 +325,7 @@ private static void ReportCertificates(IReporter reporter, IReadOnlyList Date: Mon, 7 Jun 2021 08:25:46 +0200 Subject: [PATCH 03/11] Report warning when distro doesn't support trusting. --- .../CertificateManager.cs | 2 + .../MacOSCertificateManager.cs | 2 + .../UnixCertificateManager.cs | 4 ++ .../WindowsCertificateManager.cs | 2 + src/Tools/dotnet-dev-certs/src/Program.cs | 42 +++++++++++-------- 5 files changed, 35 insertions(+), 17 deletions(-) diff --git a/src/Shared/CertificateGeneration/CertificateManager.cs b/src/Shared/CertificateGeneration/CertificateManager.cs index e76787d06204..2bc3853f45c4 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); 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/UnixCertificateManager.cs b/src/Shared/CertificateGeneration/UnixCertificateManager.cs index a8392f598c7e..b8218a0300f5 100644 --- a/src/Shared/CertificateGeneration/UnixCertificateManager.cs +++ b/src/Shared/CertificateGeneration/UnixCertificateManager.cs @@ -24,6 +24,8 @@ internal UnixCertificateManager(string subject, int version) public override bool IsTrusted(X509Certificate2 certificate) { + // TODO: support 'partial' return. + // Return true when all stores trust the cert. using var pemCertificateFile = new PemCertificateFile(certificate); foreach (var store in CertificateStores) @@ -36,6 +38,8 @@ public override bool IsTrusted(X509Certificate2 certificate) return CertificateStores.Count > 0; } + public override bool SupportsTrust => CertificateStores.Count > 0; + protected override X509Certificate2 SaveCertificateCore(X509Certificate2 certificate, StoreName storeName, StoreLocation storeLocation) { var export = certificate.Export(X509ContentType.Pkcs12, ""); 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 5ef0ba02d0eb..07bafcbc6484 100644 --- a/src/Tools/dotnet-dev-certs/src/Program.cs +++ b/src/Tools/dotnet-dev-certs/src/Program.cs @@ -350,26 +350,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. If the certificate is not " + - "already trusted we will run commands using 'sudo'." + - Environment.NewLine + "This might prompt you for your password."); + 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."); + } } } @@ -384,7 +392,7 @@ private static int EnsureHttpsCertificate(CommandOption exportPath, CommandOptio now, now.Add(HttpsCertificateValidity), exportPath.Value(), - trust == null ? false : trust.HasValue(), + trust == null ? false : trust.HasValue() && manager.SupportsTrust, password.HasValue() || (noPassword.HasValue() && format == CertificateKeyExportFormat.Pem), password.Value(), exportFormat.HasValue() ? format : CertificateKeyExportFormat.Pfx); From 4826dd9d67fe03b01cd45d3313917066deb6291a Mon Sep 17 00:00:00 2001 From: Tom Deseyn Date: Mon, 7 Jun 2021 09:09:50 +0200 Subject: [PATCH 04/11] Derive store key from certificate thumbprint --- .../CertificateFolderStore.cs | 17 +++++++++-------- .../CertificateGeneration/CertificateStore.cs | 6 +++--- .../NssCertificateDatabase.cs | 13 ++++++++++--- .../UnixCertificateManager.cs | 9 +++------ 4 files changed, 25 insertions(+), 20 deletions(-) diff --git a/src/Shared/CertificateGeneration/CertificateFolderStore.cs b/src/Shared/CertificateGeneration/CertificateFolderStore.cs index 55c00d5a79b9..d1af28d65c26 100644 --- a/src/Shared/CertificateGeneration/CertificateFolderStore.cs +++ b/src/Shared/CertificateGeneration/CertificateFolderStore.cs @@ -26,21 +26,22 @@ public override bool CheckDependencies() return CheckProgramDependency(_updateStore); } - public override bool TryInstallCertificate(string name, PemCertificateFile pemFile) + public override bool TryInstallCertificate(X509Certificate2 certificate) { - CopyFile(pemFile.FilePath, GetCertificatePath(name)); + using var pemFile = new PemCertificateFile(certificate); + CopyFile(pemFile.FilePath, GetCertificatePath(certificate)); ProcessRunner.Run(_updateStore); return true; } - public override void DeleteCertificate(string name) + public override void DeleteCertificate(X509Certificate2 certificate) { - DeleteFile(GetCertificatePath(name)); + DeleteFile(GetCertificatePath(certificate)); } - public override bool HasCertificate(string name, X509Certificate2 certificate) + public override bool HasCertificate(X509Certificate2 certificate) { - string certificatePath = GetCertificatePath(name); + string certificatePath = GetCertificatePath(certificate); if (!File.Exists(certificatePath)) { return false; @@ -49,8 +50,8 @@ public override bool HasCertificate(string name, X509Certificate2 certificate) return storeCertificate.Equals(certificate); } - private string GetCertificatePath(string name) - => Path.Combine(FolderPath, name + ".pem"); + private string GetCertificatePath(X509Certificate2 certificate) + => Path.Combine(FolderPath, "aspnet-" + certificate.Thumbprint + ".pem"); private void DeleteFile(string path) { diff --git a/src/Shared/CertificateGeneration/CertificateStore.cs b/src/Shared/CertificateGeneration/CertificateStore.cs index 5b04ea274ccf..4e217ba03631 100644 --- a/src/Shared/CertificateGeneration/CertificateStore.cs +++ b/src/Shared/CertificateGeneration/CertificateStore.cs @@ -16,11 +16,11 @@ protected CertificateStore(string name) public abstract bool CheckDependencies(); - public abstract bool TryInstallCertificate(string name, PemCertificateFile pemFile); + public abstract bool TryInstallCertificate(X509Certificate2 certificate); - public abstract void DeleteCertificate(string name); + public abstract void DeleteCertificate(X509Certificate2 certificate); - public abstract bool HasCertificate(string name, X509Certificate2 pemContent); + public abstract bool HasCertificate(X509Certificate2 certificate); protected bool CheckProgramDependency(string program) { diff --git a/src/Shared/CertificateGeneration/NssCertificateDatabase.cs b/src/Shared/CertificateGeneration/NssCertificateDatabase.cs index 8b6c7cfcab1a..6d53a70085d7 100644 --- a/src/Shared/CertificateGeneration/NssCertificateDatabase.cs +++ b/src/Shared/CertificateGeneration/NssCertificateDatabase.cs @@ -20,8 +20,10 @@ public override bool CheckDependencies() return CheckProgramDependency("certutil"); } - public override bool TryInstallCertificate(string name, PemCertificateFile pemFile) + public override bool TryInstallCertificate(X509Certificate2 certificate) { + using var pemFile = new PemCertificateFile(certificate); + string name = GetCertificateNickname(certificate); var result = ProcessRunner.Run(new() { Command = { "certutil", "-d", DatabasePath, "-A", "-t", "C,,", "-n", name, "-i", pemFile.FilePath }, ThrowOnFailure = false }); @@ -32,8 +34,9 @@ public override bool TryInstallCertificate(string name, PemCertificateFile pemFi return result.IsSuccess; } - public override bool HasCertificate(string name, X509Certificate2 certificate) + public override bool HasCertificate(X509Certificate2 certificate) { + string name = GetCertificateNickname(certificate); ProcessRunResult runResult = ProcessRunner.Run(new() { Command = { "certutil", "-d", DatabasePath, "-L", "-n", name, "-a" }, @@ -58,13 +61,17 @@ public override bool HasCertificate(string name, X509Certificate2 certificate) return false; } - public override void DeleteCertificate(string name) + public override void DeleteCertificate(X509Certificate2 certificate) { + string name = GetCertificateNickname(certificate); ProcessRunner.Run(new() { Command = { "certutil", "-d", DatabasePath, "-D", "-n", name }, ThrowOnFailure = false }); } + + private string GetCertificateNickname(X509Certificate2 certificate) + => "aspnet-" + certificate.Thumbprint; } } \ No newline at end of file diff --git a/src/Shared/CertificateGeneration/UnixCertificateManager.cs b/src/Shared/CertificateGeneration/UnixCertificateManager.cs index b8218a0300f5..2faa445403bb 100644 --- a/src/Shared/CertificateGeneration/UnixCertificateManager.cs +++ b/src/Shared/CertificateGeneration/UnixCertificateManager.cs @@ -6,8 +6,6 @@ namespace Microsoft.AspNetCore.Certificates.Generation { internal class UnixCertificateManager : CertificateManager { - private string CertificateStoreKey => $"aspnet-{Environment.UserName}"; - private List _certificateStores; private List CertificateStores @@ -30,7 +28,7 @@ public override bool IsTrusted(X509Certificate2 certificate) using var pemCertificateFile = new PemCertificateFile(certificate); foreach (var store in CertificateStores) { - if (!store.HasCertificate(CertificateStoreKey, certificate)) + if (!store.HasCertificate(certificate)) { return false; } @@ -72,11 +70,10 @@ internal override void CorrectCertificateState(X509Certificate2 candidate) protected override void TrustCertificateCore(X509Certificate2 certificate) { - using var pemFile = new PemCertificateFile(certificate); foreach (var store in CertificateStores) { CertificateManager.Log.LinuxTrustCertificate(CertificateManager.GetDescription(certificate), store.StoreName); - store.TryInstallCertificate(CertificateStoreKey, pemFile); + store.TryInstallCertificate(certificate); // TODO: handle failure. } } @@ -85,7 +82,7 @@ protected override void RemoveCertificateFromTrustedRoots(X509Certificate2 certi { foreach (var store in CertificateStores) { - store.DeleteCertificate(CertificateStoreKey); + store.DeleteCertificate(certificate); } } From b2d17477abdaa08515e794ac6d43f5ae1d31977d Mon Sep 17 00:00:00 2001 From: Tom Deseyn Date: Mon, 7 Jun 2021 09:50:33 +0200 Subject: [PATCH 05/11] Remove PemCertificateFile class --- .../CertificateFolderStore.cs | 21 +++++++++--- .../CertificateManager.cs | 2 +- .../NssCertificateDatabase.cs | 30 +++++++++++----- src/Shared/CertificateGeneration/Paths.cs | 14 ++++++-- .../PemCertificateFile.cs | 34 ------------------- .../UnixCertificateManager.cs | 1 - 6 files changed, 52 insertions(+), 50 deletions(-) delete mode 100644 src/Shared/CertificateGeneration/PemCertificateFile.cs diff --git a/src/Shared/CertificateGeneration/CertificateFolderStore.cs b/src/Shared/CertificateGeneration/CertificateFolderStore.cs index d1af28d65c26..c92aff9f603e 100644 --- a/src/Shared/CertificateGeneration/CertificateFolderStore.cs +++ b/src/Shared/CertificateGeneration/CertificateFolderStore.cs @@ -28,10 +28,23 @@ public override bool CheckDependencies() public override bool TryInstallCertificate(X509Certificate2 certificate) { - using var pemFile = new PemCertificateFile(certificate); - CopyFile(pemFile.FilePath, GetCertificatePath(certificate)); - ProcessRunner.Run(_updateStore); - return true; + string pemFile = Paths.GetUserTempFile(".pem"); + try + { + CertificateManager.ExportCertificate(certificate, pemFile, includePrivateKey: false, password: null, CertificateKeyExportFormat.Pem); + CopyFile(pemFile, GetCertificatePath(certificate)); + ProcessRunner.Run(_updateStore); + return true; + } + finally + { + try + { + File.Delete(pemFile); + } + catch + { } + } } public override void DeleteCertificate(X509Certificate2 certificate) diff --git a/src/Shared/CertificateGeneration/CertificateManager.cs b/src/Shared/CertificateGeneration/CertificateManager.cs index 2bc3853f45c4..337ed621fdde 100644 --- a/src/Shared/CertificateGeneration/CertificateManager.cs +++ b/src/Shared/CertificateGeneration/CertificateManager.cs @@ -447,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()) { diff --git a/src/Shared/CertificateGeneration/NssCertificateDatabase.cs b/src/Shared/CertificateGeneration/NssCertificateDatabase.cs index 6d53a70085d7..97b923653aa4 100644 --- a/src/Shared/CertificateGeneration/NssCertificateDatabase.cs +++ b/src/Shared/CertificateGeneration/NssCertificateDatabase.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using System.Security.Cryptography.X509Certificates; #nullable enable @@ -22,16 +23,29 @@ public override bool CheckDependencies() public override bool TryInstallCertificate(X509Certificate2 certificate) { - using var pemFile = new PemCertificateFile(certificate); - string name = GetCertificateNickname(certificate); - var result = ProcessRunner.Run(new() { - Command = { "certutil", "-d", DatabasePath, "-A", "-t", "C,,", "-n", name, "-i", pemFile.FilePath }, - ThrowOnFailure = false }); - if (!result.IsSuccess) + string pemFile = Paths.GetUserTempFile(".pem"); + try { - CertificateManager.Log.LinuxCertutilInstallFailed(result.CommandLine, result.ExitCode, result.StandardError!.ToString()); + CertificateManager.ExportCertificate(certificate, pemFile, includePrivateKey: false, password: null, CertificateKeyExportFormat.Pem); + string name = GetCertificateNickname(certificate); + var result = ProcessRunner.Run(new() { + Command = { "certutil", "-d", DatabasePath, "-A", "-t", "C,,", "-n", name, "-i", pemFile }, + ThrowOnFailure = false }); + if (!result.IsSuccess) + { + CertificateManager.Log.LinuxCertutilInstallFailed(result.CommandLine, result.ExitCode, result.StandardError!.ToString()); + } + return result.IsSuccess; + } + finally + { + try + { + File.Delete(pemFile); + } + catch + { } } - return result.IsSuccess; } public override bool HasCertificate(X509Certificate2 certificate) diff --git a/src/Shared/CertificateGeneration/Paths.cs b/src/Shared/CertificateGeneration/Paths.cs index 379567d2b847..6f35e824a928 100644 --- a/src/Shared/CertificateGeneration/Paths.cs +++ b/src/Shared/CertificateGeneration/Paths.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using static System.Environment; #nullable enable @@ -7,7 +8,16 @@ namespace Microsoft.AspNetCore.Certificates.Generation { internal static class Paths { - public static string? XdgRuntimeDir => Environment.GetEnvironmentVariable("XDG_RUNTIME_DIR"); - public static string Home => Environment.GetFolderPath(SpecialFolder.MyDocuments, SpecialFolderOption.DoNotVerify); + 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/PemCertificateFile.cs b/src/Shared/CertificateGeneration/PemCertificateFile.cs deleted file mode 100644 index 344079ed112e..000000000000 --- a/src/Shared/CertificateGeneration/PemCertificateFile.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.IO; -using System.Security.Cryptography; -using System.Security.Cryptography.X509Certificates; - -#nullable enable - -namespace Microsoft.AspNetCore.Certificates.Generation -{ - internal class PemCertificateFile : IDisposable - { - public string FilePath { get; } - public X509Certificate2 Certificate { get; } - - public PemCertificateFile(X509Certificate2 certificate) - { - Certificate = certificate; - string directory = Paths.XdgRuntimeDir ?? Paths.Home ?? Path.GetTempPath(); - FilePath = Path.Combine(directory, Guid.NewGuid() + ".pem"); - string pem = new string(PemEncoding.Write("CERTIFICATE", certificate.Export(X509ContentType.Cert))); - File.WriteAllText(FilePath, pem); - } - - public void Dispose() - { - try - { - File.Delete(FilePath); - } - finally - { } - } - } -} \ No newline at end of file diff --git a/src/Shared/CertificateGeneration/UnixCertificateManager.cs b/src/Shared/CertificateGeneration/UnixCertificateManager.cs index 2faa445403bb..3942e4086f31 100644 --- a/src/Shared/CertificateGeneration/UnixCertificateManager.cs +++ b/src/Shared/CertificateGeneration/UnixCertificateManager.cs @@ -25,7 +25,6 @@ public override bool IsTrusted(X509Certificate2 certificate) // TODO: support 'partial' return. // Return true when all stores trust the cert. - using var pemCertificateFile = new PemCertificateFile(certificate); foreach (var store in CertificateStores) { if (!store.HasCertificate(certificate)) From 5ad3eabe28e73d798f034f728db3343be8c86638 Mon Sep 17 00:00:00 2001 From: Tom Deseyn Date: Mon, 7 Jun 2021 11:11:44 +0200 Subject: [PATCH 06/11] Remove ProcessRunner --- .../CertificateFolderStore.cs | 81 ++++++++++----- .../CertificateManager.cs | 2 +- .../CertificateGeneration/CertificateStore.cs | 9 +- .../CertificateStoreFinder.cs | 16 +-- .../NssCertificateDatabase.cs | 50 ++++++---- .../CertificateGeneration/ProcessHelper.cs | 34 +++++++ .../ProcessRunOptions.cs | 15 --- .../CertificateGeneration/ProcessRunResult.cs | 15 --- .../CertificateGeneration/ProcessRunner.cs | 98 ------------------- 9 files changed, 139 insertions(+), 181 deletions(-) create mode 100644 src/Shared/CertificateGeneration/ProcessHelper.cs delete mode 100644 src/Shared/CertificateGeneration/ProcessRunOptions.cs delete mode 100644 src/Shared/CertificateGeneration/ProcessRunResult.cs delete mode 100644 src/Shared/CertificateGeneration/ProcessRunner.cs diff --git a/src/Shared/CertificateGeneration/CertificateFolderStore.cs b/src/Shared/CertificateGeneration/CertificateFolderStore.cs index c92aff9f603e..cafc725e9a6c 100644 --- a/src/Shared/CertificateGeneration/CertificateFolderStore.cs +++ b/src/Shared/CertificateGeneration/CertificateFolderStore.cs @@ -1,3 +1,5 @@ +using System; +using System.Diagnostics; using System.IO; using System.Security.Cryptography.X509Certificates; using Microsoft.AspNetCore.Certificates.Generation; @@ -10,14 +12,20 @@ internal class CertificateFolderStore : CertificateStore { public string FolderPath { get; } - private readonly ProcessRunOptions _updateStore; + private readonly ProcessStartInfo _updateStore; - private bool Elevate => _updateStore.Elevate; + private readonly bool _elevate; - public CertificateFolderStore(string name, string path, ProcessRunOptions updateStore) : + public CertificateFolderStore(string name, string path, ProcessStartInfo updateStore) : base(name) { + if (!updateStore.RedirectStandardError) + { + throw new ArgumentException(nameof(updateStore)); + } + FolderPath = path; + _elevate = updateStore.FileName == "sudo"; _updateStore = updateStore; } @@ -32,9 +40,44 @@ public override bool TryInstallCertificate(X509Certificate2 certificate) try { CertificateManager.ExportCertificate(certificate, pemFile, includePrivateKey: false, password: null, CertificateKeyExportFormat.Pem); - CopyFile(pemFile, GetCertificatePath(certificate)); - ProcessRunner.Run(_updateStore); - return true; + + string sourceFileName = pemFile; + string destFileName = GetCertificatePath(certificate); + if (_elevate) + { + var copyProcess = Process.Start(new ProcessStartInfo() + { + FileName = "sudo", + ArgumentList = { "cp", sourceFileName, destFileName }, + RedirectStandardOutput = true, + RedirectStandardError = true, + })!; + copyProcess.WaitForExit(); + 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; + } + } + else + { + File.Copy(sourceFileName, destFileName); + } + + { + Process updateStoreProcess = Process.Start(_updateStore)!; + 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; + } } finally { @@ -73,13 +116,16 @@ private void DeleteFile(string path) return; } - if (Elevate) + if (_elevate) { - ProcessRunner.Run(new() + var process = Process.Start(new ProcessStartInfo() { - Command = { "rm", path }, - Elevate = true - }); + FileName = "sudo", + ArgumentList = { "rm", path }, + RedirectStandardOutput = true, + RedirectStandardError = true, + })!; + process.WaitForExit(); } else { @@ -91,18 +137,7 @@ private void CopyFile(string sourceFileName, string destFileName) { DeleteFile(destFileName); - if (Elevate) - { - ProcessRunner.Run(new() - { - Command = { "cp", sourceFileName, destFileName }, - Elevate = true - }); - } - else - { - File.Copy(sourceFileName, destFileName); - } + } } } diff --git a/src/Shared/CertificateGeneration/CertificateManager.cs b/src/Shared/CertificateGeneration/CertificateManager.cs index 337ed621fdde..0ccfb2e50e9f 100644 --- a/src/Shared/CertificateGeneration/CertificateManager.cs +++ b/src/Shared/CertificateGeneration/CertificateManager.cs @@ -977,7 +977,7 @@ public class CertificateManagerEventSource : EventSource 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 LinuxCertutilInstallFailed(string command, int exitCode, string stderr) => WriteEvent(66, command, exitCode, stderr); + 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 index 4e217ba03631..b5d7a3772beb 100644 --- a/src/Shared/CertificateGeneration/CertificateStore.cs +++ b/src/Shared/CertificateGeneration/CertificateStore.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Security.Cryptography.X509Certificates; #nullable enable @@ -24,7 +25,7 @@ protected CertificateStore(string name) protected bool CheckProgramDependency(string program) { - if (!ProcessRunner.HasProgram(program)) + if (!ProcessHelper.HasProgram(program)) { // TODO reporter?.Warn($"Cannot use '{StoreName}' because '{program}' is not installed."); return false; @@ -32,10 +33,10 @@ protected bool CheckProgramDependency(string program) return true; } - protected bool CheckProgramDependency(ProcessRunOptions runOptions) + protected bool CheckProgramDependency(ProcessStartInfo psi) { - return CheckProgramDependency(runOptions.Command[0]) - & (!runOptions.Elevate || CheckProgramDependency("sudo")); + return CheckProgramDependency(psi.FileName) + & (psi.FileName != "sudo" || CheckProgramDependency(psi.ArgumentList[0])); } protected bool ContainsCertificate(string storeContent, string certificateContent) diff --git a/src/Shared/CertificateGeneration/CertificateStoreFinder.cs b/src/Shared/CertificateGeneration/CertificateStoreFinder.cs index e090c5c83739..eb896d3fdb68 100644 --- a/src/Shared/CertificateGeneration/CertificateStoreFinder.cs +++ b/src/Shared/CertificateGeneration/CertificateStoreFinder.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; #nullable enable @@ -20,13 +21,16 @@ public static List FindCertificateStores() private static void FindSystemCertificateStore(List stores) { const string StoreName = "System certificates"; - if (ProcessRunner.HasProgram("yum")) + if (ProcessHelper.HasProgram("yum")) { - stores.Add(new CertificateFolderStore(StoreName, "/etc/pki/tls/certs", new() - { - Command = { "update-ca-trust" }, - Elevate = true - })); + stores.Add(new CertificateFolderStore(StoreName, "/etc/pki/tls/certs", + new ProcessStartInfo() + { + FileName = "sudo", + ArgumentList = { "update-ca-trust" }, + RedirectStandardOutput = true, + RedirectStandardError = true + })); } } diff --git a/src/Shared/CertificateGeneration/NssCertificateDatabase.cs b/src/Shared/CertificateGeneration/NssCertificateDatabase.cs index 97b923653aa4..8ea9bfbf6251 100644 --- a/src/Shared/CertificateGeneration/NssCertificateDatabase.cs +++ b/src/Shared/CertificateGeneration/NssCertificateDatabase.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.IO; using System.Security.Cryptography.X509Certificates; @@ -28,14 +29,20 @@ public override bool TryInstallCertificate(X509Certificate2 certificate) { CertificateManager.ExportCertificate(certificate, pemFile, includePrivateKey: false, password: null, CertificateKeyExportFormat.Pem); string name = GetCertificateNickname(certificate); - var result = ProcessRunner.Run(new() { - Command = { "certutil", "-d", DatabasePath, "-A", "-t", "C,,", "-n", name, "-i", pemFile }, - ThrowOnFailure = false }); - if (!result.IsSuccess) + 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) { - CertificateManager.Log.LinuxCertutilInstallFailed(result.CommandLine, result.ExitCode, result.StandardError!.ToString()); + string cmdline = ProcessHelper.GetCommandLine(process.StartInfo); + CertificateManager.Log.LinuxCertificateInstallCommandFailed(cmdline, process.ExitCode, stderr); } - return result.IsSuccess; + return success; } finally { @@ -51,18 +58,20 @@ public override bool TryInstallCertificate(X509Certificate2 certificate) public override bool HasCertificate(X509Certificate2 certificate) { string name = GetCertificateNickname(certificate); - ProcessRunResult runResult = ProcessRunner.Run(new() + Process process = Process.Start(new ProcessStartInfo() { - Command = { "certutil", "-d", DatabasePath, "-L", "-n", name, "-a" }, - ReadStandardOutput = true, - ThrowOnFailure = false - }); - if (runResult.IsSuccess) + FileName = "cerutil", + ArgumentList = { "-d", DatabasePath, "-L", "-n", name, "-a" }, + RedirectStandardOutput = true, + RedirectStandardError = true + })!; + string stdout = process.StandardOutput.ReadToEnd(); + process.WaitForExit(); + if (process.ExitCode == 0) { - runResult.StandardOutput!.Replace("\r\n", "\n"); + stdout = stdout.Replace("\r\n", "\n"); const string BeginCertificate = "-----BEGIN CERTIFICATE-----"; - var pemCertificates = runResult.StandardOutput.ToString() - .Split(BeginCertificate, StringSplitOptions.RemoveEmptyEntries); + var pemCertificates = stdout.Split(BeginCertificate, StringSplitOptions.RemoveEmptyEntries); foreach (var pem in pemCertificates) { X509Certificate2 cert = X509Certificate2.CreateFromPem(BeginCertificate + "\n" + pem); @@ -78,11 +87,14 @@ public override bool HasCertificate(X509Certificate2 certificate) public override void DeleteCertificate(X509Certificate2 certificate) { string name = GetCertificateNickname(certificate); - ProcessRunner.Run(new() + var process = Process.Start(new ProcessStartInfo() { - Command = { "certutil", "-d", DatabasePath, "-D", "-n", name }, - ThrowOnFailure = false - }); + FileName = "certutil", + ArgumentList = { "-d", DatabasePath, "-D", "-n", name }, + RedirectStandardOutput = true, + RedirectStandardError = true + })!; + process.WaitForExit(); } private string GetCertificateNickname(X509Certificate2 certificate) 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/ProcessRunOptions.cs b/src/Shared/CertificateGeneration/ProcessRunOptions.cs deleted file mode 100644 index 4e1607eed217..000000000000 --- a/src/Shared/CertificateGeneration/ProcessRunOptions.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Collections.Generic; - -#nullable enable - -namespace Microsoft.AspNetCore.Certificates.Generation -{ - internal record ProcessRunOptions - { - public List Command { get; } = new(); - public bool ThrowOnFailure { get; init; } = true; - public bool ReadStandardOutput { get; init; } - public bool ReadStandardError { get; init; } = true; - public bool Elevate { get; init; } - } -} \ No newline at end of file diff --git a/src/Shared/CertificateGeneration/ProcessRunResult.cs b/src/Shared/CertificateGeneration/ProcessRunResult.cs deleted file mode 100644 index 211221197ba5..000000000000 --- a/src/Shared/CertificateGeneration/ProcessRunResult.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Text; - -#nullable enable - -namespace Microsoft.AspNetCore.Certificates.Generation -{ - internal record ProcessRunResult - { - public bool IsSuccess => ExitCode == 0; - public string CommandLine { get; init; } = ""; - public int ExitCode { get; init; } - public StringBuilder? StandardOutput { get; init; } - public StringBuilder? StandardError { get; init; } - } -} \ No newline at end of file diff --git a/src/Shared/CertificateGeneration/ProcessRunner.cs b/src/Shared/CertificateGeneration/ProcessRunner.cs deleted file mode 100644 index 3e9057c18dfd..000000000000 --- a/src/Shared/CertificateGeneration/ProcessRunner.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System; -using System.Diagnostics; -using System.IO; -using System.Text; - -#nullable enable - -namespace Microsoft.AspNetCore.Certificates.Generation -{ - internal class ProcessRunner - { - public static ProcessRunResult Run(ProcessRunOptions options) - { - Process process = new Process() - { - StartInfo = - { - RedirectStandardError = true, - RedirectStandardOutput = true, - RedirectStandardInput = true - } - }; - int i = 0; - if (options.Elevate) - { - process.StartInfo.FileName = "sudo"; - } - else - { - process.StartInfo.FileName = options.Command[i++]; - } - for (; i < options.Command.Count; i++) - { - process.StartInfo.ArgumentList.Add(options.Command[i]); - } - string commandLine = $"{process.StartInfo.FileName} {string.Join(" ", process.StartInfo.ArgumentList)}"; - bool readStdErr = options.ThrowOnFailure || options.ReadStandardError; - StringBuilder? stdErr = null; - if (readStdErr) - { - stdErr = new StringBuilder(); - process.ErrorDataReceived += (o, e) => { - if (e.Data != null) - { - stdErr.AppendLine(e.Data); - } - }; - } - StringBuilder? stdOut = null; - bool readStdOut = options.ReadStandardOutput; - if (readStdOut) - { - stdOut = new StringBuilder(); - process.OutputDataReceived += (o, e) => { - if (e.Data != null) - { - stdOut.AppendLine(e.Data); - } - }; - } - process.Start(); - process.BeginErrorReadLine(); - process.BeginOutputReadLine(); - process.StandardInput.Close(); - process.WaitForExit(); - if (options.ThrowOnFailure && process.ExitCode != 0) - { - throw new Exception($"Command '{commandLine}' failed with {process.ExitCode}: {stdErr}."); - } - return new ProcessRunResult - { - ExitCode = process.ExitCode, - CommandLine = commandLine, - StandardError = stdErr, - StandardOutput = stdOut - }; - } - - 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 From 6b9543268f34aab6df9f6766a4a7dc41ef6faedf Mon Sep 17 00:00:00 2001 From: Tom Deseyn Date: Mon, 7 Jun 2021 12:06:16 +0200 Subject: [PATCH 07/11] Fix nullable error --- src/Shared/CertificateGeneration/UnixCertificateManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Shared/CertificateGeneration/UnixCertificateManager.cs b/src/Shared/CertificateGeneration/UnixCertificateManager.cs index 3942e4086f31..59ab29d83839 100644 --- a/src/Shared/CertificateGeneration/UnixCertificateManager.cs +++ b/src/Shared/CertificateGeneration/UnixCertificateManager.cs @@ -6,7 +6,7 @@ namespace Microsoft.AspNetCore.Certificates.Generation { internal class UnixCertificateManager : CertificateManager { - private List _certificateStores; + private List? _certificateStores; private List CertificateStores => _certificateStores ??= CertificateStoreFinder.FindCertificateStores(); From feba1879956b5e2730eb500fe0194f487b4ecb16 Mon Sep 17 00:00:00 2001 From: Tom Deseyn Date: Mon, 7 Jun 2021 12:11:51 +0200 Subject: [PATCH 08/11] Delete some dead code --- .../CertificateGeneration/CertificateFolderStore.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/Shared/CertificateGeneration/CertificateFolderStore.cs b/src/Shared/CertificateGeneration/CertificateFolderStore.cs index cafc725e9a6c..26f37edebb49 100644 --- a/src/Shared/CertificateGeneration/CertificateFolderStore.cs +++ b/src/Shared/CertificateGeneration/CertificateFolderStore.cs @@ -43,6 +43,7 @@ public override bool TryInstallCertificate(X509Certificate2 certificate) string sourceFileName = pemFile; string destFileName = GetCertificatePath(certificate); + DeleteFile(destFileName); if (_elevate) { var copyProcess = Process.Start(new ProcessStartInfo() @@ -132,12 +133,5 @@ private void DeleteFile(string path) File.Delete(path); } } - - private void CopyFile(string sourceFileName, string destFileName) - { - DeleteFile(destFileName); - - - } } } From 568d2009f63464bf1c2defc973c00d680416cfdb Mon Sep 17 00:00:00 2001 From: Tom Deseyn Date: Mon, 7 Jun 2021 14:12:01 +0200 Subject: [PATCH 09/11] Fix compilation --- src/Shared/CertificateGeneration/UnixCertificateManager.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Shared/CertificateGeneration/UnixCertificateManager.cs b/src/Shared/CertificateGeneration/UnixCertificateManager.cs index 59ab29d83839..aed39b5ef0f4 100644 --- a/src/Shared/CertificateGeneration/UnixCertificateManager.cs +++ b/src/Shared/CertificateGeneration/UnixCertificateManager.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.Security.Cryptography.X509Certificates; +#nullable enable + namespace Microsoft.AspNetCore.Certificates.Generation { internal class UnixCertificateManager : CertificateManager From ec8bef815c4385c462d61167eab6d94e80aaa882 Mon Sep 17 00:00:00 2001 From: Tom Deseyn Date: Mon, 7 Jun 2021 17:00:26 +0200 Subject: [PATCH 10/11] Support adding cert to Debian/Ubuntu system store --- .../CertificateFolderStore.cs | 137 -------------- .../CertificateGeneration/CertificateStore.cs | 18 -- .../CertificateStoreFinder.cs | 13 +- .../NssCertificateDatabase.cs | 7 +- .../SystemCertificateFolderStore.cs | 173 ++++++++++++++++++ src/Tools/dotnet-dev-certs/src/Program.cs | 4 + 6 files changed, 181 insertions(+), 171 deletions(-) delete mode 100644 src/Shared/CertificateGeneration/CertificateFolderStore.cs create mode 100644 src/Shared/CertificateGeneration/SystemCertificateFolderStore.cs diff --git a/src/Shared/CertificateGeneration/CertificateFolderStore.cs b/src/Shared/CertificateGeneration/CertificateFolderStore.cs deleted file mode 100644 index 26f37edebb49..000000000000 --- a/src/Shared/CertificateGeneration/CertificateFolderStore.cs +++ /dev/null @@ -1,137 +0,0 @@ -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 CertificateFolderStore : CertificateStore - { - public string FolderPath { get; } - - private readonly ProcessStartInfo _updateStore; - - private readonly bool _elevate; - - public CertificateFolderStore(string name, string path, ProcessStartInfo updateStore) : - base(name) - { - if (!updateStore.RedirectStandardError) - { - throw new ArgumentException(nameof(updateStore)); - } - - FolderPath = path; - _elevate = updateStore.FileName == "sudo"; - _updateStore = updateStore; - } - - public override bool CheckDependencies() - { - return CheckProgramDependency(_updateStore); - } - - public override bool TryInstallCertificate(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); - if (_elevate) - { - var copyProcess = Process.Start(new ProcessStartInfo() - { - FileName = "sudo", - ArgumentList = { "cp", sourceFileName, destFileName }, - RedirectStandardOutput = true, - RedirectStandardError = true, - })!; - copyProcess.WaitForExit(); - 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; - } - } - else - { - File.Copy(sourceFileName, destFileName); - } - - { - Process updateStoreProcess = Process.Start(_updateStore)!; - 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; - } - } - 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 + ".pem"); - - private void DeleteFile(string path) - { - if (!File.Exists(path)) - { - return; - } - - if (_elevate) - { - var process = Process.Start(new ProcessStartInfo() - { - FileName = "sudo", - ArgumentList = { "rm", path }, - RedirectStandardOutput = true, - RedirectStandardError = true, - })!; - process.WaitForExit(); - } - else - { - File.Delete(path); - } - } - } -} diff --git a/src/Shared/CertificateGeneration/CertificateStore.cs b/src/Shared/CertificateGeneration/CertificateStore.cs index b5d7a3772beb..8433329aec3a 100644 --- a/src/Shared/CertificateGeneration/CertificateStore.cs +++ b/src/Shared/CertificateGeneration/CertificateStore.cs @@ -15,30 +15,12 @@ protected CertificateStore(string name) StoreName = name; } - public abstract bool CheckDependencies(); - public abstract bool TryInstallCertificate(X509Certificate2 certificate); public abstract void DeleteCertificate(X509Certificate2 certificate); public abstract bool HasCertificate(X509Certificate2 certificate); - protected bool CheckProgramDependency(string program) - { - if (!ProcessHelper.HasProgram(program)) - { - // TODO reporter?.Warn($"Cannot use '{StoreName}' because '{program}' is not installed."); - return false; - } - return true; - } - - protected bool CheckProgramDependency(ProcessStartInfo psi) - { - return CheckProgramDependency(psi.FileName) - & (psi.FileName != "sudo" || CheckProgramDependency(psi.ArgumentList[0])); - } - protected bool ContainsCertificate(string storeContent, string certificateContent) { return false; diff --git a/src/Shared/CertificateGeneration/CertificateStoreFinder.cs b/src/Shared/CertificateGeneration/CertificateStoreFinder.cs index eb896d3fdb68..f10252f05ff0 100644 --- a/src/Shared/CertificateGeneration/CertificateStoreFinder.cs +++ b/src/Shared/CertificateGeneration/CertificateStoreFinder.cs @@ -20,17 +20,10 @@ public static List FindCertificateStores() private static void FindSystemCertificateStore(List stores) { - const string StoreName = "System certificates"; - if (ProcessHelper.HasProgram("yum")) + CertificateStore? store = SystemCertificateFolderStore.Instance; + if (store is not null) { - stores.Add(new CertificateFolderStore(StoreName, "/etc/pki/tls/certs", - new ProcessStartInfo() - { - FileName = "sudo", - ArgumentList = { "update-ca-trust" }, - RedirectStandardOutput = true, - RedirectStandardError = true - })); + stores.Add(store); } } diff --git a/src/Shared/CertificateGeneration/NssCertificateDatabase.cs b/src/Shared/CertificateGeneration/NssCertificateDatabase.cs index 8ea9bfbf6251..65b31ef9f985 100644 --- a/src/Shared/CertificateGeneration/NssCertificateDatabase.cs +++ b/src/Shared/CertificateGeneration/NssCertificateDatabase.cs @@ -17,11 +17,6 @@ public NssCertificateDatabase(string name, string path) : DatabasePath = path; } - public override bool CheckDependencies() - { - return CheckProgramDependency("certutil"); - } - public override bool TryInstallCertificate(X509Certificate2 certificate) { string pemFile = Paths.GetUserTempFile(".pem"); @@ -36,7 +31,7 @@ public override bool TryInstallCertificate(X509Certificate2 certificate) RedirectStandardError = true })!; var stderr = process.StandardError.ReadToEnd(); process.WaitForExit(); - bool success = process.ExitCode != 0; + bool success = process.ExitCode == 0; if (!success) { string cmdline = ProcessHelper.GetCommandLine(process.StartInfo); 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/Tools/dotnet-dev-certs/src/Program.cs b/src/Tools/dotnet-dev-certs/src/Program.cs index 07bafcbc6484..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; @@ -388,6 +390,8 @@ 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), From 22bf6877f55e9943e3ab5fc42e4e52710ca44181 Mon Sep 17 00:00:00 2001 From: Tom Deseyn Date: Tue, 22 Jun 2021 20:29:47 +0200 Subject: [PATCH 11/11] Fix CI compilation --- src/ProjectTemplates/Shared/DevelopmentCertificate.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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; }