using StellaOps.Cryptography; using System.Linq; using System.Security.Cryptography; namespace StellaOps.Provenance.Attestation; public sealed record VerificationResult(bool IsValid, string Reason, DateTimeOffset VerifiedAt); public interface IVerifier { Task VerifyAsync(SignRequest request, SignResult signature, CancellationToken cancellationToken = default); } public sealed class HmacVerifier : IVerifier { private readonly IKeyProvider _keyProvider; private readonly TimeProvider _timeProvider; private readonly TimeSpan _maxClockSkew; public HmacVerifier(IKeyProvider keyProvider, TimeProvider? timeProvider = null, TimeSpan? maxClockSkew = null) { _keyProvider = keyProvider ?? throw new ArgumentNullException(nameof(keyProvider)); _timeProvider = timeProvider ?? TimeProvider.System; _maxClockSkew = maxClockSkew ?? TimeSpan.FromMinutes(5); } public Task VerifyAsync(SignRequest request, SignResult signature, CancellationToken cancellationToken = default) { if (request is null) throw new ArgumentNullException(nameof(request)); if (signature is null) throw new ArgumentNullException(nameof(signature)); using var hmac = new HMACSHA256(_keyProvider.KeyMaterial); var expected = hmac.ComputeHash(request.Payload); var ok = CryptographicOperations.FixedTimeEquals(expected, signature.Signature) && string.Equals(_keyProvider.KeyId, signature.KeyId, StringComparison.Ordinal); // enforce not-after validity and basic clock skew checks for offline verification var now = _timeProvider.GetUtcNow(); if (_keyProvider.NotAfter.HasValue && signature.SignedAt > _keyProvider.NotAfter.Value) { ok = false; } if (signature.SignedAt - now > _maxClockSkew) { ok = false; } var result = new VerificationResult( IsValid: ok, Reason: ok ? "verified" : "signature or time invalid", VerifiedAt: _timeProvider.GetUtcNow()); return Task.FromResult(result); } } public static class MerkleRootVerifier { public static VerificationResult VerifyRoot(ICryptoHash cryptoHash, IEnumerable leaves, byte[] expectedRoot, TimeProvider? timeProvider = null) { ArgumentNullException.ThrowIfNull(cryptoHash); var provider = timeProvider ?? TimeProvider.System; if (leaves is null) throw new ArgumentNullException(nameof(leaves)); if (expectedRoot is null) throw new ArgumentNullException(nameof(expectedRoot)); var leafList = leaves.ToList(); var computed = MerkleTree.ComputeRoot(cryptoHash, leafList); var ok = CryptographicOperations.FixedTimeEquals(computed, expectedRoot); return new VerificationResult(ok, ok ? "verified" : "merkle root mismatch", provider.GetUtcNow()); } } public static class ChainOfCustodyVerifier { /// /// Verifies a simple chain-of-custody where each hop is hashed onto the previous aggregate. /// head = Hash(hopN || ... || hop1) using the active compliance profile's attestation algorithm. /// public static VerificationResult Verify(ICryptoHash cryptoHash, IEnumerable hops, byte[] expectedHead, TimeProvider? timeProvider = null) { ArgumentNullException.ThrowIfNull(cryptoHash); var provider = timeProvider ?? TimeProvider.System; if (hops is null) throw new ArgumentNullException(nameof(hops)); if (expectedHead is null) throw new ArgumentNullException(nameof(expectedHead)); var list = hops.ToList(); if (list.Count == 0) { return new VerificationResult(false, "no hops", provider.GetUtcNow()); } byte[] aggregate = Array.Empty(); foreach (var hop in list) { aggregate = cryptoHash.ComputeHashForPurpose(aggregate.Concat(hop).ToArray(), HashPurpose.Attestation); } var ok = CryptographicOperations.FixedTimeEquals(aggregate, expectedHead); return new VerificationResult(ok, ok ? "verified" : "chain mismatch", provider.GetUtcNow()); } }