104 lines
4.2 KiB
C#
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());
|
|
}
|
|
}
|