| File: SignCommand\CertificateProvider.cs | Web Access |
| Project: 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. using System; using System.Globalization; using System.IO; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; using NuGet.Common; using NuGet.Packaging.Signing; namespace NuGet.Commands { internal static class CertificateProvider { // "The system cannot find the file specified." (ERROR_FILE_NOT_FOUND) private const int ERROR_FILE_NOT_FOUND_HRESULT = unchecked((int)0x80070002); // OpenSSL: error:2006D080:BIO routines:BIO_new_file:no such file private const int OPENSSL_BIO_R_NO_SUCH_FILE = 0x2006D080; // "The specified password is not correct." (ERROR_INVALID_PASSWORD) private const int ERROR_INVALID_PASSWORD_HRESULT = unchecked((int)0x80070056); // OpenSSL: error:23076071:PKCS12 routines:PKCS12_parse:mac verify failure private const int OPENSSL_PKCS12_R_MAC_VERIFY_FAILURE = 0x23076071; private const int MACOS_PKCS12_MAC_VERIFY_FAILURE = -25264; // "The specified certificate file is not correct." (CRYPT_E_NO_MATCH) private const int CRYPT_E_NO_MATCH_HRESULT = unchecked((int)0x80092009); private const int MACOS_INVALID_CERT = -25257; private const int CRYPT_E_BAD_DECODE = unchecked((int)0x80092002); #if IS_CORECLR //Generic exception ASN1 corrupted data private const int OPENSSL_ASN1_CORRUPTED_DATA_ERROR = unchecked((int)0x80131501); #else // OpenSSL: error:0D07803A:asn1 encoding routines:ASN1_ITEM_EX_D2I:nested asn1 error private const int OPENSSL_ERR_R_NESTED_ASN1_ERROR = 0x0D07803A; #endif /// <summary> /// Looks for X509Certificates using the CertificateSourceOptions. /// Throws an InvalidOperationException if the option specifies a CertificateFilePath with invalid password. /// </summary> /// <param name="options">CertificateSourceOptions to be used while searching for the certificates.</param> /// <returns>An X509Certificate2Collection object containing matching certificates. /// If no matching certificates are found then it returns an empty collection.</returns> public static async Task<X509Certificate2Collection> GetCertificatesAsync(CertificateSourceOptions options) { // check certificate path var resultCollection = new X509Certificate2Collection(); if (!string.IsNullOrEmpty(options.CertificatePath)) { try { var cert = await LoadCertificateFromFileAsync(options); resultCollection = new X509Certificate2Collection(cert); } catch (CryptographicException ex) { switch (ex.HResult) { case ERROR_INVALID_PASSWORD_HRESULT: case OPENSSL_PKCS12_R_MAC_VERIFY_FAILURE: case MACOS_PKCS12_MAC_VERIFY_FAILURE: throw new SignCommandException( LogMessage.CreateError(NuGetLogCode.NU3001, string.Format(CultureInfo.CurrentCulture, Strings.SignCommandInvalidPasswordException, options.CertificatePath, nameof(options.CertificatePassword)))); case ERROR_FILE_NOT_FOUND_HRESULT: case OPENSSL_BIO_R_NO_SUCH_FILE: throw new SignCommandException( LogMessage.CreateError(NuGetLogCode.NU3001, string.Format(CultureInfo.CurrentCulture, Strings.SignCommandCertificateFileNotFound, options.CertificatePath))); case CRYPT_E_NO_MATCH_HRESULT: case CRYPT_E_BAD_DECODE: #if IS_CORECLR case OPENSSL_ASN1_CORRUPTED_DATA_ERROR: #else case OPENSSL_ERR_R_NESTED_ASN1_ERROR: #endif case MACOS_INVALID_CERT: throw new SignCommandException( LogMessage.CreateError(NuGetLogCode.NU3001, string.Format(CultureInfo.CurrentCulture, Strings.SignCommandInvalidCertException, options.CertificatePath))); default: throw; } } catch (FileNotFoundException) { throw new SignCommandException( LogMessage.CreateError(NuGetLogCode.NU3001, string.Format(CultureInfo.CurrentCulture, Strings.SignCommandCertificateFileNotFound, options.CertificatePath))); } } else { resultCollection = LoadCertificateFromStore(options); } return resultCollection; } private static #if IS_DESKTOP async #endif Task<X509Certificate2> LoadCertificateFromFileAsync(CertificateSourceOptions options) { X509Certificate2 cert; if (!string.IsNullOrEmpty(options.CertificatePassword)) { // use the password if the user provided it #if NET9_0_OR_GREATER cert = X509CertificateLoader.LoadPkcs12FromFile(options.CertificatePath, options.CertificatePassword); #else cert = new X509Certificate2(options.CertificatePath, options.CertificatePassword); #endif } else { #if IS_DESKTOP try { cert = new X509Certificate2(options.CertificatePath); } catch (CryptographicException ex) { // prompt user for password if needed if (ex.HResult == ERROR_INVALID_PASSWORD_HRESULT && !options.NonInteractive) { using (var password = await options.PasswordProvider.GetPassword(options.CertificatePath, options.Token)) { cert = new X509Certificate2(options.CertificatePath, password); } } else { throw; } } #else #if NET9_0_OR_GREATER cert = X509CertificateLoader.LoadPkcs12FromFile(options.CertificatePath, null); #else cert = new X509Certificate2(options.CertificatePath); #endif #endif } #if IS_DESKTOP return cert; #else return Task.FromResult(cert); #endif } private static X509Certificate2Collection LoadCertificateFromStore(CertificateSourceOptions options) { X509Certificate2Collection resultCollection = new(); using var store = new X509Store(options.StoreName, options.StoreLocation); OpenStore(store); // Passing true for validOnly seems like a good idea; it would filter out invalid certificates. // However, "invalid certificates" is a broad category that includes untrusted self-issued certificates. // Untrusted self-issued certificates are permitted at signing time, so we must perform certificate // validity checks ourselves. const bool validOnly = false; if (!string.IsNullOrEmpty(options.Fingerprint) && CertificateUtility.TryDeduceHashAlgorithm(options.Fingerprint, out Common.HashAlgorithmName hashAlgorithmName)) { if (hashAlgorithmName == Common.HashAlgorithmName.SHA1) { resultCollection = store.Certificates.Find(X509FindType.FindByThumbprint, options.Fingerprint, validOnly); } else if (hashAlgorithmName != Common.HashAlgorithmName.Unknown) { foreach (var cert in store.Certificates) { string actualFingerprint = CertificateUtility.GetHashString(cert, hashAlgorithmName); if (string.Equals(actualFingerprint, options.Fingerprint, StringComparison.InvariantCultureIgnoreCase)) { resultCollection.Add(cert); break; } } } } else if (!string.IsNullOrEmpty(options.SubjectName)) { resultCollection = store.Certificates.Find(X509FindType.FindBySubjectName, options.SubjectName, validOnly); } store.Close(); resultCollection = GetValidCertificates(resultCollection, options.AllowUntrustedRoot); return resultCollection; } /// <summary> /// Opens an X509Store with read only access. /// Throws an InvalidOperationException if the store does not exist. /// </summary> /// <param name="store">X509Store to be opened.</param> private static void OpenStore(X509Store store) { try { store.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly); } catch (CryptographicException ex) { if (ex.HResult == ERROR_FILE_NOT_FOUND_HRESULT) { throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Strings.SignCommandCertificateStoreNotFound, store)); } } } private static X509Certificate2Collection GetValidCertificates(X509Certificate2Collection certificates, bool allowUntrustedRoot = false) { var validCertificates = new X509Certificate2Collection(); foreach (var certificate in certificates) { if (IsValid(certificate, certificates, allowUntrustedRoot)) { validCertificates.Add(certificate); } } return validCertificates; } private static bool IsValid(X509Certificate2 certificate, X509Certificate2Collection extraStore, bool allowUntrustedRoot = false) { try { using (var chain = CertificateChainUtility.GetCertificateChain( certificate, extraStore, NullLogger.Instance, CertificateType.Signature, allowUntrustedRoot)) { return chain != null && chain.Count > 0; } } catch (SignatureException) { return false; } } } }