File: Certificates\UnixCertificateManagerTests.cs
Web Access
Project: src\tests\Aspire.Cli.Tests\Aspire.Cli.Tests.csproj (Aspire.Cli.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Aspire.Cli.Certificates;
using Aspire.Cli.Tests.TestServices;
using Aspire.Cli.Tests.Utils;
using System.Diagnostics;
using Microsoft.AspNetCore.Certificates.Generation;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Logging.Testing;
 
namespace Aspire.Cli.Tests.Certificates;
 
public class UnixCertificateManagerTests
{
    [Fact]
    public void ResolveNssDbs_DiscoversFirefoxProfileFromDefaultXdgConfigDirectory()
    {
        RunWithTemporaryHome(homeDirectory =>
        {
            var profileDirectory = CreateDirectory(homeDirectory, ".config", "mozilla", "firefox", "test.default-release");
            var manager = CreateManager();
 
            var nssDb = Assert.Single(manager.GetNssDbs(homeDirectory.FullName));
 
            Assert.Equal(profileDirectory, nssDb.Path);
            AssertFirefoxNssDb(nssDb);
        });
    }
 
    [Fact]
    public void ResolveNssDbs_DiscoversFirefoxProfileFromConfiguredXdgConfigDirectory()
    {
        RunWithTemporaryHome(homeDirectory =>
        {
            var xdgConfigHome = CreateDirectory(homeDirectory, "custom-config");
            var profileDirectory = CreateDirectory(homeDirectory, "custom-config", "mozilla", "firefox", "test.default-release");
            var manager = CreateManager(new Dictionary<string, string?>
            {
                ["XDG_CONFIG_HOME"] = xdgConfigHome
            });
 
            var nssDb = Assert.Single(manager.GetNssDbs(homeDirectory.FullName));
 
            Assert.Equal(profileDirectory, nssDb.Path);
            AssertFirefoxNssDb(nssDb);
        });
    }
 
    [Fact]
    public void ResolveNssDbs_IgnoresRelativeXdgConfigDirectory()
    {
        RunWithTemporaryHome(homeDirectory =>
        {
            var profileDirectory = CreateDirectory(homeDirectory, ".config", "mozilla", "firefox", "test.default-release");
            var manager = CreateManager(new Dictionary<string, string?>
            {
                ["XDG_CONFIG_HOME"] = "relative-config"
            });
 
            var nssDb = Assert.Single(manager.GetNssDbs(homeDirectory.FullName));
 
            Assert.Equal(profileDirectory, nssDb.Path);
            AssertFirefoxNssDb(nssDb);
        });
    }
 
    [Fact]
    public void ResolveNssDbs_PrefersLegacyFirefoxDirectoryWhenPresent()
    {
        RunWithTemporaryHome(homeDirectory =>
        {
            var legacyProfileDirectory = CreateDirectory(homeDirectory, ".mozilla", "firefox", "legacy.default-release");
            CreateDirectory(homeDirectory, ".config", "mozilla", "firefox", "xdg.default-release");
            var manager = CreateManager();
 
            var nssDb = Assert.Single(manager.GetNssDbs(homeDirectory.FullName));
 
            Assert.Equal(legacyProfileDirectory, nssDb.Path);
            AssertFirefoxNssDb(nssDb);
        });
    }
 
    [Fact]
    public void ResolveNssDbs_RecognizesStandardXdgFirefoxOverride()
    {
        RunWithTemporaryHome(homeDirectory =>
        {
            var profileDirectory = CreateDirectory(homeDirectory, ".config", "mozilla", "firefox", "test.default-release");
            var manager = CreateManager(new Dictionary<string, string?>
            {
                ["DOTNET_DEV_CERTS_NSSDB_PATHS"] = profileDirectory
            });
 
            var nssDb = Assert.Single(manager.GetNssDbs(homeDirectory.FullName));
 
            Assert.Equal(profileDirectory, nssDb.Path);
            AssertFirefoxNssDb(nssDb);
        });
    }
 
    [Fact]
    public void ResolveNssDbs_RecognizesConfiguredXdgFirefoxOverride()
    {
        RunWithTemporaryHome(homeDirectory =>
        {
            var xdgConfigHome = CreateDirectory(homeDirectory, "custom-config");
            var profileDirectory = CreateDirectory(homeDirectory, "custom-config", "mozilla", "firefox", "test.default-release");
            var manager = CreateManager(new Dictionary<string, string?>
            {
                ["XDG_CONFIG_HOME"] = xdgConfigHome,
                ["DOTNET_DEV_CERTS_NSSDB_PATHS"] = profileDirectory
            });
 
            var nssDb = Assert.Single(manager.GetNssDbs(homeDirectory.FullName));
 
            Assert.Equal(profileDirectory, nssDb.Path);
            AssertFirefoxNssDb(nssDb);
        });
    }
 
    [Fact]
    public void ResolveNssDbs_RecognizesTypedFirefoxOverride()
    {
        RunWithTemporaryHome(homeDirectory =>
        {
            var profileDirectory = CreateDirectory(homeDirectory, "custom-browser", "profile");
            var manager = CreateManager(new Dictionary<string, string?>
            {
                ["DOTNET_DEV_CERTS_NSSDB_PATHS"] = $"firefox={profileDirectory}"
            });
 
            var nssDb = Assert.Single(manager.GetNssDbs(homeDirectory.FullName));
 
            Assert.Equal(profileDirectory, nssDb.Path);
            AssertFirefoxNssDb(nssDb);
        });
    }
 
    [Fact]
    public void ResolveNssDbs_RecognizesAspireConfiguration()
    {
        RunWithTemporaryHome(homeDirectory =>
        {
            var profileDirectory = CreateDirectory(homeDirectory, "custom-browser", "profile");
            var manager = CreateManager(configurationValues: new Dictionary<string, string?>
            {
                [CertificateConfiguration.NssDbPathsConfigPath] = $"firefox={profileDirectory}"
            });
 
            var nssDb = Assert.Single(manager.GetNssDbs(homeDirectory.FullName));
 
            Assert.Equal(profileDirectory, nssDb.Path);
            AssertFirefoxNssDb(nssDb);
        });
    }
 
    [Fact]
    public void ResolveNssDbs_AspireConfigurationTakesPrecedence()
    {
        RunWithTemporaryHome(homeDirectory =>
        {
            var configuredProfileDirectory = CreateDirectory(homeDirectory, "configured-browser", "profile");
            var dotnetProfileDirectory = CreateDirectory(homeDirectory, "dotnet-browser", "profile");
            var manager = CreateManager(
                new Dictionary<string, string?>
                {
                    ["DOTNET_DEV_CERTS_NSSDB_PATHS"] = $"chromium={dotnetProfileDirectory}"
                },
                new Dictionary<string, string?>
                {
                    [CertificateConfiguration.NssDbPathsConfigPath] = $"firefox={configuredProfileDirectory}"
                });
 
            var nssDb = Assert.Single(manager.GetNssDbs(homeDirectory.FullName));
 
            Assert.Equal(configuredProfileDirectory, nssDb.Path);
            AssertFirefoxNssDb(nssDb);
        });
    }
 
    [Fact]
    public void ResolveNssDbs_EmptyAspireConfigurationFallsBackToDotnetOverride()
    {
        RunWithTemporaryHome(homeDirectory =>
        {
            var dotnetProfileDirectory = CreateDirectory(homeDirectory, "dotnet-browser", "profile");
            var manager = CreateManager(
                new Dictionary<string, string?>
                {
                    ["DOTNET_DEV_CERTS_NSSDB_PATHS"] = $"chromium={dotnetProfileDirectory}"
                },
                new Dictionary<string, string?>
                {
                    [CertificateConfiguration.NssDbPathsConfigPath] = string.Empty
                });
 
            var nssDb = Assert.Single(manager.GetNssDbs(homeDirectory.FullName));
 
            Assert.Equal(dotnetProfileDirectory, nssDb.Path);
            AssertChromiumNssDb(nssDb);
        });
    }
 
    [Fact]
    public void ResolveNssDbs_AspireConfigurationNamesConfigurationKeyInDiagnostics()
    {
        RunWithTemporaryHome(homeDirectory =>
        {
            var missingProfileDirectory = Path.Combine(homeDirectory.FullName, "missing-profile");
            var sink = new TestSink();
            var logger = new TestLogger(nameof(UnixCertificateManager), sink, enabled: true);
            var environment = TestEnvironment.CreateLinux();
            var configuration = new ConfigurationBuilder()
                .AddInMemoryCollection(new Dictionary<string, string?>
                {
                    [CertificateConfiguration.NssDbPathsConfigPath] = missingProfileDirectory
                })
                .Build();
            var manager = new UnixCertificateManager(
                logger,
                environment,
                CertificateConfiguration.ResolveNssDbOverride(configuration));
 
            var nssDbs = manager.GetNssDbs(homeDirectory.FullName);
 
            Assert.Empty(nssDbs);
            Assert.Collection(
                sink.Writes,
                write => Assert.Equal(
                    $"Reading NSS database locations from {CertificateConfiguration.NssDbPathsConfigKey}.",
                    write.Message),
                write => Assert.Equal(
                    $"The NSS database '{missingProfileDirectory}' provided via {CertificateConfiguration.NssDbPathsConfigKey} does not exist.",
                    write.Message));
        });
    }
 
    [Fact]
    public void ResolveNssDbs_EmptyAspireConfigurationNamesDotnetVariableInDiagnostics()
    {
        RunWithTemporaryHome(homeDirectory =>
        {
            const string dotnetVariableName = "DOTNET_DEV_CERTS_NSSDB_PATHS";
            var missingProfileDirectory = Path.Combine(homeDirectory.FullName, "missing-profile");
            var sink = new TestSink();
            var logger = new TestLogger(nameof(UnixCertificateManager), sink, enabled: true);
            var environment = TestEnvironment.CreateLinux(new Dictionary<string, string?>
            {
                [dotnetVariableName] = missingProfileDirectory
            });
            var configuration = new ConfigurationBuilder()
                .AddInMemoryCollection(new Dictionary<string, string?>
                {
                    [CertificateConfiguration.NssDbPathsConfigPath] = string.Empty
                })
                .Build();
            var manager = new UnixCertificateManager(
                logger,
                environment,
                CertificateConfiguration.ResolveNssDbOverride(configuration));
 
            var nssDbs = manager.GetNssDbs(homeDirectory.FullName);
 
            Assert.Empty(nssDbs);
            Assert.Collection(
                sink.Writes,
                write => Assert.Equal(
                    $"Reading NSS database locations from {dotnetVariableName}.",
                    write.Message),
                write => Assert.Equal(
                    $"The NSS database '{missingProfileDirectory}' provided via {dotnetVariableName} does not exist.",
                    write.Message));
        });
    }
 
    [Fact]
    public void ResolveNssDbs_RecognizesTypedChromiumOverride()
    {
        RunWithTemporaryHome(homeDirectory =>
        {
            var profileDirectory = CreateDirectory(homeDirectory, ".mozilla", "firefox", "test.default-release");
            var manager = CreateManager(new Dictionary<string, string?>
            {
                ["DOTNET_DEV_CERTS_NSSDB_PATHS"] = $"chromium={profileDirectory}"
            });
 
            var nssDb = Assert.Single(manager.GetNssDbs(homeDirectory.FullName));
 
            Assert.Equal(profileDirectory, nssDb.Path);
            AssertChromiumNssDb(nssDb);
        });
    }
 
    [Fact]
    public void ResolveNssDbs_PreservesUntypedChromiumOverride()
    {
        RunWithTemporaryHome(homeDirectory =>
        {
            var profileDirectory = CreateDirectory(homeDirectory, "custom-browser", "profile");
            var manager = CreateManager(new Dictionary<string, string?>
            {
                ["DOTNET_DEV_CERTS_NSSDB_PATHS"] = profileDirectory
            });
 
            var nssDb = Assert.Single(manager.GetNssDbs(homeDirectory.FullName));
 
            Assert.Equal(profileDirectory, nssDb.Path);
            AssertChromiumNssDb(nssDb);
        });
    }
 
    [Fact]
    public void ResolveNssDbs_IgnoresEmptyTypedOverrides()
    {
        RunWithTemporaryHome(homeDirectory =>
        {
            var manager = CreateManager(new Dictionary<string, string?>
            {
                ["DOTNET_DEV_CERTS_NSSDB_PATHS"] = $"firefox={Path.PathSeparator}chromium="
            });
 
            var nssDbs = manager.GetNssDbs(homeDirectory.FullName);
 
            Assert.Empty(nssDbs);
        });
    }
 
    [Fact]
    public void ResolveNssDbs_OverridesReplaceDiscovery()
    {
        RunWithTemporaryHome(homeDirectory =>
        {
            CreateDirectory(homeDirectory, ".pki", "nssdb");
            var overrideDirectory = CreateDirectory(homeDirectory, "custom-browser", "profile");
            var manager = CreateManager(new Dictionary<string, string?>
            {
                ["DOTNET_DEV_CERTS_NSSDB_PATHS"] = overrideDirectory
            });
 
            var nssDb = Assert.Single(manager.GetNssDbs(homeDirectory.FullName));
 
            Assert.Equal(overrideDirectory, nssDb.Path);
        });
    }
 
    [Fact]
    public void CreateProcessStartInfos_PreserveFirefoxArgumentsContainingSpaces()
    {
        RunWithTemporaryHome(homeDirectory =>
        {
            var profileDirectory = CreateDirectory(homeDirectory, "custom browser", "profile with spaces");
            var manager = CreateManager(new Dictionary<string, string?>
            {
                ["DOTNET_DEV_CERTS_NSSDB_PATHS"] = $"firefox={profileDirectory}"
            });
            var nssDb = Assert.Single(manager.GetNssDbs(homeDirectory.FullName));
            var certificatePath = Path.Combine(homeDirectory.FullName, "certificate with spaces.pem");
            const string nickname = "nickname with spaces";
 
            AssertProcessStartInfo(
                nssDb.CreateCheckProcessStartInfo(nickname),
                "-d", $"sql:{profileDirectory}", "-n", nickname, "-L");
            AssertProcessStartInfo(
                nssDb.CreateAddProcessStartInfo(certificatePath, nickname),
                "-d", $"sql:{profileDirectory}", "-n", nickname, "-A", "-i", certificatePath, "-t", "C,,");
            AssertProcessStartInfo(
                nssDb.CreateRemoveProcessStartInfo(nickname),
                "-d", $"sql:{profileDirectory}", "-n", nickname, "-D");
        });
    }
 
    [Fact]
    public void CreateProcessStartInfos_PreserveChromiumArgumentsContainingSpaces()
    {
        RunWithTemporaryHome(homeDirectory =>
        {
            var profileDirectory = CreateDirectory(homeDirectory, "custom browser", "profile with spaces");
            var manager = CreateManager(new Dictionary<string, string?>
            {
                ["DOTNET_DEV_CERTS_NSSDB_PATHS"] = $"chromium={profileDirectory}"
            });
            var nssDb = Assert.Single(manager.GetNssDbs(homeDirectory.FullName));
            var certificatePath = Path.Combine(homeDirectory.FullName, "certificate with spaces.pem");
            const string nickname = "nickname with spaces";
 
            AssertProcessStartInfo(
                nssDb.CreateCheckProcessStartInfo(nickname),
                "-d", $"sql:{profileDirectory}", "-n", nickname, "-V", "-u", "V");
            AssertProcessStartInfo(
                nssDb.CreateAddProcessStartInfo(certificatePath, nickname),
                "-d", $"sql:{profileDirectory}", "-n", nickname, "-A", "-i", certificatePath, "-t", "P,,");
            AssertProcessStartInfo(
                nssDb.CreateRemoveProcessStartInfo(nickname),
                "-d", $"sql:{profileDirectory}", "-n", nickname, "-D");
        });
    }
 
    [Fact]
    public async Task GetTrustLevel_WhenCanceled_KillsCertUtilProcessTree()
    {
        Assert.SkipUnless(OperatingSystem.IsLinux(), "NSS certificate trust is only exercised on Linux.");
 
        var tempDirectory = Directory.CreateTempSubdirectory();
        var nssDbDirectory = Directory.CreateDirectory(Path.Combine(tempDirectory.FullName, "nssdb"));
        var parentPidFile = Path.Combine(tempDirectory.FullName, "certutil-parent.pid");
        var childPidFile = Path.Combine(tempDirectory.FullName, "certutil-child.pid");
        var parentPid = 0;
        var childPid = 0;
        using var cancellationTokenSource = new CancellationTokenSource();
 
        try
        {
            var certUtilFile = await CreateBlockingCertUtilAsync(tempDirectory, parentPidFile, childPidFile);
            var environment = TestEnvironment.CreateLinux(new Dictionary<string, string?>
            {
                ["PATH"] = tempDirectory.FullName,
                ["SSL_CERT_DIR"] = tempDirectory.FullName,
                ["DOTNET_DEV_CERTS_NSSDB_PATHS"] = nssDbDirectory.FullName
            });
            var manager = new UnixCertificateManager(NullLogger.Instance, environment, nssDbOverride: null, startInfo =>
            {
                startInfo.FileName = certUtilFile.FullName;
            });
            using var certificate = manager.CreateAspNetCoreHttpsDevelopmentCertificate(
                DateTimeOffset.UtcNow.AddDays(-1),
                DateTimeOffset.UtcNow.AddDays(365));
            var trustTask = Task.Run(() => manager.GetTrustLevel(certificate, cancellationTokenSource.Token));
 
            parentPid = await ProcessTestHelpers.WaitForProcessIdAsync(parentPidFile, TestContext.Current.CancellationToken)
                .DefaultTimeout();
            childPid = await ProcessTestHelpers.WaitForProcessIdAsync(childPidFile, TestContext.Current.CancellationToken)
                .DefaultTimeout();
 
            cancellationTokenSource.Cancel();
 
            await Assert.ThrowsAnyAsync<OperationCanceledException>(() => trustTask).DefaultTimeout();
            Assert.True(ProcessTestHelpers.WaitForProcessExit(parentPid, TimeSpan.FromSeconds(10)), $"Expected certutil process {parentPid} to exit.");
            Assert.True(ProcessTestHelpers.WaitForProcessExit(childPid, TimeSpan.FromSeconds(10)), $"Expected child process {childPid} to exit.");
        }
        finally
        {
            cancellationTokenSource.Cancel();
            ProcessTestHelpers.TryKillProcess(parentPid);
            ProcessTestHelpers.TryKillProcess(childPid);
            tempDirectory.Delete(recursive: true);
        }
    }
 
    [Fact]
    public async Task GetTrustLevel_WhenCertUtilFillsOutputPipes_Completes()
    {
        Assert.SkipUnless(OperatingSystem.IsLinux(), "NSS certificate trust is only exercised on Linux.");
 
        var tempDirectory = Directory.CreateTempSubdirectory();
        var nssDbDirectory = Directory.CreateDirectory(Path.Combine(tempDirectory.FullName, "nssdb"));
 
        try
        {
            var certUtilFile = await CreateNoisyCertUtilAsync(tempDirectory);
            var environment = TestEnvironment.CreateLinux(new Dictionary<string, string?>
            {
                ["PATH"] = tempDirectory.FullName,
                ["SSL_CERT_DIR"] = tempDirectory.FullName,
                ["DOTNET_DEV_CERTS_NSSDB_PATHS"] = nssDbDirectory.FullName
            });
            var manager = new UnixCertificateManager(NullLogger.Instance, environment, nssDbOverride: null, startInfo =>
            {
                startInfo.FileName = certUtilFile.FullName;
            });
            using var certificate = manager.CreateAspNetCoreHttpsDevelopmentCertificate(
                DateTimeOffset.UtcNow.AddDays(-1),
                DateTimeOffset.UtcNow.AddDays(365));
 
            var trustLevel = await Task.Run(() => manager.GetTrustLevel(certificate))
                .DefaultTimeout();
 
            Assert.Equal(CertificateManager.TrustLevel.None, trustLevel);
        }
        finally
        {
            tempDirectory.Delete(recursive: true);
        }
    }
 
    [Fact]
    public void GetTrustLevel_WithCorruptOpenSslCertificate_DoesNotThrow()
    {
        Assert.SkipUnless(OperatingSystem.IsLinux(), "OpenSSL certificate directory trust is only exercised on Linux.");
 
        var openSslDirectory = Directory.CreateTempSubdirectory();
 
        try
        {
            var environment = TestEnvironment.CreateLinux(new Dictionary<string, string?>
            {
                ["PATH"] = Path.Combine(openSslDirectory.FullName, "missing-tools"),
                ["SSL_CERT_DIR"] = openSslDirectory.FullName,
                ["DOTNET_DEV_CERTS_NSSDB_PATHS"] = Path.Combine(openSslDirectory.FullName, "missing-nss-db")
            });
            var manager = new UnixCertificateManager(NullLogger.Instance, environment, nssDbOverride: null);
            using var certificate = manager.CreateAspNetCoreHttpsDevelopmentCertificate(
                DateTimeOffset.UtcNow.AddDays(-1),
                DateTimeOffset.UtcNow.AddDays(365));
            var certificatePath = Path.Combine(openSslDirectory.FullName, $"aspnetcore-localhost-{certificate.Thumbprint}.pem");
            File.WriteAllText(certificatePath, "not a certificate");
 
            var trustLevel = manager.GetTrustLevel(certificate);
 
            Assert.Equal(CertificateManager.TrustLevel.None, trustLevel);
        }
        finally
        {
            openSslDirectory.Delete(recursive: true);
        }
    }
 
    [Fact]
    public void GetTrustLevel_WithCorruptOpenSslCertificateBeforeValidCertificate_DoesNotLogOpenSslWarning()
    {
        Assert.SkipUnless(OperatingSystem.IsLinux(), "OpenSSL certificate directory trust is only exercised on Linux.");
 
        var corruptOpenSslDirectory = Directory.CreateTempSubdirectory();
        var validOpenSslDirectory = Directory.CreateTempSubdirectory();
 
        try
        {
            var sink = new TestSink();
            var logger = new TestLogger(nameof(UnixCertificateManager), sink, enabled: true);
            var environment = TestEnvironment.CreateLinux(new Dictionary<string, string?>
            {
                ["PATH"] = Path.Combine(corruptOpenSslDirectory.FullName, "missing-tools"),
                ["SSL_CERT_DIR"] = string.Join(Path.PathSeparator, corruptOpenSslDirectory.FullName, validOpenSslDirectory.FullName),
                ["DOTNET_DEV_CERTS_NSSDB_PATHS"] = Path.Combine(corruptOpenSslDirectory.FullName, "missing-nss-db")
            });
            var manager = new UnixCertificateManager(logger, environment, nssDbOverride: null);
            using var certificate = manager.CreateAspNetCoreHttpsDevelopmentCertificate(
                DateTimeOffset.UtcNow.AddDays(-1),
                DateTimeOffset.UtcNow.AddDays(365));
            var certificateFileName = $"aspnetcore-localhost-{certificate.Thumbprint}.pem";
            File.WriteAllText(Path.Combine(corruptOpenSslDirectory.FullName, certificateFileName), "not a certificate");
            File.WriteAllText(Path.Combine(validOpenSslDirectory.FullName, certificateFileName), certificate.ExportCertificatePem());
 
            var trustLevel = manager.GetTrustLevel(certificate);
 
            Assert.NotEqual(CertificateManager.TrustLevel.None, trustLevel);
            Assert.DoesNotContain(sink.Writes, w => w.Message?.Contains("not trusted by OpenSSL", StringComparison.Ordinal) == true);
        }
        finally
        {
            corruptOpenSslDirectory.Delete(recursive: true);
            validOpenSslDirectory.Delete(recursive: true);
        }
    }
 
    [Fact]
    public void RemoveCertificate_WithMissingOpenSsl_DeletesOpenSslCertificate()
    {
        Assert.SkipUnless(OperatingSystem.IsLinux(), "OpenSSL certificate cleanup is only exercised on Linux.");
 
        var openSslDirectory = Directory.CreateTempSubdirectory();
 
        try
        {
            var environment = TestEnvironment.CreateLinux(new Dictionary<string, string?>
            {
                ["PATH"] = Path.Combine(openSslDirectory.FullName, "missing-tools"),
                ["DOTNET_DEV_CERTS_NSSDB_PATHS"] = Path.Combine(openSslDirectory.FullName, "missing-nss-db"),
                [CertificateHelpers.DevCertsOpenSslCertDirEnvVar] = openSslDirectory.FullName
            });
            var manager = new UnixCertificateManager(NullLogger.Instance, environment, nssDbOverride: null);
            using var certificate = manager.CreateAspNetCoreHttpsDevelopmentCertificate(
                DateTimeOffset.UtcNow.AddDays(-1),
                DateTimeOffset.UtcNow.AddDays(365));
            using var savedCertificate = manager.SaveCertificate(certificate);
            var certificatePath = Path.Combine(openSslDirectory.FullName, $"aspnetcore-localhost-{savedCertificate.Thumbprint}.pem");
            File.WriteAllText(certificatePath, "not a certificate");
 
            var exception = Record.Exception(() => manager.RemoveCertificate(savedCertificate, CertificateManager.RemoveLocations.All));
 
            Assert.Null(exception);
            Assert.False(File.Exists(certificatePath));
        }
        finally
        {
            openSslDirectory.Delete(recursive: true);
        }
    }
 
    [Fact]
    public void RemoveCertificate_WithMissingCertUtilAndNssDbs_SkipsNssCleanup()
    {
        Assert.SkipUnless(OperatingSystem.IsLinux(), "NSS certificate cleanup is only exercised on Linux.");
 
        var nssDbDirectory = Directory.CreateTempSubdirectory();
        var openSslDirectory = Directory.CreateTempSubdirectory();
 
        try
        {
            var environment = TestEnvironment.CreateLinux(new Dictionary<string, string?>
            {
                ["PATH"] = Path.Combine(nssDbDirectory.FullName, "missing-certutil"),
                ["DOTNET_DEV_CERTS_NSSDB_PATHS"] = nssDbDirectory.FullName,
                [CertificateHelpers.DevCertsOpenSslCertDirEnvVar] = openSslDirectory.FullName
            });
            var manager = new UnixCertificateManager(NullLogger.Instance, environment, nssDbOverride: null);
            using var certificate = manager.CreateAspNetCoreHttpsDevelopmentCertificate(
                DateTimeOffset.UtcNow.AddDays(-1),
                DateTimeOffset.UtcNow.AddDays(365));
 
            var exception = Record.Exception(() => manager.RemoveCertificate(certificate, CertificateManager.RemoveLocations.Trusted));
 
            Assert.Null(exception);
        }
        finally
        {
            nssDbDirectory.Delete(recursive: true);
            openSslDirectory.Delete(recursive: true);
        }
    }
 
    private static async Task<FileInfo> CreateBlockingCertUtilAsync(DirectoryInfo directory, string parentPidFile, string childPidFile)
    {
        if (!OperatingSystem.IsLinux())
        {
            throw new PlatformNotSupportedException();
        }
 
        var certUtilFile = new FileInfo(Path.Combine(directory.FullName, "certutil"));
        var script =
            "#!/usr/bin/env bash" + Environment.NewLine +
            $"echo $$ > '{EscapeShellPath(parentPidFile)}'" + Environment.NewLine +
            "sleep 60 &" + Environment.NewLine +
            $"echo $! > '{EscapeShellPath(childPidFile)}'" + Environment.NewLine +
            "wait $!" + Environment.NewLine;
        await File.WriteAllTextAsync(certUtilFile.FullName, script);
        File.SetUnixFileMode(
            certUtilFile.FullName,
            UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute |
            UnixFileMode.GroupRead | UnixFileMode.GroupExecute |
            UnixFileMode.OtherRead | UnixFileMode.OtherExecute);
 
        return certUtilFile;
    }
 
    private static async Task<FileInfo> CreateNoisyCertUtilAsync(DirectoryInfo directory)
    {
        if (!OperatingSystem.IsLinux())
        {
            throw new PlatformNotSupportedException();
        }
 
        var certUtilFile = new FileInfo(Path.Combine(directory.FullName, "certutil"));
        var script =
            "#!/usr/bin/env bash" + Environment.NewLine +
            "for _ in {1..16}; do" + Environment.NewLine +
            "  printf '%65536s' x" + Environment.NewLine +
            "  printf '%65536s' x >&2" + Environment.NewLine +
            "done" + Environment.NewLine +
            "exit 1" + Environment.NewLine;
        await File.WriteAllTextAsync(certUtilFile.FullName, script);
        File.SetUnixFileMode(
            certUtilFile.FullName,
            UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute |
            UnixFileMode.GroupRead | UnixFileMode.GroupExecute |
            UnixFileMode.OtherRead | UnixFileMode.OtherExecute);
 
        return certUtilFile;
    }
 
    private static UnixCertificateManager CreateManager(
        IReadOnlyDictionary<string, string?>? variables = null,
        IReadOnlyDictionary<string, string?>? configurationValues = null)
    {
        var environment = TestEnvironment.CreateLinux(variables);
        var configuration = new ConfigurationBuilder()
            .AddInMemoryCollection(configurationValues)
            .Build();
        var nssDbOverride = CertificateConfiguration.ResolveNssDbOverride(configuration);
 
        return new UnixCertificateManager(NullLogger.Instance, environment, nssDbOverride);
    }
 
    private static string CreateDirectory(DirectoryInfo homeDirectory, params string[] pathSegments)
    {
        var path = Path.Combine([homeDirectory.FullName, .. pathSegments]);
        Directory.CreateDirectory(path);
        return path;
    }
 
    private static void RunWithTemporaryHome(Action<DirectoryInfo> test)
    {
        var homeDirectory = Directory.CreateTempSubdirectory();
        try
        {
            test(homeDirectory);
        }
        finally
        {
            homeDirectory.Delete(recursive: true);
        }
    }
 
    private static void AssertFirefoxNssDb(UnixCertificateManager.NssDb nssDb)
    {
        Assert.Equal("Firefox", nssDb.BrowserFamily);
        Assert.Equal(["-L"], nssDb.CheckArguments);
        Assert.Equal("C", nssDb.TrustUsage);
    }
 
    private static void AssertChromiumNssDb(UnixCertificateManager.NssDb nssDb)
    {
        Assert.Equal("Chromium", nssDb.BrowserFamily);
        Assert.Equal(["-V", "-u", "V"], nssDb.CheckArguments);
        Assert.Equal("P", nssDb.TrustUsage);
    }
 
    private static void AssertProcessStartInfo(ProcessStartInfo startInfo, params string[] expectedArguments)
    {
        Assert.Equal(CertificateHelpers.CertUtilCommand, startInfo.FileName);
        Assert.Empty(startInfo.Arguments);
        Assert.Equal(expectedArguments, startInfo.ArgumentList);
    }
 
    private static string EscapeShellPath(string path) => path.Replace("'", "'\"'\"'", StringComparison.Ordinal);
}