| File: NuGet\TrustedRootsHelper.cs | Web Access |
| Project: src\src\Aspire.Managed\Aspire.Managed.csproj (aspire-managed) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. #pragma warning disable CA1852 // DispatchProxy classes can't be sealed using System.Reflection; using System.Security.Cryptography.X509Certificates; using NuGet.Packaging.Signing; namespace Aspire.Managed.NuGet; /// <summary> /// Initializes NuGet's X509 trust store from embedded trusted root PEM certificates /// for package signature verification on Linux, without writing to disk. /// </summary> internal static class TrustedRootsHelper { private static bool s_initialized; /// <summary> /// Initializes the NuGet trust store with embedded trusted root certificates. /// On Linux, when DOTNET_NUGET_SIGNATURE_VERIFICATION is set to "true", NuGet requires /// certificate bundles for signature verification. The .NET SDK ships these as PEM files /// in its trustedroots directory, but aspire-managed is a single-file app without access /// to the SDK's directory structure. This method loads embedded PEM resources in memory /// and uses DispatchProxy to create IX509ChainFactory implementations that NuGet's trust /// store can use. /// </summary> /// <remarks> /// TODO: Remove this once NuGet supports a public API for configuring the trust store, /// or when it supports single-file apps. See https://github.com/dotnet/aspire/issues/15282. /// </remarks> public static void InitializeTrustStore() { if (s_initialized) { return; } if (!OperatingSystem.IsLinux()) { // On Windows, NuGet uses the system certificate store directly. // On macOS, matching .NET SDK behavior which only enables this on Linux. return; } var envValue = Environment.GetEnvironmentVariable("DOTNET_NUGET_SIGNATURE_VERIFICATION"); if (!(bool.TryParse(envValue, out var enabled) && enabled)) { // If DOTNET_NUGET_SIGNATURE_VERIFICATION is not set to "true", NuGet won't // perform signature verification on Linux, so there's no need to initialize // the trust store. return; } try { InitializeTrustStoreFromEmbeddedResources(); s_initialized = true; } catch (Exception ex) { // Log but don't fail the restore. If trust store initialization fails, // NuGet may still work if signature verification is not required or if // the system has its own certificate bundles. Console.Error.WriteLine($"WARNING: Failed to initialize NuGet trust store from embedded certificates: {ex}"); } } private static void InitializeTrustStoreFromEmbeddedResources() { var nugetPackagingAssembly = typeof(X509TrustStore).Assembly; // Resolve internal NuGet types needed for DispatchProxy creation var chainFactoryInterfaceType = nugetPackagingAssembly.GetType("NuGet.Packaging.Signing.IX509ChainFactory"); var chainInterfaceType = nugetPackagingAssembly.GetType("NuGet.Packaging.Signing.IX509Chain"); if (chainFactoryInterfaceType is null || chainInterfaceType is null) { Console.Error.WriteLine("WARNING: Could not find IX509ChainFactory or IX509Chain types in NuGet.Packaging."); return; } // Set up code signing trust store SetTrustStoreFactory( "SetCodeSigningX509ChainFactory", "codesignctl.pem", chainFactoryInterfaceType, chainInterfaceType); // Set up timestamping trust store SetTrustStoreFactory( "SetTimestampingX509ChainFactory", "timestampctl.pem", chainFactoryInterfaceType, chainInterfaceType); } private static void SetTrustStoreFactory( string setterMethodName, string resourceName, Type chainFactoryInterfaceType, Type chainInterfaceType) { var certificates = LoadCertificatesFromResource(resourceName); if (certificates is null || certificates.Count == 0) { Console.Error.WriteLine($"WARNING: No certificates loaded from embedded resource: {resourceName}"); return; } // Create IX509ChainFactory proxy via DispatchProxy var factory = ChainFactoryDispatchProxy.CreateFactory( chainFactoryInterfaceType, chainInterfaceType, certificates); // Call the setter on X509TrustStore to register the factory var setter = typeof(X509TrustStore).GetMethod( setterMethodName, BindingFlags.NonPublic | BindingFlags.Static); if (setter is null) { Console.Error.WriteLine($"WARNING: Could not find {setterMethodName} on X509TrustStore."); return; } setter.Invoke(null, [factory]); } private static X509Certificate2Collection? LoadCertificatesFromResource(string resourceName) { using var stream = typeof(TrustedRootsHelper).Assembly.GetManifestResourceStream(resourceName); if (stream is null) { return null; } using var reader = new StreamReader(stream); var pemContents = reader.ReadToEnd(); var certificates = new X509Certificate2Collection(); certificates.ImportFromPem(pemContents); return certificates; } } /// <summary> /// DispatchProxy that implements NuGet's internal IX509ChainFactory interface. /// Creates X509Chain instances configured with custom root trust using embedded certificates. /// </summary> internal class ChainFactoryDispatchProxy : DispatchProxy { private X509Certificate2Collection _certificates = []; private Type _chainInterfaceType = null!; internal static object CreateFactory( Type chainFactoryInterfaceType, Type chainInterfaceType, X509Certificate2Collection certificates) { var proxy = (ChainFactoryDispatchProxy)Create(chainFactoryInterfaceType, typeof(ChainFactoryDispatchProxy)); proxy._certificates = certificates; proxy._chainInterfaceType = chainInterfaceType; return proxy; } protected override object? Invoke(MethodInfo? targetMethod, object?[]? args) { // IX509ChainFactory has a single method: IX509Chain Create() if (targetMethod?.Name == "Create") { return CreateChain(); } throw new NotSupportedException($"Method '{targetMethod?.Name}' is not supported on IX509ChainFactory proxy. This may indicate a NuGet.Packaging version mismatch — the library may have added new interface members."); } private object CreateChain() { // Create an IX509Chain proxy that wraps an X509Chain with custom root trust return ChainDispatchProxy.CreateChain(_chainInterfaceType, _certificates); } } /// <summary> /// DispatchProxy that implements NuGet's internal IX509Chain interface. /// Wraps an X509Chain configured with CustomRootTrust mode. /// </summary> internal class ChainDispatchProxy : DispatchProxy { private readonly X509Chain _chain = new(); internal static object CreateChain( Type chainInterfaceType, X509Certificate2Collection certificates) { var proxy = (ChainDispatchProxy)Create(chainInterfaceType, typeof(ChainDispatchProxy)); proxy._chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust; proxy._chain.ChainPolicy.CustomTrustStore.AddRange(certificates); return proxy; } protected override object? Invoke(MethodInfo? targetMethod, object?[]? args) { return targetMethod?.Name switch { "Build" => Build((X509Certificate2)args![0]!), "Dispose" => Dispose(), "get_ChainElements" => _chain.ChainElements, "get_ChainPolicy" => _chain.ChainPolicy, "get_ChainStatus" => _chain.ChainStatus, "get_PrivateReference" => _chain, "get_AdditionalContext" => (global::NuGet.Common.ILogMessage?)null, _ => throw new NotSupportedException($"Method '{targetMethod?.Name}' is not supported on IX509Chain proxy. This may indicate a NuGet.Packaging version mismatch — the library may have added new interface members.") }; } private bool Build(X509Certificate2 certificate) { return _chain.Build(certificate); } private object? Dispose() { _chain.Dispose(); return null; } }