up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
sdk-generator-smoke / sdk-smoke (push) Has been cancelled
SDK Publish & Sign / sdk-publish (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-11-27 07:46:56 +02:00
parent d63af51f84
commit ea970ead2a
302 changed files with 43161 additions and 1534 deletions

View File

@@ -0,0 +1,364 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.Json;
using FluentAssertions;
using StellaOps.Signer.Core;
using Xunit;
namespace StellaOps.Signer.Tests.Signing;
public sealed class SignerStatementBuilderTests
{
[Fact]
public void BuildStatementPayload_CreatesValidStatement()
{
// Arrange
var request = CreateSigningRequest();
// Act
var payload = SignerStatementBuilder.BuildStatementPayload(request);
// Assert
payload.Should().NotBeNullOrEmpty();
var json = Encoding.UTF8.GetString(payload);
using var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
root.GetProperty("_type").GetString().Should().Be("https://in-toto.io/Statement/v0.1");
root.GetProperty("predicateType").GetString().Should().Be("https://slsa.dev/provenance/v0.2");
root.GetProperty("subject").GetArrayLength().Should().Be(1);
}
[Fact]
public void BuildStatementPayload_UsesDeterministicSerialization()
{
// Arrange
var request = CreateSigningRequest();
// Act
var payload1 = SignerStatementBuilder.BuildStatementPayload(request);
var payload2 = SignerStatementBuilder.BuildStatementPayload(request);
// Assert - Same input should produce identical output
payload1.Should().BeEquivalentTo(payload2);
}
[Fact]
public void BuildStatementPayload_SortsDigestKeys()
{
// Arrange - Use unsorted digest keys
var predicate = JsonDocument.Parse("""{"builder": {"id": "test"}}""");
var request = new SigningRequest(
Subjects:
[
new SigningSubject("artifact.tar.gz", new Dictionary<string, string>
{
["SHA512"] = "xyz789",
["sha256"] = "abc123",
["MD5"] = "def456"
})
],
PredicateType: PredicateTypes.SlsaProvenanceV02,
Predicate: predicate,
ScannerImageDigest: "sha256:scanner",
ProofOfEntitlement: new ProofOfEntitlement(SignerPoEFormat.Jwt, "token"),
Options: new SigningOptions(SigningMode.Keyless, null, "bundle"));
// Act
var payload = SignerStatementBuilder.BuildStatementPayload(request);
var json = Encoding.UTF8.GetString(payload);
// Assert - Digest keys should be lowercase and sorted alphabetically
json.Should().Contain("\"md5\"");
json.Should().Contain("\"sha256\"");
json.Should().Contain("\"sha512\"");
// Verify order: md5 < sha256 < sha512
var md5Index = json.IndexOf("\"md5\"", StringComparison.Ordinal);
var sha256Index = json.IndexOf("\"sha256\"", StringComparison.Ordinal);
var sha512Index = json.IndexOf("\"sha512\"", StringComparison.Ordinal);
md5Index.Should().BeLessThan(sha256Index);
sha256Index.Should().BeLessThan(sha512Index);
}
[Fact]
public void BuildStatementPayload_WithExplicitStatementType_UsesProvided()
{
// Arrange
var request = CreateSigningRequest();
// Act
var payload = SignerStatementBuilder.BuildStatementPayload(request, "https://in-toto.io/Statement/v1");
var json = Encoding.UTF8.GetString(payload);
// Assert
using var doc = JsonDocument.Parse(json);
doc.RootElement.GetProperty("_type").GetString().Should().Be("https://in-toto.io/Statement/v1");
}
[Fact]
public void BuildStatement_ReturnsInTotoStatement()
{
// Arrange
var request = CreateSigningRequest();
// Act
var statement = SignerStatementBuilder.BuildStatement(request);
// Assert
statement.Should().NotBeNull();
statement.Type.Should().Be("https://in-toto.io/Statement/v0.1");
statement.PredicateType.Should().Be(PredicateTypes.SlsaProvenanceV02);
statement.Subject.Should().HaveCount(1);
statement.Subject[0].Name.Should().Be("artifact.tar.gz");
statement.Predicate.ValueKind.Should().Be(JsonValueKind.Object);
}
[Fact]
public void BuildStatementPayload_ThrowsArgumentNullException_WhenRequestIsNull()
{
// Act
var act = () => SignerStatementBuilder.BuildStatementPayload(null!);
// Assert
act.Should().Throw<ArgumentNullException>()
.WithParameterName("request");
}
[Fact]
public void BuildStatementPayload_WithStatementType_ThrowsWhenTypeIsEmpty()
{
// Arrange
var request = CreateSigningRequest();
// Act
var act = () => SignerStatementBuilder.BuildStatementPayload(request, "");
// Assert
act.Should().Throw<ArgumentException>()
.WithParameterName("statementType");
}
[Theory]
[InlineData(PredicateTypes.StellaOpsPromotion, true)]
[InlineData(PredicateTypes.StellaOpsSbom, true)]
[InlineData(PredicateTypes.StellaOpsVex, true)]
[InlineData(PredicateTypes.StellaOpsReplay, true)]
[InlineData(PredicateTypes.StellaOpsPolicy, true)]
[InlineData(PredicateTypes.StellaOpsEvidence, true)]
[InlineData(PredicateTypes.StellaOpsVexDecision, true)]
[InlineData(PredicateTypes.StellaOpsGraph, true)]
[InlineData(PredicateTypes.SlsaProvenanceV02, true)]
[InlineData(PredicateTypes.SlsaProvenanceV1, true)]
[InlineData(PredicateTypes.CycloneDxSbom, true)]
[InlineData(PredicateTypes.SpdxSbom, true)]
[InlineData(PredicateTypes.OpenVex, true)]
[InlineData("custom/predicate@v1", false)]
[InlineData("", false)]
[InlineData(null, false)]
public void IsWellKnownPredicateType_ReturnsExpected(string? predicateType, bool expected)
{
// Act
var result = SignerStatementBuilder.IsWellKnownPredicateType(predicateType!);
// Assert
result.Should().Be(expected);
}
[Theory]
[InlineData(PredicateTypes.SlsaProvenanceV1, "https://in-toto.io/Statement/v1")]
[InlineData(PredicateTypes.StellaOpsPromotion, "https://in-toto.io/Statement/v1")]
[InlineData(PredicateTypes.StellaOpsSbom, "https://in-toto.io/Statement/v1")]
[InlineData(PredicateTypes.SlsaProvenanceV02, "https://in-toto.io/Statement/v0.1")]
[InlineData(PredicateTypes.CycloneDxSbom, "https://in-toto.io/Statement/v0.1")]
public void GetRecommendedStatementType_ReturnsCorrectVersion(string predicateType, string expectedStatementType)
{
// Act
var result = SignerStatementBuilder.GetRecommendedStatementType(predicateType);
// Assert
result.Should().Be(expectedStatementType);
}
[Fact]
public void PredicateTypes_IsStellaOpsType_IdentifiesStellaOpsTypes()
{
// Assert
PredicateTypes.IsStellaOpsType("stella.ops/promotion@v1").Should().BeTrue();
PredicateTypes.IsStellaOpsType("stella.ops/custom@v2").Should().BeTrue();
PredicateTypes.IsStellaOpsType("https://slsa.dev/provenance/v1").Should().BeFalse();
PredicateTypes.IsStellaOpsType(null!).Should().BeFalse();
}
[Fact]
public void PredicateTypes_IsSlsaProvenance_IdentifiesSlsaTypes()
{
// Assert
PredicateTypes.IsSlsaProvenance("https://slsa.dev/provenance/v0.2").Should().BeTrue();
PredicateTypes.IsSlsaProvenance("https://slsa.dev/provenance/v1").Should().BeTrue();
PredicateTypes.IsSlsaProvenance("https://slsa.dev/provenance/v2").Should().BeTrue();
PredicateTypes.IsSlsaProvenance("stella.ops/promotion@v1").Should().BeFalse();
PredicateTypes.IsSlsaProvenance(null!).Should().BeFalse();
}
[Fact]
public void PredicateTypes_IsVexRelatedType_IdentifiesVexTypes()
{
// Assert
PredicateTypes.IsVexRelatedType(PredicateTypes.StellaOpsVex).Should().BeTrue();
PredicateTypes.IsVexRelatedType(PredicateTypes.StellaOpsVexDecision).Should().BeTrue();
PredicateTypes.IsVexRelatedType(PredicateTypes.OpenVex).Should().BeTrue();
PredicateTypes.IsVexRelatedType(PredicateTypes.StellaOpsSbom).Should().BeFalse();
PredicateTypes.IsVexRelatedType(PredicateTypes.StellaOpsGraph).Should().BeFalse();
PredicateTypes.IsVexRelatedType(null!).Should().BeFalse();
}
[Fact]
public void PredicateTypes_IsReachabilityRelatedType_IdentifiesReachabilityTypes()
{
// Assert
PredicateTypes.IsReachabilityRelatedType(PredicateTypes.StellaOpsGraph).Should().BeTrue();
PredicateTypes.IsReachabilityRelatedType(PredicateTypes.StellaOpsReplay).Should().BeTrue();
PredicateTypes.IsReachabilityRelatedType(PredicateTypes.StellaOpsEvidence).Should().BeTrue();
PredicateTypes.IsReachabilityRelatedType(PredicateTypes.StellaOpsVex).Should().BeFalse();
PredicateTypes.IsReachabilityRelatedType(PredicateTypes.StellaOpsSbom).Should().BeFalse();
PredicateTypes.IsReachabilityRelatedType(null!).Should().BeFalse();
}
[Fact]
public void PredicateTypes_GetAllowedPredicateTypes_ReturnsAllKnownTypes()
{
// Act
var allowedTypes = PredicateTypes.GetAllowedPredicateTypes();
// Assert
allowedTypes.Should().Contain(PredicateTypes.StellaOpsPromotion);
allowedTypes.Should().Contain(PredicateTypes.StellaOpsSbom);
allowedTypes.Should().Contain(PredicateTypes.StellaOpsVex);
allowedTypes.Should().Contain(PredicateTypes.StellaOpsReplay);
allowedTypes.Should().Contain(PredicateTypes.StellaOpsPolicy);
allowedTypes.Should().Contain(PredicateTypes.StellaOpsEvidence);
allowedTypes.Should().Contain(PredicateTypes.StellaOpsVexDecision);
allowedTypes.Should().Contain(PredicateTypes.StellaOpsGraph);
allowedTypes.Should().Contain(PredicateTypes.SlsaProvenanceV02);
allowedTypes.Should().Contain(PredicateTypes.SlsaProvenanceV1);
allowedTypes.Should().Contain(PredicateTypes.CycloneDxSbom);
allowedTypes.Should().Contain(PredicateTypes.SpdxSbom);
allowedTypes.Should().Contain(PredicateTypes.OpenVex);
allowedTypes.Should().HaveCount(13);
}
[Theory]
[InlineData(PredicateTypes.StellaOpsVexDecision, true)]
[InlineData(PredicateTypes.StellaOpsGraph, true)]
[InlineData(PredicateTypes.StellaOpsPromotion, true)]
[InlineData("custom/predicate@v1", false)]
[InlineData("", false)]
public void PredicateTypes_IsAllowedPredicateType_ReturnsExpected(string predicateType, bool expected)
{
// Act
var result = PredicateTypes.IsAllowedPredicateType(predicateType);
// Assert
result.Should().Be(expected);
}
[Fact]
public void BuildStatementPayload_HandlesMultipleSubjects()
{
// Arrange
var predicate = JsonDocument.Parse("""{"builder": {"id": "test"}}""");
var request = new SigningRequest(
Subjects:
[
new SigningSubject("artifact1.tar.gz", new Dictionary<string, string>
{
["sha256"] = "hash1"
}),
new SigningSubject("artifact2.tar.gz", new Dictionary<string, string>
{
["sha256"] = "hash2"
}),
new SigningSubject("artifact3.tar.gz", new Dictionary<string, string>
{
["sha256"] = "hash3"
})
],
PredicateType: PredicateTypes.SlsaProvenanceV02,
Predicate: predicate,
ScannerImageDigest: "sha256:scanner",
ProofOfEntitlement: new ProofOfEntitlement(SignerPoEFormat.Jwt, "token"),
Options: new SigningOptions(SigningMode.Keyless, null, "bundle"));
// Act
var payload = SignerStatementBuilder.BuildStatementPayload(request);
var json = Encoding.UTF8.GetString(payload);
// Assert
using var doc = JsonDocument.Parse(json);
doc.RootElement.GetProperty("subject").GetArrayLength().Should().Be(3);
}
[Fact]
public void BuildStatementPayload_PreservesPredicateContent()
{
// Arrange
var predicateContent = """
{
"builder": { "id": "https://github.com/actions" },
"buildType": "https://github.com/Attestations/GitHubActionsWorkflow@v1",
"invocation": {
"configSource": {
"uri": "git+https://github.com/test/repo@refs/heads/main"
}
}
}
""";
var predicate = JsonDocument.Parse(predicateContent);
var request = new SigningRequest(
Subjects:
[
new SigningSubject("artifact.tar.gz", new Dictionary<string, string>
{
["sha256"] = "abc123"
})
],
PredicateType: PredicateTypes.SlsaProvenanceV02,
Predicate: predicate,
ScannerImageDigest: "sha256:scanner",
ProofOfEntitlement: new ProofOfEntitlement(SignerPoEFormat.Jwt, "token"),
Options: new SigningOptions(SigningMode.Keyless, null, "bundle"));
// Act
var payload = SignerStatementBuilder.BuildStatementPayload(request);
var json = Encoding.UTF8.GetString(payload);
// Assert
using var doc = JsonDocument.Parse(json);
var resultPredicate = doc.RootElement.GetProperty("predicate");
resultPredicate.GetProperty("builder").GetProperty("id").GetString()
.Should().Be("https://github.com/actions");
resultPredicate.GetProperty("buildType").GetString()
.Should().Be("https://github.com/Attestations/GitHubActionsWorkflow@v1");
}
private static SigningRequest CreateSigningRequest()
{
var predicate = JsonDocument.Parse("""{"builder": {"id": "test-builder"}, "invocation": {}}""");
return new SigningRequest(
Subjects:
[
new SigningSubject("artifact.tar.gz", new Dictionary<string, string>
{
["sha256"] = "abc123def456"
})
],
PredicateType: PredicateTypes.SlsaProvenanceV02,
Predicate: predicate,
ScannerImageDigest: "sha256:scanner123",
ProofOfEntitlement: new ProofOfEntitlement(SignerPoEFormat.Jwt, "token"),
Options: new SigningOptions(SigningMode.Keyless, 3600, "bundle"));
}
}