consolidate the tests locations
This commit is contained in:
@@ -0,0 +1 @@
|
||||
AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+Pw==
|
||||
@@ -0,0 +1,78 @@
|
||||
using System.Text;
|
||||
using StellaOps.Provenance.Attestation;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Provenance.Attestation.Tests;
|
||||
|
||||
public sealed class PromotionAttestationBuilderTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task BuildAsync_SignsCanonicalPayloadAndAddsPredicateClaim()
|
||||
{
|
||||
var predicate = new PromotionPredicate(
|
||||
ImageDigest: "sha256:deadbeef",
|
||||
SbomDigest: "sha256:sbom",
|
||||
VexDigest: "sha256:vex",
|
||||
PromotionId: "promo-123",
|
||||
RekorEntry: "rekor:entry",
|
||||
Metadata: new Dictionary<string, string>
|
||||
{
|
||||
{ "z", "last" },
|
||||
{ "a", "first" }
|
||||
});
|
||||
|
||||
var signer = new RecordingSigner();
|
||||
|
||||
var attestation = await PromotionAttestationBuilder.BuildAsync(predicate, signer);
|
||||
|
||||
Assert.Equal(predicate, attestation.Predicate);
|
||||
Assert.NotNull(attestation.Payload);
|
||||
Assert.Equal(PromotionAttestationBuilder.ContentType, signer.LastRequest?.ContentType);
|
||||
Assert.Equal(PromotionAttestationBuilder.PredicateType, signer.LastRequest?.Claims!["predicateType"]);
|
||||
Assert.Equal(attestation.Payload, signer.LastRequest?.Payload);
|
||||
Assert.Equal(attestation.Signature, signer.LastResult);
|
||||
|
||||
// verify canonical order is stable (metadata keys sorted, property names sorted)
|
||||
var canonicalJson = Encoding.UTF8.GetString(attestation.Payload);
|
||||
Assert.Equal(
|
||||
"{\"ImageDigest\":\"sha256:deadbeef\",\"Metadata\":{\"a\":\"first\",\"z\":\"last\"},\"PromotionId\":\"promo-123\",\"RekorEntry\":\"rekor:entry\",\"SbomDigest\":\"sha256:sbom\",\"VexDigest\":\"sha256:vex\"}",
|
||||
canonicalJson);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_MergesClaimsWithoutOverwritingPredicateType()
|
||||
{
|
||||
var predicate = new PromotionPredicate(
|
||||
ImageDigest: "sha256:x",
|
||||
SbomDigest: "sha256:y",
|
||||
VexDigest: "sha256:z",
|
||||
PromotionId: "p-1");
|
||||
|
||||
var signer = new RecordingSigner();
|
||||
var customClaims = new Dictionary<string, string> { { "env", "stage" }, { "predicateType", "custom" } };
|
||||
|
||||
await PromotionAttestationBuilder.BuildAsync(predicate, signer, customClaims);
|
||||
|
||||
Assert.NotNull(signer.LastRequest);
|
||||
Assert.Equal(PromotionAttestationBuilder.PredicateType, signer.LastRequest!.Claims!["predicateType"]);
|
||||
Assert.Equal("stage", signer.LastRequest!.Claims!["env"]);
|
||||
}
|
||||
|
||||
private sealed class RecordingSigner : ISigner
|
||||
{
|
||||
public SignRequest? LastRequest { get; private set; }
|
||||
public SignResult? LastResult { get; private set; }
|
||||
|
||||
public Task<SignResult> SignAsync(SignRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
LastRequest = request;
|
||||
LastResult = new SignResult(
|
||||
Signature: Encoding.UTF8.GetBytes("sig"),
|
||||
KeyId: "key-1",
|
||||
SignedAt: DateTimeOffset.UtcNow,
|
||||
Claims: request.Claims);
|
||||
|
||||
return Task.FromResult(LastResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
using System.Text;
|
||||
using StellaOps.Provenance.Attestation;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Provenance.Attestation.Tests;
|
||||
|
||||
public sealed class SignersTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task HmacSigner_SignsAndAudits()
|
||||
{
|
||||
var key = new InMemoryKeyProvider("k1", Convert.FromHexString("0f0e0d0c0b0a09080706050403020100"));
|
||||
var audit = new InMemoryAuditSink();
|
||||
var time = new TestTimeProvider(new DateTimeOffset(2025, 11, 22, 12, 0, 0, TimeSpan.Zero));
|
||||
var signer = new HmacSigner(key, audit, time);
|
||||
|
||||
var request = new SignRequest(Encoding.UTF8.GetBytes("payload"), "application/json",
|
||||
Claims: new Dictionary<string, string> { { "sub", "builder" } });
|
||||
|
||||
var result = await signer.SignAsync(request);
|
||||
|
||||
Assert.Equal("k1", result.KeyId);
|
||||
Assert.Equal(time.GetUtcNow(), result.SignedAt);
|
||||
Assert.Equal(
|
||||
Convert.FromHexString("b3ae92d9a593318d03d7c4b6dca9710c416f582e88cfc08196d8c2cdabb3c480"),
|
||||
result.Signature);
|
||||
Assert.Single(audit.Signed);
|
||||
Assert.Empty(audit.Missing);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HmacSigner_EnforcesRequiredClaims()
|
||||
{
|
||||
var key = new InMemoryKeyProvider("k-claims", Encoding.UTF8.GetBytes("secret"));
|
||||
var audit = new InMemoryAuditSink();
|
||||
var signer = new HmacSigner(key, audit, new TestTimeProvider(DateTimeOffset.UtcNow));
|
||||
|
||||
var request = new SignRequest(Encoding.UTF8.GetBytes("payload"), "text/plain",
|
||||
Claims: new Dictionary<string, string>(),
|
||||
RequiredClaims: new[] { "sub" });
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => signer.SignAsync(request));
|
||||
Assert.Contains(audit.Missing, x => x.keyId == "k-claims" && x.claim == "sub");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RotatingKeyProvider_LogsRotationWhenNewKeyBecomesActive()
|
||||
{
|
||||
var now = new DateTimeOffset(2025, 11, 22, 10, 0, 0, TimeSpan.Zero);
|
||||
var time = new TestTimeProvider(now);
|
||||
var audit = new InMemoryAuditSink();
|
||||
|
||||
var expiring = new InMemoryKeyProvider("old", new byte[] { 0x01 }, now.AddMinutes(5));
|
||||
var longLived = new InMemoryKeyProvider("new", new byte[] { 0x02 }, now.AddHours(1));
|
||||
|
||||
var provider = new RotatingKeyProvider(new[] { expiring, longLived }, time, audit);
|
||||
var signer = new HmacSigner(provider, audit, time);
|
||||
|
||||
await signer.SignAsync(new SignRequest(new byte[] { 0xAB }, "demo"));
|
||||
|
||||
Assert.Contains(audit.Rotations, r => r.previousKeyId == "old" && r.nextKeyId == "new");
|
||||
Assert.Equal("new", provider.KeyId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CosignSigner_UsesClientAndAudits()
|
||||
{
|
||||
var signatureBytes = Convert.FromBase64String(await File.ReadAllTextAsync(Path.Combine("Fixtures", "cosign.sig"))); // fixture is deterministic
|
||||
var client = new FakeCosignClient(signatureBytes);
|
||||
var audit = new InMemoryAuditSink();
|
||||
var time = new TestTimeProvider(new DateTimeOffset(2025, 11, 22, 13, 0, 0, TimeSpan.Zero));
|
||||
var signer = new CosignSigner("cosign://stella", client, audit, time);
|
||||
|
||||
var request = new SignRequest(Encoding.UTF8.GetBytes("subject"), "application/vnd.stella+json",
|
||||
Claims: new Dictionary<string, string> { { "sub", "artifact" } },
|
||||
RequiredClaims: new[] { "sub" });
|
||||
|
||||
var result = await signer.SignAsync(request);
|
||||
|
||||
Assert.Equal(signatureBytes, result.Signature);
|
||||
Assert.Equal(time.GetUtcNow(), result.SignedAt);
|
||||
Assert.Equal("cosign://stella", result.KeyId);
|
||||
Assert.Single(audit.Signed);
|
||||
Assert.Empty(audit.Missing);
|
||||
|
||||
var call = Assert.Single(client.Calls);
|
||||
Assert.Equal("cosign://stella", call.keyRef);
|
||||
Assert.Equal("application/vnd.stella+json", call.contentType);
|
||||
Assert.Equal(request.Payload, call.payload);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task KmsSigner_EnforcesRequiredClaims()
|
||||
{
|
||||
var signature = new byte[] { 0xCA, 0xFE, 0xBA, 0xBE };
|
||||
var client = new FakeKmsClient(signature);
|
||||
var audit = new InMemoryAuditSink();
|
||||
var key = new InMemoryKeyProvider("kms-1", new byte[] { 0x00 }, DateTimeOffset.UtcNow.AddDays(1));
|
||||
var signer = new KmsSigner(client, key, audit, new TestTimeProvider(DateTimeOffset.UtcNow));
|
||||
|
||||
var request = new SignRequest(Encoding.UTF8.GetBytes("body"), "application/json",
|
||||
Claims: new Dictionary<string, string> { { "aud", "stella" } },
|
||||
RequiredClaims: new[] { "sub" });
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => signer.SignAsync(request));
|
||||
Assert.Contains(audit.Missing, x => x.keyId == "kms-1" && x.claim == "sub");
|
||||
|
||||
var validAudit = new InMemoryAuditSink();
|
||||
var validSigner = new KmsSigner(client, key, validAudit, new TestTimeProvider(DateTimeOffset.UtcNow));
|
||||
var validRequest = new SignRequest(Encoding.UTF8.GetBytes("body"), "application/json",
|
||||
Claims: new Dictionary<string, string> { { "aud", "stella" }, { "sub", "actor" } },
|
||||
RequiredClaims: new[] { "sub" });
|
||||
|
||||
var result = await validSigner.SignAsync(validRequest);
|
||||
|
||||
Assert.Equal(signature, result.Signature);
|
||||
Assert.Equal("kms-1", result.KeyId);
|
||||
Assert.Empty(validAudit.Missing);
|
||||
}
|
||||
|
||||
private sealed class FakeCosignClient : ICosignClient
|
||||
{
|
||||
public List<(byte[] payload, string contentType, string keyRef)> Calls { get; } = new();
|
||||
private readonly byte[] _signature;
|
||||
|
||||
public FakeCosignClient(byte[] signature)
|
||||
{
|
||||
_signature = signature ?? throw new ArgumentNullException(nameof(signature));
|
||||
}
|
||||
|
||||
public Task<byte[]> SignAsync(byte[] payload, string contentType, string keyRef, CancellationToken cancellationToken)
|
||||
{
|
||||
Calls.Add((payload, contentType, keyRef));
|
||||
return Task.FromResult(_signature);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FakeKmsClient : IKmsClient
|
||||
{
|
||||
private readonly byte[] _signature;
|
||||
public List<(byte[] payload, string contentType, string keyId)> Calls { get; } = new();
|
||||
|
||||
public FakeKmsClient(byte[] signature) => _signature = signature;
|
||||
|
||||
public Task<byte[]> SignAsync(byte[] payload, string contentType, string keyId, CancellationToken cancellationToken)
|
||||
{
|
||||
Calls.Add((payload, contentType, keyId));
|
||||
return Task.FromResult(_signature);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../Provenance/StellaOps.Provenance.Attestation/StellaOps.Provenance.Attestation.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="Fixtures/cosign.sig" CopyToOutputDirectory="Always" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,16 @@
|
||||
using System;
|
||||
|
||||
namespace StellaOps.Provenance.Attestation.Tests;
|
||||
|
||||
internal sealed class TestTimeProvider : TimeProvider
|
||||
{
|
||||
private DateTimeOffset _now;
|
||||
|
||||
public TestTimeProvider(DateTimeOffset now) => _now = now;
|
||||
|
||||
public override DateTimeOffset GetUtcNow() => _now;
|
||||
public override TimeZoneInfo LocalTimeZone => TimeZoneInfo.Utc;
|
||||
public override long GetTimestamp() => 0L;
|
||||
|
||||
public void Advance(TimeSpan delta) => _now = _now.Add(delta);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using System.Text;
|
||||
using StellaOps.Provenance.Attestation;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Provenance.Attestation.Tests;
|
||||
|
||||
public sealed class ToolEntrypointTests
|
||||
{
|
||||
[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);
|
||||
}
|
||||
|
||||
[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,73 @@
|
||||
using System.Text;
|
||||
using StellaOps.Provenance.Attestation;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Provenance.Attestation.Tests;
|
||||
|
||||
public sealed class VerificationLibraryTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task HmacVerifier_FailsWhenKeyExpired()
|
||||
{
|
||||
var key = new InMemoryKeyProvider("k1", Encoding.UTF8.GetBytes("secret"), DateTimeOffset.UtcNow.AddMinutes(-1));
|
||||
var verifier = new HmacVerifier(key, new TestTimeProvider(DateTimeOffset.UtcNow));
|
||||
|
||||
var request = new SignRequest(Encoding.UTF8.GetBytes("payload"), "ct");
|
||||
var signer = new HmacSigner(key, timeProvider: new TestTimeProvider(DateTimeOffset.UtcNow.AddMinutes(-2)));
|
||||
var signature = await signer.SignAsync(request);
|
||||
|
||||
var result = await verifier.VerifyAsync(request, signature);
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Contains("time", result.Reason);
|
||||
}
|
||||
|
||||
[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, 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);
|
||||
}
|
||||
|
||||
[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 result = MerkleRootVerifier.VerifyRoot(leaves, expected, new TestTimeProvider(DateTimeOffset.UtcNow));
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Equal("merkle root mismatch", result.Reason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ChainOfCustodyVerifier_ComputesAggregate()
|
||||
{
|
||||
var hops = new[]
|
||||
{
|
||||
Encoding.UTF8.GetBytes("hop1"),
|
||||
Encoding.UTF8.GetBytes("hop2")
|
||||
};
|
||||
|
||||
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(hops, aggregate, new TestTimeProvider(DateTimeOffset.UtcNow));
|
||||
Assert.True(result.IsValid);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user