File: VerifyCommand\VerifyCommandRunner.cs
Web Access
Project: src\src\nuget-client\src\NuGet.Core\NuGet.Commands\NuGet.Commands.csproj (NuGet.Commands)
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

#nullable disable

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NuGet.Common;
using NuGet.Packaging;
using NuGet.Packaging.Signing;
using NuGet.Protocol;
using static NuGet.Commands.VerifyArgs;

namespace NuGet.Commands
{
    /// <summary>
    /// Command Runner used to run the business logic for nuget verify command
    /// </summary>
    public class VerifyCommandRunner : IVerifyCommandRunner
    {
        private const int SuccessCode = 0;
        private const int FailureCode = 1;
        private const HashAlgorithmName _defaultFingerprintAlgorithm = HashAlgorithmName.SHA256;

        public async Task<int> ExecuteCommandAsync(VerifyArgs verifyArgs)
        {
            if (verifyArgs.Verifications.Count == 0)
            {
                verifyArgs.Logger.LogError(string.Format(CultureInfo.CurrentCulture, Strings.VerifyCommand_VerificationTypeNotSupported));
                return FailureCode;
            }

            var errorCount = 0;

            if (ShouldExecuteVerification(verifyArgs, Verification.Signatures))
            {
                if (!IsSignatureVerifyCommandSupported())
                {
                    verifyArgs.Logger.LogError(string.Format(CultureInfo.CurrentCulture, Strings.VerifyCommand_NotSupported));
                    return FailureCode;
                }

                var packagesToVerify = verifyArgs.PackagePaths.SelectMany(packagePath =>
                {
                    var packages = LocalFolderUtility.ResolvePackageFromPath(packagePath);
                    LocalFolderUtility.EnsurePackageFileExists(packagePath, packages);
                    return packages;
                });

                ClientPolicyContext clientPolicyContext = ClientPolicyContext.GetClientPolicy(verifyArgs.Settings, verifyArgs.Logger);

                // List of values passed through --certificate-fingerprint option read
                var allowListEntries = verifyArgs.CertificateFingerprint.Select(fingerprint =>
                    new CertificateHashAllowListEntry(
                        VerificationTarget.Author | VerificationTarget.Repository,
                        SignaturePlacement.PrimarySignature,
                        fingerprint,
                        _defaultFingerprintAlgorithm)).ToList();

                var verifierSettings = SignedPackageVerifierSettings.GetVerifyCommandDefaultPolicy();
                var verificationProviders = new List<ISignatureVerificationProvider>()
                {
                    new IntegrityVerificationProvider()
                };

                // trustedSigners section >> Owners are considered here.
                verificationProviders.Add(
                    new AllowListVerificationProvider(
                        clientPolicyContext.AllowList,
                        requireNonEmptyAllowList: clientPolicyContext.Policy == SignatureValidationMode.Require,
                        emptyListErrorMessage: Strings.Error_NoClientAllowList,
                        noMatchErrorMessage: Strings.Error_NoMatchingClientCertificate));

                IEnumerable<KeyValuePair<string, HashAlgorithmName>> trustedSignerAllowUntrustedRootList = clientPolicyContext.AllowList?
                    .Where(c => c.AllowUntrustedRoot)
                    .Select(c => new KeyValuePair<string, HashAlgorithmName>(c.Fingerprint, c.FingerprintAlgorithm));

                // trustedSigners section >> allowUntrustedRoot set true are considered here.
                verificationProviders.Add(new SignatureTrustAndValidityVerificationProvider(trustedSignerAllowUntrustedRootList));

                // List of values passed through --certificate-fingerprint option are considered here.
                verificationProviders.Add(
                    new AllowListVerificationProvider(
                        allowListEntries,
                        requireNonEmptyAllowList: false,
                        noMatchErrorMessage: Strings.Error_NoMatchingCertificate));

                var verifier = new PackageSignatureVerifier(verificationProviders);

                foreach (var package in packagesToVerify)
                {
                    try
                    {
                        errorCount += await VerifySignatureForPackageAsync(package, verifyArgs.Logger, verifier, verifierSettings);
                    }
                    catch (InvalidDataException e)
                    {
                        verifyArgs.Logger.LogError(string.Format(CultureInfo.CurrentCulture, Strings.VerifyCommand_PackageIsNotValid, package));
                        ExceptionUtilities.LogException(e, verifyArgs.Logger);
                    }
                }
            }

            return errorCount == 0 ? SuccessCode : FailureCode;
        }

        private async Task<int> VerifySignatureForPackageAsync(string packagePath, ILogger logger, PackageSignatureVerifier verifier, SignedPackageVerifierSettings verifierSettings)
        {
            var result = 0;
            using (var packageReader = new PackageArchiveReader(packagePath))
            {
                var verificationResult = await verifier.VerifySignaturesAsync(packageReader, verifierSettings, CancellationToken.None);

                var packageIdentity = packageReader.GetIdentity();

                logger.LogMinimal(Environment.NewLine + string.Format(CultureInfo.CurrentCulture,
                    Strings.VerifyCommand_VerifyingPackage,
                    packageIdentity.ToString()));
                string contentHash = packageReader.GetContentHash(CancellationToken.None);
                logger.LogMinimal(string.Format(CultureInfo.CurrentCulture,
                    Strings.VerifyCommand_ContentHash,
                    contentHash));
                logger.LogInformation($"{packagePath}{Environment.NewLine}");

                var logMessages = verificationResult.Results.SelectMany(p => p.Issues).ToList();

                //log issues first
                await logger.LogMessagesAsync(logMessages.Where(m => m.Level < LogLevel.Warning));

                if (logMessages.Any(m => m.Level >= LogLevel.Warning))
                {
                    logger.LogMinimal(Environment.NewLine);

                    IEnumerable<SignatureLog> warnsanderrors = logMessages.Where(m => m.Level >= LogLevel.Warning);

                    var errors = warnsanderrors.Count(m => m.Level == LogLevel.Error);
                    var warnings = warnsanderrors.Count() - errors;

                    logger.LogInformation(string.Format(CultureInfo.CurrentCulture, Strings.VerifyCommand_FinishedWithErrors, errors, warnings));

                    // log warnigs and errors at the end
                    await logger.LogMessagesAsync(warnsanderrors);

                    result = errors;
                }

                if (verificationResult.IsValid)
                {
                    logger.LogInformation(Environment.NewLine + string.Format(CultureInfo.CurrentCulture, Strings.VerifyCommand_Success, packageIdentity.ToString()));
                }
                else
                {
                    logger.LogMinimal(Environment.NewLine + Strings.VerifyCommand_Failed);
                }

                return result;
            }
        }

        private bool ShouldExecuteVerification(VerifyArgs args, Verification v)
        {
            return args.Verifications.Any(verification => verification == Verification.All || verification == v);
        }

        private bool IsSignatureVerifyCommandSupported()
        {
#if IS_DESKTOP
            if (RuntimeEnvironmentHelper.IsMono)
            {
                return false;
            }
#endif
            return true;
        }
    }
}