File: Commands\Workload\WorkloadUtilities.cs
Web Access
Project: src\src\sdk\src\Cli\dotnet\dotnet.csproj (dotnet)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.Deployment.DotNet.Releases;
using Microsoft.DotNet.Cli.Utils;

namespace Microsoft.DotNet.Cli.Commands.Workload;

internal static class WorkloadUtilities
{
    internal static int VersionCompare(string first, string second)
    {
        if (first.Equals(second))
        {
            return 0;
        }

        var firstDash = first.IndexOf('-');
        var secondDash = second.IndexOf('-');
        firstDash = firstDash < 0 ? first.Length : firstDash;
        secondDash = secondDash < 0 ? second.Length : secondDash;

        var firstVersion = new Version(first.Substring(0, firstDash));
        var secondVersion = new Version(second.Substring(0, secondDash));

        var comparison = firstVersion.CompareTo(secondVersion);
        if (comparison != 0)
        {
            return comparison;
        }

        var modifiedFirst = new ReleaseVersion(1, 1, 1, firstDash == first.Length ? null : first.Substring(firstDash));
        var modifiedSecond = new ReleaseVersion(1, 1, 1, secondDash == second.Length ? null : second.Substring(secondDash));

        return modifiedFirst.CompareTo(modifiedSecond);
    }

    /// <summary>
    /// Determines whether workload package and installer signatures should be verified,
    /// without considering any user-specified skip flags.
    /// </summary>
    /// <remarks>
    /// Use this overload for non-interactive code paths (background operations, info queries)
    /// where the user has no opportunity to pass <c>--skip-sign-check</c>. This still respects
    /// the registry policy and dotnet host signing status.
    /// </remarks>
    /// <returns><see langword="true"/> if signatures of packages and installers should be verified.</returns>
    public static bool ShouldVerifySignatures() => ShouldVerifySignatures(skipSignCheck: false);

    /// <summary>
    /// Determines whether workload package and installer signatures should be verified.
    /// </summary>
    /// <remarks>
    /// <para>
    /// This is the central policy decision point for signature verification across the workload system.
    /// The result controls <b>both</b> NuGet package signature verification (for <c>.nupkg</c> files downloaded
    /// from feeds) and MSI Authenticode signature verification (for <c>.msi</c> files extracted from those packages
    /// on Windows). See <c>SIGNING-VERIFICATION.md</c> in this directory and
    /// <c>NUGET-SIGNATURE-VERIFICATION.md</c> in the NuGetPackageDownloader directory for the full architecture.
    /// The single boolean is passed as <c>verifySignatures</c> to <c>NuGetPackageDownloader</c> and as
    /// <c>verifyMsiSignature</c> to <c>WorkloadInstallerFactory</c>.
    /// </para>
    /// <para>
    /// Decision tree (Windows only — always returns <see langword="false"/> on other platforms):
    /// <list type="number">
    ///   <item>If the dotnet host binary itself is <b>not</b> Authenticode-signed, return <see langword="false"/>.
    ///         Rationale: if the host is unsigned, the installation is already untrusted, so enforcing downstream
    ///         signatures provides no additional security guarantee.</item>
    ///   <item>If the user specified <c>--skip-sign-check</c> but the <c>VerifySignatures</c> registry policy
    ///         (<c>HKLM\SOFTWARE\Policies\Microsoft\dotnet\Workloads</c>) is enabled, throw a
    ///         <see cref="GracefulException"/>. The global policy cannot be overridden by a command-line flag.</item>
    ///   <item>Otherwise, return <c>!skipSignCheck</c> — i.e., verify unless the user explicitly opted out.</item>
    /// </list>
    /// </para>
    /// </remarks>
    /// <param name="skipSignCheck">
    /// <see langword="true"/> if the user passed <c>--skip-sign-check</c> on the command line.
    /// </param>
    /// <returns><see langword="true"/> if signatures of packages and installers should be verified.</returns>
    /// <exception cref="GracefulException">
    /// Thrown when the user attempts to skip signature verification while the global policy enforces it.
    /// </exception>
    public static bool ShouldVerifySignatures(bool skipSignCheck)
    {
#if !TARGET_WINDOWS
        // Non-Windows: workload signature verification is not currently enforced for three reasons:
        //   1. SignCheck.IsDotNetSigned() depends on Windows Authenticode APIs (Win32). There is
        //      no equivalent host-signed check on Linux/macOS to anchor the trust chain.
        //   2. NuGet signature verification on Linux uses TRP certificate bundles (.pem files)
        //      shipped as point-in-time snapshots in the SDK. These can lag behind newly-added
        //      roots, creating windows where verification fails for valid packages.
        //   3. On non-Windows, workload packs are file-based (no MSI), so there is no second
        //      verification layer to fall back on.
        //
        // Returning false here means NuGetPackageDownloader will also skip verification (its
        // constructor platform gate only applies when the caller passes verifySignatures: true).
        return false;
#else
        if (!SignCheck.IsDotNetSigned())
        {
            // The dotnet host is unsigned (e.g., a locally-built SDK, preview, or source-build).
            // Skip verification — workload packages are also likely unsigned in this environment,
            // and the user is already running untrusted code.
            return false;
        }

        bool policyEnabled = SignCheck.IsWorkloadSignVerificationPolicySet();
        if (skipSignCheck && policyEnabled)
        {
            // The VerifySignatures registry policy is set by an administrator. It cannot be bypassed
            // by individual users via --skip-sign-check.
            throw new GracefulException(CliCommandStrings.SkipSignCheckInvalidOption);
        }

        // When dotnet is signed: verify by default, unless the user explicitly opted out.
        return !skipSignCheck;
#endif
    }
}