|
// 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.Linq;
using System.Threading;
using System.Threading.Tasks;
using NuGet.Common;
using NuGet.Configuration;
using NuGet.Packaging;
using NuGet.Packaging.Signing;
using NuGet.Protocol;
using NuGet.Protocol.Core.Types;
namespace NuGet.Commands
{
public sealed class TrustedSignerActionsProvider
{
private readonly ITrustedSignersProvider _trustedSignersProvider;
private readonly ILogger _logger;
/// <summary>
/// Internal SourceRepository useful for overriding on tests to get
/// mocked responses from the server
/// </summary>
internal SourceRepository ServiceIndexSourceRepository { get; set; }
public TrustedSignerActionsProvider(ITrustedSignersProvider trustedSignersProvider, ILogger logger)
{
_trustedSignersProvider = trustedSignersProvider ?? throw new ArgumentNullException(nameof(trustedSignersProvider));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <summary>
/// Refresh the certificates of a repository item with the ones the server is announcing.
/// </summary>
/// <param name="name">Name of the repository item to refresh.</param>
/// <param name="token">Cancellation token</param>
/// <exception cref="InvalidOperationException">When a repository item with the given name is not found.</exception>
public async Task SyncTrustedRepositoryAsync(string name, CancellationToken token)
{
if (string.IsNullOrEmpty(name))
{
throw new ArgumentException(Strings.ArgumentCannotBeNullOrEmpty, nameof(name));
}
var signers = _trustedSignersProvider.GetTrustedSigners();
foreach (var existingRepository in signers.OfType<RepositoryItem>())
{
if (string.Equals(existingRepository.Name, name, StringComparison.Ordinal))
{
var certificateItems = await GetCertificateItemsFromServiceIndexAsync(existingRepository.ServiceIndex, token);
existingRepository.Certificates.Clear();
existingRepository.Certificates.AddRange(certificateItems);
_trustedSignersProvider.AddOrUpdateTrustedSigner(existingRepository);
await _logger.LogAsync(LogLevel.Minimal, string.Format(CultureInfo.CurrentCulture, Strings.SuccessfullySynchronizedTrustedRepository, name));
return;
}
}
throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Strings.Error_TrustedRepositoryDoesNotExist, name));
}
/// <summary>
/// Adds a trusted signer item to the settings based a signed package.
/// </summary>
/// <param name="name">Name of the trusted signer.</param>
/// <param name="package">Package to read signature from.</param>
/// <param name="trustTarget">Signature to trust from package.</param>
/// <param name="allowUntrustedRoot">Specifies if allowUntrustedRoot should be set to true.</param>
/// <param name="owners">Trusted owners that should be set when trusting a repository.</param>
/// <param name="token">Cancellation token for async request</param>
public async Task AddTrustedSignerAsync(string name, ISignedPackageReader package, VerificationTarget trustTarget, bool allowUntrustedRoot, IEnumerable<string> owners, CancellationToken token)
{
if (package == null)
{
throw new ArgumentNullException(nameof(package));
}
if (string.IsNullOrEmpty(name))
{
throw new ArgumentException(Strings.ArgumentCannotBeNullOrEmpty, nameof(name));
}
if (!Enum.IsDefined(typeof(VerificationTarget), trustTarget) || (trustTarget != VerificationTarget.Repository && trustTarget != VerificationTarget.Author))
{
throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Strings.Error_UnsupportedTrustTarget, trustTarget.ToString()));
}
if (trustTarget == VerificationTarget.Author && owners != null && owners.Any())
{
throw new ArgumentException(Strings.Error_TrustedAuthorNoOwners);
}
token.ThrowIfCancellationRequested();
string v3ServiceIndex = null;
IRepositorySignature repositorySignature = null;
var trustingRepository = trustTarget.HasFlag(VerificationTarget.Repository);
var primarySignature = await package.GetPrimarySignatureAsync(token);
if (primarySignature == null)
{
throw new InvalidOperationException(Strings.Error_PackageNotSigned);
}
if (trustingRepository)
{
if (primarySignature.Type == SignatureType.Repository)
{
repositorySignature = primarySignature as RepositoryPrimarySignature;
}
else
{
var countersignature = RepositoryCountersignature.GetRepositoryCountersignature(primarySignature);
repositorySignature = countersignature ?? throw new InvalidOperationException(Strings.Error_RepoTrustExpectedRepoSignature);
}
v3ServiceIndex = repositorySignature.V3ServiceIndexUrl.AbsoluteUri;
}
ValidateNoExistingSigner(name, v3ServiceIndex, trustingRepository);
if (trustingRepository)
{
var certificateItem = GetCertificateItemForSignature(repositorySignature, allowUntrustedRoot);
_trustedSignersProvider.AddOrUpdateTrustedSigner(new RepositoryItem(name, v3ServiceIndex, CreateOwnersList(owners), certificateItem));
await _logger.LogAsync(LogLevel.Minimal, string.Format(CultureInfo.CurrentCulture, Strings.SuccessfullyAddedTrustedRepository, name));
}
else
{
if (primarySignature.Type != SignatureType.Author)
{
throw new InvalidOperationException(Strings.Error_AuthorTrustExpectedAuthorSignature);
}
var certificateItem = GetCertificateItemForSignature(primarySignature, allowUntrustedRoot);
_trustedSignersProvider.AddOrUpdateTrustedSigner(new AuthorItem(name, certificateItem));
await _logger.LogAsync(LogLevel.Minimal, string.Format(CultureInfo.CurrentCulture, Strings.SuccessfullyAddedTrustedAuthor, name));
}
}
/// <summary>
/// Updates the certificate list of a trusted signer by adding the given certificate.
/// If the signer does not exists it creates a new one.
/// </summary>
/// <remarks>This method defaults to adding a trusted author if the signer doesn't exist.</remarks>
/// <param name="name">Name of the trusted author.</param>
/// <param name="fingerprint">Fingerprint to be added to the certificate entry.</param>
/// <param name="hashAlgorithm">Hash algorithm used to calculate <paramref name="fingerprint"/>.</param>
/// <param name="allowUntrustedRoot">Specifies if allowUntrustedRoot should be set to true in the certificate entry.</param>
public void AddOrUpdateTrustedSigner(string name, string fingerprint, HashAlgorithmName hashAlgorithm, bool allowUntrustedRoot)
{
if (string.IsNullOrEmpty(name))
{
throw new ArgumentException(Strings.ArgumentCannotBeNullOrEmpty, nameof(name));
}
if (string.IsNullOrEmpty(fingerprint))
{
throw new ArgumentException(Strings.ArgumentCannotBeNullOrEmpty, nameof(fingerprint));
}
if (!Enum.IsDefined(typeof(HashAlgorithmName), hashAlgorithm) || hashAlgorithm == HashAlgorithmName.Unknown)
{
throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Strings.UnsupportedHashAlgorithm, hashAlgorithm.ToString()));
}
var certificateToAdd = new CertificateItem(fingerprint, hashAlgorithm, allowUntrustedRoot);
TrustedSignerItem signerToAdd = null;
var signers = _trustedSignersProvider.GetTrustedSigners();
foreach (var existingSigner in signers)
{
if (string.Equals(existingSigner.Name, name, StringComparison.Ordinal))
{
signerToAdd = existingSigner;
break;
}
}
string logMessage = null;
if (signerToAdd == null)
{
signerToAdd = new AuthorItem(name, certificateToAdd);
logMessage = Strings.SuccessfullyAddedTrustedAuthor;
}
else
{
signerToAdd.Certificates.Add(certificateToAdd);
logMessage = Strings.SuccessfullUpdatedTrustedSigner;
}
_trustedSignersProvider.AddOrUpdateTrustedSigner(signerToAdd);
_logger.Log(LogLevel.Minimal, string.Format(CultureInfo.CurrentCulture, logMessage, name));
}
/// <summary>
/// Adds a trusted repository with information from <paramref name="serviceIndex"/>
/// </summary>
/// <param name="name">Name of the trusted repository.</param>
/// <param name="serviceIndex">Service index of the trusted repository. Trusted certificates information will be gotten from here.</param>
/// <param name="owners">Owners to be trusted from the repository.</param>
/// <param name="token">Cancellation token</param>
public async Task AddTrustedRepositoryAsync(string name, Uri serviceIndex, IEnumerable<string> owners, CancellationToken token)
{
if (string.IsNullOrEmpty(name))
{
throw new ArgumentException(Strings.ArgumentCannotBeNullOrEmpty, nameof(name));
}
if (serviceIndex == null)
{
throw new ArgumentNullException(nameof(serviceIndex));
}
ValidateNoExistingSigner(name, serviceIndex.AbsoluteUri);
var certificateItems = await GetCertificateItemsFromServiceIndexAsync(serviceIndex.AbsoluteUri, token);
_trustedSignersProvider.AddOrUpdateTrustedSigner(
new RepositoryItem(name, serviceIndex.AbsoluteUri, CreateOwnersList(owners), certificateItems));
await _logger.LogAsync(LogLevel.Minimal, string.Format(CultureInfo.CurrentCulture, Strings.SuccessfullyAddedTrustedRepository, name));
}
private void ValidateNoExistingSigner(string name, string serviceIndex, bool validateServiceIndex = true)
{
var signers = _trustedSignersProvider.GetTrustedSigners();
foreach (var existingSigner in signers)
{
if (string.Equals(existingSigner.Name, name, StringComparison.Ordinal))
{
throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Strings.Error_TrustedSignerAlreadyExists, name));
}
if (validateServiceIndex && existingSigner is RepositoryItem repoItem && string.Equals(repoItem.ServiceIndex, serviceIndex, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Strings.Error_TrustedRepoAlreadyExists, serviceIndex));
}
}
}
private CertificateItem GetCertificateItemForSignature(ISignature signature, bool allowUntrustedRoot = false)
{
var defaultHashAlgorithm = HashAlgorithmName.SHA256;
var fingerprint = CertificateUtility.GetHashString(signature.SignerInfo.Certificate, defaultHashAlgorithm);
return new CertificateItem(fingerprint, defaultHashAlgorithm, allowUntrustedRoot);
}
private async Task<CertificateItem[]> GetCertificateItemsFromServiceIndexAsync(string serviceIndex, CancellationToken token)
{
if (string.IsNullOrEmpty(serviceIndex))
{
throw new ArgumentException(Strings.ArgumentCannotBeNullOrEmpty, nameof(serviceIndex));
}
if (ServiceIndexSourceRepository == null)
{
var packageSource = new PackageSource(serviceIndex);
ServiceIndexSourceRepository = new SourceRepository(packageSource, Repository.Provider.GetCoreV3());
}
var repositorySignatureResource = await ServiceIndexSourceRepository.GetResourceAsync<RepositorySignatureResource>(token);
if (repositorySignatureResource == null)
{
throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Strings.Error_InvalidCertificateInformationFromServer, serviceIndex));
}
if (repositorySignatureResource.RepositoryCertificateInfos == null || !repositorySignatureResource.RepositoryCertificateInfos.Any())
{
throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Strings.Error_EmptyCertificateListInRepository, serviceIndex));
}
var certs = new List<CertificateItem>();
foreach (var certInfo in repositorySignatureResource.RepositoryCertificateInfos)
{
foreach (var hashAlgorithm in SigningSpecifications.V1.AllowedHashAlgorithms)
{
var fingerprint = certInfo.Fingerprints[hashAlgorithm.ConvertToOidString()];
if (!string.IsNullOrEmpty(fingerprint))
{
certs.Add(new CertificateItem(fingerprint, hashAlgorithm));
}
}
}
return certs.ToArray();
}
private string CreateOwnersList(IEnumerable<string> owners)
{
if (owners != null && owners.Any())
{
return string.Join(OwnersItem.OwnersListSeparator.ToString(CultureInfo.CurrentCulture), owners);
}
return null;
}
}
}
|