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,408 @@
// -----------------------------------------------------------------------------
// ReplayTokenSecurityTests.cs
// Sprint: SPRINT_5100_0010_0001_evidencelocker_tests
// Tasks: REPLAY-5100-001, REPLAY-5100-002, REPLAY-5100-003
// Description: Model L0 security tests for Replay Token
// -----------------------------------------------------------------------------
using FluentAssertions;
using StellaOps.Cryptography;
using Xunit;
namespace StellaOps.Audit.ReplayToken.Tests;
/// <summary>
/// Security tests for Replay Token generation and verification.
/// Implements Model L0 test requirements:
/// - Tamper detection test: modified replay token → rejected (REPLAY-5100-002)
/// - Token issuance test: valid request → token generated with correct claims (REPLAY-5100-003)
/// Note: REPLAY-5100-001 (token expiration) is BLOCKED - ReplayToken does not currently support expiration.
/// </summary>
[Trait("Category", "Unit")]
[Trait("Category", "ReplayTokenSecurity")]
public sealed class ReplayTokenSecurityTests
{
// REPLAY-5100-002: Tamper detection tests
[Fact]
public void TamperedToken_ModifiedValue_VerificationFails()
{
// Arrange
var generator = CreateGenerator();
var request = CreateRequest();
var token = generator.Generate(request);
// Tamper with the token value
var tamperedValue = TamperHash(token.Value);
var tamperedToken = new ReplayToken(tamperedValue, token.GeneratedAt, token.Algorithm, token.Version);
// Act
var isValid = generator.Verify(tamperedToken, request);
// Assert
isValid.Should().BeFalse("tampered token value should fail verification");
}
[Fact]
public void TamperedToken_SingleBitFlip_VerificationFails()
{
// Arrange
var generator = CreateGenerator();
var request = CreateRequest();
var token = generator.Generate(request);
// Flip a single bit in the token value
var chars = token.Value.ToCharArray();
chars[0] = chars[0] == 'a' ? 'b' : 'a';
var tamperedValue = new string(chars);
var tamperedToken = new ReplayToken(tamperedValue, token.GeneratedAt, token.Algorithm, token.Version);
// Act
var isValid = generator.Verify(tamperedToken, request);
// Assert
isValid.Should().BeFalse("single-bit tampered token should fail verification");
}
[Fact]
public void TamperedToken_ModifiedAlgorithm_ParsedCorrectlyButVerificationFails()
{
// Arrange
var generator = CreateGenerator();
var request = CreateRequest();
var token = generator.Generate(request);
// Create token with different algorithm claim (but same value)
var tamperedToken = new ReplayToken(token.Value, token.GeneratedAt, "SHA-512", token.Version);
// Act - Verification should still work based on computed hash
var isValid = generator.Verify(tamperedToken, request);
// Assert
isValid.Should().BeTrue("algorithm claim doesn't affect verification (value-based)");
}
[Fact]
public void TamperedRequest_AddedField_VerificationFails()
{
// Arrange
var generator = CreateGenerator();
var originalRequest = CreateRequest();
var token = generator.Generate(originalRequest);
var tamperedRequest = new ReplayTokenRequest
{
FeedManifests = originalRequest.FeedManifests,
RulesVersion = originalRequest.RulesVersion,
RulesHash = originalRequest.RulesHash,
InputHashes = originalRequest.InputHashes,
// Add extra field
AdditionalContext = new Dictionary<string, string> { ["malicious"] = "data" }
};
// Act
var isValid = generator.Verify(token, tamperedRequest);
// Assert
isValid.Should().BeFalse("request with added field should fail verification");
}
[Fact]
public void TamperedRequest_RemovedField_VerificationFails()
{
// Arrange
var generator = CreateGenerator();
var originalRequest = new ReplayTokenRequest
{
FeedManifests = ["sha256:feed1", "sha256:feed2"],
RulesVersion = "v1",
RulesHash = "sha256:rules",
InputHashes = ["sha256:input1"]
};
var token = generator.Generate(originalRequest);
var tamperedRequest = new ReplayTokenRequest
{
FeedManifests = ["sha256:feed1"], // Removed one
RulesVersion = "v1",
RulesHash = "sha256:rules",
InputHashes = ["sha256:input1"]
};
// Act
var isValid = generator.Verify(token, tamperedRequest);
// Assert
isValid.Should().BeFalse("request with removed field should fail verification");
}
[Fact]
public void TamperedRequest_ModifiedValue_VerificationFails()
{
// Arrange
var generator = CreateGenerator();
var originalRequest = CreateRequest();
var token = generator.Generate(originalRequest);
var tamperedRequest = new ReplayTokenRequest
{
FeedManifests = originalRequest.FeedManifests,
RulesVersion = "v2", // Changed version
RulesHash = originalRequest.RulesHash,
InputHashes = originalRequest.InputHashes
};
// Act
var isValid = generator.Verify(token, tamperedRequest);
// Assert
isValid.Should().BeFalse("request with modified value should fail verification");
}
// REPLAY-5100-003: Token issuance tests
[Fact]
public void GenerateToken_ValidRequest_HasCorrectAlgorithm()
{
// Arrange
var generator = CreateGenerator();
var request = CreateRequest();
// Act
var token = generator.Generate(request);
// Assert
token.Algorithm.Should().Be(ReplayToken.DefaultAlgorithm);
}
[Fact]
public void GenerateToken_ValidRequest_HasCorrectVersion()
{
// Arrange
var generator = CreateGenerator();
var request = CreateRequest();
// Act
var token = generator.Generate(request);
// Assert
token.Version.Should().Be(ReplayToken.DefaultVersion);
}
[Fact]
public void GenerateToken_ValidRequest_HasValidSha256Format()
{
// Arrange
var generator = CreateGenerator();
var request = CreateRequest();
// Act
var token = generator.Generate(request);
// Assert
token.Value.Should().NotBeNullOrEmpty();
token.Value.Length.Should().Be(64, "SHA-256 hex should be 64 characters");
token.Value.Should().MatchRegex("^[0-9a-f]{64}$", "should be lowercase hex");
}
[Fact]
public void GenerateToken_ValidRequest_HasCorrectTimestamp()
{
// Arrange
var fixedTime = new DateTimeOffset(2025, 6, 15, 12, 0, 0, TimeSpan.Zero);
var generator = CreateGenerator(fixedTime);
var request = CreateRequest();
// Act
var token = generator.Generate(request);
// Assert
token.GeneratedAt.Should().Be(fixedTime);
}
[Fact]
public void GenerateToken_ValidRequest_CanonicalFormatIsCorrect()
{
// Arrange
var generator = CreateGenerator();
var request = CreateRequest();
// Act
var token = generator.Generate(request);
// Assert
token.Canonical.Should().StartWith("replay:v");
token.Canonical.Should().Contain(":SHA-256:");
token.Canonical.Split(':').Should().HaveCount(4);
}
[Fact]
public void GenerateToken_EmptyRequest_StillGeneratesValidToken()
{
// Arrange
var generator = CreateGenerator();
var emptyRequest = new ReplayTokenRequest();
// Act
var token = generator.Generate(emptyRequest);
// Assert
token.Should().NotBeNull();
token.Value.Should().NotBeNullOrEmpty();
}
[Fact]
public void GenerateToken_NullInputs_HandledGracefully()
{
// Arrange
var generator = CreateGenerator();
var request = new ReplayTokenRequest
{
FeedManifests = null,
RulesVersion = null,
InputHashes = null
};
// Act
var token = generator.Generate(request);
// Assert
token.Should().NotBeNull();
token.Value.Length.Should().Be(64);
}
[Fact]
public void GenerateToken_DeterministicAcrossMultipleCalls()
{
// Arrange
var fixedTime = new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero);
var generator = CreateGenerator(fixedTime);
var request = CreateRequest();
// Act
var tokens = Enumerable.Range(0, 10)
.Select(_ => generator.Generate(request))
.ToList();
// Assert
tokens.Select(t => t.Value).Distinct().Should().HaveCount(1,
"same request should produce same token value");
}
[Fact]
public void GenerateToken_DifferentRequests_ProduceDifferentTokens()
{
// Arrange
var generator = CreateGenerator();
var request1 = new ReplayTokenRequest { InputHashes = ["sha256:input1"] };
var request2 = new ReplayTokenRequest { InputHashes = ["sha256:input2"] };
// Act
var token1 = generator.Generate(request1);
var token2 = generator.Generate(request2);
// Assert
token1.Value.Should().NotBe(token2.Value,
"different requests should produce different tokens");
}
[Fact]
public void ParseToken_RoundTrip_PreservesValues()
{
// Arrange
var generator = CreateGenerator();
var request = CreateRequest();
var originalToken = generator.Generate(request);
// Act
var parsed = ReplayToken.Parse(originalToken.Canonical);
// Assert
parsed.Value.Should().Be(originalToken.Value);
parsed.Algorithm.Should().Be(originalToken.Algorithm);
parsed.Version.Should().Be(originalToken.Version);
}
[Theory]
[InlineData("")]
[InlineData(" ")]
[InlineData("invalid")]
[InlineData("replay:")]
[InlineData("replay:v1.0")]
[InlineData("replay:v1.0:SHA-256")]
[InlineData("notreplay:v1.0:SHA-256:abc")]
public void ParseToken_InvalidFormat_ThrowsException(string invalidCanonical)
{
// Act
var act = () => ReplayToken.Parse(invalidCanonical);
// Assert
act.Should().Throw<Exception>();
}
[Fact]
public void Token_Equality_BasedOnValue()
{
// Arrange
var value = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
var token1 = new ReplayToken(value, DateTimeOffset.UtcNow);
var token2 = new ReplayToken(value, DateTimeOffset.UtcNow.AddHours(1)); // Different time
// Act & Assert
token1.Equals(token2).Should().BeTrue("equality should be based on value only");
token1.GetHashCode().Should().Be(token2.GetHashCode());
}
[Fact]
public void Token_Equality_CaseInsensitive()
{
// Arrange
var value1 = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
var value2 = "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF";
var token1 = new ReplayToken(value1, DateTimeOffset.UtcNow);
var token2 = new ReplayToken(value2, DateTimeOffset.UtcNow);
// Act & Assert
token1.Equals(token2).Should().BeTrue("value comparison should be case-insensitive");
}
// Helper methods
private static Sha256ReplayTokenGenerator CreateGenerator(DateTimeOffset? fixedTime = null)
{
var cryptoHash = DefaultCryptoHash.CreateForTests();
var timeProvider = new FixedTimeProvider(fixedTime ?? DateTimeOffset.UtcNow);
return new Sha256ReplayTokenGenerator(cryptoHash, timeProvider);
}
private static ReplayTokenRequest CreateRequest()
{
return new ReplayTokenRequest
{
FeedManifests = ["sha256:feed1", "sha256:feed2"],
RulesVersion = "v1",
RulesHash = "sha256:rules",
LatticePolicyVersion = "lattice-v1",
LatticePolicyHash = "sha256:lattice",
InputHashes = ["sha256:input1", "sha256:input2"],
ScoringConfigVersion = "score-v1",
EvidenceHashes = ["sha256:evidence1"]
};
}
private static string TamperHash(string hash)
{
// Create a completely different hash value
var chars = hash.ToCharArray();
for (int i = 0; i < chars.Length; i++)
{
chars[i] = chars[i] == 'f' ? '0' : (char)(chars[i] + 1);
}
return new string(chars);
}
private sealed class FixedTimeProvider(DateTimeOffset now) : TimeProvider
{
public override DateTimeOffset GetUtcNow() => now;
}
}

View File

@@ -7,7 +7,15 @@
<LangVersion>preview</LangVersion>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="8.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="xunit" Version="2.5.4" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.4" />
<PackageReference Include="coverlet.collector" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\\..\\__Libraries\\StellaOps.Audit.ReplayToken\\StellaOps.Audit.ReplayToken.csproj" />
</ItemGroup>