Add determinism tests for verdict artifact generation and update SHA256 sums script

- Implemented comprehensive tests for verdict artifact generation to ensure deterministic outputs across various scenarios, including identical inputs, parallel execution, and change ordering.
- Created helper methods for generating sample verdict inputs and computing canonical hashes.
- Added tests to validate the stability of canonical hashes, proof spine ordering, and summary statistics.
- Introduced a new PowerShell script to update SHA256 sums for files, ensuring accurate hash generation and file integrity checks.
This commit is contained in:
StellaOps Bot
2025-12-24 02:17:34 +02:00
parent e59921374e
commit 7503c19b8f
390 changed files with 37389 additions and 5380 deletions

View File

@@ -0,0 +1,427 @@
// -----------------------------------------------------------------------------
// CanonicalPayloadDeterminismTests.cs
// Sprint: SPRINT_5100_0009_0006_signer_tests
// Tasks: SIGNER-5100-001, SIGNER-5100-002, SIGNER-5100-003
// Description: Model L0 tests for canonical payload and digest determinism
// -----------------------------------------------------------------------------
using System.Text;
using System.Text.Json;
using FluentAssertions;
using StellaOps.Canonical.Json;
using StellaOps.Signer.Core;
using StellaOps.Signer.Tests.Fixtures;
using Xunit;
namespace StellaOps.Signer.Tests.Signing;
/// <summary>
/// Tests for canonical payload bytes and deterministic hash computation.
/// Implements Model L0 test requirements:
/// - SIGNER-5100-001: Canonical payload bytes snapshot tests for DSSE/in-toto envelopes
/// - SIGNER-5100-002: Stable digest computation tests: same input -> same SHA-256 hash
/// - SIGNER-5100-003: Determinism test: canonical payload hash stable across runs
/// </summary>
[Trait("Category", "Unit")]
[Trait("Category", "Determinism")]
[Trait("Category", "CanonicalPayload")]
public sealed class CanonicalPayloadDeterminismTests
{
// SIGNER-5100-001: Canonical payload bytes snapshot tests
[Fact]
public void InTotoStatement_CanonicalBytes_MatchesExpectedSnapshot()
{
// Arrange - Create a deterministic in-toto statement
var statement = CreateDeterministicInTotoStatement();
// Act
var canonicalBytes = CanonJson.Canonicalize(statement);
var canonicalJson = Encoding.UTF8.GetString(canonicalBytes);
// Assert - Keys should be sorted, no whitespace
canonicalJson.Should().NotContain("\n", "canonical JSON should have no newlines");
canonicalJson.Should().NotContain(" ", "canonical JSON should have no extra spaces");
// Verify key ordering (alphabetical)
var predicateIndex = canonicalJson.IndexOf("\"predicate\"", StringComparison.Ordinal);
var predicateTypeIndex = canonicalJson.IndexOf("\"predicateType\"", StringComparison.Ordinal);
var subjectIndex = canonicalJson.IndexOf("\"subject\"", StringComparison.Ordinal);
var typeIndex = canonicalJson.IndexOf("\"_type\"", StringComparison.Ordinal);
typeIndex.Should().BeLessThan(predicateIndex, "_type should come before predicate (alphabetical)");
predicateIndex.Should().BeLessThan(predicateTypeIndex, "predicate should come before predicateType");
predicateTypeIndex.Should().BeLessThan(subjectIndex, "predicateType should come before subject");
}
[Fact]
public void InTotoStatement_DifferentKeyOrder_ProducesSameCanonicalBytes()
{
// Arrange - Create same data with different key order in source JSON
var json1 = """{"predicateType":"https://slsa.dev/provenance/v1","_type":"https://in-toto.io/Statement/v1","predicate":{"builder":{"id":"test"}},"subject":[{"name":"artifact","digest":{"sha256":"abc123"}}]}""";
var json2 = """{"_type":"https://in-toto.io/Statement/v1","subject":[{"name":"artifact","digest":{"sha256":"abc123"}}],"predicateType":"https://slsa.dev/provenance/v1","predicate":{"builder":{"id":"test"}}}""";
// Act
var bytes1 = CanonJson.CanonicalizeParsedJson(Encoding.UTF8.GetBytes(json1));
var bytes2 = CanonJson.CanonicalizeParsedJson(Encoding.UTF8.GetBytes(json2));
// Assert
bytes1.Should().BeEquivalentTo(bytes2, "canonical bytes should be identical regardless of input key order");
}
[Fact]
public void DsseEnvelope_CanonicalBytes_PayloadTypePreserved()
{
// Arrange
var envelope = CreateDeterministicDsseEnvelope();
// Act
var canonicalBytes = CanonJson.Canonicalize(envelope);
var canonicalJson = Encoding.UTF8.GetString(canonicalBytes);
// Assert
canonicalJson.Should().Contain("\"payloadType\":\"application/vnd.in-toto+json\"");
}
[Fact]
public void DsseEnvelope_CanonicalBytes_SignaturesArrayPreserved()
{
// Arrange
var envelope = CreateDeterministicDsseEnvelope();
// Act
var canonicalBytes = CanonJson.Canonicalize(envelope);
var canonicalJson = Encoding.UTF8.GetString(canonicalBytes);
// Assert
canonicalJson.Should().Contain("\"signatures\":[");
canonicalJson.Should().Contain("\"keyId\":\"test-key-id\"");
}
[Fact]
public void InTotoStatement_MultipleSubjects_CanonicalOrderPreserved()
{
// Arrange - Statement with multiple subjects
var statement = new
{
_type = "https://in-toto.io/Statement/v1",
predicateType = "https://slsa.dev/provenance/v1",
subject = new[]
{
new { name = "artifact-b", digest = new { sha256 = "def456" } },
new { name = "artifact-a", digest = new { sha256 = "abc123" } },
new { name = "artifact-c", digest = new { sha256 = "ghi789" } }
},
predicate = new { builder = new { id = "test-builder" } }
};
// Act
var canonicalBytes = CanonJson.Canonicalize(statement);
var canonicalJson = Encoding.UTF8.GetString(canonicalBytes);
// Assert - Array order should be preserved (not sorted)
var indexB = canonicalJson.IndexOf("artifact-b", StringComparison.Ordinal);
var indexA = canonicalJson.IndexOf("artifact-a", StringComparison.Ordinal);
var indexC = canonicalJson.IndexOf("artifact-c", StringComparison.Ordinal);
indexB.Should().BeLessThan(indexA, "array order should be preserved");
indexA.Should().BeLessThan(indexC, "array order should be preserved");
}
// SIGNER-5100-002: Stable digest computation tests
[Fact]
public void Sha256Hash_SameInput_ProducesIdenticalHash()
{
// Arrange
var statement = CreateDeterministicInTotoStatement();
// Act
var hash1 = CanonJson.Hash(statement);
var hash2 = CanonJson.Hash(statement);
// Assert
hash1.Should().Be(hash2, "same input should produce same hash");
hash1.Length.Should().Be(64, "SHA-256 hash should be 64 hex characters");
}
[Fact]
public void Sha256Hash_LowercaseHex_Format()
{
// Arrange
var statement = CreateDeterministicInTotoStatement();
// Act
var hash = CanonJson.Hash(statement);
// Assert
hash.Should().MatchRegex("^[0-9a-f]{64}$", "hash should be lowercase hex");
hash.Should().NotMatchRegex("[A-F]", "hash should not contain uppercase letters");
}
[Fact]
public void Sha256Hash_Prefixed_HasCorrectPrefix()
{
// Arrange
var statement = CreateDeterministicInTotoStatement();
// Act
var prefixedHash = CanonJson.HashPrefixed(statement);
// Assert
prefixedHash.Should().StartWith("sha256:");
prefixedHash.Length.Should().Be(71, "sha256: prefix (7) + hash (64) = 71");
}
[Fact]
public void Sha256Hash_DifferentInputs_ProduceDifferentHashes()
{
// Arrange
var statement1 = new { type = "test", value = "input1" };
var statement2 = new { type = "test", value = "input2" };
// Act
var hash1 = CanonJson.Hash(statement1);
var hash2 = CanonJson.Hash(statement2);
// Assert
hash1.Should().NotBe(hash2, "different inputs should produce different hashes");
}
[Fact]
public void Sha256Hash_EmptyObject_ProducesConsistentHash()
{
// Arrange
var emptyObject = new { };
// Act
var hash1 = CanonJson.Hash(emptyObject);
var hash2 = CanonJson.Hash(emptyObject);
// Assert
hash1.Should().Be(hash2);
hash1.Should().Be("44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
"empty object {} should have deterministic hash");
}
[Fact]
public void Sha256Hash_NestedObject_ProducesConsistentHash()
{
// Arrange
var nested = new
{
level1 = new
{
level2 = new
{
level3 = new
{
value = "deep"
}
}
}
};
// Act
var hash1 = CanonJson.Hash(nested);
var hash2 = CanonJson.Hash(nested);
// Assert
hash1.Should().Be(hash2, "nested objects should produce consistent hashes");
}
// SIGNER-5100-003: Determinism test - hash stable across runs
[Fact]
public void CanonicalPayload_HashStableAcrossMultipleRuns()
{
// Arrange - Create identical statements multiple times
var hashes = new HashSet<string>();
// Act - Generate hash 100 times
for (int i = 0; i < 100; i++)
{
var statement = CreateDeterministicInTotoStatement();
var hash = CanonJson.Hash(statement);
hashes.Add(hash);
}
// Assert - All hashes should be identical
hashes.Should().HaveCount(1, "all 100 runs should produce the same hash");
}
[Fact]
public void CanonicalPayload_StableWithTimestampField()
{
// Arrange - Fixed timestamp for determinism
var fixedTimestamp = DeterministicTestData.FixedTimestamp;
var statement = new
{
_type = "https://in-toto.io/Statement/v1",
timestamp = fixedTimestamp.ToString("O"),
subject = new[] { new { name = "test", digest = new { sha256 = "abc" } } }
};
// Act
var hash1 = CanonJson.Hash(statement);
var hash2 = CanonJson.Hash(statement);
// Assert
hash1.Should().Be(hash2, "fixed timestamp should produce stable hash");
}
[Fact]
public void CanonicalPayload_DeterministicWithSigningSubjects()
{
// Arrange - Use DeterministicTestData for subjects
var subjects = DeterministicTestData.CreateDefaultSubjects();
// Act
var hash1 = CanonJson.Hash(subjects);
var hash2 = CanonJson.Hash(subjects);
// Assert
hash1.Should().Be(hash2, "signing subjects should hash deterministically");
}
[Fact]
public void CanonicalPayload_DeterministicWithMultipleSubjects()
{
// Arrange
var subjects = DeterministicTestData.CreateMultipleSubjects();
// Act
var hash1 = CanonJson.Hash(subjects);
var hash2 = CanonJson.Hash(subjects);
// Assert
hash1.Should().Be(hash2, "multiple subjects should hash deterministically");
}
[Fact]
public void CanonicalPayload_UnicodeCharacters_HashDeterministically()
{
// Arrange - Statement with Unicode characters
var statement = new
{
_type = "https://in-toto.io/Statement/v1",
subject = new[]
{
new { name = "artifact-\u4e2d\u6587", digest = new { sha256 = "abc123" } }, // Chinese characters
new { name = "artifact-\u00e9\u00e8\u00ea", digest = new { sha256 = "def456" } } // French accents
}
};
// Act
var hash1 = CanonJson.Hash(statement);
var hash2 = CanonJson.Hash(statement);
// Assert
hash1.Should().Be(hash2, "Unicode characters should hash deterministically");
}
[Fact]
public void CanonicalPayload_NumbersPreserved_HashDeterministically()
{
// Arrange - Statement with various number types
var statement = new
{
integer = 42,
negative = -17,
floating = 3.14159,
scientific = 1.5e-10,
large = 9007199254740992L
};
// Act
var hash1 = CanonJson.Hash(statement);
var hash2 = CanonJson.Hash(statement);
// Assert
hash1.Should().Be(hash2, "numbers should hash deterministically");
}
[Fact]
public void CanonicalPayload_BooleanAndNull_HashDeterministically()
{
// Arrange
var statement = new
{
active = true,
disabled = false,
missing = (string?)null
};
// Act
var hash1 = CanonJson.Hash(statement);
var hash2 = CanonJson.Hash(statement);
// Assert
hash1.Should().Be(hash2, "boolean and null values should hash deterministically");
}
[Fact]
public void DssePayload_Base64Url_DeterministicEncoding()
{
// Arrange - Create statement that would have base64url special chars
var statement = new
{
_type = "https://in-toto.io/Statement/v1",
binary = Convert.ToBase64String(new byte[] { 0xFB, 0xFF, 0xFE })
};
// Act
var hash1 = CanonJson.Hash(statement);
var hash2 = CanonJson.Hash(statement);
// Assert
hash1.Should().Be(hash2);
}
// Helper methods
private static object CreateDeterministicInTotoStatement()
{
return new
{
_type = "https://in-toto.io/Statement/v1",
predicateType = "https://slsa.dev/provenance/v1",
subject = new[]
{
new
{
name = DeterministicTestData.DefaultSubjectName,
digest = new { sha256 = DeterministicTestData.DefaultSubjectDigest }
}
},
predicate = new
{
builder = new { id = "https://github.com/stellaops/scanner-action@v2" },
buildType = "https://slsa.dev/container-build/v0.1",
invocation = new
{
configSource = new { uri = "git+https://github.com/stellaops/example@refs/heads/main" }
}
}
};
}
private static DsseEnvelope CreateDeterministicDsseEnvelope()
{
var payloadBytes = Encoding.UTF8.GetBytes(CanonJson.Serialize(CreateDeterministicInTotoStatement()));
var payloadBase64 = Convert.ToBase64String(payloadBytes)
.Replace('+', '-')
.Replace('/', '_')
.TrimEnd('=');
return new DsseEnvelope(
Payload: payloadBase64,
PayloadType: "application/vnd.in-toto+json",
Signatures: new[]
{
new DsseSignature(
Signature: "MEUCIQD_test_signature_base64url",
KeyId: "test-key-id")
});
}
}

View File

@@ -27,5 +27,6 @@
<ProjectReference Include="../../../__Libraries/StellaOps.Configuration/StellaOps.Configuration.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.DependencyInjection/StellaOps.DependencyInjection.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.Cryptography/StellaOps.Cryptography.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.Canonical.Json/StellaOps.Canonical.Json.csproj" />
</ItemGroup>
</Project>