feat: Implement DefaultCryptoHmac for compliance-aware HMAC operations

- Added DefaultCryptoHmac class implementing ICryptoHmac interface.
- Introduced purpose-based HMAC computation methods.
- Implemented verification methods for HMACs with constant-time comparison.
- Created HmacAlgorithms and HmacPurpose classes for well-known identifiers.
- Added compliance profile support for HMAC algorithms.
- Included asynchronous methods for HMAC computation from streams.
This commit is contained in:
StellaOps Bot
2025-12-06 00:41:04 +02:00
parent 43c281a8b2
commit f0662dd45f
362 changed files with 8441 additions and 22338 deletions

View File

@@ -2,6 +2,7 @@ using System.Text;
using System.Text.Json;
using System.Linq;
using System.Security.Cryptography;
using StellaOps.Cryptography;
namespace StellaOps.Provenance.Attestation;
@@ -69,13 +70,13 @@ public static class CanonicalJson
public static class MerkleTree
{
public static byte[] ComputeRoot(IEnumerable<byte[]> leaves)
public static byte[] ComputeRoot(ICryptoHash cryptoHash, IEnumerable<byte[]> leaves)
{
ArgumentNullException.ThrowIfNull(cryptoHash);
var leafList = leaves?.ToList() ?? throw new ArgumentNullException(nameof(leaves));
if (leafList.Count == 0) throw new ArgumentException("At least one leaf required", nameof(leaves));
var level = leafList.Select(NormalizeLeaf).ToList();
using var sha = SHA256.Create();
var level = leafList.Select(data => NormalizeLeaf(cryptoHash, data)).ToList();
while (level.Count > 1)
{
@@ -87,19 +88,18 @@ public static class MerkleTree
var combined = new byte[left.Length + right.Length];
Buffer.BlockCopy(left, 0, combined, 0, left.Length);
Buffer.BlockCopy(right, 0, combined, left.Length, right.Length);
next.Add(sha.ComputeHash(combined));
next.Add(cryptoHash.ComputeHashForPurpose(combined, HashPurpose.Merkle));
}
level = next;
}
return level[0];
}
static byte[] NormalizeLeaf(byte[] data)
{
if (data.Length == 32) return data;
using var sha = SHA256.Create();
return sha.ComputeHash(data);
}
private static byte[] NormalizeLeaf(ICryptoHash cryptoHash, byte[] data)
{
if (data.Length == 32) return data;
return cryptoHash.ComputeHashForPurpose(data, HashPurpose.Merkle);
}
}
@@ -114,32 +114,34 @@ public static class BuildStatementFactory
public static class BuildStatementDigest
{
public static byte[] ComputeSha256(BuildStatement statement)
public static byte[] ComputeHash(ICryptoHash cryptoHash, BuildStatement statement)
{
ArgumentNullException.ThrowIfNull(cryptoHash);
ArgumentNullException.ThrowIfNull(statement);
var canonicalBytes = CanonicalJson.SerializeToUtf8Bytes(statement);
return SHA256.HashData(canonicalBytes);
return cryptoHash.ComputeHashForPurpose(canonicalBytes, HashPurpose.Attestation);
}
public static string ComputeSha256Hex(BuildStatement statement)
public static string ComputeHashHex(ICryptoHash cryptoHash, BuildStatement statement)
{
return Convert.ToHexString(ComputeSha256(statement)).ToLowerInvariant();
return Convert.ToHexStringLower(ComputeHash(cryptoHash, statement));
}
public static byte[] ComputeMerkleRoot(IEnumerable<BuildStatement> statements)
public static byte[] ComputeMerkleRoot(ICryptoHash cryptoHash, IEnumerable<BuildStatement> statements)
{
ArgumentNullException.ThrowIfNull(cryptoHash);
ArgumentNullException.ThrowIfNull(statements);
var leaves = statements.Select(ComputeSha256).ToArray();
var leaves = statements.Select(s => ComputeHash(cryptoHash, s)).ToArray();
if (leaves.Length == 0)
{
throw new ArgumentException("At least one build statement required", nameof(statements));
}
return MerkleTree.ComputeRoot(leaves);
return MerkleTree.ComputeRoot(cryptoHash, leaves);
}
public static string ComputeMerkleRootHex(IEnumerable<BuildStatement> statements)
public static string ComputeMerkleRootHex(ICryptoHash cryptoHash, IEnumerable<BuildStatement> statements)
{
return Convert.ToHexString(ComputeMerkleRoot(statements)).ToLowerInvariant();
return Convert.ToHexStringLower(ComputeMerkleRoot(cryptoHash, statements));
}
}

View File

@@ -1,4 +1,4 @@
using System.Security.Cryptography;
using StellaOps.Cryptography;
namespace StellaOps.Provenance.Attestation;
@@ -40,12 +40,14 @@ public sealed class NullAuditSink : IAuditSink
public sealed class HmacSigner : ISigner
{
private readonly IKeyProvider _keyProvider;
private readonly ICryptoHmac _cryptoHmac;
private readonly IAuditSink _audit;
private readonly TimeProvider _timeProvider;
public HmacSigner(IKeyProvider keyProvider, IAuditSink? audit = null, TimeProvider? timeProvider = null)
public HmacSigner(IKeyProvider keyProvider, ICryptoHmac cryptoHmac, IAuditSink? audit = null, TimeProvider? timeProvider = null)
{
_keyProvider = keyProvider ?? throw new ArgumentNullException(nameof(keyProvider));
_cryptoHmac = cryptoHmac ?? throw new ArgumentNullException(nameof(cryptoHmac));
_audit = audit ?? NullAuditSink.Instance;
_timeProvider = timeProvider ?? TimeProvider.System;
}
@@ -71,8 +73,7 @@ public sealed class HmacSigner : ISigner
// (predicateType enforcement happens at PromotionAttestationBuilder layer)
}
using var hmac = new HMACSHA256(_keyProvider.KeyMaterial);
var signature = hmac.ComputeHash(request.Payload);
var signature = _cryptoHmac.ComputeHmacForPurpose(_keyProvider.KeyMaterial, request.Payload, HmacPurpose.Signing);
var signedAt = _timeProvider.GetUtcNow();
_audit.LogSigned(_keyProvider.KeyId, request.ContentType, request.Claims, signedAt);

View File

@@ -6,4 +6,8 @@
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\__Libraries\StellaOps.Cryptography\StellaOps.Cryptography.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,5 +1,6 @@
using System.Security.Cryptography;
using System.Linq;
using StellaOps.Cryptography;
namespace StellaOps.Provenance.Attestation;
@@ -56,14 +57,15 @@ public sealed class HmacVerifier : IVerifier
public static class MerkleRootVerifier
{
public static VerificationResult VerifyRoot(IEnumerable<byte[]> leaves, byte[] expectedRoot, TimeProvider? timeProvider = null)
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(leafList);
var computed = MerkleTree.ComputeRoot(cryptoHash, leafList);
var ok = CryptographicOperations.FixedTimeEquals(computed, expectedRoot);
return new VerificationResult(ok, ok ? "verified" : "merkle root mismatch", provider.GetUtcNow());
}
@@ -73,10 +75,11 @@ public static class ChainOfCustodyVerifier
{
/// <summary>
/// Verifies a simple chain-of-custody where each hop is hashed onto the previous aggregate.
/// head = SHA256(hopN || ... || hop1)
/// head = Hash(hopN || ... || hop1) using the active compliance profile's attestation algorithm.
/// </summary>
public static VerificationResult Verify(IEnumerable<byte[]> hops, byte[] expectedHead, TimeProvider? timeProvider = null)
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));
@@ -87,11 +90,10 @@ public static class ChainOfCustodyVerifier
return new VerificationResult(false, "no hops", provider.GetUtcNow());
}
using var sha = SHA256.Create();
byte[] aggregate = Array.Empty<byte>();
foreach (var hop in list)
{
aggregate = sha.ComputeHash(aggregate.Concat(hop).ToArray());
aggregate = cryptoHash.ComputeHashForPurpose(aggregate.Concat(hop).ToArray(), HashPurpose.Attestation);
}
var ok = CryptographicOperations.FixedTimeEquals(aggregate, expectedHead);

View File

@@ -1,6 +1,7 @@
using System.Security.Cryptography;
using System.Text;
using FluentAssertions;
using StellaOps.Cryptography;
using StellaOps.Provenance.Attestation;
using Xunit;
@@ -8,6 +9,8 @@ namespace StellaOps.Provenance.Attestation.Tests;
public class MerkleTreeTests
{
private readonly ICryptoHash _cryptoHash = DefaultCryptoHash.CreateForTests();
[Fact]
public void Computes_deterministic_root_for_same_inputs()
{
@@ -18,8 +21,8 @@ public class MerkleTreeTests
Encoding.UTF8.GetBytes("c")
};
var root1 = MerkleTree.ComputeRoot(leaves);
var root2 = MerkleTree.ComputeRoot(leaves);
var root1 = MerkleTree.ComputeRoot(_cryptoHash, leaves);
var root2 = MerkleTree.ComputeRoot(_cryptoHash, leaves);
root1.Should().BeEquivalentTo(root2);
}
@@ -28,10 +31,10 @@ public class MerkleTreeTests
public void Normalizes_non_hash_leaves()
{
var leaves = new[] { Encoding.UTF8.GetBytes("single") };
var root = MerkleTree.ComputeRoot(leaves);
var root = MerkleTree.ComputeRoot(_cryptoHash, leaves);
using var sha = SHA256.Create();
var expected = sha.ComputeHash(leaves[0]);
// For FIPS profile (default test profile), expect SHA-256
var expected = _cryptoHash.ComputeHashForPurpose(leaves[0], HashPurpose.Merkle);
root.Should().BeEquivalentTo(expected);
}

View File

@@ -4,6 +4,7 @@ using System.IO;
using System.Linq;
using System.Text.Json;
using FluentAssertions;
using StellaOps.Cryptography;
using StellaOps.Provenance.Attestation;
using Xunit;
@@ -11,6 +12,8 @@ namespace StellaOps.Provenance.Attestation.Tests;
public class SampleStatementDigestTests
{
private readonly ICryptoHash _cryptoHash = DefaultCryptoHash.CreateForTests();
private static readonly JsonSerializerOptions SerializerOptions = new()
{
PropertyNamingPolicy = null,
@@ -55,8 +58,9 @@ public class SampleStatementDigestTests
}
[Fact]
public void Sha256_hashes_match_expected_samples()
public void Hashes_match_expected_samples()
{
// Expected hashes using FIPS profile (SHA-256 for attestation purpose)
var expectations = new Dictionary<string, string>(StringComparer.Ordinal)
{
["build-statement-sample.json"] = "3d9f673803f711940f47c85b33ad9776dc90bdfaf58922903cc9bd401b9f56b0",
@@ -67,7 +71,7 @@ public class SampleStatementDigestTests
foreach (var (name, statement) in LoadSamples())
{
BuildStatementDigest.ComputeSha256Hex(statement)
BuildStatementDigest.ComputeHashHex(_cryptoHash, statement)
.Should()
.Be(expectations[name], because: $"{name} hash must be deterministic");
}
@@ -77,7 +81,7 @@ public class SampleStatementDigestTests
public void Merkle_root_is_stable_across_sample_set()
{
var statements = LoadSamples().Select(pair => pair.Statement).ToArray();
BuildStatementDigest.ComputeMerkleRootHex(statements)
BuildStatementDigest.ComputeMerkleRootHex(_cryptoHash, statements)
.Should()
.Be("958465d432c9c8497f9ea5c1476cc7f2bea2a87d3ca37d8293586bf73922dd73");
}

View File

@@ -11,6 +11,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../StellaOps.Provenance.Attestation/StellaOps.Provenance.Attestation.csproj" />
<ProjectReference Include="../../../../src/__Libraries/StellaOps.Cryptography/StellaOps.Cryptography.csproj" />
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />