Files
git.stella-ops.org/src/Attestor/StellaOps.Provenance.Attestation/Verification.cs

104 lines
4.2 KiB
C#

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<VerificationResult> 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<VerificationResult> 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<byte[]> 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
{
/// <summary>
/// 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.
/// </summary>
public static VerificationResult Verify(ICryptoHash cryptoHash, IEnumerable<byte[]> 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<byte>();
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());
}
}