- Implement `SbomVexOrderingDeterminismProperties` for testing component list and vulnerability metadata hash consistency. - Create `UnicodeNormalizationDeterminismProperties` to validate NFC normalization and Unicode string handling. - Add project file for `StellaOps.Testing.Determinism.Properties` with necessary dependencies. - Introduce CI/CD template validation tests including YAML syntax checks and documentation content verification. - Create validation script for CI/CD templates ensuring all required files and structures are present.
405 lines
15 KiB
C#
405 lines
15 KiB
C#
// -----------------------------------------------------------------------------
|
|
// TemporalKeyVerificationTests.cs
|
|
// Sprint: SPRINT_0501_0008_0001_proof_chain_key_rotation
|
|
// Task: PROOF-KEY-0014 - Temporal verification tests (key valid at time T)
|
|
// Description: Tests verifying key validity at specific points in time
|
|
// -----------------------------------------------------------------------------
|
|
|
|
using System;
|
|
using System.Threading.Tasks;
|
|
|
|
using FluentAssertions;
|
|
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using Microsoft.Extensions.Options;
|
|
|
|
using StellaOps.Signer.KeyManagement;
|
|
using StellaOps.Signer.KeyManagement.Entities;
|
|
|
|
using Xunit;
|
|
|
|
namespace StellaOps.Signer.Tests.KeyManagement;
|
|
|
|
/// <summary>
|
|
/// Temporal key verification tests.
|
|
/// Validates that keys are correctly checked for validity at specific points in time.
|
|
/// This is critical for verifying historical proofs that were signed before key rotation.
|
|
/// </summary>
|
|
public class TemporalKeyVerificationTests : IDisposable
|
|
{
|
|
private readonly KeyManagementDbContext _dbContext;
|
|
private readonly KeyRotationService _service;
|
|
private readonly FakeTimeProvider _timeProvider;
|
|
|
|
// Timeline:
|
|
// 2024-01-15: key-2024 added
|
|
// 2024-06-15: key-2025 added (overlap period begins)
|
|
// 2025-01-15: key-2024 revoked (overlap period ends)
|
|
// 2025-06-15: current time
|
|
private readonly DateTimeOffset _key2024AddedAt = new(2024, 1, 15, 0, 0, 0, TimeSpan.Zero);
|
|
private readonly DateTimeOffset _key2025AddedAt = new(2024, 6, 15, 0, 0, 0, TimeSpan.Zero);
|
|
private readonly DateTimeOffset _key2024RevokedAt = new(2025, 1, 15, 0, 0, 0, TimeSpan.Zero);
|
|
private readonly DateTimeOffset _currentTime = new(2025, 6, 15, 12, 0, 0, TimeSpan.Zero);
|
|
|
|
public TemporalKeyVerificationTests()
|
|
{
|
|
var options = new DbContextOptionsBuilder<KeyManagementDbContext>()
|
|
.UseInMemoryDatabase(databaseName: $"TemporalTestDb_{Guid.NewGuid()}")
|
|
.Options;
|
|
|
|
_dbContext = new KeyManagementDbContext(options);
|
|
_timeProvider = new FakeTimeProvider(_currentTime);
|
|
|
|
_service = new KeyRotationService(
|
|
_dbContext,
|
|
NullLogger<KeyRotationService>.Instance,
|
|
Options.Create(new KeyRotationOptions
|
|
{
|
|
DefaultActor = "test-user",
|
|
ExpiryWarningDays = 60,
|
|
MaxKeyAgeDays = 365,
|
|
DeprecatedAlgorithms = ["RSA-2048", "SHA1-RSA"]
|
|
}),
|
|
_timeProvider);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_dbContext.Dispose();
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
|
|
#region Key Lifecycle Timeline Tests
|
|
|
|
[Fact]
|
|
public async Task CheckKeyValidity_KeyNotYetAdded_ReturnsNotYetValid()
|
|
{
|
|
// Arrange
|
|
var anchor = await CreateTestAnchorWithTimelineAsync();
|
|
var beforeKeyAdded = _key2024AddedAt.AddDays(-30); // Dec 2023
|
|
|
|
// Act
|
|
var result = await _service.CheckKeyValidityAsync(anchor.AnchorId, "key-2024", beforeKeyAdded);
|
|
|
|
// Assert
|
|
result.IsValid.Should().BeFalse();
|
|
result.Status.Should().Be(KeyStatus.NotYetValid);
|
|
result.InvalidReason.Should().Contain("not yet added");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CheckKeyValidity_KeyActiveNoRevocation_ReturnsValid()
|
|
{
|
|
// Arrange
|
|
var anchor = await CreateTestAnchorWithTimelineAsync();
|
|
var duringActiveWindow = _key2024AddedAt.AddMonths(3); // April 2024
|
|
|
|
// Act
|
|
var result = await _service.CheckKeyValidityAsync(anchor.AnchorId, "key-2024", duringActiveWindow);
|
|
|
|
// Assert
|
|
result.IsValid.Should().BeTrue();
|
|
result.Status.Should().Be(KeyStatus.Active);
|
|
result.AddedAt.Should().Be(_key2024AddedAt);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CheckKeyValidity_KeyRevokedButSignedBefore_ReturnsValid()
|
|
{
|
|
// Arrange - proof was signed during overlap period before key-2024 was revoked
|
|
var anchor = await CreateTestAnchorWithTimelineAsync();
|
|
var signedDuringOverlap = _key2024RevokedAt.AddDays(-30); // Dec 2024
|
|
|
|
// Act
|
|
var result = await _service.CheckKeyValidityAsync(anchor.AnchorId, "key-2024", signedDuringOverlap);
|
|
|
|
// Assert - key-2024 should be valid because signature was made before revocation
|
|
result.IsValid.Should().BeTrue();
|
|
result.Status.Should().Be(KeyStatus.Active);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CheckKeyValidity_KeyRevokedAndSignedAfter_ReturnsRevoked()
|
|
{
|
|
// Arrange - proof was signed after key-2024 was revoked
|
|
var anchor = await CreateTestAnchorWithTimelineAsync();
|
|
var signedAfterRevocation = _key2024RevokedAt.AddDays(30); // Feb 2025
|
|
|
|
// Act
|
|
var result = await _service.CheckKeyValidityAsync(anchor.AnchorId, "key-2024", signedAfterRevocation);
|
|
|
|
// Assert - key-2024 should be invalid because signature was made after revocation
|
|
result.IsValid.Should().BeFalse();
|
|
result.Status.Should().Be(KeyStatus.Revoked);
|
|
result.RevokedAt.Should().Be(_key2024RevokedAt);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CheckKeyValidity_NewKeyAfterOldRevoked_ReturnsValid()
|
|
{
|
|
// Arrange - proof was signed with key-2025 after key-2024 was revoked
|
|
var anchor = await CreateTestAnchorWithTimelineAsync();
|
|
var signedWithNewKey = _key2024RevokedAt.AddDays(30); // Feb 2025
|
|
|
|
// Act
|
|
var result = await _service.CheckKeyValidityAsync(anchor.AnchorId, "key-2025", signedWithNewKey);
|
|
|
|
// Assert - key-2025 should be valid
|
|
result.IsValid.Should().BeTrue();
|
|
result.Status.Should().Be(KeyStatus.Active);
|
|
result.AddedAt.Should().Be(_key2025AddedAt);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Overlap Period Tests
|
|
|
|
[Fact]
|
|
public async Task CheckKeyValidity_BothKeysValidDuringOverlap_BothReturnValid()
|
|
{
|
|
// Arrange - during overlap period (Jun 2024 - Jan 2025), both keys should be valid
|
|
var anchor = await CreateTestAnchorWithTimelineAsync();
|
|
var duringOverlap = new DateTimeOffset(2024, 9, 15, 0, 0, 0, TimeSpan.Zero); // Sep 2024
|
|
|
|
// Act
|
|
var result2024 = await _service.CheckKeyValidityAsync(anchor.AnchorId, "key-2024", duringOverlap);
|
|
var result2025 = await _service.CheckKeyValidityAsync(anchor.AnchorId, "key-2025", duringOverlap);
|
|
|
|
// Assert - both keys should be valid during overlap
|
|
result2024.IsValid.Should().BeTrue();
|
|
result2024.Status.Should().Be(KeyStatus.Active);
|
|
|
|
result2025.IsValid.Should().BeTrue();
|
|
result2025.Status.Should().Be(KeyStatus.Active);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CheckKeyValidity_ExactlyAtRevocationTime_ReturnsRevoked()
|
|
{
|
|
// Arrange - checking exactly at the moment of revocation
|
|
var anchor = await CreateTestAnchorWithTimelineAsync();
|
|
|
|
// Act - at exact revocation time, key is already revoked
|
|
var result = await _service.CheckKeyValidityAsync(anchor.AnchorId, "key-2024", _key2024RevokedAt);
|
|
|
|
// Assert - at revocation time, key should be considered revoked
|
|
result.IsValid.Should().BeFalse();
|
|
result.Status.Should().Be(KeyStatus.Revoked);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CheckKeyValidity_OneMillisecondBeforeRevocation_ReturnsValid()
|
|
{
|
|
// Arrange - one millisecond before revocation
|
|
var anchor = await CreateTestAnchorWithTimelineAsync();
|
|
var justBeforeRevocation = _key2024RevokedAt.AddMilliseconds(-1);
|
|
|
|
// Act
|
|
var result = await _service.CheckKeyValidityAsync(anchor.AnchorId, "key-2024", justBeforeRevocation);
|
|
|
|
// Assert - key should still be valid
|
|
result.IsValid.Should().BeTrue();
|
|
result.Status.Should().Be(KeyStatus.Active);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Key Expiry Tests
|
|
|
|
[Fact]
|
|
public async Task CheckKeyValidity_KeyExpiredButSignedBefore_ReturnsValid()
|
|
{
|
|
// Arrange - key with expiry date
|
|
var anchor = await CreateTestAnchorWithExpiringKeyAsync();
|
|
var expiryDate = new DateTimeOffset(2025, 3, 1, 0, 0, 0, TimeSpan.Zero);
|
|
var signedBeforeExpiry = expiryDate.AddDays(-30); // Feb 2025
|
|
|
|
// Act
|
|
var result = await _service.CheckKeyValidityAsync(anchor.AnchorId, "expiring-key", signedBeforeExpiry);
|
|
|
|
// Assert - should be valid because signed before expiry
|
|
result.IsValid.Should().BeTrue();
|
|
result.Status.Should().Be(KeyStatus.Active);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CheckKeyValidity_KeyExpiredAndSignedAfter_ReturnsExpired()
|
|
{
|
|
// Arrange - key with expiry date
|
|
var anchor = await CreateTestAnchorWithExpiringKeyAsync();
|
|
var expiryDate = new DateTimeOffset(2025, 3, 1, 0, 0, 0, TimeSpan.Zero);
|
|
var signedAfterExpiry = expiryDate.AddDays(30); // April 2025
|
|
|
|
// Act
|
|
var result = await _service.CheckKeyValidityAsync(anchor.AnchorId, "expiring-key", signedAfterExpiry);
|
|
|
|
// Assert - should be invalid because signed after expiry
|
|
result.IsValid.Should().BeFalse();
|
|
result.Status.Should().Be(KeyStatus.Expired);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Unknown Key Tests
|
|
|
|
[Fact]
|
|
public async Task CheckKeyValidity_UnknownKey_ReturnsUnknown()
|
|
{
|
|
// Arrange
|
|
var anchor = await CreateTestAnchorWithTimelineAsync();
|
|
|
|
// Act
|
|
var result = await _service.CheckKeyValidityAsync(anchor.AnchorId, "nonexistent-key", _currentTime);
|
|
|
|
// Assert
|
|
result.IsValid.Should().BeFalse();
|
|
result.Status.Should().Be(KeyStatus.Unknown);
|
|
result.InvalidReason.Should().Contain("not found");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CheckKeyValidity_UnknownAnchor_ThrowsKeyNotFoundException()
|
|
{
|
|
// Arrange
|
|
var unknownAnchorId = Guid.NewGuid();
|
|
|
|
// Act & Assert
|
|
await Assert.ThrowsAsync<KeyNotFoundException>(
|
|
() => _service.CheckKeyValidityAsync(unknownAnchorId, "any-key", _currentTime));
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Determinism Tests
|
|
|
|
[Fact]
|
|
public async Task CheckKeyValidity_SameInputs_ReturnsSameResult()
|
|
{
|
|
// Arrange - determinism is critical for audit verification
|
|
var anchor = await CreateTestAnchorWithTimelineAsync();
|
|
var checkTime = new DateTimeOffset(2024, 9, 15, 10, 30, 45, TimeSpan.Zero);
|
|
|
|
// Act - call multiple times
|
|
var result1 = await _service.CheckKeyValidityAsync(anchor.AnchorId, "key-2024", checkTime);
|
|
var result2 = await _service.CheckKeyValidityAsync(anchor.AnchorId, "key-2024", checkTime);
|
|
var result3 = await _service.CheckKeyValidityAsync(anchor.AnchorId, "key-2024", checkTime);
|
|
|
|
// Assert - all results should be identical
|
|
result1.Should().BeEquivalentTo(result2);
|
|
result2.Should().BeEquivalentTo(result3);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CheckKeyValidity_DifferentTimezones_SameUtcTime_ReturnsSameResult()
|
|
{
|
|
// Arrange - different timezone representations of same moment
|
|
var anchor = await CreateTestAnchorWithTimelineAsync();
|
|
|
|
var utcTime = new DateTimeOffset(2024, 9, 15, 12, 0, 0, TimeSpan.Zero);
|
|
var pstTime = new DateTimeOffset(2024, 9, 15, 4, 0, 0, TimeSpan.FromHours(-8));
|
|
var jstTime = new DateTimeOffset(2024, 9, 15, 21, 0, 0, TimeSpan.FromHours(9));
|
|
|
|
// Act
|
|
var resultUtc = await _service.CheckKeyValidityAsync(anchor.AnchorId, "key-2024", utcTime);
|
|
var resultPst = await _service.CheckKeyValidityAsync(anchor.AnchorId, "key-2024", pstTime);
|
|
var resultJst = await _service.CheckKeyValidityAsync(anchor.AnchorId, "key-2024", jstTime);
|
|
|
|
// Assert - all should return same result (same UTC instant)
|
|
resultUtc.IsValid.Should().Be(resultPst.IsValid);
|
|
resultPst.IsValid.Should().Be(resultJst.IsValid);
|
|
resultUtc.Status.Should().Be(resultPst.Status);
|
|
resultPst.Status.Should().Be(resultJst.Status);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Helper Methods
|
|
|
|
private async Task<TrustAnchorEntity> CreateTestAnchorWithTimelineAsync()
|
|
{
|
|
var anchor = new TrustAnchorEntity
|
|
{
|
|
AnchorId = Guid.NewGuid(),
|
|
PurlPattern = "pkg:npm/*",
|
|
AllowedKeyIds = ["key-2024", "key-2025"],
|
|
RevokedKeyIds = ["key-2024"],
|
|
PolicyVersion = "v1.0.0",
|
|
CreatedAt = _key2024AddedAt,
|
|
UpdatedAt = _key2024RevokedAt
|
|
};
|
|
|
|
var keyHistory = new[]
|
|
{
|
|
new KeyHistoryEntity
|
|
{
|
|
HistoryId = Guid.NewGuid(),
|
|
AnchorId = anchor.AnchorId,
|
|
KeyId = "key-2024",
|
|
PublicKey = "-----BEGIN PUBLIC KEY-----\ntest-key-2024\n-----END PUBLIC KEY-----",
|
|
Algorithm = "Ed25519",
|
|
AddedAt = _key2024AddedAt,
|
|
RevokedAt = _key2024RevokedAt,
|
|
RevokeReason = "annual-rotation",
|
|
CreatedAt = _key2024AddedAt
|
|
},
|
|
new KeyHistoryEntity
|
|
{
|
|
HistoryId = Guid.NewGuid(),
|
|
AnchorId = anchor.AnchorId,
|
|
KeyId = "key-2025",
|
|
PublicKey = "-----BEGIN PUBLIC KEY-----\ntest-key-2025\n-----END PUBLIC KEY-----",
|
|
Algorithm = "Ed25519",
|
|
AddedAt = _key2025AddedAt,
|
|
RevokedAt = null,
|
|
RevokeReason = null,
|
|
CreatedAt = _key2025AddedAt
|
|
}
|
|
};
|
|
|
|
_dbContext.TrustAnchors.Add(anchor);
|
|
_dbContext.KeyHistory.AddRange(keyHistory);
|
|
await _dbContext.SaveChangesAsync();
|
|
|
|
return anchor;
|
|
}
|
|
|
|
private async Task<TrustAnchorEntity> CreateTestAnchorWithExpiringKeyAsync()
|
|
{
|
|
var anchor = new TrustAnchorEntity
|
|
{
|
|
AnchorId = Guid.NewGuid(),
|
|
PurlPattern = "pkg:pypi/*",
|
|
AllowedKeyIds = ["expiring-key"],
|
|
RevokedKeyIds = [],
|
|
PolicyVersion = "v1.0.0",
|
|
CreatedAt = new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero),
|
|
UpdatedAt = new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero)
|
|
};
|
|
|
|
var keyHistory = new KeyHistoryEntity
|
|
{
|
|
HistoryId = Guid.NewGuid(),
|
|
AnchorId = anchor.AnchorId,
|
|
KeyId = "expiring-key",
|
|
PublicKey = "-----BEGIN PUBLIC KEY-----\ntest-expiring-key\n-----END PUBLIC KEY-----",
|
|
Algorithm = "Ed25519",
|
|
AddedAt = new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero),
|
|
ExpiresAt = new DateTimeOffset(2025, 3, 1, 0, 0, 0, TimeSpan.Zero),
|
|
RevokedAt = null,
|
|
RevokeReason = null,
|
|
CreatedAt = new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero)
|
|
};
|
|
|
|
_dbContext.TrustAnchors.Add(anchor);
|
|
_dbContext.KeyHistory.Add(keyHistory);
|
|
await _dbContext.SaveChangesAsync();
|
|
|
|
return anchor;
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
|
|
// Note: FakeTimeProvider is defined in KeyRotationServiceTests.cs
|