-
Notifications
You must be signed in to change notification settings - Fork 10.5k
dotnet-dev-certs: prototype Linux trust support #33279
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
bb1364d
3638af2
98204a0
4826dd9
b2d1747
5ad3eab
6b95432
feba187
568d200
ec8bef8
22bf687
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| using System; | ||
| using System.Diagnostics; | ||
| using System.Security.Cryptography.X509Certificates; | ||
|
|
||
| #nullable enable | ||
|
|
||
| namespace Microsoft.AspNetCore.Certificates.Generation | ||
| { | ||
| internal abstract class CertificateStore | ||
| { | ||
| public string StoreName { get; } | ||
|
|
||
| protected CertificateStore(string name) | ||
| { | ||
| StoreName = name; | ||
| } | ||
|
|
||
| public abstract bool TryInstallCertificate(X509Certificate2 certificate); | ||
|
|
||
| public abstract void DeleteCertificate(X509Certificate2 certificate); | ||
|
|
||
| public abstract bool HasCertificate(X509Certificate2 certificate); | ||
|
|
||
| protected bool ContainsCertificate(string storeContent, string certificateContent) | ||
| { | ||
| return false; | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,159 @@ | ||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.Diagnostics; | ||
| using System.IO; | ||
|
|
||
| #nullable enable | ||
|
|
||
| namespace Microsoft.AspNetCore.Certificates.Generation | ||
| { | ||
| internal static class CertificateStoreFinder | ||
| { | ||
| public static List<CertificateStore> FindCertificateStores() | ||
| { | ||
| List<CertificateStore> stores = new(); | ||
| FindChromeEdgeCertificateStores(stores); | ||
| FindFirefoxCertificateStores(stores); | ||
| FindSystemCertificateStore(stores); | ||
| return stores; | ||
| } | ||
|
|
||
| private static void FindSystemCertificateStore(List<CertificateStore> stores) | ||
| { | ||
| CertificateStore? store = SystemCertificateFolderStore.Instance; | ||
| if (store is not null) | ||
| { | ||
| stores.Add(store); | ||
| } | ||
| } | ||
|
|
||
| private static void FindChromeEdgeCertificateStores(List<CertificateStore> 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<CertificateStore> 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<IniSection> sections = ReadIniFile(profilesIniFile); | ||
| List<string> 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<string, string> Properties { get; } = new(); | ||
| } | ||
|
|
||
| private static List<IniSection> 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>(); | ||
| 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; | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| using System; | ||
| using System.Diagnostics; | ||
| using System.IO; | ||
| using System.Security.Cryptography.X509Certificates; | ||
|
|
||
| #nullable enable | ||
|
|
||
| namespace Microsoft.AspNetCore.Certificates.Generation | ||
| { | ||
| internal class NssCertificateDatabase : CertificateStore | ||
| { | ||
| public string DatabasePath { get; } | ||
|
|
||
| public NssCertificateDatabase(string name, string path) : | ||
| base(name) | ||
| { | ||
| DatabasePath = path; | ||
| } | ||
|
|
||
| public override bool TryInstallCertificate(X509Certificate2 certificate) | ||
| { | ||
| string pemFile = Paths.GetUserTempFile(".pem"); | ||
| try | ||
| { | ||
| CertificateManager.ExportCertificate(certificate, pemFile, includePrivateKey: false, password: null, CertificateKeyExportFormat.Pem); | ||
| string name = GetCertificateNickname(certificate); | ||
| Process process = Process.Start(new ProcessStartInfo() { | ||
| FileName = "certutil", | ||
| ArgumentList = { "-d", DatabasePath, "-A", "-t", "C,,", "-n", name, "-i", pemFile }, | ||
| RedirectStandardOutput = true, | ||
| RedirectStandardError = true })!; | ||
| var stderr = process.StandardError.ReadToEnd(); | ||
| process.WaitForExit(); | ||
| bool success = process.ExitCode == 0; | ||
| if (!success) | ||
| { | ||
| string cmdline = ProcessHelper.GetCommandLine(process.StartInfo); | ||
| CertificateManager.Log.LinuxCertificateInstallCommandFailed(cmdline, process.ExitCode, stderr); | ||
| } | ||
| return success; | ||
| } | ||
| finally | ||
| { | ||
| try | ||
| { | ||
| File.Delete(pemFile); | ||
| } | ||
| catch | ||
| { } | ||
| } | ||
| } | ||
|
|
||
| public override bool HasCertificate(X509Certificate2 certificate) | ||
| { | ||
| string name = GetCertificateNickname(certificate); | ||
| Process process = Process.Start(new ProcessStartInfo() | ||
| { | ||
| FileName = "cerutil", | ||
| ArgumentList = { "-d", DatabasePath, "-L", "-n", name, "-a" }, | ||
| RedirectStandardOutput = true, | ||
| RedirectStandardError = true | ||
| })!; | ||
| string stdout = process.StandardOutput.ReadToEnd(); | ||
| process.WaitForExit(); | ||
| if (process.ExitCode == 0) | ||
| { | ||
| stdout = stdout.Replace("\r\n", "\n"); | ||
| const string BeginCertificate = "-----BEGIN CERTIFICATE-----"; | ||
| var pemCertificates = stdout.Split(BeginCertificate, StringSplitOptions.RemoveEmptyEntries); | ||
| foreach (var pem in pemCertificates) | ||
| { | ||
| X509Certificate2 cert = X509Certificate2.CreateFromPem(BeginCertificate + "\n" + pem); | ||
| if (cert.Equals(certificate)) | ||
| { | ||
| return true; | ||
| } | ||
| } | ||
| } | ||
| return false; | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of doing this, can we make the certificate name be
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I've used
We don't want to delete other user's certificates. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. aspnetcore-{thumbprint} sounds good.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, we should only clean the certificates we create (that's why we use prefix/thumbprint so we can tell which are ours) |
||
|
|
||
| public override void DeleteCertificate(X509Certificate2 certificate) | ||
| { | ||
| string name = GetCertificateNickname(certificate); | ||
| var process = Process.Start(new ProcessStartInfo() | ||
| { | ||
| FileName = "certutil", | ||
| ArgumentList = { "-d", DatabasePath, "-D", "-n", name }, | ||
| RedirectStandardOutput = true, | ||
| RedirectStandardError = true | ||
| })!; | ||
| process.WaitForExit(); | ||
| } | ||
|
|
||
| private string GetCertificateNickname(X509Certificate2 certificate) | ||
| => "aspnet-" + certificate.Thumbprint; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| using System; | ||
| using System.IO; | ||
| using static System.Environment; | ||
|
|
||
| #nullable enable | ||
|
|
||
| namespace Microsoft.AspNetCore.Certificates.Generation | ||
| { | ||
| internal static class Paths | ||
| { | ||
| public static string Home => Environment.GetFolderPath(SpecialFolder.MyDocuments); | ||
|
|
||
| public static string GetUserTempFile(string suffix = ".tmp") | ||
| { | ||
| string directory = Paths.XdgRuntimeDir ?? Home ?? // Should be user folders. | ||
| Path.GetTempPath(); // Probably global on Linux. | ||
|
|
||
| return Path.Combine(directory, Guid.NewGuid() + suffix); | ||
| } | ||
|
|
||
| private static string? XdgRuntimeDir => Environment.GetEnvironmentVariable("XDG_RUNTIME_DIR"); | ||
|
Comment on lines
+13
to
+21
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why does https://docs.microsoft.com/en-us/dotnet/api/system.io.path.gettempfilename?view=net-5.0 is not enough? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not secure to use a word writable location like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was wrong about this. |
||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.