File: Signing\Timestamp\Timestamp.cs
Web Access
Project: src\src\nuget-client\src\NuGet.Core\NuGet.Packaging\NuGet.Packaging.csproj (NuGet.Packaging)
// 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 NuGet.Common;
using System.Security.Cryptography.Pkcs;

namespace NuGet.Packaging.Signing
{
    public sealed class Timestamp
    {
        /// <summary>
        /// Upper limit of Timestamp.
        /// </summary>
        public DateTimeOffset UpperLimit { get; }

        /// <summary>
        /// Lower limit of Timestamp.
        /// </summary>
        public DateTimeOffset LowerLimit { get; }

        /// <summary>
        /// Time timestamp was created by the Time Stamp Authority.
        /// </summary>
        public DateTimeOffset GeneralizedTime { get; }

        /// <summary>
        /// A SignedCms object holding the timestamp and SignerInfo.
        /// </summary>
        public SignedCms? SignedCms { get; }

        /// <summary>
        /// SignerInfo for this timestamp.
        /// </summary>
        public SignerInfo SignerInfo => SignedCms!.SignerInfos[0];

        /// <summary>
        /// Timestamp token info for this timestamp.
        /// </summary>
        internal IRfc3161TimestampTokenInfo? TstInfo { get; }

        /// <summary>
        /// Default constructor. Limits are set to current time.
        /// </summary>
        public Timestamp()
        {
            GeneralizedTime = DateTimeOffset.Now;
            UpperLimit = GeneralizedTime;
            LowerLimit = GeneralizedTime;
        }

        /// <summary>
        /// SignedCms containing a time stamp authority token reponse
        /// </summary>
        /// <param name="timestampCms">SignedCms from Time Stamp Authority</param>
        public Timestamp(SignedCms timestampCms)
        {
            SignedCms = timestampCms ?? throw new ArgumentNullException(nameof(timestampCms));

            if (Rfc3161TimestampVerificationUtility.TryReadTSTInfoFromSignedCms(timestampCms, out var tstInfo))
            {
                try
                {
                    SignedCms.CheckSignature(verifySignatureOnly: true);
                }
                catch (Exception ex)
                {
                    throw new TimestampException(NuGetLogCode.NU3021, Strings.VerifyError_TimestampSignatureValidationFailed, ex);
                }

                TstInfo = tstInfo;
                GeneralizedTime = tstInfo.Timestamp;

                var accuracyInMilliseconds = Rfc3161TimestampVerificationUtility.GetAccuracyInMilliseconds(tstInfo);
                UpperLimit = tstInfo.Timestamp.AddMilliseconds(accuracyInMilliseconds);
                LowerLimit = tstInfo.Timestamp.AddMilliseconds(-accuracyInMilliseconds);
            }
            else
            {
                throw new TimestampException(NuGetLogCode.NU3021, Strings.VerifyError_TimestampSignatureValidationFailed);
            }
        }

        /// <summary>
        /// Verify if the timestamp object meets the specification requirements.
        /// </summary>
        /// <param name="signature">Signature which this timestamp is for.</param>
        /// <param name="allowIgnoreTimestamp">Setting that tells if a timestamp can be ignored if it doesn't meet the requirements. Used to know if warnings or errors should be logged for an issue.</param>
        /// <param name="allowUnknownRevocation">Setting that tells if unkown revocation is valid when building the chain.</param>
        /// <param name="issues">List of log messages.</param>
        /// <returns>true if the timestamp meets the requierements, false otherwise.</returns>
        internal SignatureVerificationStatusFlags Verify(
            Signature signature,
            SignedPackageVerifierSettings settings,
            HashAlgorithmName fingerprintAlgorithm,
            List<SignatureLog> issues)
        {
            if (settings == null)
            {
                throw new ArgumentNullException(nameof(settings));
            }

            if (signature == null)
            {
                throw new ArgumentNullException(nameof(signature));
            }
            if (issues == null)
            {
                throw new ArgumentNullException(nameof(issues));
            }

            var treatIssueAsError = !settings.AllowIgnoreTimestamp;
            var timestamperCertificate = SignerInfo.Certificate;
            if (timestamperCertificate == null)
            {
                issues.Add(SignatureLog.Issue(treatIssueAsError, NuGetLogCode.NU3020, string.Format(CultureInfo.CurrentCulture, Strings.VerifyError_TimestampNoCertificate, signature.FriendlyName)));
                return SignatureVerificationStatusFlags.NoCertificate;
            }

            var timestampFlags = VerificationUtility.ValidateTimestamp(this, signature, treatIssueAsError, issues, SigningSpecifications.V1);
            if (timestampFlags != SignatureVerificationStatusFlags.NoErrors)
            {
                return timestampFlags;
            }
            else
            {
                issues.Add(SignatureLog.InformationLog(string.Format(CultureInfo.CurrentCulture, Strings.TimestampValue, GeneralizedTime.LocalDateTime.ToString(CultureInfo.CurrentCulture)) + Environment.NewLine));

                issues.Add(SignatureLog.InformationLog(string.Format(CultureInfo.CurrentCulture,
                    Strings.VerificationTimestamperCertDisplay,
                    signature.FriendlyName,
                    $"{Environment.NewLine}{CertificateUtility.X509Certificate2ToString(timestamperCertificate, fingerprintAlgorithm)}")));

                SignatureVerificationStatusFlags flags = SignatureVerificationStatusFlags.NoErrors;
                var certificateExtraStore = SignedCms!.Certificates;

                using (X509ChainHolder chainHolder = X509ChainHolder.CreateForTimestamping())
                {
                    IX509Chain chain = chainHolder.Chain2;

                    // This flag should only be set for verification scenarios, not signing.
                    chain.ChainPolicy.VerificationFlags = X509VerificationFlags.IgnoreNotTimeValid;

                    CertificateChainUtility.SetCertBuildChainPolicy(chain.ChainPolicy, certificateExtraStore, DateTime.Now, CertificateType.Timestamp);

                    if (settings.RevocationMode == RevocationMode.Offline)
                    {
                        chain.ChainPolicy.RevocationMode = X509RevocationMode.Offline;
                    }
                    else
                    {
                        chain.ChainPolicy.RevocationMode = X509RevocationMode.Online;
                    }

                    var chainBuildSucceed = CertificateChainUtility.BuildCertificateChain(chain, timestamperCertificate, out var chainStatusList);
                    string x509ChainString = CertificateUtility.X509ChainToString(chain.PrivateReference, fingerprintAlgorithm);

                    if (!string.IsNullOrWhiteSpace(x509ChainString))
                    {
                        issues.Add(SignatureLog.DetailedLog(x509ChainString));
                    }

                    if (chainBuildSucceed)
                    {
                        return SignatureVerificationStatusFlags.NoErrors;
                    }
                    else if (chainStatusList.Length == 0)
                    {
                        return SignatureVerificationStatusFlags.UnknownBuildStatus;
                    }
                    else
                    {
                        var chainBuildingHasIssues = false;

                        var timestampInvalidCertificateFlags = CertificateChainUtility.DefaultObservedStatusFlags;

                        if (CertificateChainUtility.TryGetStatusAndMessage(chainStatusList, timestampInvalidCertificateFlags, out IEnumerable<string>? messages))
                        {
                            foreach (string message in messages)
                            {
                                issues.Add(SignatureLog.Issue(treatIssueAsError, NuGetLogCode.NU3028, string.Format(CultureInfo.CurrentCulture, Strings.VerifyError_TimestampVerifyChainBuildingIssue, signature.FriendlyName, message)));
                            }

                            flags |= SignatureVerificationStatusFlags.ChainBuildingFailure;
                            chainBuildingHasIssues = true;
                        }

                        // For all the special cases, chain status list only has unique elements for each chain status flag present
                        // therefore if we are checking for one specific chain status we can use the first of the returned list
                        // if we are combining checks for more than one, then we have to use the whole list.

                        if (CertificateChainUtility.TryGetStatusAndMessage(chainStatusList, X509ChainStatusFlags.UntrustedRoot, out messages))
                        {
                            SignatureUtility.LogAdditionalContext(chain, issues);

                            issues.Add(SignatureLog.Error(NuGetLogCode.NU3028, string.Format(CultureInfo.CurrentCulture, Strings.VerifyTimestampChainBuildingIssue_UntrustedRoot, signature.FriendlyName)));

                            flags |= SignatureVerificationStatusFlags.UntrustedRoot;
                            chainBuildingHasIssues = true;
                        }

                        if (CertificateChainUtility.TryGetStatusAndMessage(chainStatusList, X509ChainStatusFlags.Revoked, out messages))
                        {
                            issues.Add(SignatureLog.Error(NuGetLogCode.NU3028, string.Format(CultureInfo.CurrentCulture, Strings.VerifyError_TimestampVerifyChainBuildingIssue, signature.FriendlyName, messages!.First())));
                            flags |= SignatureVerificationStatusFlags.CertificateRevoked;

                            return flags;
                        }

                        var offlineRevocationErrors = CertificateChainUtility.TryGetStatusAndMessage(chainStatusList, X509ChainStatusFlags.OfflineRevocation, out var _);
                        var unknownRevocationErrors = CertificateChainUtility.TryGetStatusAndMessage(chainStatusList, X509ChainStatusFlags.RevocationStatusUnknown, out var unknownRevocationStatusMessages);
                        if (offlineRevocationErrors || unknownRevocationErrors)
                        {
                            if (treatIssueAsError)
                            {
                                string? unknownRevocationMessage = null;

                                if (unknownRevocationErrors)
                                {
                                    unknownRevocationMessage = string.Format(CultureInfo.CurrentCulture, Strings.VerifyError_TimestampVerifyChainBuildingIssue, signature.FriendlyName, unknownRevocationStatusMessages!.First());
                                }

                                if (settings.RevocationMode == RevocationMode.Offline)
                                {
                                    if (offlineRevocationErrors)
                                    {
                                        issues.Add(SignatureLog.InformationLog(string.Format(CultureInfo.CurrentCulture, Strings.VerifyError_TimestampVerifyChainBuildingIssue, signature.FriendlyName, Strings.VerifyCertTrustOfflineWhileRevocationModeOffline)));
                                    }

                                    if (unknownRevocationMessage != null)
                                    {
                                        issues.Add(SignatureLog.InformationLog(unknownRevocationMessage));
                                    }
                                }
                                else
                                {
                                    if (offlineRevocationErrors)
                                    {
                                        issues.Add(SignatureLog.Issue(!settings.AllowUnknownRevocation, NuGetLogCode.NU3028, string.Format(CultureInfo.CurrentCulture, Strings.VerifyError_TimestampVerifyChainBuildingIssue, signature.FriendlyName, Strings.VerifyCertTrustOfflineWhileRevocationModeOnline)));
                                    }

                                    if (unknownRevocationMessage != null)
                                    {
                                        issues.Add(SignatureLog.Issue(!settings.AllowUnknownRevocation, NuGetLogCode.NU3028, unknownRevocationMessage));
                                    }

                                }
                            }

                            if (!chainBuildingHasIssues && (settings.AllowIgnoreTimestamp || settings.AllowUnknownRevocation))
                            {
                                return SignatureVerificationStatusFlags.NoErrors;
                            }

                            flags |= SignatureVerificationStatusFlags.UnknownRevocation;
                            chainBuildingHasIssues = true;
                        }
                    }

                    // Debug log any errors
                    issues.Add(
                        SignatureLog.DebugLog(
                            string.Format(
                                CultureInfo.CurrentCulture,
                                $"{signature.FriendlyName}'s timestamp",
                                Strings.VerifyError_InvalidCertificateChain,
                                string.Join(", ", chainStatusList.Select(x => x.Status.ToString())))));
                }

                return flags;
            }
        }
    }
}