consolidation of some of the modules, localization fixes, product advisories work, qa work

This commit is contained in:
master
2026-03-05 03:54:22 +02:00
parent 7bafcc3eef
commit 8e1cb9448d
3878 changed files with 72600 additions and 46861 deletions

View File

@@ -0,0 +1,29 @@
using System.Collections.Generic;
using FluentAssertions;
using StellaOps.Provenance.Attestation;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Provenance.Attestation.Tests;
public class CanonicalJsonTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Canonicalizes_property_order_and_omits_nulls()
{
var model = new BuildDefinition(
BuildType: "https://slsa.dev/provenance/v1",
ExternalParameters: new Dictionary<string, string>
{
["b"] = "2",
["a"] = "1",
["c"] = "3"
},
ResolvedDependencies: null);
var json = CanonicalJson.SerializeToString(model);
json.Should().Be("{\"BuildType\":\"https://slsa.dev/provenance/v1\",\"ExternalParameters\":{\"a\":\"1\",\"b\":\"2\",\"c\":\"3\"}}");
}
}

View File

@@ -0,0 +1,100 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using StellaOps.Provenance.Attestation;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Provenance.Attestation.Tests;
public class CosignAndKmsSignerTests
{
private sealed class FakeCosignClient : ICosignClient
{
public List<(byte[] payload, string contentType, string keyRef)> Calls { get; } = new();
public Task<byte[]> SignAsync(byte[] payload, string contentType, string keyRef, CancellationToken cancellationToken)
{
Calls.Add((payload, contentType, keyRef));
return Task.FromResult(Encoding.UTF8.GetBytes("cosign-" + keyRef));
}
}
private sealed class FakeKmsClient : IKmsClient
{
public List<(byte[] payload, string contentType, string keyId)> Calls { get; } = new();
public Task<byte[]> SignAsync(byte[] payload, string contentType, string keyId, CancellationToken cancellationToken)
{
Calls.Add((payload, contentType, keyId));
return Task.FromResult(Encoding.UTF8.GetBytes("kms-" + keyId));
}
}
private sealed class FixedTimeProvider : TimeProvider
{
private readonly DateTimeOffset _now;
public FixedTimeProvider(DateTimeOffset now) => _now = now;
public override DateTimeOffset GetUtcNow() => _now;
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task CosignSigner_enforces_required_claims_and_logs()
{
var client = new FakeCosignClient();
var audit = new InMemoryAuditSink();
var signer = new CosignSigner("cosign-key", client, audit, new FixedTimeProvider(DateTimeOffset.UnixEpoch));
var request = new SignRequest(
Payload: Encoding.UTF8.GetBytes("payload"),
ContentType: "application/vnd.dsse",
Claims: new Dictionary<string, string> { ["sub"] = "artifact" },
RequiredClaims: new[] { "sub" });
var result = await signer.SignAsync(request);
result.KeyId.Should().Be("cosign-key");
result.Signature.Should().BeEquivalentTo(Encoding.UTF8.GetBytes("cosign-cosign-key"));
audit.Signed.Should().ContainSingle();
client.Calls.Should().ContainSingle(call => call.keyRef == "cosign-key" && call.contentType == "application/vnd.dsse");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task CosignSigner_throws_on_missing_required_claim()
{
var client = new FakeCosignClient();
var audit = new InMemoryAuditSink();
var signer = new CosignSigner("cosign-key", client, audit, new FixedTimeProvider(DateTimeOffset.UnixEpoch));
var request = new SignRequest(
Payload: Encoding.UTF8.GetBytes("payload"),
ContentType: "application/vnd.dsse",
Claims: new Dictionary<string, string>(),
RequiredClaims: new[] { "sub" });
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => signer.SignAsync(request));
ex.Message.Should().Contain("sub");
audit.Missing.Should().ContainSingle(m => m.claim == "sub");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task KmsSigner_signs_with_current_key_and_logs()
{
var kms = new FakeKmsClient();
var key = new InMemoryKeyProvider("kms-key-1", Encoding.UTF8.GetBytes("secret-kms"));
var audit = new InMemoryAuditSink();
var signer = new KmsSigner(kms, key, audit, new FixedTimeProvider(DateTimeOffset.UnixEpoch));
var request = new SignRequest(Encoding.UTF8.GetBytes("payload"), "application/vnd.dsse");
var result = await signer.SignAsync(request);
result.KeyId.Should().Be("kms-key-1");
result.Signature.Should().BeEquivalentTo(Encoding.UTF8.GetBytes("kms-kms-key-1"));
audit.Signed.Should().ContainSingle();
kms.Calls.Should().ContainSingle(call => call.keyId == "kms-key-1");
}
}

View File

@@ -0,0 +1,25 @@
using System;
using FluentAssertions;
using StellaOps.Provenance.Attestation;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Provenance.Attestation.Tests;
public class HexTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Parses_even_length_hex()
{
Hex.FromHex("0A0b").Should().BeEquivalentTo(new byte[] { 0x0A, 0x0B });
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Throws_on_odd_length()
{
Action act = () => Hex.FromHex("ABC");
act.Should().Throw<FormatException>();
}
}

View File

@@ -0,0 +1,44 @@
using System.Security.Cryptography;
using System.Text;
using FluentAssertions;
using StellaOps.Cryptography;
using StellaOps.Provenance.Attestation;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Provenance.Attestation.Tests;
public class MerkleTreeTests
{
private readonly ICryptoHash _cryptoHash = DefaultCryptoHash.CreateForTests();
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Computes_deterministic_root_for_same_inputs()
{
var leaves = new[]
{
Encoding.UTF8.GetBytes("a"),
Encoding.UTF8.GetBytes("b"),
Encoding.UTF8.GetBytes("c")
};
var root1 = MerkleTree.ComputeRoot(_cryptoHash, leaves);
var root2 = MerkleTree.ComputeRoot(_cryptoHash, leaves);
root1.Should().BeEquivalentTo(root2);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Normalizes_non_hash_leaves()
{
var leaves = new[] { Encoding.UTF8.GetBytes("single") };
var root = MerkleTree.ComputeRoot(_cryptoHash, leaves);
// For FIPS profile (default test profile), expect SHA-256
var expected = _cryptoHash.ComputeHashForPurpose(leaves[0], HashPurpose.Merkle);
root.Should().BeEquivalentTo(expected);
}
}

View File

@@ -0,0 +1,56 @@
using System.Text;
using System.Collections.Generic;
using FluentAssertions;
using System.Threading.Tasks;
using StellaOps.Provenance.Attestation;
using StellaOps.Cryptography;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Provenance.Attestation.Tests;
public class PromotionAttestationBuilderTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Produces_canonical_json_for_predicate()
{
var predicate = new PromotionPredicate(
ImageDigest: "sha256:img",
SbomDigest: "sha256:sbom",
VexDigest: "sha256:vex",
PromotionId: "prom-1",
RekorEntry: "uuid",
// Intentionally shuffled input order; canonical JSON must be sorted.
Metadata: new Dictionary<string, string> { { "env", "prod" }, { "region", "us-east" } });
var bytes = PromotionAttestationBuilder.CreateCanonicalJson(predicate);
var json = Encoding.UTF8.GetString(bytes);
json.Should().Be("{\"ImageDigest\":\"sha256:img\",\"Metadata\":{\"env\":\"prod\",\"region\":\"us-east\"},\"PromotionId\":\"prom-1\",\"RekorEntry\":\"uuid\",\"SbomDigest\":\"sha256:sbom\",\"VexDigest\":\"sha256:vex\"}");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task BuildAsync_adds_predicate_claim_and_signs_payload()
{
var predicate = new PromotionPredicate(
ImageDigest: "sha256:img",
SbomDigest: "sha256:sbom",
VexDigest: "sha256:vex",
PromotionId: "prom-1");
var key = new InMemoryKeyProvider("kid-1", Encoding.UTF8.GetBytes("secret"));
var signer = new HmacSigner(key, DefaultCryptoHmac.CreateForTests());
var attestation = await PromotionAttestationBuilder.BuildAsync(
predicate,
signer,
claims: new Dictionary<string, string> { { "traceId", "abc123" } });
attestation.Payload.Should().BeEquivalentTo(PromotionAttestationBuilder.CreateCanonicalJson(predicate));
attestation.Signature.KeyId.Should().Be("kid-1");
attestation.Signature.Claims.Should().ContainKey("predicateType").WhoseValue.Should().Be(PromotionAttestationBuilder.PredicateType);
attestation.Signature.Claims.Should().ContainKey("traceId").WhoseValue.Should().Be("abc123");
}
}

View File

@@ -0,0 +1,48 @@
using System;
using System.Text;
using System.Collections.Generic;
using System.Threading.Tasks;
using FluentAssertions;
using StellaOps.Provenance.Attestation;
using StellaOps.Cryptography;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Provenance.Attestation.Tests;
/// <summary>
/// Tests for rotating signers in provenance attestation.
/// Note: Key rotation logic is primarily covered in StellaOps.Signer.Tests.
/// These tests validate integration with provenance-specific types.
/// </summary>
public sealed class RotatingSignerTests
{
private sealed class TestTimeProvider : TimeProvider
{
private DateTimeOffset _now;
public TestTimeProvider(DateTimeOffset now) => _now = now;
public void Advance(TimeSpan delta) => _now = _now.Add(delta);
public override DateTimeOffset GetUtcNow() => _now;
}
/// <summary>
/// Validates that the RotatingKeyProvider type exists and integrates correctly.
/// Detailed rotation behavior is tested in StellaOps.Signer.Tests.
/// </summary>
[Trait("Category", TestCategories.Unit)]
[Fact]
public void RotatingKeyProvider_CanBeInstantiated()
{
// Arrange
var t = new TestTimeProvider(DateTimeOffset.Parse("2025-11-17T00:00:00Z"));
var keyOld = new InMemoryKeyProvider("k1", Encoding.UTF8.GetBytes("old"), t.GetUtcNow().AddMinutes(-1));
var keyNew = new InMemoryKeyProvider("k2", Encoding.UTF8.GetBytes("new"), t.GetUtcNow().AddHours(1));
var audit = new InMemoryAuditSink();
// Act
var rotating = new RotatingKeyProvider(new[] { keyOld, keyNew }, t, audit);
// Assert - just verifies construction works
rotating.Should().NotBeNull();
}
}

View File

@@ -0,0 +1,91 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using FluentAssertions;
using StellaOps.Cryptography;
using StellaOps.Provenance.Attestation;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Provenance.Attestation.Tests;
public class SampleStatementDigestTests
{
private readonly ICryptoHash _cryptoHash = DefaultCryptoHash.CreateForTests();
private static readonly JsonSerializerOptions SerializerOptions = new()
{
PropertyNamingPolicy = null,
WriteIndented = false
};
private static string RepoRoot
{
get
{
var dir = AppContext.BaseDirectory;
while (!string.IsNullOrEmpty(dir))
{
var candidate = Path.Combine(dir, "samples", "provenance");
if (Directory.Exists(candidate))
{
return dir;
}
var parent = Directory.GetParent(dir);
dir = parent?.FullName ?? string.Empty;
}
throw new DirectoryNotFoundException("Could not locate repository root containing samples/provenance.");
}
}
private static IEnumerable<(string Name, BuildStatement Statement)> LoadSamples()
{
var samplesDir = Path.Combine(RepoRoot, "samples", "provenance");
foreach (var path in Directory.EnumerateFiles(samplesDir, "*.json").OrderBy(p => p, StringComparer.Ordinal))
{
var json = File.ReadAllText(path);
var statement = JsonSerializer.Deserialize<BuildStatement>(json, SerializerOptions);
if (statement is null)
{
continue;
}
yield return (Path.GetFileName(path), statement);
}
}
[Trait("Category", TestCategories.Unit)]
[Fact]
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",
["export-service-statement.json"] = "fa73e8664566d45497d4c18d439b42ff38b1ed6e3e25ca8e29001d1201f1d41b",
["job-runner-statement.json"] = "27a5b433c320fed2984166641390953d02b9204ed1d75076ec9c000e04f3a82a",
["orchestrator-statement.json"] = "d79467d03da33d0b8f848d7a340c8cde845802bad7dadcb553125e8553615b28"
};
foreach (var (name, statement) in LoadSamples())
{
BuildStatementDigest.ComputeHashHex(_cryptoHash, statement)
.Should()
.Be(expectations[name], because: $"{name} hash must be deterministic");
}
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Merkle_root_is_stable_across_sample_set()
{
var statements = LoadSamples().Select(pair => pair.Statement).ToArray();
BuildStatementDigest.ComputeMerkleRootHex(_cryptoHash, statements)
.Should()
.Be("958465d432c9c8497f9ea5c1476cc7f2bea2a87d3ca37d8293586bf73922dd73");
}
}

View File

@@ -0,0 +1,51 @@
using System;
using System.Text;
using System.Threading.Tasks;
using System.Collections.Generic;
using FluentAssertions;
using StellaOps.Provenance.Attestation;
using StellaOps.Cryptography;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Provenance.Attestation.Tests;
public class SignerTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task HmacSigner_is_deterministic_for_same_input()
{
var key = new InMemoryKeyProvider("test-key", Encoding.UTF8.GetBytes("secret"));
var audit = new InMemoryAuditSink();
var signer = new HmacSigner(key, DefaultCryptoHmac.CreateForTests(), audit, TimeProvider.System);
var request = new SignRequest(Encoding.UTF8.GetBytes("payload"), "application/json");
var r1 = await signer.SignAsync(request);
var r2 = await signer.SignAsync(request);
r1.Signature.Should().BeEquivalentTo(r2.Signature);
r1.KeyId.Should().Be("test-key");
audit.Signed.Should().HaveCount(2);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task HmacSigner_enforces_required_claims()
{
var key = new InMemoryKeyProvider("test-key", Encoding.UTF8.GetBytes("secret"));
var audit = new InMemoryAuditSink();
var signer = new HmacSigner(key, DefaultCryptoHmac.CreateForTests(), audit, TimeProvider.System);
var request = new SignRequest(
Payload: Encoding.UTF8.GetBytes("payload"),
ContentType: "application/json",
Claims: new Dictionary<string, string> { ["foo"] = "bar" },
RequiredClaims: new[] { "foo", "bar" });
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => signer.SignAsync(request));
ex.Message.Should().Contain("bar");
audit.Missing.Should().ContainSingle(m => m.claim == "bar");
}
}

View File

@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<IsPackable>false</IsPackable>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../../StellaOps.Provenance.Attestation/StellaOps.Provenance.Attestation.csproj" />
<ProjectReference Include="../../StellaOps.Provenance.Attestation.Tool/StellaOps.Provenance.Attestation.Tool.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.Cryptography/StellaOps.Cryptography.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
<PackageReference Include="FluentAssertions" />
</ItemGroup>
<ItemGroup>
<None Include="Fixtures\**\*" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,8 @@
# StellaOps.Provenance.Attestation.Tests Task Board
This board mirrors active sprint tasks for this module.
Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_solid_review.md`.
| Task ID | Status | Notes |
| --- | --- | --- |
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/Provenance/__Tests/StellaOps.Provenance.Attestation.Tests/StellaOps.Provenance.Attestation.Tests.md. |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |

View File

@@ -0,0 +1,43 @@
using System.Text;
using StellaOps.Provenance.Attestation;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Provenance.Attestation.Tests;
public sealed class ToolEntrypointTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task RunAsync_ReturnsInvalidOnMissingArgs()
{
var code = await ToolEntrypoint.RunAsync(Array.Empty<string>(), TextWriter.Null, new StringWriter(), new TestTimeProvider(DateTimeOffset.UtcNow));
Assert.Equal(1, code);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task RunAsync_VerifiesValidSignature()
{
var payload = Encoding.UTF8.GetBytes("payload");
var key = Convert.ToHexString(Encoding.UTF8.GetBytes("secret"));
using var hmac = new System.Security.Cryptography.HMACSHA256(Encoding.UTF8.GetBytes("secret"));
var sig = Convert.ToHexString(hmac.ComputeHash(payload));
var tmp = Path.GetTempFileName();
await File.WriteAllBytesAsync(tmp, payload);
var stdout = new StringWriter();
var code = await ToolEntrypoint.RunAsync(new[]
{
"--payload", tmp,
"--signature-hex", sig,
"--key-hex", key,
"--signed-at", "2025-11-22T00:00:00Z"
}, stdout, new StringWriter(), new TestTimeProvider(new DateTimeOffset(2025,11,22,0,0,0,TimeSpan.Zero)));
Assert.Equal(0, code);
Assert.Contains("\"valid\":true", stdout.ToString());
}
}

View File

@@ -0,0 +1 @@
믯疿楳杮匠獹整⹭敔瑸഻甊楳杮匠整汬佡獰倮潲敶慮据⹥瑁整瑳瑡潩㭮਍獵湩⁧畘楮㭴਍਍਍獵湩⁧瑓汥慬灏⹳敔瑳楋㭴਍慮敭灳捡⁥瑓汥慬灏⹳牐癯湥湡散䄮瑴獥慴楴湯吮獥獴഻ഊ瀊扵楬⁣敳污摥挠慬獳吠潯䕬瑮祲潰湩呴獥獴਍ൻ †嬠牔楡⡴䌢瑡来牯≹‬敔瑳慃整潧楲獥售楮⥴൝ †嬠慆瑣൝ †瀠扵楬⁣獡湹⁣慔歳删湵獁湹彣敒畴湲䥳癮污摩湏楍獳湩䅧杲⡳ഩ †笠਍††††慶⁲潣敤㴠愠慷瑩吠潯䕬瑮祲潰湩⹴畒䅮祳据䄨牲祡䔮灭祴猼牴湩㹧⤨‬敔瑸牗瑩牥丮汵ⱬ渠睥匠牴湩坧楲整⡲Ⱙ渠睥吠獥呴浩健潲楶敤⡲慄整楔敭晏獦瑥售捴潎⥷㬩਍††††獁敳瑲䔮畱污ㄨ‬潣敤㬩਍††ൽഊ †嬠牔楡⡴䌢瑡来牯≹‬敔瑳慃整潧楲獥售楮⥴൝ †嬠慆瑣൝ †瀠扵楬⁣獡湹⁣慔歳删湵獁湹彣敖楲楦獥慖楬卤杩慮畴敲⤨਍††ൻ †††瘠牡瀠祡潬摡㴠䔠据摯湩⹧呕㡆䜮瑥祂整⡳瀢祡潬摡⤢഻ †††瘠牡欠祥㴠䌠湯敶瑲吮䡯硥瑓楲杮䔨据摯湩⹧呕㡆䜮瑥祂整⡳猢捥敲≴⤩഻ †††甠楳杮瘠牡栠慭⁣‽敮⁷祓瑳浥匮捥牵瑩⹹牃灹潴牧灡票䠮䅍千䅈㔲⠶湅潣楤杮售䙔⸸敇䉴瑹獥∨敳牣瑥⤢㬩਍††††慶⁲楳⁧‽潃癮牥⹴潔效卸牴湩⡧浨捡䌮浯異整慈桳瀨祡潬摡⤩഻ഊ †††瘠牡琠灭㴠倠瑡⹨敇呴浥䙰汩乥浡⡥㬩਍††††睡楡⁴楆敬圮楲整汁䉬瑹獥獁湹⡣浴Ɒ瀠祡潬摡㬩਍਍††††慶⁲瑳潤瑵㴠渠睥匠牴湩坧楲整⡲㬩਍††††慶⁲潣敤㴠愠慷瑩吠潯䕬瑮祲潰湩⹴畒䅮祳据渨睥嵛਍††††ൻ †††††∠ⴭ慰汹慯≤‬浴Ɒ਍††††††ⴢ猭杩慮畴敲栭硥Ⱒ猠杩ബ †††††∠ⴭ敫⵹敨≸‬敫ⱹ਍††††††ⴢ猭杩敮ⵤ瑡Ⱒ∠〲㔲ㄭⴱ㈲ご㨰〰〺娰ഢ †††素‬瑳潤瑵‬敮⁷瑓楲杮牗瑩牥⤨‬敮⁷敔瑳楔敭牐癯摩牥渨睥䐠瑡呥浩佥晦敳⡴〲㔲ㄬⰱ㈲〬〬〬听浩卥慰⹮敚潲⤩㬩਍਍††††獁敳瑲䔮畱污〨‬潣敤㬩਍††††獁敳瑲䌮湯慴湩⡳尢瘢污摩≜琺畲≥‬瑳潤瑵吮卯牴湩⡧⤩഻ †素਍ൽ

View File

@@ -0,0 +1,126 @@
using System.Text;
using StellaOps.Cryptography;
using StellaOps.Provenance.Attestation;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Provenance.Attestation.Tests;
public sealed class VerificationLibraryTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task HmacVerifier_FailsWhenKeyExpired()
{
// Key expired 2 minutes ago, signature made 1 minute ago (after key expired)
var now = DateTimeOffset.UtcNow;
var key = new InMemoryKeyProvider("k1", Encoding.UTF8.GetBytes("secret"), now.AddMinutes(-2)); // expired 2 min ago
var verifier = new HmacVerifier(key, new TestTimeProvider(now));
var request = new SignRequest(Encoding.UTF8.GetBytes("payload"), "ct");
// Sign at 1 min ago (after key expiry)
var signer = new HmacSigner(key, new FakeCryptoHmac(), timeProvider: new TestTimeProvider(now.AddMinutes(-1)));
var signature = await signer.SignAsync(request);
var result = await verifier.VerifyAsync(request, signature);
Assert.False(result.IsValid);
Assert.Contains("time", result.Reason, StringComparison.OrdinalIgnoreCase);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task HmacVerifier_FailsWhenClockSkewTooLarge()
{
var now = new DateTimeOffset(2025, 11, 22, 12, 0, 0, TimeSpan.Zero);
var key = new InMemoryKeyProvider("k", Encoding.UTF8.GetBytes("secret"));
var signer = new HmacSigner(key, new FakeCryptoHmac(), timeProvider: new TestTimeProvider(now.AddMinutes(10)));
var request = new SignRequest(Encoding.UTF8.GetBytes("payload"), "ct");
var sig = await signer.SignAsync(request);
var verifier = new HmacVerifier(key, new TestTimeProvider(now), TimeSpan.FromMinutes(5));
var result = await verifier.VerifyAsync(request, sig);
Assert.False(result.IsValid);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void MerkleRootVerifier_DetectsMismatch()
{
var leaves = new[]
{
Encoding.UTF8.GetBytes("a"),
Encoding.UTF8.GetBytes("b"),
Encoding.UTF8.GetBytes("c")
};
var expected = Convert.FromHexString("00");
var cryptoHash = DefaultCryptoHash.CreateForTests();
var result = MerkleRootVerifier.VerifyRoot(cryptoHash, leaves, expected, new TestTimeProvider(DateTimeOffset.UtcNow));
Assert.False(result.IsValid);
Assert.Equal("merkle root mismatch", result.Reason);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ChainOfCustodyVerifier_ComputesAggregate()
{
var hops = new[]
{
Encoding.UTF8.GetBytes("hop1"),
Encoding.UTF8.GetBytes("hop2")
};
var cryptoHash = DefaultCryptoHash.CreateForTests();
using var sha = System.Security.Cryptography.SHA256.Create();
var aggregate = sha.ComputeHash(Array.Empty<byte>().Concat(hops[0]).ToArray());
aggregate = sha.ComputeHash(aggregate.Concat(hops[1]).ToArray());
var result = ChainOfCustodyVerifier.Verify(cryptoHash, hops, aggregate, new TestTimeProvider(DateTimeOffset.UtcNow));
Assert.True(result.IsValid);
}
private sealed class FakeCryptoHmac : ICryptoHmac
{
public byte[] ComputeHmacForPurpose(ReadOnlySpan<byte> key, ReadOnlySpan<byte> data, string purpose)
{
using var hmac = new System.Security.Cryptography.HMACSHA256(key.ToArray());
return hmac.ComputeHash(data.ToArray());
}
public string ComputeHmacHexForPurpose(ReadOnlySpan<byte> key, ReadOnlySpan<byte> data, string purpose)
=> Convert.ToHexStringLower(ComputeHmacForPurpose(key, data, purpose));
public string ComputeHmacBase64ForPurpose(ReadOnlySpan<byte> key, ReadOnlySpan<byte> data, string purpose)
=> Convert.ToBase64String(ComputeHmacForPurpose(key, data, purpose));
public async ValueTask<byte[]> ComputeHmacForPurposeAsync(ReadOnlyMemory<byte> key, Stream stream, string purpose, CancellationToken cancellationToken = default)
{
using var ms = new MemoryStream();
await stream.CopyToAsync(ms, cancellationToken);
return ComputeHmacForPurpose(key.Span, ms.ToArray(), purpose);
}
public async ValueTask<string> ComputeHmacHexForPurposeAsync(ReadOnlyMemory<byte> key, Stream stream, string purpose, CancellationToken cancellationToken = default)
=> Convert.ToHexStringLower(await ComputeHmacForPurposeAsync(key, stream, purpose, cancellationToken));
public async ValueTask<string> ComputeHmacBase64ForPurposeAsync(ReadOnlyMemory<byte> key, Stream stream, string purpose, CancellationToken cancellationToken = default)
=> Convert.ToBase64String(await ComputeHmacForPurposeAsync(key, stream, purpose, cancellationToken));
public bool VerifyHmacForPurpose(ReadOnlySpan<byte> key, ReadOnlySpan<byte> data, ReadOnlySpan<byte> expectedHmac, string purpose)
=> ComputeHmacForPurpose(key, data, purpose).AsSpan().SequenceEqual(expectedHmac);
public bool VerifyHmacHexForPurpose(ReadOnlySpan<byte> key, ReadOnlySpan<byte> data, string expectedHmacHex, string purpose)
=> ComputeHmacHexForPurpose(key, data, purpose).Equals(expectedHmacHex, StringComparison.OrdinalIgnoreCase);
public bool VerifyHmacBase64ForPurpose(ReadOnlySpan<byte> key, ReadOnlySpan<byte> data, string expectedHmacBase64, string purpose)
=> ComputeHmacBase64ForPurpose(key, data, purpose).Equals(expectedHmacBase64, StringComparison.Ordinal);
public string GetAlgorithmForPurpose(string purpose) => "HMACSHA256";
public int GetOutputLengthForPurpose(string purpose) => 32;
}
}

View File

@@ -0,0 +1 @@
믯疿楳杮匠獹整⹭敔瑸഻甊楳杮匠整汬佡獰倮潲敶慮据⹥瑁整瑳瑡潩㭮਍獵湩⁧畘楮㭴਍਍਍獵湩⁧瑓汥慬灏⹳敔瑳楋㭴਍慮敭灳捡⁥瑓汥慬灏⹳牐癯湥湡散䄮瑴獥慴楴湯吮獥獴഻ഊ瀊扵楬⁣敳污摥挠慬獳嘠牥晩捩瑡潩䱮扩慲祲敔瑳൳笊਍††呛慲瑩∨慃整潧祲Ⱒ吠獥䍴瑡来牯敩⹳湕瑩崩਍††††䙛捡嵴਍††異汢捩愠祳据吠獡浈捡敖楲楦牥䙟楡獬桗湥敋䕹灸物摥⤨਍††ൻ †††瘠牡欠祥㴠渠睥䤠䵮浥牯䭹祥牐癯摩牥∨ㅫⰢ䔠据摯湩⹧呕㡆䜮瑥祂整⡳猢捥敲≴Ⱙ䐠瑡呥浩佥晦敳⹴瑕乣睯䄮摤楍畮整⡳ㄭ⤩഻ †††瘠牡瘠牥晩敩⁲‽敮⁷浈捡敖楲楦牥欨祥‬敮⁷敔瑳楔敭牐癯摩牥䐨瑡呥浩佥晦敳⹴瑕乣睯⤩഻ഊ †††瘠牡爠煥敵瑳㴠渠睥匠杩剮煥敵瑳䔨据摯湩⹧呕㡆䜮瑥祂整⡳瀢祡潬摡⤢‬挢≴㬩਍††††慶⁲楳湧牥㴠渠睥䠠慭卣杩敮⡲敫ⱹ琠浩健潲楶敤㩲渠睥吠獥呴浩健潲楶敤⡲慄整楔敭晏獦瑥售捴潎⹷摁䵤湩瑵獥⴨⤲⤩഻ †††瘠牡猠杩慮畴敲㴠愠慷瑩猠杩敮⹲楓湧獁湹⡣敲畱獥⥴഻ഊ †††瘠牡爠獥汵⁴‽睡楡⁴敶楲楦牥嘮牥晩䅹祳据爨煥敵瑳‬楳湧瑡牵⥥഻ഊ †††䄠獳牥⹴慆獬⡥敲畳瑬䤮噳污摩㬩਍††††獁敳瑲䌮湯慴湩⡳琢浩≥‬敲畳瑬刮慥潳⥮഻ †素਍਍††呛慲瑩∨慃整潧祲Ⱒ吠獥䍴瑡来牯敩⹳湕瑩崩਍††††䙛捡嵴਍††異汢捩愠祳据吠獡浈捡敖楲楦牥䙟楡獬桗湥汃捯卫敫呷潯慌杲⡥ഩ †笠਍††††慶⁲潮⁷‽敮⁷慄整楔敭晏獦瑥㈨㈰ⰵㄠⰱ㈠ⰲㄠⰲ〠‬ⰰ吠浩卥慰⹮敚潲㬩਍††††慶⁲敫⁹‽敮⁷湉敍潭祲敋偹潲楶敤⡲欢Ⱒ䔠据摯湩⹧呕㡆䜮瑥祂整⡳猢捥敲≴⤩഻ †††瘠牡猠杩敮⁲‽敮⁷浈捡楓湧牥欨祥‬楴敭牐癯摩牥›敮⁷敔瑳楔敭牐癯摩牥渨睯䄮摤楍畮整⡳〱⤩㬩਍††††慶⁲敲畱獥⁴‽敮⁷楓湧敒畱獥⡴湅潣楤杮售䙔⸸敇䉴瑹獥∨慰汹慯≤Ⱙ∠瑣⤢഻ †††瘠牡猠杩㴠愠慷瑩猠杩敮⹲楓湧獁湹⡣敲畱獥⥴഻ഊ †††瘠牡瘠牥晩敩⁲‽敮⁷浈捡敖楲楦牥欨祥‬敮⁷敔瑳楔敭牐癯摩牥渨睯Ⱙ吠浩卥慰⹮牆浯楍畮整⡳⤵㬩਍††††慶⁲敲畳瑬㴠愠慷瑩瘠牥晩敩⹲敖楲祦獁湹⡣敲畱獥ⱴ猠杩㬩਍਍††††獁敳瑲䘮污敳爨獥汵⹴獉慖楬⥤഻ †素਍਍††呛慲瑩∨慃整潧祲Ⱒ吠獥䍴瑡来牯敩⹳湕瑩崩਍††††䙛捡嵴਍††異汢捩瘠楯⁤敍歲敬潒瑯敖楲楦牥䑟瑥捥獴楍浳瑡档⤨਍††ൻ †††瘠牡氠慥敶⁳‽敮孷൝ †††笠਍††††††湅潣楤杮售䙔⸸敇䉴瑹獥∨≡Ⱙ਍††††††湅潣楤杮售䙔⸸敇䉴瑹獥∨≢Ⱙ਍††††††湅潣楤杮售䙔⸸敇䉴瑹獥∨≣ഩ †††素഻ †††瘠牡攠灸捥整⁤‽潃癮牥⹴牆浯效卸牴湩⡧〢∰㬩਍਍††††慶⁲敲畳瑬㴠䴠牥汫剥潯噴牥晩敩⹲敖楲祦潒瑯氨慥敶ⱳ攠灸捥整Ɽ渠睥吠獥呴浩健潲楶敤⡲慄整楔敭晏獦瑥售捴潎⥷㬩਍਍††††獁敳瑲䘮污敳爨獥汵⹴獉慖楬⥤഻ †††䄠獳牥⹴煅慵⡬洢牥汫⁥潲瑯洠獩慭捴≨‬敲畳瑬刮慥潳⥮഻ †素਍਍††呛慲瑩∨慃整潧祲Ⱒ吠獥䍴瑡来牯敩⹳湕瑩崩਍††††䙛捡嵴਍††異汢捩瘠楯⁤桃楡佮䍦獵潴祤敖楲楦牥䍟浯異整䅳杧敲慧整⤨਍††ൻ †††瘠牡栠灯⁳‽敮孷൝ †††笠਍††††††湅潣楤杮售䙔⸸敇䉴瑹獥∨潨ㅰ⤢ബ †††††䔠据摯湩⹧呕㡆䜮瑥祂整⡳栢灯∲ഩ †††素഻ഊ †††甠楳杮瘠牡猠慨㴠匠獹整⹭敓畣楲祴䌮祲瑰杯慲桰⹹䡓㉁㘵䌮敲瑡⡥㬩਍††††慶⁲条牧来瑡⁥‽桳⹡潃灭瑵䡥獡⡨牁慲⹹浅瑰㱹祢整⠾⸩潃据瑡栨灯孳崰⸩潔牁慲⡹⤩഻ †††愠杧敲慧整㴠猠慨䌮浯異整慈桳愨杧敲慧整䌮湯慣⡴潨獰ㅛ⥝吮䅯牲祡⤨㬩਍਍††††慶⁲敲畳瑬㴠䌠慨湩晏畃瑳摯噹牥晩敩⹲敖楲祦栨灯ⱳ愠杧敲慧整‬敮⁷敔瑳楔敭牐癯摩牥䐨瑡呥浩佥晦敳⹴瑕乣睯⤩഻ †††䄠獳牥⹴牔敵爨獥汵⹴獉慖楬⥤഻ †素਍ൽ

View File

@@ -0,0 +1,49 @@
using System.Text;
using FluentAssertions;
using System.Threading.Tasks;
using StellaOps.Provenance.Attestation;
using StellaOps.Cryptography;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Provenance.Attestation.Tests;
public class VerificationTests
{
private const string Payload = "{\"hello\":\"world\"}";
private const string ContentType = "application/json";
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Verifier_accepts_valid_signature()
{
var key = new InMemoryKeyProvider("test-key", Encoding.UTF8.GetBytes("secret"));
var signer = new HmacSigner(key, DefaultCryptoHmac.CreateForTests());
var verifier = new HmacVerifier(key);
var request = new SignRequest(Encoding.UTF8.GetBytes(Payload), ContentType);
var signature = await signer.SignAsync(request);
var result = await verifier.VerifyAsync(request, signature);
result.IsValid.Should().BeTrue();
result.Reason.Should().Be("verified");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Verifier_rejects_tampered_payload()
{
var key = new InMemoryKeyProvider("test-key", Encoding.UTF8.GetBytes("secret"));
var signer = new HmacSigner(key, DefaultCryptoHmac.CreateForTests());
var verifier = new HmacVerifier(key);
var request = new SignRequest(Encoding.UTF8.GetBytes(Payload), ContentType);
var signature = await signer.SignAsync(request);
var tampered = new SignRequest(Encoding.UTF8.GetBytes(Payload + "-tampered"), ContentType);
var result = await verifier.VerifyAsync(tampered, signature);
result.IsValid.Should().BeFalse();
result.Reason.Should().Be("signature or time invalid");
}
}