|
// 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 System.Security.Cryptography.X509Certificates;
using System.Text;
using NuGet.Common;
using NuGet.Packaging.Signing.DerEncoding;
namespace NuGet.Packaging.Signing
{
public static class CertificateUtility
{
private const int ChainDepthLimit = 10;
/// <summary>
/// Converts a X509Certificate2 to a human friendly string of the following format -
/// Subject Name: CN=name
/// SHA1 hash: hash
/// Issued by: CN=issuer
/// Valid from: issue date time to expiry date time in local time
/// </summary>
/// <param name="cert">X509Certificate2 to be converted to string.</param>
/// <param name="fingerprintAlgorithm">Algorithm used to calculate certificate fingerprint</param>
/// <returns>string representation of the X509Certificate2.</returns>
public static string X509Certificate2ToString(X509Certificate2 cert, HashAlgorithmName fingerprintAlgorithm)
{
var certStringBuilder = new StringBuilder();
X509Certificate2ToString(cert, certStringBuilder, fingerprintAlgorithm, indentation: " ");
return certStringBuilder.ToString();
}
/// <summary>
/// Converts a X509Certificate2 to a collection of log messages for various verbosity levels -
/// Subject Name: CN=name
/// SHA1 hash: hash
/// Issued by: CN=issuer
/// Valid from: issue date time to expiry date time in local time
/// </summary>
/// <param name="cert">X509Certificate2 to be converted to string.</param>
/// <param name="fingerprintAlgorithm">Algorithm used to calculate certificate fingerprint</param>
/// <returns>string representation of the X509Certificate2.</returns>
internal static IReadOnlyList<SignatureLog> X509Certificate2ToLogMessages(X509Certificate2 cert, HashAlgorithmName fingerprintAlgorithm, string indentation = " ")
{
var certificateFingerprint = GetHashString(cert, fingerprintAlgorithm);
var issues = new List<SignatureLog>();
issues.Add(SignatureLog.MinimalLog($"{indentation}{string.Format(CultureInfo.CurrentCulture, Strings.CertUtilityCertificateSubjectName, cert.Subject)}"));
issues.Add(SignatureLog.InformationLog($"{indentation}{string.Format(CultureInfo.CurrentCulture, Strings.CertUtilityCertificateHashSha1, cert.Thumbprint)}"));
issues.Add(SignatureLog.MinimalLog($"{indentation}{string.Format(CultureInfo.CurrentCulture, Strings.CertUtilityCertificateHash, fingerprintAlgorithm.ToString(), certificateFingerprint)}"));
issues.Add(SignatureLog.InformationLog($"{indentation}{string.Format(CultureInfo.CurrentCulture, Strings.CertUtilityCertificateIssuer, cert.IssuerName.Name)}"));
issues.Add(SignatureLog.MinimalLog($"{indentation}{string.Format(CultureInfo.CurrentCulture, Strings.CertUtilityCertificateValidity, cert.NotBefore, cert.NotAfter)}"));
foreach (string url in GetCrlDistributionPointUrls(cert))
{
issues.Add(SignatureLog.InformationLog($"{indentation}{string.Format(CultureInfo.CurrentCulture, Strings.CertUtilityCertificateCrlUrl, url)}"));
}
foreach (string url in GetOcspUrls(cert))
{
issues.Add(SignatureLog.InformationLog($"{indentation}{string.Format(CultureInfo.CurrentCulture, Strings.CertUtilityCertificateOcspUrl, url)}"));
}
return issues;
}
private static void X509Certificate2ToString(X509Certificate2 cert, StringBuilder certStringBuilder, HashAlgorithmName fingerprintAlgorithm, string indentation)
{
var certificateFingerprint = GetHashString(cert, fingerprintAlgorithm);
certStringBuilder.AppendLine(indentation + string.Format(CultureInfo.CurrentCulture, Strings.CertUtilityCertificateSubjectName, cert.Subject));
certStringBuilder.AppendLine(indentation + string.Format(CultureInfo.CurrentCulture, Strings.CertUtilityCertificateHashSha1, cert.Thumbprint));
certStringBuilder.AppendLine(indentation + string.Format(CultureInfo.CurrentCulture, Strings.CertUtilityCertificateHash, fingerprintAlgorithm.ToString(), certificateFingerprint));
certStringBuilder.AppendLine(indentation + string.Format(CultureInfo.CurrentCulture, Strings.CertUtilityCertificateIssuer, cert.IssuerName.Name));
certStringBuilder.AppendLine(indentation + string.Format(CultureInfo.CurrentCulture, Strings.CertUtilityCertificateValidity, cert.NotBefore, cert.NotAfter));
foreach (string url in GetCrlDistributionPointUrls(cert))
{
certStringBuilder.AppendLine(indentation + string.Format(CultureInfo.CurrentCulture, Strings.CertUtilityCertificateCrlUrl, url));
}
foreach (string url in GetOcspUrls(cert))
{
certStringBuilder.AppendLine(indentation + string.Format(CultureInfo.CurrentCulture, Strings.CertUtilityCertificateOcspUrl, url));
}
}
/// <summary>
/// Converts a X509Certificate2Collection to a human friendly string of the following format -
/// Subject Name: CN=name
/// SHA1 hash: hash
/// Issued by: CN=issuer
/// Valid from: issue date time to expiry date time in local time
///
/// Subject Name: CN=name
/// SHA1 hash: hash
/// Issued by: CN=issuer
/// Valid from: issue date time to expiry date time in local time
///
/// ... N more.
/// </summary>
/// <param name="certCollection">X509Certificate2Collection to be converted to string.</param>
/// <param name="fingerprintAlgorithm">Algorithm used to calculate certificate fingerprint</param>
/// <returns>string representation of the X509Certificate2Collection.</returns>
public static string X509Certificate2CollectionToString(X509Certificate2Collection certCollection, HashAlgorithmName fingerprintAlgorithm)
{
var collectionStringBuilder = new StringBuilder();
collectionStringBuilder.AppendLine(Strings.CertUtilityMultipleCertificatesHeader);
for (var i = 0; i < Math.Min(ChainDepthLimit, certCollection.Count); i++)
{
var cert = certCollection[i];
X509Certificate2ToString(cert, collectionStringBuilder, fingerprintAlgorithm, indentation: " ");
collectionStringBuilder.AppendLine();
}
if (certCollection.Count > ChainDepthLimit)
{
collectionStringBuilder.AppendLine(string.Format(CultureInfo.CurrentCulture, Strings.CertUtilityMultipleCertificatesFooter, certCollection.Count - ChainDepthLimit));
}
return collectionStringBuilder.ToString();
}
public static string X509ChainToString(X509Chain chain, HashAlgorithmName fingerprintAlgorithm)
{
var collectionStringBuilder = new StringBuilder();
var indentationLevel = " ";
var indentation = indentationLevel;
var chainElementsCount = chain.ChainElements.Count;
// Start in 1 to omit main certificate (only build the chain)
for (var i = 1; i < Math.Min(ChainDepthLimit, chainElementsCount); i++)
{
X509Certificate2ToString(chain.ChainElements[i].Certificate, collectionStringBuilder, fingerprintAlgorithm, indentation);
indentation += indentationLevel;
}
if (chainElementsCount > ChainDepthLimit)
{
collectionStringBuilder.AppendLine(string.Format(CultureInfo.CurrentCulture, Strings.CertUtilityMultipleCertificatesFooter, chainElementsCount - ChainDepthLimit));
}
return collectionStringBuilder.ToString();
}
/// <summary>
/// Determines if a certificate's signature algorithm is supported.
/// </summary>
/// <param name="certificate">Certificate to validate</param>
/// <returns>True if the certificate's signature algorithm is supported.</returns>
public static bool IsSignatureAlgorithmSupported(X509Certificate2 certificate)
{
switch (certificate.SignatureAlgorithm.Value)
{
case Oids.Sha256WithRSAEncryption:
case Oids.Sha384WithRSAEncryption:
case Oids.Sha512WithRSAEncryption:
return true;
default:
return false;
}
}
/// <summary>
/// Validates the public key requirements for a certificate
/// </summary>
/// <param name="certificate">Certificate to validate</param>
/// <returns>True if the certificate's public key is valid within NuGet signature requirements</returns>
public static bool IsCertificatePublicKeyValid(X509Certificate2 certificate)
{
// Check if the public key is RSA with a valid keysize
using (System.Security.Cryptography.RSA? publicKey = RSACertificateExtensions.GetRSAPublicKey(certificate))
{
if (publicKey != null)
{
return publicKey.KeySize >= SigningSpecifications.V1.RSAPublicKeyMinLength;
}
}
return false;
}
/// <summary>
/// Validates if the certificate contains the lifetime signing EKU
/// </summary>
/// <param name="certificate">Certificate to validate</param>
/// <returns>True if the certificate has the lifetime signing EKU</returns>
public static bool HasLifetimeSigningEku(X509Certificate2 certificate)
{
return HasExtendedKeyUsage(certificate, Oids.LifetimeSigningEku);
}
/// <summary>
/// Checks if an X509Certificate2 contains a particular Extended Key Usage (EKU).
/// </summary>
/// <param name="certificate">X509Certificate2 to be checked.</param>
/// <param name="ekuOid">String OID of the Extended Key Usage</param>
/// <returns>A bool indicating if the X509Certificate2 contains specified OID in its Extended Key Usage.</returns>
public static bool HasExtendedKeyUsage(X509Certificate2 certificate, string ekuOid)
{
foreach (var extension in certificate.Extensions)
{
if (string.Equals(extension.Oid!.Value, Oids.EnhancedKeyUsage, StringComparison.Ordinal))
{
var ekuExtension = (X509EnhancedKeyUsageExtension)extension;
foreach (var eku in ekuExtension.EnhancedKeyUsages)
{
if (eku.Value == ekuOid)
{
return true;
}
}
break;
}
}
return false;
}
/// <summary>
/// Checks if an X509Certificate2 is valid for a particular purpose.
/// </summary>
/// <remarks>
/// This must not be used in evaluation of a signed package.
/// A more accurate test is building a chain with the specified EKU asserted in the application policy.
/// </remarks>
/// <param name="certificate">X509Certificate2 to be checked.</param>
/// <param name="ekuOid">String OID of the Extended Key Usage</param>
/// <returns>A bool indicating if the X509Certificate2 contains specified OID string in its Extended Key Usage.</returns>
public static bool IsValidForPurposeFast(X509Certificate2 certificate, string ekuOid)
{
foreach (var extension in certificate.Extensions)
{
if (string.Equals(extension.Oid!.Value, Oids.EnhancedKeyUsage, StringComparison.Ordinal))
{
var ekuExtension = (X509EnhancedKeyUsageExtension)extension;
if (ekuExtension.EnhancedKeyUsages.Count == 0)
{
return true;
}
foreach (var eku in ekuExtension.EnhancedKeyUsages)
{
if (eku.Value == ekuOid)
{
return true;
}
}
return false;
}
}
return true;
}
public static bool IsCertificateValidityPeriodInTheFuture(X509Certificate2 certificate)
{
return DateTime.Now < certificate.NotBefore;
}
public static bool IsDateInsideValidityPeriod(X509Certificate2 certificate, DateTimeOffset date)
{
DateTimeOffset signerCertExpiry = DateTime.SpecifyKind(certificate.NotAfter, DateTimeKind.Local);
DateTimeOffset signerCertBegin = DateTime.SpecifyKind(certificate.NotBefore, DateTimeKind.Local);
return signerCertBegin <= date && date < signerCertExpiry;
}
/// <summary>
/// Gets the certificate fingerprint with the given hashing algorithm
/// </summary>
/// <param name="certificate">X509Certificate2 to be compute fingerprint</param>
/// <param name="hashAlgorithm">Hash algorithm for fingerprint</param>
/// <returns>A byte array representing the certificate hash.</returns>
public static byte[] GetHash(X509Certificate2 certificate, HashAlgorithmName hashAlgorithm)
{
if (certificate == null)
{
throw new ArgumentNullException(nameof(certificate));
}
return hashAlgorithm.ComputeHash(certificate.RawData);
}
/// <summary>
/// Gets the certificate fingerprint string with the given hashing algorithm
/// </summary>
/// <param name="certificate">X509Certificate2 to be compute fingerprint</param>
/// <param name="hashAlgorithm">Hash algorithm for fingerprint</param>
/// <returns>A string representing the certificate hash.</returns>
public static string GetHashString(X509Certificate2 certificate, HashAlgorithmName hashAlgorithm)
{
if (certificate == null)
{
throw new ArgumentNullException(nameof(certificate));
}
var certificateFingerprint = GetHash(certificate, hashAlgorithm);
#if NETCOREAPP
return BitConverter.ToString(certificateFingerprint).Replace("-", "", StringComparison.Ordinal);
#else
return BitConverter.ToString(certificateFingerprint).Replace("-", "");
#endif
}
/// <summary>
/// Determines if a certificate is self-issued.
/// </summary>
/// <remarks>Warning: this method does not evaluate certificate trust, revocation status, or validity!
/// This method attempts to build a chain for the provided certificate, and although revocation status
/// checking is explicitly skipped, the underlying chain building engine may go online to fetch
/// additional information (e.g.: the issuer's certificate). This method is not a guaranteed offline
/// check.</remarks>
/// <param name="certificate">The certificate to check.</param>
/// <returns><see langword="true" /> if the certificate is self-issued; otherwise, <see langword="false" />.</returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="certificate" /> is <see langword="null" />.</exception>
public static bool IsSelfIssued(X509Certificate2 certificate)
{
if (certificate == null)
{
throw new ArgumentNullException(nameof(certificate));
}
using (X509ChainHolder chainHolder = X509ChainHolder.CreateForCodeSigning())
{
IX509Chain chain = chainHolder.Chain2;
chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority |
X509VerificationFlags.IgnoreRootRevocationUnknown |
X509VerificationFlags.IgnoreCertificateAuthorityRevocationUnknown |
X509VerificationFlags.IgnoreEndRevocationUnknown;
bool buildSuccess = CertificateChainUtility.BuildWithPolicy(chain, certificate);
if (!buildSuccess && chain.ChainStatus.Length == 0)
{
throw new SignatureException(Strings.CertificateChainValidationFailed);
}
if (chain.ChainElements.Count != 1)
{
return false;
}
if (chain.ChainStatus.Any(
chainStatus => chainStatus.Status.HasFlag(X509ChainStatusFlags.Cyclic) ||
chainStatus.Status.HasFlag(X509ChainStatusFlags.PartialChain) ||
chainStatus.Status.HasFlag(X509ChainStatusFlags.NotSignatureValid)))
{
return false;
}
if (!certificate.IssuerName.RawData.SequenceEqual(certificate.SubjectName.RawData))
{
return false;
}
var akiExtension = certificate.Extensions[Oids.AuthorityKeyIdentifier];
var skiExtension = certificate.Extensions[Oids.SubjectKeyIdentifier] as X509SubjectKeyIdentifierExtension;
if (akiExtension != null && skiExtension != null)
{
var reader = new DerSequenceReader(akiExtension.RawData);
var keyIdentifierTag = (DerSequenceReader.DerTag)DerSequenceReader.ContextSpecificTagFlag;
if (reader.HasTag(keyIdentifierTag))
{
var keyIdentifier = reader.ReadValue(keyIdentifierTag);
#if NETCOREAPP
var akiKeyIdentifier = BitConverter.ToString(keyIdentifier).Replace("-", "", StringComparison.Ordinal);
#else
var akiKeyIdentifier = BitConverter.ToString(keyIdentifier).Replace("-", "");
#endif
return string.Equals(skiExtension.SubjectKeyIdentifier, akiKeyIdentifier, StringComparison.OrdinalIgnoreCase);
}
}
return true;
}
}
public static IReadOnlyList<byte[]> GetRawDataForCollection(X509Certificate2Collection certificates)
{
var certificatesRawData = new List<byte[]>(capacity: certificates.Count);
foreach (var certificate in certificates)
{
certificatesRawData.Add(certificate.RawData);
}
return certificatesRawData.AsReadOnly();
}
/// <summary>
/// Tries to deduce the hash algorithm from the given certificate fingerprint.
/// </summary>
/// <param name="certificateFingerprint">The certificate fingerprint.</param>
/// <param name="hashAlgorithmName">The deduced hash algorithm name.</param>
/// <returns><c>true</c> if the hash algorithm was successfully deduced; otherwise, <c>false</c>.</returns>
public static bool TryDeduceHashAlgorithm(string certificateFingerprint, out HashAlgorithmName hashAlgorithmName)
{
hashAlgorithmName = HashAlgorithmName.Unknown;
if (!IsHex(certificateFingerprint))
return false;
// One hexadecimal character is 4 bits.
switch (certificateFingerprint.Length)
{
case 40: // 64 characters * 4 bits/character = 160 bits
hashAlgorithmName = HashAlgorithmName.SHA1;
return true;
case 64: // 64 characters * 4 bits/character = 256 bits
hashAlgorithmName = HashAlgorithmName.SHA256;
return true;
case 96: // 96 characters * 4 bits/character = 384 bits
hashAlgorithmName = HashAlgorithmName.SHA384;
return true;
case 128: // 128 characters * 4 bits/character = 512 bits
hashAlgorithmName = HashAlgorithmName.SHA512;
return true;
default:
return false;
}
}
/// <summary>
/// Determines if the given string represents a valid hexadecimal value.
/// </summary>
/// <param name="certificateFingerprint">The string to check.</param>
/// <returns><c>true</c> if the string is a valid hexadecimal value; otherwise, <c>false</c>.</returns>
private static bool IsHex(string certificateFingerprint)
{
if (string.IsNullOrEmpty(certificateFingerprint))
{
return false;
}
for (var i = 0; i < certificateFingerprint.Length; ++i)
{
char c = certificateFingerprint[i];
if (!char.IsDigit(c) && !(c >= 'a' && c <= 'f') && !(c >= 'A' && c <= 'F'))
{
return false;
}
}
return true;
}
/// <summary>
/// Extracts CRL Distribution Point URLs from the certificate's CRL Distribution Points extension (OID 2.5.29.31).
/// </summary>
internal static IReadOnlyList<string> GetCrlDistributionPointUrls(X509Certificate2 cert)
{
const string CrlDistributionPointsOid = "2.5.29.31";
// context-specific primitive tag [6] for uniformResourceIdentifier in GeneralName
const byte GeneralNameUriTag = 0x86;
var urls = new List<string>();
var extension = cert.Extensions[CrlDistributionPointsOid];
if (extension == null)
{
return urls;
}
try
{
// CRLDistributionPoints ::= SEQUENCE OF DistributionPoint
var reader = new DerEncoding.DerSequenceReader(extension.RawData);
while (reader.HasData)
{
// DistributionPoint ::= SEQUENCE { distributionPoint [0] ... }
var dpReader = reader.ReadSequence();
if (dpReader.HasData && dpReader.HasTag(DerEncoding.DerSequenceReader.ContextSpecificConstructedTag0))
{
// distributionPoint [0] CONSTRUCTED
byte[] dpNameData = dpReader.ReadValue((DerEncoding.DerSequenceReader.DerTag)DerEncoding.DerSequenceReader.ContextSpecificConstructedTag0);
var dpNameReader = DerEncoding.DerSequenceReader.CreateForPayload(dpNameData);
if (dpNameReader.HasData && dpNameReader.HasTag(DerEncoding.DerSequenceReader.ContextSpecificConstructedTag0))
{
// fullName [0] CONSTRUCTED = GeneralNames
byte[] fullNameData = dpNameReader.ReadValue((DerEncoding.DerSequenceReader.DerTag)DerEncoding.DerSequenceReader.ContextSpecificConstructedTag0);
var gnReader = DerEncoding.DerSequenceReader.CreateForPayload(fullNameData);
while (gnReader.HasData)
{
byte tag = gnReader.PeekTag();
if (tag == GeneralNameUriTag)
{
byte[] uriBytes = gnReader.ReadValue((DerEncoding.DerSequenceReader.DerTag)GeneralNameUriTag);
urls.Add(Encoding.ASCII.GetString(uriBytes));
}
else
{
gnReader.SkipValue();
}
}
}
}
}
}
catch (System.Security.Cryptography.CryptographicException exception)
{
return [exception.Message];
}
return urls;
}
/// <summary>
/// Extracts OCSP responder URLs from the certificate's Authority Information Access extension (OID 1.3.6.1.5.5.7.1.1).
/// </summary>
internal static IReadOnlyList<string> GetOcspUrls(X509Certificate2 cert)
{
const string AuthorityInfoAccessOid = "1.3.6.1.5.5.7.1.1";
const string OcspAccessMethodOid = "1.3.6.1.5.5.7.48.1";
// context-specific primitive tag [6] for uniformResourceIdentifier in GeneralName
const byte GeneralNameUriTag = 0x86;
var urls = new List<string>();
var extension = cert.Extensions[AuthorityInfoAccessOid];
if (extension == null)
{
return urls;
}
try
{
// AuthorityInfoAccessSyntax ::= SEQUENCE OF AccessDescription
var reader = new DerEncoding.DerSequenceReader(extension.RawData);
while (reader.HasData)
{
// AccessDescription ::= SEQUENCE { accessMethod OID, accessLocation GeneralName }
var adReader = reader.ReadSequence();
string oid = adReader.ReadOidAsString();
if (string.Equals(oid, OcspAccessMethodOid, StringComparison.Ordinal) && adReader.HasData)
{
byte tag = adReader.PeekTag();
if (tag == GeneralNameUriTag)
{
byte[] uriBytes = adReader.ReadValue((DerEncoding.DerSequenceReader.DerTag)GeneralNameUriTag);
urls.Add(Encoding.ASCII.GetString(uriBytes));
}
}
}
}
catch (System.Security.Cryptography.CryptographicException exception)
{
return [exception.Message];
}
return urls;
}
}
}
|