101 lines
4.0 KiB
C#
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");
|
|
}
|
|
}
|