File: System\Security\Cryptography\Pkcs\Rfc3161TimestampTokenInfo.cs
Web Access
Project: src\src\libraries\System.Security.Cryptography.Pkcs\src\System.Security.Cryptography.Pkcs.csproj (System.Security.Cryptography.Pkcs)
// 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.Pkcs.Asn1;
using System.Security.Cryptography.X509Certificates;
using Internal.Cryptography;
 
namespace System.Security.Cryptography.Pkcs
{
    /// <summary>
    /// Represents the timestamp token information class defined in RFC3161 as TSTInfo.
    /// </summary>
    public sealed class Rfc3161TimestampTokenInfo
    {
        private readonly byte[] _encodedBytes;
        private readonly Rfc3161TstInfo _parsedData;
        private Oid? _policyOid;
        private Oid? _hashAlgorithmId;
        private ReadOnlyMemory<byte>? _tsaNameBytes;
 
        /// <summary>
        /// Initializes a new instance of the <see cref="Rfc3161TimestampTokenInfo" /> class with the specified parameters.
        /// </summary>
        /// <param name="policyId">An OID representing the TSA's policy under which the response was produced.</param>
        /// <param name="hashAlgorithmId">A hash algorithm OID of the data to be timestamped.</param>
        /// <param name="messageHash">A hash value of the data to be timestamped.</param>
        /// <param name="serialNumber">An integer assigned by the TSA to the <see cref="Rfc3161TimestampTokenInfo"/>.</param>
        /// <param name="timestamp">The timestamp encoded in the token.</param>
        /// <param name="accuracyInMicroseconds">The accuracy with which <paramref name="timestamp"/> is compared. Also see <paramref name="isOrdering"/>.</param>
        /// <param name="isOrdering"><see langword="true" /> to ensure that every timestamp token from the same TSA can always be ordered based on the <paramref name="timestamp"/>, regardless of the accuracy; <see langword="false" /> to make <paramref name="timestamp"/> indicate when token has been created by the TSA.</param>
        /// <param name="nonce">The nonce associated with this timestamp token. Using a nonce always allows to detect replays, and hence its use is recommended.</param>
        /// <param name="timestampAuthorityName">The hint in the TSA name identification. The actual identification of the entity that signed the response will always occur through the use of the certificate identifier.</param>
        /// <param name="extensions">The extension values associated with the timestamp.</param>
        /// <remarks>If <paramref name="hashAlgorithmId" />, <paramref name="messageHash" />, <paramref name="policyId" /> or <paramref name="nonce" /> are present in the <see cref="Rfc3161TimestampRequest"/>, then the same value should be used. If <paramref name="accuracyInMicroseconds"/> is not provided, then the accuracy may be available through other means such as i.e. <paramref name="policyId" />.</remarks>
        /// <exception cref="CryptographicException">ASN.1 corrupted data.</exception>
        public Rfc3161TimestampTokenInfo(
            Oid policyId,
            Oid hashAlgorithmId,
            ReadOnlyMemory<byte> messageHash,
            ReadOnlyMemory<byte> serialNumber,
            DateTimeOffset timestamp,
            long? accuracyInMicroseconds = null,
            bool isOrdering = false,
            ReadOnlyMemory<byte>? nonce = null,
            ReadOnlyMemory<byte>? timestampAuthorityName = null,
            X509ExtensionCollection? extensions = null)
        {
            _encodedBytes = Encode(
                policyId,
                hashAlgorithmId,
                messageHash,
                serialNumber,
                timestamp,
                isOrdering,
                accuracyInMicroseconds,
                nonce,
                timestampAuthorityName,
                extensions);
 
            if (!TryDecode(_encodedBytes, true, out _parsedData, out _, out _))
            {
                Debug.Fail("Unable to decode the data we encoded");
                throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding);
            }
        }
 
        private Rfc3161TimestampTokenInfo(byte[] copiedBytes, Rfc3161TstInfo tstInfo)
        {
            _encodedBytes = copiedBytes;
            _parsedData = tstInfo;
        }
 
        /// <summary>
        /// Gets the version of the timestamp token.
        /// </summary>
        /// <value>The version of the timestamp token.</value>
        public int Version => _parsedData.Version;
 
        /// <summary>
        /// Gets an OID representing the TSA's policy under which the response was produced.
        /// </summary>
        /// <value>An OID representing the TSA's policy under which the response was produced.</value>
        public Oid PolicyId => (_policyOid ??= new Oid(_parsedData.Policy, null));
 
        /// <summary>
        /// Gets an OID of the hash algorithm.
        /// </summary>
        /// <value>An OID of the hash algorithm.</value>
        public Oid HashAlgorithmId => (_hashAlgorithmId ??= new Oid(_parsedData.MessageImprint.HashAlgorithm.Algorithm, null));
 
        /// <summary>
        /// Gets the data representing the message hash.
        /// </summary>
        /// <returns>The data representing the message hash.</returns>
        public ReadOnlyMemory<byte> GetMessageHash() => _parsedData.MessageImprint.HashedMessage;
 
        /// <summary>
        /// Gets an integer assigned by the TSA to the <see cref="Rfc3161TimestampTokenInfo"/>.
        /// </summary>
        /// <returns>An integer assigned by the TSA to the <see cref="Rfc3161TimestampTokenInfo"/>.</returns>
        public ReadOnlyMemory<byte> GetSerialNumber() => _parsedData.SerialNumber;
 
        /// <summary>
        /// Gets the timestamp encoded in the token.
        /// </summary>
        /// <value>The timestamp encoded in the token.</value>
        public DateTimeOffset Timestamp => _parsedData.GenTime;
 
        /// <summary>
        /// Gets the accuracy with which <see cref="Timestamp"/> is compared.
        /// </summary>
        /// <seealso cref="IsOrdering" />
        /// <value>The accuracy with which <see cref="Timestamp"/> is compared.</value>
        public long? AccuracyInMicroseconds => _parsedData.Accuracy?.TotalMicros;
 
        /// <summary>
        /// Gets a value indicating if every timestamp token from the same TSA can always be ordered based on the <see cref="Timestamp"/>, regardless of the accuracy; If <see langword="false" />, <see cref="Timestamp"/> indicates when the token has been created by the TSA.
        /// </summary>
        /// <value>A value indicating if every timestamp token from the same TSA can always be ordered based on the <see cref="Timestamp"/>.</value>
        public bool IsOrdering => _parsedData.Ordering;
 
        /// <summary>
        /// Gets the nonce associated with this timestamp token.
        /// </summary>
        /// <returns>The nonce associated with this timestamp token.</returns>
        public ReadOnlyMemory<byte>? GetNonce() => _parsedData.Nonce;
 
        /// <summary>
        /// Gets a value indicating whether there are any extensions associated with this timestamp token.
        /// </summary>
        /// <value>A value indicating whether there are any extensions associated with this timestamp token.</value>
        public bool HasExtensions => _parsedData.Extensions?.Length > 0;
 
        /// <summary>
        /// Gets the data representing the hint in the TSA name identification.
        /// </summary>
        /// <returns>The data representing the hint in the TSA name identification.</returns>
        /// <remarks>
        /// The actual identification of the entity that signed the response
        /// will always occur through the use of the certificate identifier (ESSCertID Attribute)
        /// inside a SigningCertificate attribute which is part of the signer info.
        /// </remarks>
        public ReadOnlyMemory<byte>? GetTimestampAuthorityName()
        {
            if (_tsaNameBytes == null)
            {
                GeneralNameAsn? tsaName = _parsedData.Tsa;
 
                if (tsaName == null)
                {
                    return null;
                }
 
                AsnWriter writer = new AsnWriter(AsnEncodingRules.DER);
                tsaName.Value.Encode(writer);
                _tsaNameBytes = writer.Encode();
                Debug.Assert(_tsaNameBytes.HasValue);
            }
 
            return _tsaNameBytes.Value;
        }
 
        /// <summary>
        /// Gets the extension values associated with the timestamp.
        /// </summary>
        /// <returns>The extension values associated with the timestamp.</returns>
        public X509ExtensionCollection GetExtensions()
        {
            var coll = new X509ExtensionCollection();
 
            if (!HasExtensions)
            {
                return coll;
            }
 
            X509ExtensionAsn[] rawExtensions = _parsedData.Extensions!;
 
            foreach (X509ExtensionAsn rawExtension in rawExtensions)
            {
                X509Extension extension = new X509Extension(
                    rawExtension.ExtnId,
                    rawExtension.ExtnValue.ToArray(),
                    rawExtension.Critical);
 
                // Currently there are no extensions defined.
                // Should this dip into CryptoConfig or other extensible
                // mechanisms for the CopyTo rich type uplift?
                coll.Add(extension);
            }
 
            return coll;
        }
 
        /// <summary>
        /// Encodes this object into a TSTInfo value
        /// </summary>
        /// <returns>The encoded TSTInfo value.</returns>
        public byte[] Encode()
        {
            return _encodedBytes.CloneByteArray();
        }
 
        /// <summary>
        /// Attempts to encode this object as a TSTInfo value, writing the result into the provided buffer.
        /// </summary>
        /// <param name="destination">The destination buffer.</param>
        /// <param name="bytesWritten">When this method returns <see langword="true" />, contains the bytes written to the <paramref name="destination" /> buffer.</param>
        /// <returns><see langword="true" /> if the operation succeeded; <see langword="false" /> if the buffer size was insufficient.</returns>
        public bool TryEncode(Span<byte> destination, out int bytesWritten)
        {
            if (destination.Length < _encodedBytes.Length)
            {
                bytesWritten = 0;
                return false;
            }
 
            _encodedBytes.AsSpan().CopyTo(destination);
            bytesWritten = _encodedBytes.Length;
            return true;
        }
 
        /// <summary>
        /// Decodes an encoded TSTInfo value.
        /// </summary>
        /// <param name="encodedBytes">The input or source buffer.</param>
        /// <param name="timestampTokenInfo">When this method returns <see langword="true" />, the decoded data. When this method returns <see langword="false" />, the value is <see langword="null" />, meaning the data could not be decoded.</param>
        /// <param name="bytesConsumed">The number of bytes used for decoding.</param>
        /// <returns><see langword="true" /> if the operation succeeded; <see langword="false" /> otherwise.</returns>
        public static bool TryDecode(
            ReadOnlyMemory<byte> encodedBytes,
            [NotNullWhen(true)] out Rfc3161TimestampTokenInfo? timestampTokenInfo,
            out int bytesConsumed)
        {
            if (TryDecode(encodedBytes, false, out Rfc3161TstInfo tstInfo, out bytesConsumed, out byte[]? copiedBytes))
            {
                timestampTokenInfo = new Rfc3161TimestampTokenInfo(copiedBytes!, tstInfo);
                return true;
            }
 
            bytesConsumed = 0;
            timestampTokenInfo = null;
            return false;
        }
 
        private static bool TryDecode(
            ReadOnlyMemory<byte> source,
            bool ownsMemory,
            out Rfc3161TstInfo tstInfo,
            out int bytesConsumed,
            out byte[]? copiedBytes)
        {
            // https://tools.ietf.org/html/rfc3161#section-2.4.2
            // The eContent SHALL be the DER-encoded value of TSTInfo.
            AsnReader reader = new AsnReader(source, AsnEncodingRules.DER);
 
            try
            {
                ReadOnlyMemory<byte> firstElement = reader.PeekEncodedValue();
 
                if (ownsMemory)
                {
                    copiedBytes = null;
                }
                else
                {
                    // Copy the data so no ReadOnlyMemory values are pointing back to user data.
                    copiedBytes = firstElement.ToArray();
                    firstElement = copiedBytes;
                }
 
                Rfc3161TstInfo parsedInfo = Rfc3161TstInfo.Decode(firstElement, AsnEncodingRules.DER);
 
                // The deserializer doesn't do bounds checks.
                // Micros and Millis are defined as (1..999)
                // Seconds doesn't define that it's bounded by 0,
                // but negative accuracy doesn't make sense.
                //
                // (Reminder to readers: a null int? with an inequality operator
                // has the value false, so if accuracy is missing, or millis or micro is missing,
                // then the respective checks return false and don't throw).
                if (parsedInfo.Accuracy?.Micros > 999 ||
                    parsedInfo.Accuracy?.Micros < 1 ||
                    parsedInfo.Accuracy?.Millis > 999 ||
                    parsedInfo.Accuracy?.Millis < 1 ||
                    parsedInfo.Accuracy?.Seconds < 0)
                {
                    throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding);
                }
 
                tstInfo = parsedInfo;
                bytesConsumed = firstElement.Length;
                return true;
            }
            catch (AsnContentException)
            {
                tstInfo = default;
                bytesConsumed = 0;
                copiedBytes = null;
                return false;
            }
            catch (CryptographicException)
            {
                tstInfo = default;
                bytesConsumed = 0;
                copiedBytes = null;
                return false;
            }
        }
 
        private static byte[] Encode(
            Oid policyId,
            Oid hashAlgorithmId,
            ReadOnlyMemory<byte> messageHash,
            ReadOnlyMemory<byte> serialNumber,
            DateTimeOffset timestamp,
            bool isOrdering,
            long? accuracyInMicroseconds,
            ReadOnlyMemory<byte>? nonce,
            ReadOnlyMemory<byte>? tsaName,
            X509ExtensionCollection? extensions)
        {
            if (policyId is null)
            {
                throw new ArgumentNullException(nameof(policyId));
            }
            if (hashAlgorithmId is null)
            {
                throw new ArgumentNullException(nameof(hashAlgorithmId));
            }
 
            var tstInfo = new Rfc3161TstInfo
            {
                // The only legal value as of 2017.
                Version = 1,
                Policy = policyId.Value!,
                MessageImprint =
                {
                    HashAlgorithm =
                    {
                        Algorithm = hashAlgorithmId.Value!,
                        Parameters = AlgorithmIdentifierAsn.ExplicitDerNull,
                    },
 
                    HashedMessage = messageHash,
                },
                SerialNumber = serialNumber,
                GenTime = timestamp,
                Ordering = isOrdering,
                Nonce = nonce,
            };
 
            if (accuracyInMicroseconds != null)
            {
                tstInfo.Accuracy = new Rfc3161Accuracy(accuracyInMicroseconds.Value);
            }
 
            if (tsaName != null)
            {
                tstInfo.Tsa = GeneralNameAsn.Decode(tsaName.Value, AsnEncodingRules.DER);
            }
 
            if (extensions != null)
            {
                tstInfo.Extensions = new X509ExtensionAsn[extensions.Count];
                for (int i = 0; i < extensions.Count; i++)
                {
                    tstInfo.Extensions[i] = new X509ExtensionAsn(extensions[i]);
                }
            }
 
            AsnWriter writer = new AsnWriter(AsnEncodingRules.DER);
            tstInfo.Encode(writer);
            return writer.Encode();
        }
    }
}