Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/ProjectTemplates/Shared/DevelopmentCertificate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
10 changes: 9 additions & 1 deletion src/Shared/CertificateGeneration/CertificateManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -445,7 +447,7 @@ public void CleanupHttpsCertificates()

protected abstract IList<X509Certificate2> 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())
{
Expand Down Expand Up @@ -970,6 +972,12 @@ public class CertificateManagerEventSource : EventSource

[Event(64, Level = EventLevel.Error, Message = "The provided certificate '{0}' is not a valid ASP.NET Core HTTPS development certificate.")]
internal void NoHttpsDevelopmentCertificate(string description) => WriteEvent(64, description);

[Event(65, Level = EventLevel.Informational, Message = "Adding '{0}' to '{1}'.")]
internal void LinuxTrustCertificate(string certificate, string storeName) => WriteEvent(65, certificate, storeName);

[Event(66, Level = EventLevel.Error, Message = "An error has occurred while running command '{0}' to install certificate. Exit code: {1}. Error: {2}.")]
internal void LinuxCertificateInstallCommandFailed(string command, int exitCode, string stderr) => WriteEvent(66, command, exitCode, stderr);
}

internal class UserCancelledTrustException : Exception
Expand Down
29 changes: 29 additions & 0 deletions src/Shared/CertificateGeneration/CertificateStore.cs
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;
}
}
}
159 changes: 159 additions & 0 deletions src/Shared/CertificateGeneration/CertificateStoreFinder.cs
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;
}
}
}
2 changes: 2 additions & 0 deletions src/Shared/CertificateGeneration/MacOSCertificateManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
98 changes: 98 additions & 0 deletions src/Shared/CertificateGeneration/NssCertificateDatabase.cs
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;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of doing this, can we make the certificate name be aspnetcore-https-{sha256}? with two goals in mind:

  1. We can exactly identify each certificate by querying for the name.
  2. We can clean all certificates by listing those that start with aspnetcore-https.

Copy link
Member Author

@tmds tmds Jun 7, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we make the certificate name be aspnetcore-https-{sha256}

I've used aspnet-{Thumbprint}. Is that ok?

We can clean all certificates by listing those that start with aspnetcore-https.

We don't want to delete other user's certificates.
Now, it deletes specific certificates.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aspnetcore-{thumbprint} sounds good.

We don't want to delete other user's certificates.

dotnet dev-certs https --clean needs to remove all certificates that we have created. I'm fine if we claim that namespace and remove any certificate that has been saved as aspnet-{thumbprint}. If you are not happy with that, then we need to load the certificates and check for the presence of the specific OID our tool sets up on the certificate. There is a function in certificate manager to check if the certificate is an asp.net core https certificate (And if someone has decided to create their own certificate and use our OID (which we explicitly discourage people from) then tough luck for them)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should dotnet dev-certs https --clean remove certificates that were generated and trusted by other users?

Copy link
Member

Choose a reason for hiding this comment

The 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;
}
}
23 changes: 23 additions & 0 deletions src/Shared/CertificateGeneration/Paths.cs
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not secure to use a word writable location like /tmp.
By the time you use the file, someone can replace it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was wrong about this. Path.GetTempFileName is secure enough. It's created rw only for the current user, and only that user can remove the file.

}
}
Loading