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:
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -6,4 +6,8 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Cryptography\StellaOps.Cryptography.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user