-
Notifications
You must be signed in to change notification settings - Fork 894
Enable NuGet signature verification for aspire-managed on Linux #16049
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
3a02a0d
c886b22
aa36b51
8a040c9
41707f9
e7e62b4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| // 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.Configuration; | ||
|
|
||
| namespace Aspire.Cli.NuGet; | ||
|
|
||
| /// <summary> | ||
| /// Enables NuGet signature verification when spawning aspire-managed processes. | ||
| /// Mirrors the .NET SDK's NuGetSignatureVerificationEnabler behavior. | ||
| /// </summary> | ||
| internal static class NuGetSignatureVerificationEnabler | ||
| { | ||
| internal const string DotNetNuGetSignatureVerification = "DOTNET_NUGET_SIGNATURE_VERIFICATION"; | ||
|
|
||
| /// <summary> | ||
| /// Applies NuGet signature verification environment variables to the given dictionary. | ||
| /// On Linux, sets DOTNET_NUGET_SIGNATURE_VERIFICATION to "true" unless the user | ||
| /// has explicitly set it to "false". The behavior can be disabled via the | ||
| /// <see cref="KnownFeatures.NuGetSignatureVerificationEnabled"/> feature flag. | ||
| /// </summary> | ||
| public static void Apply(Dictionary<string, string> environmentVariables, IFeatures features, CliExecutionContext executionContext) | ||
| { | ||
| if (!OperatingSystem.IsLinux() || | ||
| !features.IsFeatureEnabled( | ||
| KnownFeatures.NuGetSignatureVerificationEnabled, | ||
| KnownFeatures.GetFeatureMetadata(KnownFeatures.NuGetSignatureVerificationEnabled)!.DefaultValue)) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| var value = executionContext.GetEnvironmentVariable(DotNetNuGetSignatureVerification); | ||
|
|
||
| // If the user explicitly set it to "false", respect that | ||
| var effectiveValue = bool.TryParse(value, out var boolValue) && !boolValue | ||
| ? bool.FalseString | ||
| : bool.TrueString; | ||
|
|
||
| environmentVariables[DotNetNuGetSignatureVerification] = effectiveValue; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,230 @@ | ||
| // 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"); | ||
|
eerhardt marked this conversation as resolved.
|
||
| 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"); | ||
|
Comment on lines
+75
to
+76
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 😬 Have we talked with NuGet team about making these public?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need NuGet/NuGet.Client#7197 in order to not use reflection. It hasn't been released yet. When it is, we can write the files to a folder next to us, and won't need this reflection.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there an issue to track fixing the reflection?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| 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 | ||
|
eerhardt marked this conversation as resolved.
|
||
| { | ||
| "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; | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.