|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Formats.Asn1;
using System.Security.Cryptography.Asn1;
using System.Security.Cryptography.Asn1.Pkcs7;
using System.Security.Cryptography.Pkcs.Asn1;
using System.Security.Cryptography.X509Certificates;
using System.Security.Cryptography.Xml;
using Internal.Cryptography;
namespace System.Security.Cryptography.Pkcs
{
public sealed class Rfc3161TimestampToken
{
private SignedCms _parsedDocument = null!; // Initialized by object initializer
private SignerInfo? _signerInfo;
private EssCertId? _essCertId;
private EssCertIdV2? _essCertIdV2;
public Rfc3161TimestampTokenInfo TokenInfo { get; private set; } = null!;
private Rfc3161TimestampToken()
{
}
/// <summary>
/// Get a SignedCms representation of the RFC3161 Timestamp Token.
/// </summary>
/// <returns>The SignedCms representation of the RFC3161 Timestamp Token.</returns>
/// <remarks>
/// Successive calls to this method return the same object.
/// The SignedCms class is mutable, but changes to that object are not reflected in the
/// <see cref="Rfc3161TimestampToken"/> object which produced it.
/// The value from calling <see cref="SignedCms.Encode"/> can be interpreted again as an
/// <see cref="Rfc3161TimestampToken"/> via another call to <see cref="TryDecode"/>.
/// </remarks>
public SignedCms AsSignedCms() => _parsedDocument;
private X509Certificate2? GetSignerCertificate(X509Certificate2Collection? extraCandidates)
{
Debug.Assert(_signerInfo != null, "_signerInfo != null");
X509Certificate2? signerCert = _signerInfo.Certificate;
if (signerCert != null)
{
if (CheckCertificate(signerCert, _signerInfo, in _essCertId, in _essCertIdV2, TokenInfo))
{
return signerCert;
}
// SignedCms will not try another certificate in this state, so just fail.
return null;
}
if (extraCandidates == null || extraCandidates.Count == 0)
{
return null;
}
foreach (X509Certificate2 candidate in extraCandidates)
{
if (CheckCertificate(candidate, _signerInfo, in _essCertId, in _essCertIdV2, TokenInfo))
{
return candidate;
}
}
return null;
}
public bool VerifySignatureForData(
ReadOnlySpan<byte> data,
[NotNullWhen(true)] out X509Certificate2? signerCertificate,
X509Certificate2Collection? extraCandidates = null)
{
signerCertificate = null;
X509Certificate2? cert = GetSignerCertificate(extraCandidates);
if (cert == null)
{
return false;
}
if (VerifyData(data))
{
signerCertificate = cert;
return true;
}
return false;
}
public bool VerifySignatureForHash(
ReadOnlySpan<byte> hash,
HashAlgorithmName hashAlgorithm,
[NotNullWhen(true)] out X509Certificate2? signerCertificate,
X509Certificate2Collection? extraCandidates = null)
{
signerCertificate = null;
X509Certificate2? cert = GetSignerCertificate(extraCandidates);
if (cert == null)
{
return false;
}
if (VerifyHash(hash, PkcsHelpers.GetOidFromHashAlgorithm(hashAlgorithm)))
{
signerCertificate = cert;
return true;
}
return false;
}
public bool VerifySignatureForHash(
ReadOnlySpan<byte> hash,
Oid hashAlgorithmId,
[NotNullWhen(true)] out X509Certificate2? signerCertificate,
X509Certificate2Collection? extraCandidates = null)
{
if (hashAlgorithmId is null)
{
throw new ArgumentNullException(nameof(hashAlgorithmId));
}
signerCertificate = null;
X509Certificate2? cert = GetSignerCertificate(extraCandidates);
if (cert == null)
{
return false;
}
if (VerifyHash(hash, hashAlgorithmId.Value))
{
// REVIEW: Should this return the cert, or new X509Certificate2(cert.RawData)?
// SignedCms.SignerInfos builds new objects each call, which makes
// ReferenceEquals(cms.SignerInfos[0].Certificate, cms.SignerInfos[0].Certificate) be false.
// So maybe it's weird to give back a cert we've copied from that?
signerCertificate = cert;
return true;
}
return false;
}
public bool VerifySignatureForSignerInfo(
SignerInfo signerInfo,
[NotNullWhen(true)] out X509Certificate2? signerCertificate,
X509Certificate2Collection? extraCandidates = null)
{
if (signerInfo is null)
{
throw new ArgumentNullException(nameof(signerInfo));
}
return VerifySignatureForData(
signerInfo.GetSignatureMemory().Span,
out signerCertificate,
extraCandidates);
}
internal bool VerifyHash(ReadOnlySpan<byte> hash, string? hashAlgorithmId)
{
return
hash.SequenceEqual(TokenInfo.GetMessageHash().Span) &&
hashAlgorithmId == TokenInfo.HashAlgorithmId.Value;
}
private bool VerifyData(ReadOnlySpan<byte> data)
{
Oid hashAlgorithmId = TokenInfo.HashAlgorithmId;
HashAlgorithmName hashAlgorithmName = PkcsHelpers.GetDigestAlgorithm(hashAlgorithmId);
using (IncrementalHash hasher = IncrementalHash.CreateHash(hashAlgorithmName))
{
hasher.AppendData(data);
// SHA-2-512 is the biggest hash we currently know about.
Span<byte> stackSpan = stackalloc byte[512 / 8];
if (hasher.TryGetHashAndReset(stackSpan, out int bytesWritten))
{
return VerifyHash(stackSpan.Slice(0, bytesWritten), hashAlgorithmId.Value);
}
// Something we understood, but is bigger than 512-bit.
// Allocate at runtime, trip in a debug build so we can re-evaluate this.
Debug.Fail(
$"TryGetHashAndReset did not fit in {stackSpan.Length} for hash {hashAlgorithmId.Value}");
return VerifyHash(hasher.GetHashAndReset(), hashAlgorithmId.Value);
}
}
private static bool CheckCertificate(
X509Certificate2 tsaCertificate,
SignerInfo signer,
in EssCertId? certId,
in EssCertIdV2? certId2,
Rfc3161TimestampTokenInfo tokenInfo)
{
Debug.Assert(tsaCertificate != null);
Debug.Assert(signer != null);
Debug.Assert(tokenInfo != null);
// certId and certId2 are allowed to be null, they get checked in CertMatchesIds.
if (!CertMatchesIds(tsaCertificate, certId, certId2))
{
return false;
}
// Nothing in RFC3161 actually mentions checking the certificate's validity
// against the TSTInfo timestamp value, but it seems sensible.
//
// Accuracy is ignored here, for better replicability in user code.
if (tsaCertificate.NotAfter < tokenInfo.Timestamp ||
tsaCertificate.NotBefore > tokenInfo.Timestamp)
{
return false;
}
// https://tools.ietf.org/html/rfc3161#section-2.3
//
// The TSA MUST sign each time-stamp message with a key reserved
// specifically for that purpose. A TSA MAY have distinct private keys,
// e.g., to accommodate different policies, different algorithms,
// different private key sizes or to increase the performance. The
// corresponding certificate MUST contain only one instance of the
// extended key usage field extension as defined in [RFC2459] Section
// 4.2.1.13 with KeyPurposeID having value:
//
// id-kp-timeStamping. This extension MUST be critical.
X509ExtensionCollection extensions = tsaCertificate.Extensions;
bool anyFound = false;
for (int i = 0; i < extensions.Count; i++)
{
if (extensions[i] is not X509EnhancedKeyUsageExtension ekuExt)
{
continue;
}
if (anyFound)
{
return false;
}
anyFound = true;
if (!ekuExt.Critical)
{
return false;
}
bool hasPurpose = false;
foreach (Oid oid in ekuExt.EnhancedKeyUsages)
{
if (oid.Value == Oids.TimeStampingPurpose)
{
hasPurpose = true;
break;
}
}
if (!hasPurpose)
{
return false;
}
}
if (anyFound)
{
try
{
signer.CheckSignature(new X509Certificate2Collection(tsaCertificate), true);
return true;
}
catch (CryptographicException)
{
}
}
return false;
}
public static bool TryDecode(ReadOnlyMemory<byte> encodedBytes, [NotNullWhen(true)] out Rfc3161TimestampToken? token, out int bytesConsumed)
{
bytesConsumed = 0;
token = null;
try
{
AsnValueReader reader = new AsnValueReader(encodedBytes.Span, AsnEncodingRules.BER);
int bytesActuallyRead = reader.PeekEncodedValue().Length;
ContentInfoAsn.Decode(
ref reader,
encodedBytes,
out ContentInfoAsn contentInfo);
// https://tools.ietf.org/html/rfc3161#section-2.4.2
//
// A TimeStampToken is as follows. It is defined as a ContentInfo
// ([CMS]) and SHALL encapsulate a signed data content type.
//
// TimeStampToken::= ContentInfo
// --contentType is id-signedData([CMS])
// --content is SignedData ([CMS])
if (contentInfo.ContentType != Oids.Pkcs7Signed)
{
return false;
}
SignedCms cms = new SignedCms();
cms.Decode(encodedBytes.Span);
// The fields of type EncapsulatedContentInfo of the SignedData
// construct have the following meanings:
//
// eContentType is an object identifier that uniquely specifies the
// content type. For a time-stamp token it is defined as:
//
// id-ct-TSTInfo OBJECT IDENTIFIER ::= { iso(1) member-body(2)
// us(840) rsadsi(113549) pkcs(1) pkcs-9(9) smime(16) ct(1) 4}
//
// eContent is the content itself, carried as an octet string.
// The eContent SHALL be the DER-encoded value of TSTInfo.
if (cms.ContentInfo.ContentType.Value != Oids.TstInfo)
{
return false;
}
// RFC3161:
// The time-stamp token MUST NOT contain any signatures other than the
// signature of the TSA. The certificate identifier (ESSCertID) of the
// TSA certificate MUST be included as a signerInfo attribute inside a
// SigningCertificate attribute.
// RFC5816 says that ESSCertIDv2 should be allowed instead.
SignerInfoCollection signerInfos = cms.SignerInfos;
if (signerInfos.Count != 1)
{
return false;
}
SignerInfo signer = signerInfos[0];
EssCertId? certId;
EssCertIdV2? certId2;
if (!TryGetCertIds(signer, out certId, out certId2))
{
return false;
}
X509Certificate2? signerCert = signer.Certificate;
if (signerCert == null &&
signer.SignerIdentifier.Type == SubjectIdentifierType.IssuerAndSerialNumber)
{
// If the cert wasn't provided, but the identifier was IssuerAndSerialNumber,
// and the ESSCertId(V2) has specified an issuerSerial value, ensure it's a match.
X509IssuerSerial issuerSerial = (X509IssuerSerial)signer.SignerIdentifier.Value!;
if (certId.HasValue && certId.Value.IssuerSerial != null)
{
if (!IssuerAndSerialMatch(
certId.Value.IssuerSerial.Value,
issuerSerial.IssuerName,
issuerSerial.SerialNumber))
{
return false;
}
}
if (certId2.HasValue && certId2.Value.IssuerSerial != null)
{
if (!IssuerAndSerialMatch(
certId2.Value.IssuerSerial.Value,
issuerSerial.IssuerName,
issuerSerial.SerialNumber))
{
return false;
}
}
}
if (Rfc3161TimestampTokenInfo.TryDecode(cms.ContentInfo.Content, out Rfc3161TimestampTokenInfo? tokenInfo, out _))
{
if (signerCert != null &&
!CheckCertificate(signerCert, signer, in certId, in certId2, tokenInfo))
{
return false;
}
token = new Rfc3161TimestampToken
{
_parsedDocument = cms,
_signerInfo = signer,
_essCertId = certId,
_essCertIdV2 = certId2,
TokenInfo = tokenInfo,
};
bytesConsumed = bytesActuallyRead;
return true;
}
}
catch (AsnContentException)
{
}
catch (CryptographicException)
{
}
return false;
}
private static bool IssuerAndSerialMatch(
CadesIssuerSerial issuerSerial,
string issuerDirectoryName,
string serialNumber)
{
GeneralNameAsn[] issuerNames = issuerSerial.Issuer;
if (issuerNames == null || issuerNames.Length != 1)
{
return false;
}
GeneralNameAsn requiredName = issuerNames[0];
if (requiredName.DirectoryName == null)
{
return false;
}
if (issuerDirectoryName != new X500DistinguishedName(requiredName.DirectoryName.Value.ToArray()).Name)
{
return false;
}
return serialNumber == issuerSerial.SerialNumber.Span.ToBigEndianHex();
}
private static bool IssuerAndSerialMatch(
CadesIssuerSerial issuerSerial,
ReadOnlySpan<byte> issuerDirectoryName,
ReadOnlySpan<byte> serialNumber)
{
GeneralNameAsn[] issuerNames = issuerSerial.Issuer;
if (issuerNames == null || issuerNames.Length != 1)
{
return false;
}
GeneralNameAsn requiredName = issuerNames[0];
if (requiredName.DirectoryName == null)
{
return false;
}
if (!requiredName.DirectoryName.Value.Span.SequenceEqual(issuerDirectoryName))
{
return false;
}
return serialNumber.SequenceEqual(issuerSerial.SerialNumber.Span);
}
private static bool CertMatchesIds(X509Certificate2 signerCert, in EssCertId? certId, in EssCertIdV2? certId2)
{
Debug.Assert(signerCert != null);
Debug.Assert(certId.HasValue || certId2.HasValue);
byte[]? serialNumber = null;
if (certId.HasValue)
{
Span<byte> thumbprint = stackalloc byte[20];
if (!signerCert.TryGetCertHash(HashAlgorithmName.SHA1, thumbprint, out int written) ||
written != thumbprint.Length ||
!thumbprint.SequenceEqual(certId.Value.Hash.Span))
{
return false;
}
if (certId.Value.IssuerSerial.HasValue)
{
serialNumber = signerCert.GetSerialNumber();
Array.Reverse(serialNumber);
if (!IssuerAndSerialMatch(
certId.Value.IssuerSerial.Value,
signerCert.IssuerName.RawData,
serialNumber))
{
return false;
}
}
}
if (certId2.HasValue)
{
HashAlgorithmName alg;
// SHA-2-512 is the biggest we know about.
Span<byte> thumbprint = stackalloc byte[512 / 8];
try
{
alg = PkcsHelpers.GetDigestAlgorithm(certId2.Value.HashAlgorithm.Algorithm);
if (signerCert.TryGetCertHash(alg, thumbprint, out int written))
{
thumbprint = thumbprint.Slice(0, written);
}
else
{
Debug.Fail(
$"TryGetCertHash did not fit in {thumbprint.Length} for hash {certId2.Value.HashAlgorithm.Algorithm}");
thumbprint = signerCert.GetCertHash(alg);
}
}
catch (CryptographicException)
{
return false;
}
if (!thumbprint.SequenceEqual(certId2.Value.Hash.Span))
{
return false;
}
if (certId2.Value.IssuerSerial.HasValue)
{
if (serialNumber == null)
{
serialNumber = signerCert.GetSerialNumber();
Array.Reverse(serialNumber);
}
if (!IssuerAndSerialMatch(
certId2.Value.IssuerSerial.Value,
signerCert.IssuerName.RawData,
serialNumber))
{
return false;
}
}
}
return true;
}
private static bool TryGetCertIds(SignerInfo signer, out EssCertId? certId, out EssCertIdV2? certId2)
{
// RFC 5035 says that SigningCertificateV2 (contains ESSCertIDv2) is a signed
// attribute, with OID 1.2.840.113549.1.9.16.2.47, and that it must not be multiply defined.
// RFC 2634 says that SigningCertificate (contains ESSCertID) is a signed attribute,
// with OID 1.2.840.113549.1.9.16.2.12, and that it must not be multiply defined.
certId = null;
certId2 = null;
foreach (CryptographicAttributeObject attrSet in signer.SignedAttributes)
{
string? setOid = attrSet.Oid?.Value;
if (setOid != null &&
setOid != Oids.SigningCertificate &&
setOid != Oids.SigningCertificateV2)
{
continue;
}
foreach (AsnEncodedData attr in attrSet.Values)
{
string? attrOid = attr.Oid?.Value;
if (attrOid == Oids.SigningCertificate)
{
if (certId != null)
{
return false;
}
try
{
SigningCertificateAsn signingCert = SigningCertificateAsn.Decode(
attr.RawData,
AsnEncodingRules.BER);
if (signingCert.Certs.Length < 1)
{
return false;
}
// The first one is the signing cert, the rest constrain the chain.
certId = signingCert.Certs[0];
}
catch (CryptographicException)
{
return false;
}
}
if (attrOid == Oids.SigningCertificateV2)
{
if (certId2 != null)
{
return false;
}
try
{
SigningCertificateV2Asn signingCert = SigningCertificateV2Asn.Decode(
attr.RawData,
AsnEncodingRules.BER);
if (signingCert.Certs.Length < 1)
{
return false;
}
// The first one is the signing cert, the rest constrain the chain.
certId2 = signingCert.Certs[0];
}
catch (CryptographicException)
{
return false;
}
}
}
}
return certId2 != null || certId != null;
}
}
}
|