using System.Formats.Asn1; using System.Security.Cryptography; using System.Security.Cryptography.Pkcs; using System.Security.Cryptography.X509Certificates; using StellaOps.AirGap.Time.Models; using StellaOps.AirGap.Time.Parsing; namespace StellaOps.AirGap.Time.Services; /// /// Verifies RFC 3161 timestamp tokens using SignedCms and X509 certificate chain validation. /// Per AIRGAP-TIME-57-001: Provides trusted time-anchor service with real crypto verification. /// public sealed class Rfc3161Verifier : ITimeTokenVerifier { // RFC 3161 OIDs private static readonly Oid TstInfoOid = new("1.2.840.113549.1.9.16.1.4"); // id-ct-TSTInfo private static readonly Oid SigningTimeOid = new("1.2.840.113549.1.9.5"); public TimeTokenFormat Format => TimeTokenFormat.Rfc3161; public TimeAnchorValidationResult Verify( ReadOnlySpan tokenBytes, IReadOnlyList trustRoots, out TimeAnchor anchor, TimeTokenVerificationOptions? options = null) { anchor = TimeAnchor.Unknown; if (trustRoots.Count == 0) { return TimeAnchorValidationResult.Failure("rfc3161-trust-roots-required"); } if (tokenBytes.IsEmpty) { return TimeAnchorValidationResult.Failure("rfc3161-token-empty"); } // Compute token digest for reference var tokenDigest = Convert.ToHexString(SHA256.HashData(tokenBytes)).ToLowerInvariant(); try { // Parse the SignedCms structure var signedCms = new SignedCms(); signedCms.Decode(tokenBytes.ToArray()); // Verify signature (basic check without chain building) try { signedCms.CheckSignature(verifySignatureOnly: true); } catch (CryptographicException ex) { return TimeAnchorValidationResult.Failure($"rfc3161-signature-invalid:{ex.Message}"); } // Extract the signing certificate if (signedCms.SignerInfos.Count == 0) { return TimeAnchorValidationResult.Failure("rfc3161-no-signer"); } var signerInfo = signedCms.SignerInfos[0]; var signerCert = signerInfo.Certificate; if (signerCert is null) { return TimeAnchorValidationResult.Failure("rfc3161-no-signer-certificate"); } // Extract signing time from the TSTInfo or signed attributes var signingTime = ExtractSigningTime(signedCms, signerInfo); if (signingTime is null) { return TimeAnchorValidationResult.Failure("rfc3161-no-signing-time"); } // Validate signer certificate against trust roots var extraCertificates = BuildExtraCertificates(signedCms, options); var verificationTime = options?.VerificationTime ?? signingTime.Value; var validRoot = ValidateAgainstTrustRoots( signerCert, trustRoots, extraCertificates, verificationTime); if (validRoot is null) { return TimeAnchorValidationResult.Failure("rfc3161-certificate-not-trusted"); } if (options?.Offline == true) { if (!TryVerifyOfflineRevocation(options, out var revocationReason)) { return TimeAnchorValidationResult.Failure(revocationReason); } } // Compute certificate fingerprint var certFingerprint = Convert.ToHexString(SHA256.HashData(signerCert.RawData)).ToLowerInvariant()[..16]; anchor = new TimeAnchor( signingTime.Value, $"rfc3161:{validRoot.KeyId}", "RFC3161", certFingerprint, tokenDigest); return TimeAnchorValidationResult.Success("rfc3161-verified"); } catch (CryptographicException ex) { return TimeAnchorValidationResult.Failure($"rfc3161-decode-error:{ex.Message}"); } catch (Exception ex) { return TimeAnchorValidationResult.Failure($"rfc3161-error:{ex.Message}"); } } private static TimeTrustRoot? ValidateAgainstTrustRoots( X509Certificate2 signerCert, IReadOnlyList trustRoots, IReadOnlyList extraCertificates, DateTimeOffset verificationTime) { foreach (var root in trustRoots) { // Match by certificate thumbprint or subject key identifier try { // Try direct certificate match var rootCert = X509CertificateLoader.LoadCertificate(root.PublicKey); if (signerCert.Thumbprint.Equals(rootCert.Thumbprint, StringComparison.OrdinalIgnoreCase)) { return root; } // Try chain validation against root using var chain = new X509Chain(); chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust; chain.ChainPolicy.CustomTrustStore.Add(rootCert); chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; // Offline mode chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority; chain.ChainPolicy.VerificationTime = verificationTime.UtcDateTime; foreach (var cert in extraCertificates) { if (!string.Equals(cert.Thumbprint, rootCert.Thumbprint, StringComparison.OrdinalIgnoreCase)) { chain.ChainPolicy.ExtraStore.Add(cert); } } if (chain.Build(signerCert)) { return root; } } catch { // Invalid root certificate format, try next continue; } } return null; } private static IReadOnlyList BuildExtraCertificates( SignedCms signedCms, TimeTokenVerificationOptions? options) { var extra = new List(); if (options?.CertificateChain is { Count: > 0 }) { extra.AddRange(options.CertificateChain); } foreach (var cert in signedCms.Certificates.Cast()) { if (!extra.Any(existing => existing.Thumbprint.Equals(cert.Thumbprint, StringComparison.OrdinalIgnoreCase))) { extra.Add(cert); } } return extra; } private static bool TryVerifyOfflineRevocation( TimeTokenVerificationOptions options, out string reason) { var hasOcsp = options.OcspResponses.Count > 0; var hasCrl = options.Crls.Count > 0; if (!hasOcsp && !hasCrl) { reason = "rfc3161-revocation-missing"; return false; } if (hasOcsp && options.OcspResponses.Any(IsOcspSuccess)) { reason = "rfc3161-revocation-ocsp"; return true; } if (hasCrl && options.Crls.Any(IsCrlParseable)) { reason = "rfc3161-revocation-crl"; return true; } reason = "rfc3161-revocation-invalid"; return false; } private static bool IsOcspSuccess(byte[] response) { try { var reader = new AsnReader(response, AsnEncodingRules.DER); var sequence = reader.ReadSequence(); var status = sequence.ReadEnumeratedValue(); return status == OcspResponseStatus.Successful; } catch { return false; } } private static bool IsCrlParseable(byte[] crl) { try { var reader = new AsnReader(crl, AsnEncodingRules.DER); reader.ReadSequence(); return true; } catch { return false; } } private static DateTimeOffset? ExtractSigningTime(SignedCms signedCms, SignerInfo signerInfo) { // Try to get signing time from signed attributes foreach (var attr in signerInfo.SignedAttributes) { if (attr.Oid.Value == SigningTimeOid.Value) { try { var reader = new AsnReader(attr.Values[0].RawData, AsnEncodingRules.DER); var time = reader.ReadUtcTime(); return time; } catch { continue; } } } // Try to extract from TSTInfo content try { var content = signedCms.ContentInfo; if (content.ContentType.Value == TstInfoOid.Value) { var tstInfo = ParseTstInfo(content.Content); if (tstInfo.HasValue) { return tstInfo.Value; } } } catch { // Fall through } return null; } private static DateTimeOffset? ParseTstInfo(ReadOnlyMemory tstInfoBytes) { // TSTInfo ::= SEQUENCE { // version INTEGER, // policy OBJECT IDENTIFIER, // messageImprint MessageImprint, // serialNumber INTEGER, // genTime GeneralizedTime, // ... // } try { var reader = new AsnReader(tstInfoBytes, AsnEncodingRules.DER); var sequenceReader = reader.ReadSequence(); // Skip version sequenceReader.ReadInteger(); // Skip policy OID sequenceReader.ReadObjectIdentifier(); // Skip messageImprint (SEQUENCE) sequenceReader.ReadSequence(); // Skip serialNumber sequenceReader.ReadInteger(); // Read genTime (GeneralizedTime) var genTime = sequenceReader.ReadGeneralizedTime(); return genTime; } catch { return null; } } private enum OcspResponseStatus { Successful = 0, MalformedRequest = 1, InternalError = 2, TryLater = 3, SigRequired = 5, Unauthorized = 6 } }