Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- Implemented the PhpAnalyzerPlugin to analyze PHP projects. - Created ComposerLockData class to represent data from composer.lock files. - Developed ComposerLockReader to load and parse composer.lock files asynchronously. - Introduced ComposerPackage class to encapsulate package details. - Added PhpPackage class to represent PHP packages with metadata and evidence. - Implemented PhpPackageCollector to gather packages from ComposerLockData. - Created PhpLanguageAnalyzer to perform analysis and emit results. - Added capability signals for known PHP frameworks and CMS. - Developed unit tests for the PHP language analyzer and its components. - Included sample composer.lock and expected output for testing. - Updated project files for the new PHP analyzer library and tests.
165 lines
6.7 KiB
C#
165 lines
6.7 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|