File: Signing\TrustedSigners\TrustedSignersProvider.cs
Web Access
Project: src\src\nuget-client\src\NuGet.Core\NuGet.Packaging\NuGet.Packaging.csproj (NuGet.Packaging)
// 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.

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using NuGet.Common;
using NuGet.Configuration;

namespace NuGet.Packaging.Signing
{
    public sealed class TrustedSignersProvider : ITrustedSignersProvider
    {
        private readonly ISettings _settings;

        public TrustedSignersProvider(ISettings settings)
        {
            _settings = settings ?? throw new ArgumentNullException(nameof(settings));
        }

        public IReadOnlyList<TrustedSignerItem> GetTrustedSigners()
        {
            var trustedSignersSection = _settings.GetSection(ConfigurationConstants.TrustedSigners);
            if (trustedSignersSection == null)
            {
                return Enumerable.Empty<TrustedSignerItem>().ToList();
            }

            return trustedSignersSection.Items.OfType<TrustedSignerItem>().ToList();
        }

        public void Remove(IReadOnlyList<TrustedSignerItem> trustedSigners)
        {
            if (trustedSigners == null || !trustedSigners.Any())
            {
                throw new ArgumentException(Strings.ArgumentCannotBeNullOrEmpty, nameof(trustedSigners));
            }

            foreach (var signer in trustedSigners)
            {
                try
                {
                    _settings.Remove(ConfigurationConstants.TrustedSigners, signer);
                }
                // An error means the item doesn't exist or is in a machine wide config, therefore just ignore it
                catch { }
            }

            _settings.SaveToDisk();
        }

        public void AddOrUpdateTrustedSigner(TrustedSignerItem trustedSigner)
        {
            if (trustedSigner == null)
            {
                throw new ArgumentNullException(nameof(trustedSigner));
            }

            _settings.AddOrUpdate(ConfigurationConstants.TrustedSigners, trustedSigner);

            _settings.SaveToDisk();
        }

        public static IReadOnlyList<TrustedSignerAllowListEntry> GetAllowListEntries(ISettings settings, ILogger logger)
        {
            if (settings == null)
            {
                throw new ArgumentNullException(nameof(settings));
            }

            if (logger == null)
            {
                throw new ArgumentNullException(nameof(logger));
            }

            var trustedSignersSection = settings.GetSection(ConfigurationConstants.TrustedSigners);
            if (trustedSignersSection == null)
            {
                return Enumerable.Empty<TrustedSignerAllowListEntry>().ToList();
            }

            // We will dedup certificates based on fingerprint and hash algorithm, therefore
            // the key to this lookup will be hashAlgorithm-fingerprint
            var certificateLookup = new Dictionary<string, CertificateEntryLookupEntry>();

            foreach (var item in trustedSignersSection.Items.OfType<TrustedSignerItem>())
            {
                var itemTarget = GetItemTarget(item, out var itemPlacement);

                foreach (var certificate in item.Certificates)
                {
                    ICollection<string>? owners = null;
                    if (itemTarget == VerificationTarget.Repository)
                    {
                        owners = new HashSet<string>(((RepositoryItem)item).Owners);
                    }

                    if (certificateLookup.TryGetValue(GetCertLookupKey(certificate), out var existingEntry))
                    {
                        if (existingEntry.Certificate.AllowUntrustedRoot != certificate.AllowUntrustedRoot)
                        {
                            // warn and take the most restrictive setting
                            logger.Log(new LogMessage(LogLevel.Warning,
                                string.Format(CultureInfo.CurrentCulture, Strings.ConflictingAllowUntrustedRoot, certificate.HashAlgorithm.ToString(), certificate.Fingerprint),
                                NuGetLogCode.NU3040));
                            existingEntry.Certificate.AllowUntrustedRoot = false;
                        }

                        existingEntry.Target |= itemTarget;
                        existingEntry.Placement |= itemPlacement;

                        if (owners != null)
                        {
                            if (existingEntry.Owners == null)
                            {
                                existingEntry.Owners = owners;
                            }
                            else
                            {
                                existingEntry.Owners.AddRange(owners);
                            }
                        }
                    }
                    else
                    {
                        certificateLookup.Add(GetCertLookupKey(certificate), new CertificateEntryLookupEntry(itemTarget, itemPlacement, certificate, owners));
                    }
                }
            }

            return certificateLookup.Select(e => e.Value.ToAllowListEntry()).ToList();
        }

        private static string GetCertLookupKey(CertificateItem certificate)
        {
            return $"{certificate.HashAlgorithm.ToString()}-{certificate.Fingerprint}";
        }

        private static VerificationTarget GetItemTarget(TrustedSignerItem item, out SignaturePlacement placement)
        {
            if (item is RepositoryItem)
            {
                placement = SignaturePlacement.Any;
                return VerificationTarget.Repository;
            }

            placement = SignaturePlacement.PrimarySignature;
            return VerificationTarget.Author;
        }

        private class CertificateEntryLookupEntry
        {
            public VerificationTarget Target { get; set; }

            public SignaturePlacement Placement { get; set; }

            public ICollection<string>? Owners { get; set; }

            public CertificateItem Certificate { get; }

            public CertificateEntryLookupEntry(VerificationTarget target, SignaturePlacement placement, CertificateItem certificate, ICollection<string>? owners = null)
            {
                Target = target;
                Placement = placement;
                Certificate = certificate ?? throw new ArgumentNullException(nameof(certificate));
                Owners = owners;
            }

            public TrustedSignerAllowListEntry ToAllowListEntry()
            {
                return new TrustedSignerAllowListEntry(Target, Placement, Certificate.Fingerprint, Certificate.HashAlgorithm, Certificate.AllowUntrustedRoot, Owners?.ToList());
            }
        }
    }
}