consolidation of some of the modules, localization fixes, product advisories work, qa work
This commit is contained in:
103
src/Attestor/StellaOps.Provenance.Attestation/Verification.cs
Normal file
103
src/Attestor/StellaOps.Provenance.Attestation/Verification.cs
Normal file
@@ -0,0 +1,103 @@
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user