consolidation of some of the modules, localization fixes, product advisories work, qa work
This commit is contained in:
@@ -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\"}}");
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -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>();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -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>
|
||||
@@ -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. |
|
||||
Binary file not shown.
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
믯疿楳杮匠獹整敔瑸഻甊楳杮匠整汬佡獰倮潲敶慮据瑁整瑳瑡潩㭮獵湩畘楮㭴獵湩瑓汥慬灏敔瑳楋㭴慮敭灳捡瑓汥慬灏牐癯湥湡散䄮瑴獥慴楴湯吮獥獴഻ഊ瀊扵楬敳污摥挠慬獳吠潯䕬瑮祲潰湩呴獥獴ൻ †嬠牔楡⡴䌢瑡来牯≹敔瑳慃整潧楲獥售楮⥴൝ †嬠慆瑣൝ †瀠扵楬獡湹慔歳删湵獁湹彣敒畴湲䥳癮污摩湏楍獳湩䅧杲⡳ഩ †笠††††慶潣敤㴠愠慷瑩吠潯䕬瑮祲潰湩畒䅮祳据䄨牲祡䔮灭祴猼牴湩㹧⤨敔瑸牗瑩牥丮汵ⱬ渠睥匠牴湩坧楲整⡲Ⱙ渠睥吠獥呴浩健潲楶敤⡲慄整楔敭晏獦瑥售捴潎⥷㬩††††獁敳瑲䔮畱污ㄨ潣敤㬩††ൽഊ †嬠牔楡⡴䌢瑡来牯≹敔瑳慃整潧楲獥售楮⥴൝ †嬠慆瑣൝ †瀠扵楬獡湹慔歳删湵獁湹彣敖楲楦獥慖楬卤杩慮畴敲⤨††ൻ †††瘠牡瀠祡潬摡㴠䔠据摯湩呕㡆䜮瑥祂整⡳瀢祡潬摡⤢഻ †††瘠牡欠祥㴠䌠湯敶瑲吮䡯硥瑓楲杮䔨据摯湩呕㡆䜮瑥祂整⡳猢捥敲≴⤩഻ †††甠楳杮瘠牡栠慭‽敮⁷祓瑳浥匮捥牵瑩牃灹潴牧灡票䠮䅍千䅈㔲⠶湅潣楤杮售䙔⸸敇䉴瑹獥∨敳牣瑥⤢㬩††††慶楳‽潃癮牥潔效卸牴湩⡧浨捡䌮浯異整慈桳瀨祡潬摡⤩഻ഊ †††瘠牡琠灭㴠倠瑡敇呴浥䙰汩乥浡⡥㬩††††睡楡⁴楆敬圮楲整汁䉬瑹獥獁湹⡣浴Ɒ瀠祡潬摡㬩††††慶瑳潤瑵㴠渠睥匠牴湩坧楲整⡲㬩††††慶潣敤㴠愠慷瑩吠潯䕬瑮祲潰湩畒䅮祳据渨睥嵛††††ൻ †††††∠ⴭ慰汹慯≤浴Ɒ††††††ⴢ猭杩慮畴敲栭硥Ⱒ猠杩ബ †††††∠ⴭ敫敨≸敫ⱹ††††††ⴢ猭杩敮ⵤ瑡Ⱒ∠〲㔲ㄭⴱ㈲ご㨰〰〺娰ഢ †††素瑳潤瑵敮⁷瑓楲杮牗瑩牥⤨敮⁷敔瑳楔敭牐癯摩牥渨睥䐠瑡呥浩佥晦敳⡴〲㔲ㄬⰱ㈲〬〬〬听浩卥慰敚潲⤩㬩††††獁敳瑲䔮畱污〨潣敤㬩††††獁敳瑲䌮湯慴湩⡳尢瘢污摩≜琺畲≥瑳潤瑵吮卯牴湩⡧⤩഻ †素ൽ
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
믯疿楳杮匠獹整敔瑸഻甊楳杮匠整汬佡獰倮潲敶慮据瑁整瑳瑡潩㭮獵湩畘楮㭴獵湩瑓汥慬灏敔瑳楋㭴慮敭灳捡瑓汥慬灏牐癯湥湡散䄮瑴獥慴楴湯吮獥獴഻ഊ瀊扵楬敳污摥挠慬獳嘠牥晩捩瑡潩䱮扩慲祲敔瑳൳笊††呛慲瑩∨慃整潧祲Ⱒ吠獥䍴瑡来牯敩湕瑩崩††††䙛捡嵴††異汢捩愠祳据吠獡浈捡敖楲楦牥䙟楡獬桗湥敋䕹灸物摥⤨††ൻ †††瘠牡欠祥㴠渠睥䤠䵮浥牯䭹祥牐癯摩牥∨ㅫⰢ䔠据摯湩呕㡆䜮瑥祂整⡳猢捥敲≴Ⱙ䐠瑡呥浩佥晦敳瑕乣睯䄮摤楍畮整⡳ㄭ⤩഻ †††瘠牡瘠牥晩敩‽敮⁷浈捡敖楲楦牥欨祥敮⁷敔瑳楔敭牐癯摩牥䐨瑡呥浩佥晦敳瑕乣睯⤩഻ഊ †††瘠牡爠煥敵瑳㴠渠睥匠杩剮煥敵瑳䔨据摯湩呕㡆䜮瑥祂整⡳瀢祡潬摡⤢挢≴㬩††††慶楳湧牥㴠渠睥䠠慭卣杩敮⡲敫ⱹ琠浩健潲楶敤㩲渠睥吠獥呴浩健潲楶敤⡲慄整楔敭晏獦瑥售捴潎摁䵤湩瑵獥⤲⤩഻ †††瘠牡猠杩慮畴敲㴠愠慷瑩猠杩敮楓湧獁湹⡣敲畱獥⥴഻ഊ †††瘠牡爠獥汵⁴‽睡楡⁴敶楲楦牥嘮牥晩䅹祳据爨煥敵瑳楳湧瑡牵⥥഻ഊ †††䄠獳牥慆獬⡥敲畳瑬䤮噳污摩㬩††††獁敳瑲䌮湯慴湩⡳琢浩≥敲畳瑬刮慥潳⥮഻ †素††呛慲瑩∨慃整潧祲Ⱒ吠獥䍴瑡来牯敩湕瑩崩††††䙛捡嵴††異汢捩愠祳据吠獡浈捡敖楲楦牥䙟楡獬桗湥汃捯卫敫呷潯慌杲⡥ഩ †笠††††慶潮⁷‽敮⁷慄整楔敭晏獦瑥㈨㈰ⰵㄠⰱ㈠ⰲㄠⰲ〠ⰰ吠浩卥慰敚潲㬩††††慶敫⁹‽敮⁷湉敍潭祲敋偹潲楶敤⡲欢Ⱒ䔠据摯湩呕㡆䜮瑥祂整⡳猢捥敲≴⤩഻ †††瘠牡猠杩敮‽敮⁷浈捡楓湧牥欨祥楴敭牐癯摩牥›敮⁷敔瑳楔敭牐癯摩牥渨睯䄮摤楍畮整⡳〱⤩㬩††††慶敲畱獥⁴‽敮⁷楓湧敒畱獥⡴湅潣楤杮售䙔⸸敇䉴瑹獥∨慰汹慯≤Ⱙ∠瑣⤢഻ †††瘠牡猠杩㴠愠慷瑩猠杩敮楓湧獁湹⡣敲畱獥⥴഻ഊ †††瘠牡瘠牥晩敩‽敮⁷浈捡敖楲楦牥欨祥敮⁷敔瑳楔敭牐癯摩牥渨睯Ⱙ吠浩卥慰牆浯楍畮整⡳⤵㬩††††慶敲畳瑬㴠愠慷瑩瘠牥晩敩敖楲祦獁湹⡣敲畱獥ⱴ猠杩㬩††††獁敳瑲䘮污敳爨獥汵獉慖楬⥤഻ †素††呛慲瑩∨慃整潧祲Ⱒ吠獥䍴瑡来牯敩湕瑩崩††††䙛捡嵴††異汢捩瘠楯敍歲敬潒瑯敖楲楦牥䑟瑥捥獴楍浳瑡档⤨††ൻ †††瘠牡氠慥敶‽敮孷൝ †††笠††††††湅潣楤杮售䙔⸸敇䉴瑹獥∨≡Ⱙ††††††湅潣楤杮售䙔⸸敇䉴瑹獥∨≢Ⱙ††††††湅潣楤杮售䙔⸸敇䉴瑹獥∨≣ഩ †††素഻ †††瘠牡攠灸捥整‽潃癮牥牆浯效卸牴湩⡧〢∰㬩††††慶敲畳瑬㴠䴠牥汫剥潯噴牥晩敩敖楲祦潒瑯氨慥敶ⱳ攠灸捥整Ɽ渠睥吠獥呴浩健潲楶敤⡲慄整楔敭晏獦瑥售捴潎⥷㬩††††獁敳瑲䘮污敳爨獥汵獉慖楬⥤഻ †††䄠獳牥煅慵⡬洢牥汫潲瑯洠獩慭捴≨敲畳瑬刮慥潳⥮഻ †素††呛慲瑩∨慃整潧祲Ⱒ吠獥䍴瑡来牯敩湕瑩崩††††䙛捡嵴††異汢捩瘠楯桃楡佮䍦獵潴祤敖楲楦牥䍟浯異整䅳杧敲慧整⤨††ൻ †††瘠牡栠灯‽敮孷൝ †††笠††††††湅潣楤杮售䙔⸸敇䉴瑹獥∨潨ㅰ⤢ബ †††††䔠据摯湩呕㡆䜮瑥祂整⡳栢灯∲ഩ †††素഻ഊ †††甠楳杮瘠牡猠慨㴠匠獹整敓畣楲祴䌮祲瑰杯慲桰䡓㉁㘵䌮敲瑡⡥㬩††††慶条牧来瑡‽桳潃灭瑵䡥獡⡨牁慲浅瑰㱹祢整⠾⸩潃据瑡栨灯孳崰⸩潔牁慲⡹⤩഻ †††愠杧敲慧整㴠猠慨䌮浯異整慈桳愨杧敲慧整䌮湯慣⡴潨獰ㅛ⥝吮䅯牲祡⤨㬩††††慶敲畳瑬㴠䌠慨湩晏畃瑳摯噹牥晩敩敖楲祦栨灯ⱳ愠杧敲慧整敮⁷敔瑳楔敭牐癯摩牥䐨瑡呥浩佥晦敳瑕乣睯⤩഻ †††䄠獳牥牔敵爨獥汵獉慖楬⥤഻ †素ൽ
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user