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
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:
@@ -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"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user