From 3eafa1f2b4ca77803071d1e759f75f8f71e3435a Mon Sep 17 00:00:00 2001 From: Damon Tivel Date: Thu, 20 Apr 2023 13:09:53 -0700 Subject: [PATCH] Signing: enable NuGet signature verification by default on Linux (#31868) Resolve https://github.com/NuGet/Home/issues/11262 --- src/Cli/dotnet/NuGetForwardingApp.cs | 2 + .../NuGetSignatureVerificationEnabler.cs | 61 +++++++ src/Cli/dotnet/commands/RestoringCommand.cs | 5 + .../dotnet/commands/dotnet-restore/Program.cs | 1 + .../NuGetSignatureVerificationEnablerTests.cs | 167 ++++++++++++++++++ 5 files changed, 236 insertions(+) create mode 100644 src/Cli/dotnet/NuGetSignatureVerificationEnabler.cs create mode 100644 src/Tests/dotnet.Tests/NuGetSignatureVerificationEnablerTests.cs diff --git a/src/Cli/dotnet/NuGetForwardingApp.cs b/src/Cli/dotnet/NuGetForwardingApp.cs index e3babec48eb0..4b0d060b6121 100644 --- a/src/Cli/dotnet/NuGetForwardingApp.cs +++ b/src/Cli/dotnet/NuGetForwardingApp.cs @@ -19,6 +19,8 @@ public NuGetForwardingApp(IEnumerable argsToForward) _forwardingApp = new ForwardingApp( GetNuGetExePath(), argsToForward); + + NuGetSignatureVerificationEnabler.ConditionallyEnable(_forwardingApp); } public int Execute() diff --git a/src/Cli/dotnet/NuGetSignatureVerificationEnabler.cs b/src/Cli/dotnet/NuGetSignatureVerificationEnabler.cs new file mode 100644 index 000000000000..021f0ef24370 --- /dev/null +++ b/src/Cli/dotnet/NuGetSignatureVerificationEnabler.cs @@ -0,0 +1,61 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable + +using System; +using System.Runtime.InteropServices; +using Microsoft.DotNet.Cli; +using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.Tools.MSBuild; + +namespace Microsoft.DotNet.Tools +{ + public static class NuGetSignatureVerificationEnabler + { + private static readonly EnvironmentProvider s_environmentProvider = new(); + + internal static readonly string DotNetNuGetSignatureVerification = "DOTNET_NUGET_SIGNATURE_VERIFICATION"; + + public static void ConditionallyEnable(ForwardingApp forwardingApp, IEnvironmentProvider? environmentProvider = null) + { + ArgumentNullException.ThrowIfNull(forwardingApp, nameof(forwardingApp)); + + if (!IsLinux()) + { + return; + } + + string value = GetSignatureVerificationEnablementValue(environmentProvider); + + forwardingApp.WithEnvironmentVariable(DotNetNuGetSignatureVerification, value); + } + + public static void ConditionallyEnable(MSBuildForwardingApp forwardingApp, IEnvironmentProvider? environmentProvider = null) + { + ArgumentNullException.ThrowIfNull(forwardingApp, nameof(forwardingApp)); + + if (!IsLinux()) + { + return; + } + + string value = GetSignatureVerificationEnablementValue(environmentProvider); + + forwardingApp.EnvironmentVariable(DotNetNuGetSignatureVerification, value); + } + + private static string GetSignatureVerificationEnablementValue(IEnvironmentProvider? environmentProvider) + { + string? value = (environmentProvider ?? s_environmentProvider).GetEnvironmentVariable(DotNetNuGetSignatureVerification); + + return string.Equals(bool.FalseString, value, StringComparison.OrdinalIgnoreCase) + ? bool.FalseString : bool.TrueString; + } + + private static bool IsLinux() + { + return RuntimeInformation.IsOSPlatform(OSPlatform.Linux); + } + } +} diff --git a/src/Cli/dotnet/commands/RestoringCommand.cs b/src/Cli/dotnet/commands/RestoringCommand.cs index 8412ef08593e..c029fef7b6b4 100644 --- a/src/Cli/dotnet/commands/RestoringCommand.cs +++ b/src/Cli/dotnet/commands/RestoringCommand.cs @@ -30,6 +30,11 @@ public RestoringCommand( Task.Run(() => WorkloadManifestUpdater.BackgroundUpdateAdvertisingManifestsAsync(userProfileDir)); SeparateRestoreCommand = GetSeparateRestoreCommand(msbuildArgs, noRestore, msbuildPath); AdvertiseWorkloadUpdates = advertiseWorkloadUpdates; + + if (!noRestore) + { + NuGetSignatureVerificationEnabler.ConditionallyEnable(this); + } } private static IEnumerable GetCommandArguments( diff --git a/src/Cli/dotnet/commands/dotnet-restore/Program.cs b/src/Cli/dotnet/commands/dotnet-restore/Program.cs index a6a412513563..e4d4422d0996 100644 --- a/src/Cli/dotnet/commands/dotnet-restore/Program.cs +++ b/src/Cli/dotnet/commands/dotnet-restore/Program.cs @@ -17,6 +17,7 @@ public class RestoreCommand : MSBuildForwardingApp public RestoreCommand(IEnumerable msbuildArgs, string msbuildPath = null) : base(msbuildArgs, msbuildPath) { + NuGetSignatureVerificationEnabler.ConditionallyEnable(this); } public static RestoreCommand FromArgs(string[] args, string msbuildPath = null, bool noLogo = true) diff --git a/src/Tests/dotnet.Tests/NuGetSignatureVerificationEnablerTests.cs b/src/Tests/dotnet.Tests/NuGetSignatureVerificationEnablerTests.cs new file mode 100644 index 000000000000..95c105692548 --- /dev/null +++ b/src/Tests/dotnet.Tests/NuGetSignatureVerificationEnablerTests.cs @@ -0,0 +1,167 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using Xunit; +using Microsoft.DotNet.Cli; +using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.Tools; +using Microsoft.DotNet.Tools.MSBuild; +using Moq; + +namespace Microsoft.DotNet.Tests +{ + public class NuGetSignatureVerificationEnablerTests + { + private static readonly string FakeFilePath = Path.Combine(Path.GetTempPath(), "file.fake"); + + public static IEnumerable GetNonFalseValues() + { + yield return new object[] { null! }; + yield return new object[] { string.Empty }; + yield return new object[] { "0" }; + yield return new object[] { "1" }; + yield return new object[] { "no" }; + yield return new object[] { "yes" }; + yield return new object[] { "true" }; + yield return new object[] { "TRUE" }; + } + + public static IEnumerable GetFalseValues() + { + yield return new object[] { "false" }; + yield return new object[] { "FALSE" }; + } + + [Fact] + public void GivenANullForwardingAppThrows() + { + ForwardingApp forwardingApp = null!; + + ArgumentNullException exception = Assert.Throws( + () => NuGetSignatureVerificationEnabler.ConditionallyEnable(forwardingApp)); + + Assert.Equal("forwardingApp", exception.ParamName); + } + + [Fact] + public void GivenANullMSBuildForwardingAppThrows() + { + MSBuildForwardingApp forwardingApp = null!; + + ArgumentNullException exception = Assert.Throws( + () => NuGetSignatureVerificationEnabler.ConditionallyEnable(forwardingApp)); + + Assert.Equal("forwardingApp", exception.ParamName); + } + + [LinuxOnlyTheory] + [MemberData(nameof(GetNonFalseValues))] + public void GivenAForwardingAppAndAnEnvironmentVariableValueThatIsNotFalseSetsTrueOnLinux(string? value) + { + Mock environmentProvider = CreateEnvironmentProvider(value); + ForwardingApp forwardingApp = new(FakeFilePath, Array.Empty()); + + NuGetSignatureVerificationEnabler.ConditionallyEnable(forwardingApp, environmentProvider.Object); + + environmentProvider.VerifyAll(); + + VerifyEnvironmentVariable(forwardingApp.GetProcessStartInfo(), bool.TrueString); + } + + [LinuxOnlyTheory] + [MemberData(nameof(GetFalseValues))] + public void GivenAForwardingAppAndAnEnvironmentVariableValueThatIsFalseSetsFalseOnLinux(string value) + { + Mock environmentProvider = CreateEnvironmentProvider(value); + ForwardingApp forwardingApp = new(FakeFilePath, Array.Empty()); + + NuGetSignatureVerificationEnabler.ConditionallyEnable(forwardingApp, environmentProvider.Object); + + environmentProvider.VerifyAll(); + + VerifyEnvironmentVariable(forwardingApp.GetProcessStartInfo(), bool.FalseString); + } + + [LinuxOnlyTheory] + [MemberData(nameof(GetNonFalseValues))] + public void GivenAnMSBuildForwardingAppAndAnEnvironmentVariableValueThatIsNotFalseSetsTrueOnLinux(string? value) + { + Mock environmentProvider = CreateEnvironmentProvider(value); + MSBuildForwardingApp forwardingApp = new(Array.Empty(), FakeFilePath); + + NuGetSignatureVerificationEnabler.ConditionallyEnable(forwardingApp, environmentProvider.Object); + + environmentProvider.VerifyAll(); + + VerifyEnvironmentVariable(forwardingApp.GetProcessStartInfo(), bool.TrueString); + } + + [LinuxOnlyTheory] + [MemberData(nameof(GetFalseValues))] + public void GivenAnMSBuildForwardingAppAndAnEnvironmentVariableValueThatIsFalseSetsFalseOnLinux(string value) + { + Mock environmentProvider = CreateEnvironmentProvider(value); + MSBuildForwardingApp forwardingApp = new(Array.Empty(), FakeFilePath); + + NuGetSignatureVerificationEnabler.ConditionallyEnable(forwardingApp, environmentProvider.Object); + + environmentProvider.VerifyAll(); + + VerifyEnvironmentVariable(forwardingApp.GetProcessStartInfo(), bool.FalseString); + } + + [MacOSOnlyFact] + public void GivenAForwardingAppDoesNothingOnMacOs() + { + var environmentProvider = new Mock(MockBehavior.Strict); + ForwardingApp forwardingApp = new(FakeFilePath, Array.Empty()); + + NuGetSignatureVerificationEnabler.ConditionallyEnable(forwardingApp, environmentProvider.Object); + + environmentProvider.VerifyAll(); + + VerifyNoEnvironmentVariable(forwardingApp.GetProcessStartInfo()); + } + + [MacOSOnlyFact] + public void GivenAnMSBuildForwardingAppDoesNothingOnMacOs() + { + var environmentProvider = new Mock(MockBehavior.Strict); + MSBuildForwardingApp forwardingApp = new(Array.Empty(), FakeFilePath); + + NuGetSignatureVerificationEnabler.ConditionallyEnable(forwardingApp, environmentProvider.Object); + + environmentProvider.VerifyAll(); + + VerifyNoEnvironmentVariable(forwardingApp.GetProcessStartInfo()); + } + + private static Mock CreateEnvironmentProvider(string? value) + { + Mock provider = new(MockBehavior.Strict); + + provider + .Setup(p => p.GetEnvironmentVariable(NuGetSignatureVerificationEnabler.DotNetNuGetSignatureVerification)) + .Returns(value!); + + return provider; + } + + private static void VerifyEnvironmentVariable(ProcessStartInfo startInfo, string expectedValue) + { + Assert.True(startInfo.EnvironmentVariables.ContainsKey(NuGetSignatureVerificationEnabler.DotNetNuGetSignatureVerification)); + Assert.Equal(expectedValue, startInfo.EnvironmentVariables[NuGetSignatureVerificationEnabler.DotNetNuGetSignatureVerification]); + } + + private static void VerifyNoEnvironmentVariable(ProcessStartInfo startInfo) + { + Assert.False(startInfo.EnvironmentVariables.ContainsKey(NuGetSignatureVerificationEnabler.DotNetNuGetSignatureVerification)); + } + } +}