Skip to content
Merged
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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ dotnet linux-dev-certs install

# Supported distros

- Fedora and derived (RHEL, Rocky, ...)
- Fedora and derived (RHEL, AlmaLinux, ...)
- Debian and derived (Ubuntu, ...)

Limitations:
- Ubuntu provides browser applications as snaps. These snaps do not use the system certificate store (https://bugs.launchpad.net/ubuntu/+source/chromium-browser/+bug/1901586). For .NET itself and CLI applications the development certificate will be trusted. For browsers, the user still needs manually accept the development certificate.
- Ubuntu browsers are packaged as snaps. Snaps do not use system certificates. If the user uses a snap-based Firefox, the CA certificate is added to its certificate store. Other browsers are (currently) not configured.

# How it works

Expand Down
154 changes: 154 additions & 0 deletions src/linux-dev-certs/CertificateManager.aspnetcore.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
Expand Down Expand Up @@ -90,4 +92,156 @@ protected X509Certificate2 SaveCertificateCore(X509Certificate2 certificate, Sto

return certificate;
}

// Copied from aspnetcore CertificateManager.cs
internal static void ExportCertificate(X509Certificate2 certificate, string path, bool includePrivateKey, string? password, CertificateKeyExportFormat format)
{
// if (Log.IsEnabled())
// {
// Log.ExportCertificateStart(GetDescription(certificate), path, includePrivateKey);
// }

// if (includePrivateKey && password == null)
// {
// Log.NoPasswordForCertificate();
// }

var targetDirectoryPath = Path.GetDirectoryName(path);
if (!string.IsNullOrEmpty(targetDirectoryPath))
{
// Log.CreateExportCertificateDirectory(targetDirectoryPath);
Directory.CreateDirectory(targetDirectoryPath);
}

byte[] bytes;
byte[] keyBytes;
byte[]? pemEnvelope = null;
RSA? key = null;

try
{
if (includePrivateKey)
{
switch (format)
{
case CertificateKeyExportFormat.Pfx:
bytes = certificate.Export(X509ContentType.Pkcs12, password);
break;
case CertificateKeyExportFormat.Pem:
key = certificate.GetRSAPrivateKey()!;

char[] pem;
if (password != null)
{
keyBytes = key.ExportEncryptedPkcs8PrivateKey(password, new PbeParameters(PbeEncryptionAlgorithm.Aes256Cbc, HashAlgorithmName.SHA256, 100000));
pem = PemEncoding.Write("ENCRYPTED PRIVATE KEY", keyBytes);
pemEnvelope = Encoding.ASCII.GetBytes(pem);
}
else
{
// Export the key first to an encrypted PEM to avoid issues with System.Security.Cryptography.Cng indicating that the operation is not supported.
// This is likely by design to avoid exporting the key by mistake.
// To bypass it, we export the certificate to pem temporarily and then we import it and export it as unprotected PEM.
keyBytes = key.ExportEncryptedPkcs8PrivateKey(string.Empty, new PbeParameters(PbeEncryptionAlgorithm.Aes256Cbc, HashAlgorithmName.SHA256, 1));
pem = PemEncoding.Write("ENCRYPTED PRIVATE KEY", keyBytes);
key.Dispose();
key = RSA.Create();
key.ImportFromEncryptedPem(pem, string.Empty);
Array.Clear(keyBytes, 0, keyBytes.Length);
Array.Clear(pem, 0, pem.Length);
keyBytes = key.ExportPkcs8PrivateKey();
pem = PemEncoding.Write("PRIVATE KEY", keyBytes);
pemEnvelope = Encoding.ASCII.GetBytes(pem);
}

Array.Clear(keyBytes, 0, keyBytes.Length);
Array.Clear(pem, 0, pem.Length);

bytes = Encoding.ASCII.GetBytes(PemEncoding.Write("CERTIFICATE", certificate.Export(X509ContentType.Cert)));
break;
default:
throw new InvalidOperationException("Unknown format.");
}
}
else
{
if (format == CertificateKeyExportFormat.Pem)
{
bytes = Encoding.ASCII.GetBytes(PemEncoding.Write("CERTIFICATE", certificate.Export(X509ContentType.Cert)));
}
else
{
bytes = certificate.Export(X509ContentType.Cert);
}
}
}
// catch (Exception e) when (Log.IsEnabled())
// {
// Log.ExportCertificateError(e.ToString());
// throw;
// }
finally
{
key?.Dispose();
}

try
{
// Log.WriteCertificateToDisk(path);

// Create a temp file with the correct Unix file mode before moving it to the expected path.
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
var tempFilename = Path.GetTempFileName();
File.Move(tempFilename, path, overwrite: true);
}

File.WriteAllBytes(path, bytes);
}
// catch (Exception ex) when (Log.IsEnabled())
// {
// Log.WriteCertificateToDiskError(ex.ToString());
// throw;
// }
finally
{
Array.Clear(bytes, 0, bytes.Length);
}

if (includePrivateKey && format == CertificateKeyExportFormat.Pem)
{
Debug.Assert(pemEnvelope != null);

try
{
var keyPath = Path.ChangeExtension(path, ".key");
// Log.WritePemKeyToDisk(keyPath);

// Create a temp file with the correct Unix file mode before moving it to the expected path.
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
var tempFilename = Path.GetTempFileName();
File.Move(tempFilename, keyPath, overwrite: true);
}

File.WriteAllBytes(keyPath, pemEnvelope);
}
// catch (Exception ex) when (Log.IsEnabled())
// {
// Log.WritePemKeyToDiskError(ex.ToString());
// throw;
// }
finally
{
Array.Clear(pemEnvelope, 0, pemEnvelope.Length);
}
}
}
}

// Copied from aspnetcore CertificateExportFormat.cs
internal enum CertificateKeyExportFormat
{
Pfx,
Pem,
}
47 changes: 38 additions & 9 deletions src/linux-dev-certs/CertificateManager.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Runtime.ConstrainedExecution;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
Expand All @@ -17,40 +18,68 @@ partial class CertificateManager

public void InstallAndTrust()
{
string username = Environment.UserName;
string certificateName = $"aspnet-dev-{username}";

Console.WriteLine("Removing existing development certificates.");
Execute("dotnet", "dev-certs", "https", "--clean");

Console.WriteLine("Creating CA certificate.");
_caCertificate = CreateAspNetDevelopmentCACertificate(DateTime.UtcNow, DateTime.UtcNow.AddYears(10));

Console.WriteLine("Installing CA certificate.");
InstallCaCertificate(_caCertificate);
InstallCaCertificate(certificateName, _caCertificate);

Console.WriteLine("Creating development certificate.");
var devCert = CreateAspNetCoreHttpsDevelopmentCertificate(DateTime.UtcNow, DateTime.UtcNow.AddYears(1));
Console.WriteLine("Installing development certificate.");
SaveCertificateCore(devCert, StoreName.My, StoreLocation.CurrentUser);
devCert = SaveCertificateCore(devCert, StoreName.My, StoreLocation.CurrentUser);

var additionalStores = FindAdditionaCertificateStores();
foreach (ICertificateStore store in additionalStores)
{
Console.WriteLine($"Installing CA certificate to {store.Name}.");
if (!store.TryInstallCertificate(certificateName, devCert))
{
Console.Error.WriteLine("Failed to install certificate.");
}
}
}

private List<ICertificateStore> FindAdditionaCertificateStores()
{
List<ICertificateStore> stores = new();

string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile, Environment.SpecialFolderOption.DoNotVerify);

// Snap applications don't use '/etc/ssl' certificates. Add the certificate explicitly.
// https://bugs.launchpad.net/ubuntu/+source/chromium-browser/+bug/1901586
string firefoxSnapUserDirectory = Path.Combine(home, "snap/firefox/common/.mozilla/firefox");
if (Directory.Exists(firefoxSnapUserDirectory))
{
FindFirefoxCertificateStores(firefoxSnapUserDirectory, stores);
}

return stores;
}

private static void InstallCaCertificate(X509Certificate2 caCertificate)
private static void InstallCaCertificate(string name, X509Certificate2 caCertificate)
{
// Only the public key is stored.
// The private key will only exist in the memory of this program
// and no other certificates can be signed with it after the program terminates.
char[] caCertPem = PemEncoding.Write("CERTIFICATE", caCertificate.Export(X509ContentType.Cert));
string username = Environment.UserName;
string fileNameWithoutExtension = $"aspnet-{username}";
string certFilePath;
string[] trustCommand;
if (Directory.Exists(FedoraFamilyCaSourceDirectory))
{
certFilePath = $"{FedoraFamilyCaSourceDirectory}/{fileNameWithoutExtension}.pem";
trustCommand = [ "update-ca-trust", "extract" ];
certFilePath = $"{FedoraFamilyCaSourceDirectory}/{name}.pem";
trustCommand = ["update-ca-trust", "extract"];
}
else if (Directory.Exists(DebianFamilyCaSourceDirectory))
{
certFilePath = $"{DebianFamilyCaSourceDirectory}/{fileNameWithoutExtension}.crt";
trustCommand = [ "update-ca-certificates" ];
certFilePath = $"{DebianFamilyCaSourceDirectory}/{name}.crt";
trustCommand = ["update-ca-certificates"];
}
else
{
Expand Down
129 changes: 129 additions & 0 deletions src/linux-dev-certs/CertificateManager.firefox.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
using System.Runtime.ConstrainedExecution;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using static LinuxDevCerts.ProcessHelper;

namespace LinuxDevCerts;

partial class CertificateManager
{
private static void FindFirefoxCertificateStores(string firefoxUserDirectory, List<ICertificateStore> stores)
{
string profilesIniFileName = Path.Combine(firefoxUserDirectory, "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(firefoxUserDirectory, path);
if (!profileFolders.Contains(profileFolder))
{
profileFolders.Add(profileFolder);
if (Directory.Exists(profileFolder))
{
stores.Add(new NssCertificateDatabase($"Firefox profile '{profileFolder}'", 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;
}
}
Loading