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