| File: Certificates\CertificateService.cs | Web Access |
| Project: src\src\Aspire.Cli\Aspire.Cli.csproj (aspire) |
// 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.Globalization; using System.Runtime.InteropServices; using Aspire.Cli.Interaction; using Aspire.Cli.Resources; using Aspire.Cli.Telemetry; using Microsoft.AspNetCore.Certificates.Generation; namespace Aspire.Cli.Certificates; /// <summary> /// The result of ensuring certificates are trusted. /// </summary> internal sealed class EnsureCertificatesTrustedResult { /// <summary> /// Gets the environment variables that should be set for the AppHost process /// to ensure certificates are properly trusted. /// </summary> public required IDictionary<string, string> EnvironmentVariables { get; init; } } internal interface ICertificateService { Task<EnsureCertificatesTrustedResult> EnsureCertificatesTrustedAsync(CancellationToken cancellationToken); } internal sealed class CertificateService( ICertificateToolRunner certificateToolRunner, IInteractionService interactionService, AspireCliTelemetry telemetry) : ICertificateService { private const string SslCertDirEnvVar = "SSL_CERT_DIR"; public async Task<EnsureCertificatesTrustedResult> EnsureCertificatesTrustedAsync(CancellationToken cancellationToken) { using var activity = telemetry.StartDiagnosticActivity(kind: ActivityKind.Client); var environmentVariables = new Dictionary<string, string>(); // Use the machine-readable check (available in .NET 10 SDK which is the minimum required) var trustResult = await CheckMachineReadableAsync(); await HandleMachineReadableTrustAsync(trustResult, environmentVariables); return new EnsureCertificatesTrustedResult { EnvironmentVariables = environmentVariables }; } private async Task<CertificateTrustResult> CheckMachineReadableAsync() { var result = await interactionService.ShowStatusAsync( InteractionServiceStrings.CheckingCertificates, () => Task.FromResult(certificateToolRunner.CheckHttpCertificate()), emoji: KnownEmojis.LockedWithKey); return result; } private async Task HandleMachineReadableTrustAsync( CertificateTrustResult trustResult, Dictionary<string, string> environmentVariables) { // If fully trusted, nothing more to do if (trustResult.IsFullyTrusted) { return; } // If not trusted at all, run the trust operation if (trustResult.IsNotTrusted) { var trustResultCode = await interactionService.ShowStatusAsync( InteractionServiceStrings.TrustingCertificates, () => Task.FromResult(certificateToolRunner.TrustHttpCertificate()), emoji: KnownEmojis.LockedWithKey); if (trustResultCode == EnsureCertificateResult.UserCancelledTrustStep) { interactionService.DisplayMessage(KnownEmojis.Warning, CertificatesCommandStrings.TrustCancelled); } else if (!CertificateHelpers.IsSuccessfulTrustResult(trustResultCode)) { interactionService.DisplayMessage(KnownEmojis.Warning, string.Format(CultureInfo.CurrentCulture, ErrorStrings.CertificatesMayNotBeFullyTrusted, trustResultCode)); } // Re-check trust status after trust operation trustResult = certificateToolRunner.CheckHttpCertificate(); } // If partially trusted (either initially or after trust), configure SSL_CERT_DIR on Linux if (trustResult.IsPartiallyTrusted && RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { ConfigureSslCertDir(environmentVariables); } } private static void ConfigureSslCertDir(Dictionary<string, string> environmentVariables) { // Get the dev-certs trust path (respects DOTNET_DEV_CERTS_OPENSSL_CERTIFICATE_DIRECTORY override) var devCertsTrustPath = CertificateHelpers.GetDevCertsTrustPath(); // Get the current SSL_CERT_DIR value (if any) var currentSslCertDir = Environment.GetEnvironmentVariable(SslCertDirEnvVar); // Check if the dev-certs trust path is already included if (!string.IsNullOrEmpty(currentSslCertDir)) { var paths = currentSslCertDir.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries); if (paths.Any(p => string.Equals(p.TrimEnd(Path.DirectorySeparatorChar), devCertsTrustPath.TrimEnd(Path.DirectorySeparatorChar), StringComparison.OrdinalIgnoreCase))) { // Already included, nothing to do return; } // Append the dev-certs trust path to the existing value environmentVariables[SslCertDirEnvVar] = $"{currentSslCertDir}{Path.PathSeparator}{devCertsTrustPath}"; } else { // Set the dev-certs trust path combined with the system certificate directory. var systemCertDirs = CertificateHelpers.GetSystemCertificateDirectories(); systemCertDirs.Add(devCertsTrustPath); environmentVariables[SslCertDirEnvVar] = string.Join(Path.PathSeparator, systemCertDirs); } } } internal sealed class CertificateServiceException(string message) : Exception(message) { }