From af97b414ee7819cb5bf3bcb8d3f81909f7a8cab7 Mon Sep 17 00:00:00 2001 From: MARPAULT Julien Date: Mon, 8 Jul 2024 10:52:35 +0200 Subject: [PATCH] - Update to .Net 8 - Update dependencies - C# 12 code style --- .../KeyVaultSignatureProvider.cs | 352 ++++++++-------- .../NuGetKeyVaultSignTool.Core.csproj | 19 +- NuGetKeyVaultSignTool.Core/NuGetLogger.cs | 139 +++---- NuGetKeyVaultSignTool.Core/SignCommand.cs | 196 +++++---- NuGetKeyVaultSignTool.Core/VerifyCommand.cs | 115 +++--- .../AccessTokenCredential.cs | 46 +-- .../NuGetKeyVaultSignTool.csproj | 13 +- NuGetKeyVaultSignTool/Program.cs | 377 +++++++++--------- 8 files changed, 583 insertions(+), 674 deletions(-) diff --git a/NuGetKeyVaultSignTool.Core/KeyVaultSignatureProvider.cs b/NuGetKeyVaultSignTool.Core/KeyVaultSignatureProvider.cs index 5a9b63a..ac5a646 100644 --- a/NuGetKeyVaultSignTool.Core/KeyVaultSignatureProvider.cs +++ b/NuGetKeyVaultSignTool.Core/KeyVaultSignatureProvider.cs @@ -1,6 +1,7 @@ -using System; +using NuGet.Common; +using NuGet.Packaging.Signing; +using System; using System.Collections.Generic; -using System.Linq; using System.Reflection; using System.Security.Cryptography; using System.Security.Cryptography.Pkcs; @@ -8,233 +9,198 @@ using System.Text; using System.Threading; using System.Threading.Tasks; -using NuGet.Common; -using NuGet.Packaging.Signing; -namespace NuGetKeyVaultSignTool +namespace NuGetKeyVaultSignTool; + +internal class KeyVaultSignatureProvider(RSA provider, ITimestampProvider timestampProvider) : ISignatureProvider { - class KeyVaultSignatureProvider : ISignatureProvider + // Occurs when SignedCms.ComputeSignature cannot read the certificate private key + // "Invalid provider type specified." (INVALID_PROVIDER_TYPE) + private const int INVALID_PROVIDER_TYPE_HRESULT = unchecked((int)0x80090014); + + private readonly ITimestampProvider timestampProvider = timestampProvider ?? throw new ArgumentNullException(nameof(timestampProvider)); + + public async Task CreatePrimarySignatureAsync(SignPackageRequest request, SignatureContent signatureContent, ILogger logger, CancellationToken token) { - // Occurs when SignedCms.ComputeSignature cannot read the certificate private key - // "Invalid provider type specified." (INVALID_PROVIDER_TYPE) - private const int INVALID_PROVIDER_TYPE_HRESULT = unchecked((int)0x80090014); + ArgumentNullException.ThrowIfNull(request); - private readonly RSA provider; - private readonly ITimestampProvider timestampProvider; + ArgumentNullException.ThrowIfNull(signatureContent); - public KeyVaultSignatureProvider(RSA provider, ITimestampProvider timestampProvider) - { - this.provider = provider; - this.timestampProvider = timestampProvider ?? throw new ArgumentNullException(nameof(timestampProvider)); - } + ArgumentNullException.ThrowIfNull(logger); - public async Task CreatePrimarySignatureAsync(SignPackageRequest request, SignatureContent signatureContent, ILogger logger, CancellationToken token) - { - if (request == null) - { - throw new ArgumentNullException(nameof(request)); - } + logger.LogInformation($"{nameof(CreatePrimarySignatureAsync)}: Creating Primary signature"); + PrimarySignature authorSignature = CreateKeyVaultPrimarySignature(request, signatureContent, logger); + logger.LogInformation($"{nameof(CreatePrimarySignatureAsync)}: Primary signature completed"); + + logger.LogInformation($"{nameof(CreatePrimarySignatureAsync)}: Timestamp primary signature"); + PrimarySignature timestamped = await TimestampPrimarySignatureAsync(request, logger, authorSignature, token); + logger.LogInformation($"{nameof(CreatePrimarySignatureAsync)}: Timestamp completed"); - if (signatureContent == null) - { - throw new ArgumentNullException(nameof(signatureContent)); - } + return timestamped; + } - if (logger == null) - { - throw new ArgumentNullException(nameof(logger)); - } + public async Task CreateRepositoryCountersignatureAsync(RepositorySignPackageRequest request, PrimarySignature primarySignature, ILogger logger, CancellationToken token) + { + ArgumentNullException.ThrowIfNull(request); - logger.LogInformation($"{nameof(CreatePrimarySignatureAsync)}: Creating Primary signature"); - var authorSignature = CreateKeyVaultPrimarySignature(request, signatureContent, logger); - logger.LogInformation($"{nameof(CreatePrimarySignatureAsync)}: Primary signature completed"); + ArgumentNullException.ThrowIfNull(primarySignature); - logger.LogInformation($"{nameof(CreatePrimarySignatureAsync)}: Timestamp primary signature"); - var timestamped = await TimestampPrimarySignatureAsync(request, logger, authorSignature, token); - logger.LogInformation($"{nameof(CreatePrimarySignatureAsync)}: Timestamp completed"); + ArgumentNullException.ThrowIfNull(logger); - return timestamped; - } + token.ThrowIfCancellationRequested(); - public async Task CreateRepositoryCountersignatureAsync(RepositorySignPackageRequest request, PrimarySignature primarySignature, ILogger logger, CancellationToken token) + MethodInfo getter = typeof(SignPackageRequest).GetProperty("Chain", BindingFlags.Instance | BindingFlags.NonPublic) + .GetGetMethod(true); + + IReadOnlyList certs = (IReadOnlyList)getter.Invoke(request, null); + + CmsSigner cmsSigner = CreateCmsSigner(request, certs, logger); + + logger.LogInformation($"{nameof(CreateRepositoryCountersignatureAsync)}: Creating Counter signature"); + PrimarySignature signature = CreateKeyVaultRepositoryCountersignature(cmsSigner, request, primarySignature); + logger.LogInformation($"{nameof(CreateRepositoryCountersignatureAsync)}: Counter signature completed"); + if(timestampProvider == null) { - if (request == null) - { - throw new ArgumentNullException(nameof(request)); - } - - if (primarySignature == null) - { - throw new ArgumentNullException(nameof(primarySignature)); - } - - if (logger == null) - { - throw new ArgumentNullException(nameof(logger)); - } - - token.ThrowIfCancellationRequested(); - - var getter = typeof(SignPackageRequest).GetProperty("Chain", BindingFlags.Instance | BindingFlags.NonPublic) - .GetGetMethod(true); - - var certs = (IReadOnlyList)getter.Invoke(request, null); - - var cmsSigner = CreateCmsSigner(request, certs, logger); - - logger.LogInformation($"{nameof(CreateRepositoryCountersignatureAsync)}: Creating Counter signature"); - var signature = CreateKeyVaultRepositoryCountersignature(cmsSigner, request, primarySignature); - logger.LogInformation($"{nameof(CreateRepositoryCountersignatureAsync)}: Counter signature completed"); - if (timestampProvider == null) - { - return signature; - } - else - { - logger.LogInformation($"{nameof(CreateRepositoryCountersignatureAsync)}: Timestamp Counter signature"); - var timestamped = await TimestampRepositoryCountersignatureAsync(request, logger, signature, token).ConfigureAwait(false); - logger.LogInformation($"{nameof(CreateRepositoryCountersignatureAsync)}: Timestamp completed"); - return timestamped; - } + return signature; } - - PrimarySignature CreateKeyVaultRepositoryCountersignature(CmsSigner cmsSigner, SignPackageRequest request, PrimarySignature primarySignature) + else { - var cms = new SignedCms(); - cms.Decode(primarySignature.GetBytes()); - - try - { - cms.SignerInfos[0].ComputeCounterSignature(cmsSigner); - } - catch (CryptographicException ex) when (ex.HResult == INVALID_PROVIDER_TYPE_HRESULT) - { - var exceptionBuilder = new StringBuilder(); - exceptionBuilder.AppendLine("Invalid provider type"); - exceptionBuilder.AppendLine(CertificateUtility.X509Certificate2ToString(request.Certificate, NuGet.Common.HashAlgorithmName.SHA256)); - - throw new SignatureException(NuGetLogCode.NU3001, exceptionBuilder.ToString()); - } - - return PrimarySignature.Load(cms); + logger.LogInformation($"{nameof(CreateRepositoryCountersignatureAsync)}: Timestamp Counter signature"); + PrimarySignature timestamped = await TimestampRepositoryCountersignatureAsync(request, logger, signature, token).ConfigureAwait(false); + logger.LogInformation($"{nameof(CreateRepositoryCountersignatureAsync)}: Timestamp completed"); + return timestamped; } + } - PrimarySignature CreateKeyVaultPrimarySignature(SignPackageRequest request, SignatureContent signatureContent, ILogger logger) + private static PrimarySignature CreateKeyVaultRepositoryCountersignature(CmsSigner cmsSigner, SignPackageRequest request, PrimarySignature primarySignature) + { + SignedCms cms = new(); + cms.Decode(primarySignature.GetBytes()); + + try + { + cms.SignerInfos[0].ComputeCounterSignature(cmsSigner); + } + catch(CryptographicException ex) when(ex.HResult == INVALID_PROVIDER_TYPE_HRESULT) { - // Get the chain + StringBuilder exceptionBuilder = new(); + exceptionBuilder.AppendLine("Invalid provider type"); + exceptionBuilder.AppendLine(CertificateUtility.X509Certificate2ToString(request.Certificate, NuGet.Common.HashAlgorithmName.SHA256)); - var getter = typeof(SignPackageRequest).GetProperty("Chain", BindingFlags.Instance | BindingFlags.NonPublic) - .GetGetMethod(true); + throw new SignatureException(NuGetLogCode.NU3001, exceptionBuilder.ToString()); + } + + return PrimarySignature.Load(cms); + } - var certs = (IReadOnlyList)getter.Invoke(request, null); + private PrimarySignature CreateKeyVaultPrimarySignature(SignPackageRequest request, SignatureContent signatureContent, ILogger logger) + { + // Get the chain + MethodInfo getter = typeof(SignPackageRequest).GetProperty("Chain", BindingFlags.Instance | BindingFlags.NonPublic) + .GetGetMethod(true); - var cmsSigner = CreateCmsSigner(request, certs, logger); + IReadOnlyList certs = (IReadOnlyList)getter.Invoke(request, null); - var contentInfo = new ContentInfo(signatureContent.GetBytes()); - var cms = new SignedCms(contentInfo); + CmsSigner cmsSigner = CreateCmsSigner(request, certs, logger); - try - { - cms.ComputeSignature(cmsSigner, false); // silent is false to ensure PIN prompts appear if CNG/CAPI requires it - } - catch (CryptographicException ex) when (ex.HResult == INVALID_PROVIDER_TYPE_HRESULT) - { - var exceptionBuilder = new StringBuilder(); - exceptionBuilder.AppendLine("Invalid provider type"); - exceptionBuilder.AppendLine(CertificateUtility.X509Certificate2ToString(request.Certificate, NuGet.Common.HashAlgorithmName.SHA256)); + ContentInfo contentInfo = new(signatureContent.GetBytes()); + SignedCms cms = new(contentInfo); - throw new SignatureException(NuGetLogCode.NU3001, exceptionBuilder.ToString()); - } + try + { + cms.ComputeSignature(cmsSigner, false); // silent is false to ensure PIN prompts appear if CNG/CAPI requires it + } + catch(CryptographicException ex) when(ex.HResult == INVALID_PROVIDER_TYPE_HRESULT) + { + StringBuilder exceptionBuilder = new(); + exceptionBuilder.AppendLine("Invalid provider type"); + exceptionBuilder.AppendLine(CertificateUtility.X509Certificate2ToString(request.Certificate, NuGet.Common.HashAlgorithmName.SHA256)); - return PrimarySignature.Load(cms); + throw new SignatureException(NuGetLogCode.NU3001, exceptionBuilder.ToString()); } - CmsSigner CreateCmsSigner(SignPackageRequest request, IReadOnlyList chain, ILogger logger) + return PrimarySignature.Load(cms); + } + + private CmsSigner CreateCmsSigner(SignPackageRequest request, IReadOnlyList chain, ILogger logger) + { + ArgumentNullException.ThrowIfNull(request); + + ArgumentNullException.ThrowIfNull(logger); + + // Subject Key Identifier (SKI) is smaller and less prone to accidental matching than issuer and serial + // number. However, to ensure cross-platform verification, SKI should only be used if the certificate + // has the SKI extension attribute. + CmsSigner signer; + + if(request.Certificate.Extensions[Oids.SubjectKeyIdentifier] == null) { - if (request == null) - { - throw new ArgumentNullException(nameof(request)); - } - - if (logger == null) - { - throw new ArgumentNullException(nameof(logger)); - } - - // Subject Key Identifier (SKI) is smaller and less prone to accidental matching than issuer and serial - // number. However, to ensure cross-platform verification, SKI should only be used if the certificate - // has the SKI extension attribute. - CmsSigner signer; - - if (request.Certificate.Extensions[Oids.SubjectKeyIdentifier] == null) - { - signer = new CmsSigner(SubjectIdentifierType.IssuerAndSerialNumber, request.Certificate, provider); - } - else - { - signer = new CmsSigner(SubjectIdentifierType.SubjectKeyIdentifier, request.Certificate, provider); - } - - foreach (var certificate in chain) - { - signer.Certificates.Add(certificate); - } - - CryptographicAttributeObjectCollection attributes; - - if (request.SignatureType == SignatureType.Repository) - { - attributes = SigningUtility.CreateSignedAttributes((RepositorySignPackageRequest)request, chain); - } - else - { - attributes = SigningUtility.CreateSignedAttributes(request, chain); - } - - foreach (var attribute in attributes) - { - signer.SignedAttributes.Add(attribute); - } - - // We built the chain ourselves and added certificates. - // Passing any other value here would trigger another chain build - // and possibly add duplicate certs to the collection. - signer.IncludeOption = X509IncludeOption.None; - signer.DigestAlgorithm = request.SignatureHashAlgorithm.ConvertToOid(); - - return signer; + signer = new CmsSigner(SubjectIdentifierType.IssuerAndSerialNumber, request.Certificate, provider); + } + else + { + signer = new CmsSigner(SubjectIdentifierType.SubjectKeyIdentifier, request.Certificate, provider); } - Task TimestampPrimarySignatureAsync(SignPackageRequest request, ILogger logger, PrimarySignature signature, CancellationToken token) + foreach(X509Certificate2 certificate in chain) { - var signatureValue = signature.GetSignatureValue(); - var messageHash = request.TimestampHashAlgorithm.ComputeHash(signatureValue); + signer.Certificates.Add(certificate); + } - var timestampRequest = new TimestampRequest( - signingSpecifications: SigningSpecifications.V1, - hashedMessage: messageHash, - hashAlgorithm: request.TimestampHashAlgorithm, - target: SignaturePlacement.PrimarySignature - ); + CryptographicAttributeObjectCollection attributes; - return timestampProvider.TimestampSignatureAsync(signature, timestampRequest, logger, token); + if(request.SignatureType == SignatureType.Repository) + { + attributes = SigningUtility.CreateSignedAttributes((RepositorySignPackageRequest)request, chain); + } + else + { + attributes = SigningUtility.CreateSignedAttributes(request, chain); } - private Task TimestampRepositoryCountersignatureAsync(SignPackageRequest request, ILogger logger, PrimarySignature primarySignature, CancellationToken token) + foreach(CryptographicAttributeObject attribute in attributes) { - var repositoryCountersignature = RepositoryCountersignature.GetRepositoryCountersignature(primarySignature); - var signatureValue = repositoryCountersignature.GetSignatureValue(); - var messageHash = request.TimestampHashAlgorithm.ComputeHash(signatureValue); - - var timestampRequest = new TimestampRequest( - signingSpecifications: SigningSpecifications.V1, - hashedMessage: messageHash, - hashAlgorithm: request.TimestampHashAlgorithm, - target: SignaturePlacement.Countersignature - ); - - return timestampProvider.TimestampSignatureAsync(primarySignature, timestampRequest, logger, token); + signer.SignedAttributes.Add(attribute); } + + // We built the chain ourselves and added certificates. + // Passing any other value here would trigger another chain build + // and possibly add duplicate certs to the collection. + signer.IncludeOption = X509IncludeOption.None; + signer.DigestAlgorithm = request.SignatureHashAlgorithm.ConvertToOid(); + + return signer; + } + + private Task TimestampPrimarySignatureAsync(SignPackageRequest request, ILogger logger, PrimarySignature signature, CancellationToken token) + { + byte[] signatureValue = signature.GetSignatureValue(); + byte[] messageHash = request.TimestampHashAlgorithm.ComputeHash(signatureValue); + + TimestampRequest timestampRequest = new( + signingSpecifications: SigningSpecifications.V1, + hashedMessage: messageHash, + hashAlgorithm: request.TimestampHashAlgorithm, + target: SignaturePlacement.PrimarySignature + ); + + return timestampProvider.TimestampSignatureAsync(signature, timestampRequest, logger, token); + } + + private Task TimestampRepositoryCountersignatureAsync(SignPackageRequest request, ILogger logger, PrimarySignature primarySignature, CancellationToken token) + { + RepositoryCountersignature repositoryCountersignature = RepositoryCountersignature.GetRepositoryCountersignature(primarySignature); + byte[] signatureValue = repositoryCountersignature.GetSignatureValue(); + byte[] messageHash = request.TimestampHashAlgorithm.ComputeHash(signatureValue); + + TimestampRequest timestampRequest = new( + signingSpecifications: SigningSpecifications.V1, + hashedMessage: messageHash, + hashAlgorithm: request.TimestampHashAlgorithm, + target: SignaturePlacement.Countersignature + ); + + return timestampProvider.TimestampSignatureAsync(primarySignature, timestampRequest, logger, token); } -} +} \ No newline at end of file diff --git a/NuGetKeyVaultSignTool.Core/NuGetKeyVaultSignTool.Core.csproj b/NuGetKeyVaultSignTool.Core/NuGetKeyVaultSignTool.Core.csproj index 82cb6ea..792038a 100644 --- a/NuGetKeyVaultSignTool.Core/NuGetKeyVaultSignTool.Core.csproj +++ b/NuGetKeyVaultSignTool.Core/NuGetKeyVaultSignTool.Core.csproj @@ -1,21 +1,26 @@  - netcoreapp3.1;net5.0 + net8.0 - - - - + + + + - + - + + + + + + diff --git a/NuGetKeyVaultSignTool.Core/NuGetLogger.cs b/NuGetKeyVaultSignTool.Core/NuGetLogger.cs index 5e9cee7..1090ce8 100644 --- a/NuGetKeyVaultSignTool.Core/NuGetLogger.cs +++ b/NuGetKeyVaultSignTool.Core/NuGetLogger.cs @@ -1,96 +1,83 @@ -using System; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using NuGet.Common; +using System.Threading.Tasks; using ILogger = Microsoft.Extensions.Logging.ILogger; using LogLevel = NuGet.Common.LogLevel; -namespace NuGetKeyVaultSignTool -{ - class NuGetLogger : NuGet.Common.ILogger - { - readonly ILogger logger; - private readonly string fileName; +namespace NuGetKeyVaultSignTool; - public NuGetLogger(ILogger logger, string fileName) - { - this.logger = logger; - this.fileName = fileName; - } +internal class NuGetLogger(ILogger logger, string fileName) : NuGet.Common.ILogger +{ + public void Log(NuGet.Common.LogLevel level, string data) + { + logger.Log(ConvertLevel(level), "NuGet [{fileName}]: {data}", fileName, data); + } - public void Log(NuGet.Common.LogLevel level, string data) - { - logger.Log(ConvertLevel(level), $"NuGet [{fileName}]: {data}"); - } + public void Log(ILogMessage message) + { + Log(message.Level, message.FormatWithCode()); + } - public void Log(ILogMessage message) - { - Log(message.Level, message.FormatWithCode()); - } + public Task LogAsync(NuGet.Common.LogLevel level, string data) + { + Log(level, data); + return Task.CompletedTask; + } - public Task LogAsync(NuGet.Common.LogLevel level, string data) - { - Log(level, data); - return Task.CompletedTask; - } + public Task LogAsync(ILogMessage message) + { + Log(message.Level, message.FormatWithCode()); - public Task LogAsync(ILogMessage message) - { - Log(message.Level, message.FormatWithCode()); + return Task.CompletedTask; + } - return Task.CompletedTask; - } + public void LogDebug(string data) + { + Log(LogLevel.Debug, data); + } - public void LogDebug(string data) - { - Log(LogLevel.Debug, data); - } + public void LogError(string data) + { + Log(LogLevel.Error, data); + } - public void LogError(string data) - { - Log(LogLevel.Error, data); - } + public void LogInformation(string data) + { + Log(LogLevel.Information, data); + } - public void LogInformation(string data) - { - Log(LogLevel.Information, data); - } + public void LogInformationSummary(string data) + { + Log(LogLevel.Information, data); + } - public void LogInformationSummary(string data) - { - Log(LogLevel.Information, data); - } + public void LogMinimal(string data) + { + Log(LogLevel.Minimal, data); + } - public void LogMinimal(string data) - { - Log(LogLevel.Minimal, data); - } + public void LogVerbose(string data) + { + Log(LogLevel.Verbose, data); + } - public void LogVerbose(string data) - { - Log(LogLevel.Verbose, data); - } + public void LogWarning(string data) + { + Log(LogLevel.Warning, data); + } - public void LogWarning(string data) + private static Microsoft.Extensions.Logging.LogLevel ConvertLevel(LogLevel level) + { + return level switch { - Log(LogLevel.Warning, data); - } - - static Microsoft.Extensions.Logging.LogLevel ConvertLevel(LogLevel level) - { - return level switch - { - LogLevel.Debug => Microsoft.Extensions.Logging.LogLevel.Debug, - LogLevel.Verbose => Microsoft.Extensions.Logging.LogLevel.Trace, - LogLevel.Information => Microsoft.Extensions.Logging.LogLevel.Information, - LogLevel.Minimal => Microsoft.Extensions.Logging.LogLevel.Information, - LogLevel.Warning => Microsoft.Extensions.Logging.LogLevel.Warning, - LogLevel.Error => Microsoft.Extensions.Logging.LogLevel.Error, - - _ => Microsoft.Extensions.Logging.LogLevel.Information, - }; - } - - + LogLevel.Debug => Microsoft.Extensions.Logging.LogLevel.Debug, + LogLevel.Verbose => Microsoft.Extensions.Logging.LogLevel.Trace, + LogLevel.Information => Microsoft.Extensions.Logging.LogLevel.Information, + LogLevel.Minimal => Microsoft.Extensions.Logging.LogLevel.Information, + LogLevel.Warning => Microsoft.Extensions.Logging.LogLevel.Warning, + LogLevel.Error => Microsoft.Extensions.Logging.LogLevel.Error, + + _ => Microsoft.Extensions.Logging.LogLevel.Information, + }; } } \ No newline at end of file diff --git a/NuGetKeyVaultSignTool.Core/SignCommand.cs b/NuGetKeyVaultSignTool.Core/SignCommand.cs index c7e4d43..a31b155 100644 --- a/NuGetKeyVaultSignTool.Core/SignCommand.cs +++ b/NuGetKeyVaultSignTool.Core/SignCommand.cs @@ -1,136 +1,124 @@ -using System; +using Azure.Core; +using Azure.Security.KeyVault.Certificates; +using Microsoft.Extensions.Logging; +using NuGet.Common; +using NuGet.Packaging.Signing; +using NuGet.Protocol; +using RSAKeyVaultProvider; +using System; using System.Collections.Generic; using System.IO; using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using NuGet.Common; -using NuGet.Packaging.Signing; using ILogger = Microsoft.Extensions.Logging.ILogger; -using NuGet.Protocol; -using Azure.Core; -using Azure.Security.KeyVault.Certificates; -using RSAKeyVaultProvider; -namespace NuGetKeyVaultSignTool +namespace NuGetKeyVaultSignTool; + +public class SignCommand(ILogger logger) { - public class SignCommand + public async Task SignAsync(string packagePath, + string outputPath, + string timestampUrl, + HashAlgorithmName signatureHashAlgorithm, + HashAlgorithmName timestampHashAlgorithm, + SignatureType signatureType, + bool overwrite, + Uri v3ServiceIndexUrl, + IReadOnlyList packageOwners, + string keyVaultCertificateName, + Uri keyVaultUrl, + TokenCredential credential, + CancellationToken cancellationToken = default) { - readonly ILogger logger; - - public SignCommand(ILogger logger) - { - this.logger = logger; - } - - public async Task SignAsync(string packagePath, - string outputPath, - string timestampUrl, - HashAlgorithmName signatureHashAlgorithm, - HashAlgorithmName timestampHashAlgorithm, - SignatureType signatureType, - bool overwrite, - Uri v3ServiceIndexUrl, - IReadOnlyList packageOwners, - string keyVaultCertificateName, - Uri keyVaultUrl, - TokenCredential credential, - CancellationToken cancellationToken = default) - { - - - var client = new CertificateClient(keyVaultUrl, credential); - // We call this here to verify it's a valid cert - // It also implicitly validates the access token or credentials - var kvcert = await client.GetCertificateAsync(keyVaultCertificateName, cancellationToken) - .ConfigureAwait(false); - var publicCertificate = new X509Certificate2(kvcert.Value.Cer); + CertificateClient client = new(keyVaultUrl, credential); + // We call this here to verify it's a valid cert + // It also implicitly validates the access token or credentials + Azure.Response kvcert = await client.GetCertificateAsync(keyVaultCertificateName, cancellationToken) + .ConfigureAwait(false); + X509Certificate2 publicCertificate = new(kvcert.Value.Cer); + System.Security.Cryptography.RSA rsa = RSAFactory.Create(credential, kvcert.Value.KeyId, publicCertificate); - var rsa = RSAFactory.Create(credential, kvcert.Value.KeyId, publicCertificate); + return await SignAsync(packagePath, outputPath, timestampUrl, v3ServiceIndexUrl, packageOwners, signatureType, signatureHashAlgorithm, timestampHashAlgorithm, overwrite, publicCertificate, rsa, cancellationToken); + } - return await SignAsync(packagePath, outputPath, timestampUrl, v3ServiceIndexUrl, packageOwners, signatureType, signatureHashAlgorithm, timestampHashAlgorithm, overwrite, publicCertificate, rsa, cancellationToken); - } + public async Task SignAsync(string packagePath, string outputPath, string timestampUrl, Uri v3ServiceIndex, IReadOnlyList packageOwners, + SignatureType signatureType, HashAlgorithmName signatureHashAlgorithm, HashAlgorithmName timestampHashAlgorithm, + bool overwrite, X509Certificate2 publicCertificate, System.Security.Cryptography.RSA rsa, CancellationToken cancellationToken = default) + { + bool inPlaceSigning = string.Equals(packagePath, outputPath); + bool usingWildCards = packagePath.Contains('*') || packagePath.Contains('?'); + IEnumerable packagesToSign = LocalFolderUtility.ResolvePackageFromPath(packagePath); - public async Task SignAsync(string packagePath, string outputPath, string timestampUrl, Uri v3ServiceIndex, IReadOnlyList packageOwners, - SignatureType signatureType, HashAlgorithmName signatureHashAlgorithm, HashAlgorithmName timestampHashAlgorithm, - bool overwrite, X509Certificate2 publicCertificate, System.Security.Cryptography.RSA rsa, CancellationToken cancellationToken = default) - { - bool inPlaceSigning = String.Equals(packagePath, outputPath); - bool usingWildCards = packagePath.Contains('*') || packagePath.Contains('?'); - var packagesToSign = LocalFolderUtility.ResolvePackageFromPath(packagePath); - - var signatureProvider = new KeyVaultSignatureProvider(rsa, new Rfc3161TimestampProvider(new Uri(timestampUrl))); + KeyVaultSignatureProvider signatureProvider = new(rsa, new Rfc3161TimestampProvider(new Uri(timestampUrl))); - SignPackageRequest request = null; + SignPackageRequest request = null; - if (signatureType == SignatureType.Author) - request = new AuthorSignPackageRequest(publicCertificate, signatureHashAlgorithm, timestampHashAlgorithm); - else if (signatureType == SignatureType.Repository) - request = new RepositorySignPackageRequest(publicCertificate, signatureHashAlgorithm, timestampHashAlgorithm, v3ServiceIndex, packageOwners); - else throw new ArgumentOutOfRangeException(nameof(signatureType)); + if(signatureType == SignatureType.Author) + request = new AuthorSignPackageRequest(publicCertificate, signatureHashAlgorithm, timestampHashAlgorithm); + else if(signatureType == SignatureType.Repository) + request = new RepositorySignPackageRequest(publicCertificate, signatureHashAlgorithm, timestampHashAlgorithm, v3ServiceIndex, packageOwners); + else throw new ArgumentOutOfRangeException(nameof(signatureType)); - string originalPackageCopyPath = null; - foreach (var package in packagesToSign) + string originalPackageCopyPath = null; + foreach(string package in packagesToSign) + { + cancellationToken.ThrowIfCancellationRequested(); + logger.LogInformation("{SignAsync} [{package}]: Begin Signing {fileName}", nameof(SignAsync), package, Path.GetFileName(package)); + try { - cancellationToken.ThrowIfCancellationRequested(); - logger.LogInformation($"{nameof(SignAsync)} [{package}]: Begin Signing {Path.GetFileName(package)}"); - try + originalPackageCopyPath = CopyPackage(package); + string signedPackagePath = outputPath; + if(inPlaceSigning) { - originalPackageCopyPath = CopyPackage(package); - string signedPackagePath = outputPath; - if (inPlaceSigning) - { - signedPackagePath = package; - } - else if (usingWildCards) + signedPackagePath = package; + } + else if(usingWildCards) + { + string packageFile = Path.GetFileName(package); + string pathName = Path.GetDirectoryName(outputPath + Path.DirectorySeparatorChar); + if(!Directory.Exists(pathName)) { - var packageFile = Path.GetFileName(package); - string pathName = Path.GetDirectoryName(outputPath + Path.DirectorySeparatorChar); - if (!Directory.Exists(pathName)) - { - Directory.CreateDirectory(pathName); - } - signedPackagePath = pathName + Path.DirectorySeparatorChar + packageFile; + Directory.CreateDirectory(pathName); } - using var options = SigningOptions.CreateFromFilePaths(originalPackageCopyPath, signedPackagePath, overwrite, signatureProvider, new NuGetLogger(logger, package)); - await SigningUtility.SignAsync(options, request, cancellationToken); + signedPackagePath = pathName + Path.DirectorySeparatorChar + packageFile; } - catch (Exception e) + using SigningOptions options = SigningOptions.CreateFromFilePaths(originalPackageCopyPath, signedPackagePath, overwrite, signatureProvider, new NuGetLogger(logger, package)); + await SigningUtility.SignAsync(options, request, cancellationToken); + } + catch(Exception e) + { + logger.LogError(e, "{errorMessage}", e.Message); + return false; + } + finally + { + try { - logger.LogError(e, e.Message); - return false; + FileUtility.Delete(originalPackageCopyPath); } - finally + catch { - try - { - FileUtility.Delete(originalPackageCopyPath); - } - catch - { - } - - logger.LogInformation($"{nameof(SignAsync)} [{package}]: End Signing {Path.GetFileName(package)}"); } - } - return true; + logger.LogInformation("{method} [{package}]: End Signing {fileName}", nameof(SignAsync), package, Path.GetFileName(package)); + } } + return true; + } - static string CopyPackage(string sourceFilePath) - { - var destFilePath = Path.GetTempFileName(); - File.Copy(sourceFilePath, destFilePath, overwrite: true); + private static string CopyPackage(string sourceFilePath) + { + string destFilePath = Path.GetTempFileName(); + File.Copy(sourceFilePath, destFilePath, overwrite: true); - return destFilePath; - } + return destFilePath; + } - static void OverwritePackage(string sourceFilePath, string destFilePath) - { - File.Copy(sourceFilePath, destFilePath, overwrite: true); - } + private static void OverwritePackage(string sourceFilePath, string destFilePath) + { + File.Copy(sourceFilePath, destFilePath, overwrite: true); } -} +} \ No newline at end of file diff --git a/NuGetKeyVaultSignTool.Core/VerifyCommand.cs b/NuGetKeyVaultSignTool.Core/VerifyCommand.cs index a330cb7..23828c7 100644 --- a/NuGetKeyVaultSignTool.Core/VerifyCommand.cs +++ b/NuGetKeyVaultSignTool.Core/VerifyCommand.cs @@ -1,94 +1,73 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.IO.Compression; +using Microsoft.Extensions.Logging; +using NuGet.Packaging; +using NuGet.Packaging.Signing; +using NuGet.Protocol; +using System; using System.Linq; -using System.Net.Http; using System.Text; using System.Threading; using System.Threading.Tasks; -using NuGet.Common; -using NuGet.Packaging; -using NuGet.Packaging.Signing; -using Microsoft.Extensions.Logging; using ILogger = Microsoft.Extensions.Logging.ILogger; -using NuGet.Protocol; -namespace NuGetKeyVaultSignTool +namespace NuGetKeyVaultSignTool; + +public class VerifyCommand(ILogger logger) { - public class VerifyCommand + public async Task VerifyAsync(string file, StringBuilder buffer) { - readonly ILogger logger; + ArgumentNullException.ThrowIfNull(file); - public VerifyCommand(ILogger logger) - { - this.logger = logger; - } + ArgumentNullException.ThrowIfNull(buffer); - public async Task VerifyAsync(string file, StringBuilder buffer) - { - if (file == null) - { - throw new ArgumentNullException(nameof(file)); - } + ISignatureVerificationProvider[] trustProviders = + [ + new IntegrityVerificationProvider(), + new SignatureTrustAndValidityVerificationProvider() + ]; + PackageSignatureVerifier verifier = new(trustProviders); - if (buffer == null) - { - throw new ArgumentNullException(nameof(buffer)); - } + bool allPackagesVerified = true; + try + { + int result = 0; + System.Collections.Generic.IEnumerable packagesToVerify = LocalFolderUtility.ResolvePackageFromPath(file); - var trustProviders = new ISignatureVerificationProvider[] - { - new IntegrityVerificationProvider(), - new SignatureTrustAndValidityVerificationProvider() - }; - var verifier = new PackageSignatureVerifier(trustProviders); - - var allPackagesVerified = true; - - try + foreach(string packageFile in packagesToVerify) { - var result = 0; - var packagesToVerify = LocalFolderUtility.ResolvePackageFromPath(file); + using PackageArchiveReader package = new(packageFile); + VerifySignaturesResult verificationResult = await verifier.VerifySignaturesAsync(package, SignedPackageVerifierSettings.GetVerifyCommandDefaultPolicy(), CancellationToken.None); - foreach (var packageFile in packagesToVerify) + if(verificationResult.IsValid) { - using var package = new PackageArchiveReader(packageFile); - var verificationResult = await verifier.VerifySignaturesAsync(package, SignedPackageVerifierSettings.GetVerifyCommandDefaultPolicy(), CancellationToken.None); - - if (verificationResult.IsValid) + allPackagesVerified = true; + } + else + { + System.Collections.Generic.List logMessages = verificationResult.Results.SelectMany(p => p.Issues).Select(p => p.AsRestoreLogMessage()).ToList(); + foreach(NuGet.Common.RestoreLogMessage msg in logMessages) { - allPackagesVerified = true; + buffer.AppendLine(msg.Message); } - else + if(logMessages.Any(m => m.Level >= NuGet.Common.LogLevel.Warning)) { - var logMessages = verificationResult.Results.SelectMany(p => p.Issues).Select(p => p.AsRestoreLogMessage()).ToList(); - foreach (var msg in logMessages) - { - buffer.AppendLine(msg.Message); - } - if (logMessages.Any(m => m.Level >= NuGet.Common.LogLevel.Warning)) - { - var errors = logMessages.Where(m => m.Level == NuGet.Common.LogLevel.Error).Count(); - var warnings = logMessages.Where(m => m.Level == NuGet.Common.LogLevel.Warning).Count(); + int errors = logMessages.Where(m => m.Level == NuGet.Common.LogLevel.Error).Count(); + int warnings = logMessages.Where(m => m.Level == NuGet.Common.LogLevel.Warning).Count(); - buffer.AppendLine($"Finished with {errors} errors and {warnings} warnings."); + buffer.AppendLine($"Finished with {errors} errors and {warnings} warnings."); - result = errors; - } - allPackagesVerified = false; + result = errors; } + allPackagesVerified = false; } } - catch (Exception e) - { - logger.LogError(e, e.Message); - return false; - } - - return allPackagesVerified; } + catch(Exception e) + { + logger.LogError(e, "{errorMessage}", e.Message); + return false; + } + + return allPackagesVerified; } -} +} \ No newline at end of file diff --git a/NuGetKeyVaultSignTool/AccessTokenCredential.cs b/NuGetKeyVaultSignTool/AccessTokenCredential.cs index b84f433..f0effa0 100644 --- a/NuGetKeyVaultSignTool/AccessTokenCredential.cs +++ b/NuGetKeyVaultSignTool/AccessTokenCredential.cs @@ -1,40 +1,36 @@ using Azure.Core; using System; -using System.Collections.Generic; -using System.Text; using System.Threading; using System.Threading.Tasks; -namespace NuGetKeyVaultSignTool +namespace NuGetKeyVaultSignTool; + +internal class AccessTokenCredential : TokenCredential { - class AccessTokenCredential : TokenCredential - { - private readonly AccessToken accessToken; + private readonly AccessToken accessToken; - public AccessTokenCredential(string token, DateTimeOffset expiresOn) + public AccessTokenCredential(string token, DateTimeOffset expiresOn) + { + if(string.IsNullOrWhiteSpace(token)) { - if (string.IsNullOrWhiteSpace(token)) - { - throw new ArgumentException("Access Token cannot be null or empty", nameof(token)); - } - - accessToken = new AccessToken(token, expiresOn); + throw new ArgumentException("Access Token cannot be null or empty", nameof(token)); } - public AccessTokenCredential(string token) : this(token, DateTimeOffset.UtcNow.AddHours(1)) - { + accessToken = new AccessToken(token, expiresOn); + } - } + public AccessTokenCredential(string token) : this(token, DateTimeOffset.UtcNow.AddHours(1)) + { + } - public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) - { - return accessToken; - } + public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) + { + return accessToken; + } - public override ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) - { - return new ValueTask(accessToken); - } + public override ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) + { + return new ValueTask(accessToken); } -} +} \ No newline at end of file diff --git a/NuGetKeyVaultSignTool/NuGetKeyVaultSignTool.csproj b/NuGetKeyVaultSignTool/NuGetKeyVaultSignTool.csproj index ca02693..979d281 100644 --- a/NuGetKeyVaultSignTool/NuGetKeyVaultSignTool.csproj +++ b/NuGetKeyVaultSignTool/NuGetKeyVaultSignTool.csproj @@ -2,17 +2,22 @@ Exe - netcoreapp3.1;net5.0;net6.0 + net8.0 true Major - - - + + + + + + + + diff --git a/NuGetKeyVaultSignTool/Program.cs b/NuGetKeyVaultSignTool/Program.cs index 7864fe6..5b6316e 100644 --- a/NuGetKeyVaultSignTool/Program.cs +++ b/NuGetKeyVaultSignTool/Program.cs @@ -1,240 +1,223 @@ -using Microsoft.Extensions.CommandLineUtils; -using System; -using System.IO; -using System.Text; -using System.Threading.Tasks; -using NuGet.Common; -using NuGet.Packaging.Signing; +using Azure.Core; +using Azure.Identity; +using Microsoft.Extensions.CommandLineUtils; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Azure.Core; -using Azure.Identity; +using NuGet.Common; +using NuGet.Packaging.Signing; +using System; +using System.Text; -namespace NuGetKeyVaultSignTool +namespace NuGetKeyVaultSignTool; + +internal class Program { - class Program + internal static int Main(string[] args) { - internal static int Main(string[] args) + IServiceCollection serviceCollection = new ServiceCollection() + .AddLogging(builder => + { + builder.AddConsole(); + }); + ServiceProvider serviceProvider = serviceCollection.BuildServiceProvider(); + ILogger logger = serviceProvider.GetRequiredService>(); + + CommandLineApplication application = new(throwOnUnexpectedArg: false); + CommandLineApplication signCommand = application.Command("sign", throwOnUnexpectedArg: false, configuration: signConfiguration => { + signConfiguration.Description = "Signs NuGet packages with the specified Key Vault certificate."; + signConfiguration.HelpOption("-? | -h | --help"); + + CommandArgument packagePath = signConfiguration.Argument("packagePath", "Package to sign."); + CommandOption outputPath = signConfiguration.Option("-o | --output", "The output file. If omitted, overwrites input.", CommandOptionType.SingleValue); + CommandOption force = signConfiguration.Option("-f | --force", "Overwrites a signature if it exists.", CommandOptionType.NoValue); + CommandOption fileDigestAlgorithm = signConfiguration.Option("-fd | --file-digest", "The digest algorithm to hash the file with. Default option is sha256", CommandOptionType.SingleValue); + CommandOption rfc3161TimeStamp = signConfiguration.Option("-tr | --timestamp-rfc3161", "Specifies the RFC 3161 timestamp server's URL. If this option (or -t) is not specified, the signed file will not be timestamped.", CommandOptionType.SingleValue); + CommandOption rfc3161Digest = signConfiguration.Option("-td | --timestamp-digest", "Used with the -tr switch to request a digest algorithm used by the RFC 3161 timestamp server. Default option is sha256", CommandOptionType.SingleValue); + CommandOption signatureType = signConfiguration.Option("-st | --signature-type", "The signature type (omit for author, default. Only author is supported currently).", CommandOptionType.SingleValue); + CommandOption v3ServiceIndexUrl = signConfiguration.Option("-v3si | --v3-service-index-url", "Specifies V3 Service Index Url. Required if SignatureType is Repository", CommandOptionType.SingleValue); + CommandOption packageOwners = signConfiguration.Option("-own | --package-owner", "Package Owners. Required if SignatureType is Repository", CommandOptionType.MultipleValue); + CommandOption azureKeyVaultUrl = signConfiguration.Option("-kvu | --azure-key-vault-url", "The URL to an Azure Key Vault.", CommandOptionType.SingleValue); + CommandOption azureKeyVaultClientId = signConfiguration.Option("-kvi | --azure-key-vault-client-id", "The Client ID to authenticate to the Azure Key Vault.", CommandOptionType.SingleValue); + CommandOption azureKeyVaultClientSecret = signConfiguration.Option("-kvs | --azure-key-vault-client-secret", "The Client Secret to authenticate to the Azure Key Vault.", CommandOptionType.SingleValue); + CommandOption azureKeyVaultTenantId = signConfiguration.Option("-kvt | --azure-key-vault-tenant-id", "The Tenant Id to authenticate to the Azure Key Vault.", CommandOptionType.SingleValue); + CommandOption azureKeyVaultCertificateName = signConfiguration.Option("-kvc | --azure-key-vault-certificate", "The name of the certificate in Azure Key Vault.", CommandOptionType.SingleValue); + CommandOption azureKeyVaultAccessToken = signConfiguration.Option("-kva | --azure-key-vault-accesstoken", "The Access Token to authenticate to the Azure Key Vault.", CommandOptionType.SingleValue); + CommandOption azureKeyVaultMsi = signConfiguration.Option("-kvm | --azure-key-vault-managed-identity", "Use a Managed Identity to access Azure Key Vault.", CommandOptionType.NoValue); + + signConfiguration.OnExecute(async () => + { + if(string.IsNullOrWhiteSpace(packagePath.Value)) + { + logger.LogError("Path to file(s) to sign are required"); + return -1; + } - var serviceCollection = new ServiceCollection() - .AddLogging(builder => + if(!azureKeyVaultUrl.HasValue()) { - builder.AddConsole(); - }); - var serviceProvider = serviceCollection.BuildServiceProvider(); - var logger = serviceProvider.GetRequiredService>(); + logger.LogError("Key Vault URL not specified"); + return -1; + } - var application = new CommandLineApplication(throwOnUnexpectedArg: false); - var signCommand = application.Command("sign", throwOnUnexpectedArg: false, configuration: signConfiguration => - { - signConfiguration.Description = "Signs NuGet packages with the specified Key Vault certificate."; - signConfiguration.HelpOption("-? | -h | --help"); - - var packagePath = signConfiguration.Argument("packagePath", "Package to sign."); - var outputPath = signConfiguration.Option("-o | --output", "The output file. If omitted, overwrites input.", CommandOptionType.SingleValue); - var force = signConfiguration.Option("-f | --force", "Overwrites a signature if it exists.", CommandOptionType.NoValue); - var fileDigestAlgorithm = signConfiguration.Option("-fd | --file-digest", "The digest algorithm to hash the file with. Default option is sha256", CommandOptionType.SingleValue); - var rfc3161TimeStamp = signConfiguration.Option("-tr | --timestamp-rfc3161", "Specifies the RFC 3161 timestamp server's URL. If this option (or -t) is not specified, the signed file will not be timestamped.", CommandOptionType.SingleValue); - var rfc3161Digest = signConfiguration.Option("-td | --timestamp-digest", "Used with the -tr switch to request a digest algorithm used by the RFC 3161 timestamp server. Default option is sha256", CommandOptionType.SingleValue); - var signatureType = signConfiguration.Option("-st | --signature-type", "The signature type (omit for author, default. Only author is supported currently).", CommandOptionType.SingleValue); - var v3ServiceIndexUrl = signConfiguration.Option("-v3si | --v3-service-index-url", "Specifies V3 Service Index Url. Required if SignatureType is Repository", CommandOptionType.SingleValue); - var packageOwners = signConfiguration.Option("-own | --package-owner", "Package Owners. Required if SignatureType is Repository", CommandOptionType.MultipleValue); - var azureKeyVaultUrl = signConfiguration.Option("-kvu | --azure-key-vault-url", "The URL to an Azure Key Vault.", CommandOptionType.SingleValue); - var azureKeyVaultClientId = signConfiguration.Option("-kvi | --azure-key-vault-client-id", "The Client ID to authenticate to the Azure Key Vault.", CommandOptionType.SingleValue); - var azureKeyVaultClientSecret = signConfiguration.Option("-kvs | --azure-key-vault-client-secret", "The Client Secret to authenticate to the Azure Key Vault.", CommandOptionType.SingleValue); - var azureKeyVaultTenantId = signConfiguration.Option("-kvt | --azure-key-vault-tenant-id", "The Tenant Id to authenticate to the Azure Key Vault.", CommandOptionType.SingleValue); - var azureKeyVaultCertificateName = signConfiguration.Option("-kvc | --azure-key-vault-certificate", "The name of the certificate in Azure Key Vault.", CommandOptionType.SingleValue); - var azureKeyVaultAccessToken = signConfiguration.Option("-kva | --azure-key-vault-accesstoken", "The Access Token to authenticate to the Azure Key Vault.", CommandOptionType.SingleValue); - var azureKeyVaultMsi = signConfiguration.Option("-kvm | --azure-key-vault-managed-identity", "Use a Managed Identity to access Azure Key Vault.", CommandOptionType.NoValue); - - signConfiguration.OnExecute(async () => + if(!azureKeyVaultCertificateName.HasValue()) { - if (string.IsNullOrWhiteSpace(packagePath.Value)) - { - logger.LogError("Path to file(s) to sign are required"); - return -1; - } + logger.LogError("Certificate name not specified"); + return -1; + } - if (!azureKeyVaultUrl.HasValue()) - { - logger.LogError("Key Vault URL not specified"); - return -1; - } + if(!rfc3161TimeStamp.HasValue()) + { + logger.LogError("Timestamp url not specified"); + return -1; + } - if (!azureKeyVaultCertificateName.HasValue()) - { - logger.LogError("Certificate name not specified"); - return -1; - } + bool valid = (azureKeyVaultAccessToken.HasValue() || azureKeyVaultMsi.HasValue() || (azureKeyVaultClientId.HasValue() && azureKeyVaultClientSecret.HasValue() && azureKeyVaultTenantId.HasValue())); + if(!valid) + { + logger.LogError("Either access token or clientId, client secret, and tenant id must be specified"); + return -1; + } - if (!rfc3161TimeStamp.HasValue()) - { - logger.LogError("Timestamp url not specified"); - return -1; - } + HashAlgorithmName sigHashAlg = GetValueFromOption(fileDigestAlgorithm, AlgorithmFromInput, HashAlgorithmName.SHA256); + HashAlgorithmName timeHashAlg = GetValueFromOption(rfc3161Digest, AlgorithmFromInput, HashAlgorithmName.SHA256); + SignatureType sigType = GetValueFromOption(signatureType, SignatureTypeFromInput, SignatureType.Author); + + Uri v3ServiceIndex = null; - var valid = (azureKeyVaultAccessToken.HasValue() || azureKeyVaultMsi.HasValue() || (azureKeyVaultClientId.HasValue() && azureKeyVaultClientSecret.HasValue() && azureKeyVaultTenantId.HasValue())); - if (!valid) + if(sigType != SignatureType.Author) + { + // Check for service index and owners + if(!v3ServiceIndexUrl.HasValue()) { - logger.LogError("Either access token or clientId, client secret, and tenant id must be specified"); + logger.LogError("Service index url must be specified for repository signatures"); return -1; } - var sigHashAlg = GetValueFromOption(fileDigestAlgorithm, AlgorithmFromInput, HashAlgorithmName.SHA256); - var timeHashAlg = GetValueFromOption(rfc3161Digest, AlgorithmFromInput, HashAlgorithmName.SHA256); - var sigType = GetValueFromOption(signatureType, SignatureTypeFromInput, SignatureType.Author); - - Uri v3ServiceIndex = null; - - if (sigType != SignatureType.Author) + if(!Uri.TryCreate(v3ServiceIndexUrl.Value(), UriKind.Absolute, out v3ServiceIndex)) { - // Check for service index and owners - if (!v3ServiceIndexUrl.HasValue()) - { - logger.LogError("Service index url must be specified for repository signatures"); - return -1; - } - - if (!Uri.TryCreate(v3ServiceIndexUrl.Value(), UriKind.Absolute, out v3ServiceIndex)) - { - logger.LogError($"Could not parse '{v3ServiceIndexUrl.Value()}' as a Uri"); - return -1; - } - - if (!packageOwners.HasValue()) - { - logger.LogError("At least one package owner must be specified for repository signatures"); - return -1; - } + logger.LogError("Could not parse '{v3ServiceIndexUrl}' as a Uri", v3ServiceIndexUrl.Value()); + return -1; } - if (!Uri.TryCreate(azureKeyVaultUrl.Value(), UriKind.Absolute, out Uri keyVaultUri)) + if(!packageOwners.HasValue()) { - logger.LogError($"Could not parse '{azureKeyVaultUrl.Value()}' as a Uri"); + logger.LogError("At least one package owner must be specified for repository signatures"); return -1; } + } - var output = string.IsNullOrWhiteSpace(outputPath.Value()) ? packagePath.Value : outputPath.Value(); - - - TokenCredential credential = null; - - if (azureKeyVaultMsi.HasValue()) - { - credential = new DefaultAzureCredential(); - } - else if (!string.IsNullOrWhiteSpace(azureKeyVaultAccessToken.Value())) - { - credential = new AccessTokenCredential(azureKeyVaultAccessToken.Value(), DateTimeOffset.UtcNow.AddHours(1)); - } - else - { - credential = new ClientSecretCredential(azureKeyVaultTenantId.Value(), azureKeyVaultClientId.Value(), azureKeyVaultClientSecret.Value()); - } + if(!Uri.TryCreate(azureKeyVaultUrl.Value(), UriKind.Absolute, out Uri keyVaultUri)) + { + logger.LogError("Could not parse '{azureKeyVaultUrl}' as a Uri", azureKeyVaultUrl.Value()); + return -1; + } - var cmd = new SignCommand(logger); - var result = await cmd.SignAsync(packagePath.Value, - output, - rfc3161TimeStamp.Value(), - sigHashAlg, - timeHashAlg, - sigType, - force.HasValue(), - v3ServiceIndex, - packageOwners.Values, - azureKeyVaultCertificateName.Value(), - keyVaultUri, - credential - ); - - return result ? 0 : -1; - }); - } - ); - - // Verify - var verifyCommand = application.Command("verify", throwOnUnexpectedArg: false, configuration: verifyConfiguration => - { - verifyConfiguration.Description = "Verifies NuGet packages are signed correctly"; - verifyConfiguration.HelpOption("-? | -h | --help"); + string output = string.IsNullOrWhiteSpace(outputPath.Value()) ? packagePath.Value : outputPath.Value(); - var file = verifyConfiguration.Argument("file", "File to sign."); + TokenCredential credential = null; - verifyConfiguration.OnExecute(async () => + if(azureKeyVaultMsi.HasValue()) { - if (string.IsNullOrWhiteSpace(file.Value)) - { - application.Error.WriteLine("All arguments are required"); - return -1; - } + credential = new DefaultAzureCredential(); + } + else if(!string.IsNullOrWhiteSpace(azureKeyVaultAccessToken.Value())) + { + credential = new AccessTokenCredential(azureKeyVaultAccessToken.Value(), DateTimeOffset.UtcNow.AddHours(1)); + } + else + { + credential = new ClientSecretCredential(azureKeyVaultTenantId.Value(), azureKeyVaultClientId.Value(), azureKeyVaultClientSecret.Value()); + } + + SignCommand cmd = new(logger); + bool result = await cmd.SignAsync(packagePath.Value, + output, + rfc3161TimeStamp.Value(), + sigHashAlg, + timeHashAlg, + sigType, + force.HasValue(), + v3ServiceIndex, + packageOwners.Values, + azureKeyVaultCertificateName.Value(), + keyVaultUri, + credential + ); + + return result ? 0 : -1; + }); + } + ); - var cmd = new VerifyCommand(logger); - var buffer = new StringBuilder(); - var result = await cmd.VerifyAsync(file.Value, buffer); - Console.WriteLine(buffer.ToString()); - if (result) - { - Console.WriteLine("Signature is valid"); - } - else - { - Console.Write("Signature is invalid"); - } - return result ? 0 : -1; - }); - } - ); + // Verify + CommandLineApplication verifyCommand = application.Command("verify", throwOnUnexpectedArg: false, configuration: verifyConfiguration => + { + verifyConfiguration.Description = "Verifies NuGet packages are signed correctly"; + verifyConfiguration.HelpOption("-? | -h | --help"); + CommandArgument file = verifyConfiguration.Argument("file", "File to sign."); - application.HelpOption("-? | -h | --help"); - application.VersionOption("-v | --version", typeof(Program).Assembly.GetName().Version.ToString(3)); - if (args.Length == 0) + verifyConfiguration.OnExecute(async () => { - application.ShowHelp(); - } - return application.Execute(args); + if(string.IsNullOrWhiteSpace(file.Value)) + { + application.Error.WriteLine("All arguments are required"); + return -1; + } + + VerifyCommand cmd = new(logger); + StringBuilder buffer = new(); + bool result = await cmd.VerifyAsync(file.Value, buffer); + Console.WriteLine(buffer.ToString()); + if(result) + { + Console.WriteLine("Signature is valid"); + } + else + { + Console.Write("Signature is invalid"); + } + return result ? 0 : -1; + }); } + ); - static HashAlgorithmName? AlgorithmFromInput(string value) + application.HelpOption("-? | -h | --help"); + application.VersionOption("-v | --version", typeof(Program).Assembly.GetName().Version.ToString(3)); + if(args.Length == 0) { - switch (value?.ToLower()) - { - case "sha384": - return HashAlgorithmName.SHA384; - case "sha512": - return HashAlgorithmName.SHA512; - case null: - case "sha256": - return HashAlgorithmName.SHA256; - default: - return null; - - } + application.ShowHelp(); } + return application.Execute(args); + } - static SignatureType? SignatureTypeFromInput(string value) + private static HashAlgorithmName? AlgorithmFromInput(string value) + { + return (value?.ToLower()) switch { - switch (value?.ToLower()) - { - case "author": - return SignatureType.Author; - case "repository": - return SignatureType.Repository; - - default: - return null; - } - } + "sha384" => (HashAlgorithmName?)HashAlgorithmName.SHA384, + "sha512" => (HashAlgorithmName?)HashAlgorithmName.SHA512, + null or "sha256" => (HashAlgorithmName?)HashAlgorithmName.SHA256, + _ => null, + }; + } - static T GetValueFromOption(CommandOption option, Func transform, T defaultIfNull) where T : struct + private static SignatureType? SignatureTypeFromInput(string value) + { + return (value?.ToLower()) switch { - if (!option.HasValue()) - { - return defaultIfNull; - } - return transform(option.Value()) ?? defaultIfNull; - } + "author" => (SignatureType?)SignatureType.Author, + "repository" => (SignatureType?)SignatureType.Repository, + _ => null, + }; + } + private static T GetValueFromOption(CommandOption option, Func transform, T defaultIfNull) where T : struct + { + if(!option.HasValue()) + { + return defaultIfNull; + } + return transform(option.Value()) ?? defaultIfNull; } -} +} \ No newline at end of file