Files
git.stella-ops.org/src/Attestor/__Tests/StellaOps.Provenance.Attestation.Tests/CosignAndKmsSignerTests.cs

101 lines
4.0 KiB
C#

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");
}
}