652 lines
21 KiB
C#
652 lines
21 KiB
C#
// -----------------------------------------------------------------------------
|
|
// 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:
|
|
/// - Token expiration test: expired replay token → rejected (REPLAY-5100-001)
|
|
/// - Tamper detection test: modified replay token → rejected (REPLAY-5100-002)
|
|
/// - Token issuance test: valid request → token generated with correct claims (REPLAY-5100-003)
|
|
/// </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");
|
|
}
|
|
|
|
// REPLAY-5100-001: Token expiration tests
|
|
|
|
[Fact]
|
|
public void ExpiredToken_VerifyWithExpiration_ReturnsExpired()
|
|
{
|
|
// Arrange
|
|
var fixedTime = new DateTimeOffset(2025, 6, 15, 12, 0, 0, TimeSpan.Zero);
|
|
var generator = CreateGenerator(fixedTime);
|
|
var request = CreateRequest();
|
|
|
|
// Generate token with 1 hour expiration
|
|
var token = generator.GenerateWithExpiration(request, TimeSpan.FromHours(1));
|
|
|
|
// Advance time past expiration
|
|
var futureGenerator = CreateGenerator(fixedTime.AddHours(2));
|
|
|
|
// Act
|
|
var result = futureGenerator.VerifyWithExpiration(token, request);
|
|
|
|
// Assert
|
|
result.Should().Be(ReplayTokenVerificationResult.Expired, "expired token should be rejected");
|
|
}
|
|
|
|
[Fact]
|
|
public void NotYetExpiredToken_VerifyWithExpiration_ReturnsValid()
|
|
{
|
|
// Arrange
|
|
var fixedTime = new DateTimeOffset(2025, 6, 15, 12, 0, 0, TimeSpan.Zero);
|
|
var generator = CreateGenerator(fixedTime);
|
|
var request = CreateRequest();
|
|
|
|
// Generate token with 1 hour expiration
|
|
var token = generator.GenerateWithExpiration(request, TimeSpan.FromHours(1));
|
|
|
|
// Use same time (not expired)
|
|
var result = generator.VerifyWithExpiration(token, request);
|
|
|
|
// Assert
|
|
result.Should().Be(ReplayTokenVerificationResult.Valid, "valid, not-yet-expired token should be accepted");
|
|
}
|
|
|
|
[Fact]
|
|
public void ExpiredToken_IsExpired_ReturnsTrue()
|
|
{
|
|
// Arrange
|
|
var now = DateTimeOffset.UtcNow;
|
|
var expiresAt = now.AddHours(-1); // Already expired
|
|
var token = new ReplayToken("abc123", now.AddHours(-2), expiresAt, ReplayToken.DefaultAlgorithm, ReplayToken.VersionWithExpiration);
|
|
|
|
// Act
|
|
var isExpired = token.IsExpired(now);
|
|
|
|
// Assert
|
|
isExpired.Should().BeTrue("token with past expiry should be expired");
|
|
}
|
|
|
|
[Fact]
|
|
public void NotYetExpiredToken_IsExpired_ReturnsFalse()
|
|
{
|
|
// Arrange
|
|
var now = DateTimeOffset.UtcNow;
|
|
var expiresAt = now.AddHours(1); // Expires in 1 hour
|
|
var token = new ReplayToken("abc123", now, expiresAt, ReplayToken.DefaultAlgorithm, ReplayToken.VersionWithExpiration);
|
|
|
|
// Act
|
|
var isExpired = token.IsExpired(now);
|
|
|
|
// Assert
|
|
isExpired.Should().BeFalse("token with future expiry should not be expired");
|
|
}
|
|
|
|
[Fact]
|
|
public void TokenWithoutExpiration_IsExpired_ReturnsFalse()
|
|
{
|
|
// Arrange - v1.0 token without expiration
|
|
var token = new ReplayToken("abc123", DateTimeOffset.UtcNow);
|
|
|
|
// Act
|
|
var isExpired = token.IsExpired();
|
|
|
|
// Assert
|
|
isExpired.Should().BeFalse("v1.0 token without expiration should never be expired");
|
|
}
|
|
|
|
[Fact]
|
|
public void GenerateWithExpiration_UsesDefaultExpiration_WhenNotSpecified()
|
|
{
|
|
// Arrange
|
|
var fixedTime = new DateTimeOffset(2025, 6, 15, 12, 0, 0, TimeSpan.Zero);
|
|
var generator = CreateGenerator(fixedTime);
|
|
var request = CreateRequest();
|
|
|
|
// Act
|
|
var token = generator.GenerateWithExpiration(request);
|
|
|
|
// Assert
|
|
token.ExpiresAt.Should().Be(fixedTime + ReplayToken.DefaultExpiration);
|
|
token.Version.Should().Be(ReplayToken.VersionWithExpiration);
|
|
}
|
|
|
|
[Fact]
|
|
public void GenerateWithExpiration_UsesCustomExpiration()
|
|
{
|
|
// Arrange
|
|
var fixedTime = new DateTimeOffset(2025, 6, 15, 12, 0, 0, TimeSpan.Zero);
|
|
var generator = CreateGenerator(fixedTime);
|
|
var request = CreateRequest();
|
|
var customExpiration = TimeSpan.FromMinutes(30);
|
|
|
|
// Act
|
|
var token = generator.GenerateWithExpiration(request, customExpiration);
|
|
|
|
// Assert
|
|
token.ExpiresAt.Should().Be(fixedTime + customExpiration);
|
|
}
|
|
|
|
[Fact]
|
|
public void TokenWithExpiration_CanonicalFormat_IncludesExpiryTimestamp()
|
|
{
|
|
// Arrange
|
|
var fixedTime = new DateTimeOffset(2025, 6, 15, 12, 0, 0, TimeSpan.Zero);
|
|
var expiresAt = fixedTime.AddHours(1);
|
|
var token = new ReplayToken("abc123", fixedTime, expiresAt, ReplayToken.DefaultAlgorithm, ReplayToken.VersionWithExpiration);
|
|
|
|
// Act
|
|
var canonical = token.Canonical;
|
|
|
|
// Assert
|
|
canonical.Should().Contain(expiresAt.ToUnixTimeSeconds().ToString());
|
|
canonical.Split(':').Should().HaveCount(5, "v2.0 format should have 5 parts including expiry");
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseToken_WithExpiration_RoundTrip_PreservesExpiration()
|
|
{
|
|
// Arrange
|
|
var fixedTime = new DateTimeOffset(2025, 6, 15, 12, 0, 0, TimeSpan.Zero);
|
|
var generator = CreateGenerator(fixedTime);
|
|
var request = CreateRequest();
|
|
var originalToken = generator.GenerateWithExpiration(request, TimeSpan.FromHours(2));
|
|
|
|
// Act
|
|
var parsed = ReplayToken.Parse(originalToken.Canonical);
|
|
|
|
// Assert
|
|
parsed.Value.Should().Be(originalToken.Value);
|
|
parsed.Version.Should().Be(ReplayToken.VersionWithExpiration);
|
|
parsed.ExpiresAt.Should().Be(originalToken.ExpiresAt);
|
|
}
|
|
|
|
[Fact]
|
|
public void TamperedToken_WithExpiration_VerifyWithExpiration_ReturnsInvalid()
|
|
{
|
|
// Arrange
|
|
var fixedTime = new DateTimeOffset(2025, 6, 15, 12, 0, 0, TimeSpan.Zero);
|
|
var generator = CreateGenerator(fixedTime);
|
|
var request = CreateRequest();
|
|
var token = generator.GenerateWithExpiration(request, TimeSpan.FromHours(1));
|
|
|
|
// Tamper with the token value
|
|
var tamperedValue = TamperHash(token.Value);
|
|
var tamperedToken = new ReplayToken(tamperedValue, token.GeneratedAt, token.ExpiresAt, token.Algorithm, token.Version);
|
|
|
|
// Act
|
|
var result = generator.VerifyWithExpiration(tamperedToken, request);
|
|
|
|
// Assert
|
|
result.Should().Be(ReplayTokenVerificationResult.Invalid, "tampered token should be invalid");
|
|
}
|
|
|
|
[Fact]
|
|
public void GetTimeToExpiration_ReturnsRemainingTime()
|
|
{
|
|
// Arrange
|
|
var now = DateTimeOffset.UtcNow;
|
|
var expiresAt = now.AddHours(2);
|
|
var token = new ReplayToken("abc123", now, expiresAt, ReplayToken.DefaultAlgorithm, ReplayToken.VersionWithExpiration);
|
|
|
|
// Act
|
|
var remaining = token.GetTimeToExpiration(now);
|
|
|
|
// Assert
|
|
remaining.Should().NotBeNull();
|
|
remaining.Value.Should().BeCloseTo(TimeSpan.FromHours(2), TimeSpan.FromSeconds(1));
|
|
}
|
|
|
|
[Fact]
|
|
public void GetTimeToExpiration_ExpiredToken_ReturnsNull()
|
|
{
|
|
// Arrange
|
|
var now = DateTimeOffset.UtcNow;
|
|
var expiresAt = now.AddHours(-1); // Already expired
|
|
var token = new ReplayToken("abc123", now.AddHours(-2), expiresAt, ReplayToken.DefaultAlgorithm, ReplayToken.VersionWithExpiration);
|
|
|
|
// Act
|
|
var remaining = token.GetTimeToExpiration(now);
|
|
|
|
// Assert
|
|
remaining.Should().BeNull("expired token should have no remaining time");
|
|
}
|
|
|
|
[Fact]
|
|
public void GetTimeToExpiration_NoExpiration_ReturnsNull()
|
|
{
|
|
// Arrange - v1.0 token without expiration
|
|
var token = new ReplayToken("abc123", DateTimeOffset.UtcNow);
|
|
|
|
// Act
|
|
var remaining = token.GetTimeToExpiration();
|
|
|
|
// Assert
|
|
remaining.Should().BeNull("v1.0 token without expiration has no remaining time concept");
|
|
}
|
|
|
|
[Fact]
|
|
public void TryParse_ValidToken_ReturnsTrue()
|
|
{
|
|
// Arrange
|
|
var canonical = "replay:v1.0:SHA-256:abc123def456abc123def456abc123def456abc123def456abc123def456abc1";
|
|
|
|
// Act
|
|
var success = ReplayToken.TryParse(canonical, out var token);
|
|
|
|
// Assert
|
|
success.Should().BeTrue();
|
|
token.Should().NotBeNull();
|
|
token!.Value.Should().Be("abc123def456abc123def456abc123def456abc123def456abc123def456abc1");
|
|
}
|
|
|
|
[Fact]
|
|
public void TryParse_InvalidToken_ReturnsFalse()
|
|
{
|
|
// Arrange
|
|
var invalid = "not-a-valid-token";
|
|
|
|
// Act
|
|
var success = ReplayToken.TryParse(invalid, out var token);
|
|
|
|
// Assert
|
|
success.Should().BeFalse();
|
|
token.Should().BeNull();
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
}
|