sprints work

This commit is contained in:
StellaOps Bot
2025-12-25 12:19:12 +02:00
parent 223843f1d1
commit 2a06f780cf
224 changed files with 41796 additions and 1515 deletions

View File

@@ -14,9 +14,9 @@ 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)
/// Note: REPLAY-5100-001 (token expiration) is BLOCKED - ReplayToken does not currently support expiration.
/// </summary>
[Trait("Category", "Unit")]
[Trait("Category", "ReplayTokenSecurity")]
@@ -366,6 +366,249 @@ public sealed class ReplayTokenSecurityTests
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)