using StellaOps.AirGap.Time.Models; using StellaOps.AirGap.Time.Services; namespace StellaOps.AirGap.Time.Tests; /// /// Tests for RoughtimeVerifier with real Ed25519 signature verification. /// Per AIRGAP-TIME-57-001: Trusted time-anchor service. /// public class RoughtimeVerifierTests { private readonly RoughtimeVerifier _verifier = new(); [Fact] public void Verify_ReturnsFailure_WhenTrustRootsEmpty() { var token = new byte[] { 0x01, 0x02, 0x03, 0x04 }; var result = _verifier.Verify(token, Array.Empty(), out var anchor); Assert.False(result.IsValid); Assert.Equal("roughtime-trust-roots-required", result.Reason); Assert.Equal(TimeAnchor.Unknown, anchor); } [Fact] public void Verify_ReturnsFailure_WhenTokenEmpty() { var trust = new[] { new TimeTrustRoot("root1", new byte[32], "ed25519") }; var result = _verifier.Verify(ReadOnlySpan.Empty, trust, out var anchor); Assert.False(result.IsValid); Assert.Equal("roughtime-token-empty", result.Reason); Assert.Equal(TimeAnchor.Unknown, anchor); } [Fact] public void Verify_ReturnsFailure_WhenTokenTooShort() { var token = new byte[] { 0x01, 0x02, 0x03 }; var trust = new[] { new TimeTrustRoot("root1", new byte[32], "ed25519") }; var result = _verifier.Verify(token, trust, out var anchor); Assert.False(result.IsValid); Assert.Equal("roughtime-message-too-short", result.Reason); } [Fact] public void Verify_ReturnsFailure_WhenInvalidTagCount() { // Create a minimal wire format with invalid tag count var token = new byte[8]; // Set num_tags to 0 (invalid) BitConverter.TryWriteBytes(token.AsSpan(0, 4), (uint)0); var trust = new[] { new TimeTrustRoot("root1", new byte[32], "ed25519") }; var result = _verifier.Verify(token, trust, out var anchor); Assert.False(result.IsValid); Assert.Equal("roughtime-invalid-tag-count", result.Reason); } [Fact] public void Verify_ReturnsFailure_WhenNonEd25519Algorithm() { // Create a minimal valid-looking wire format var token = CreateMinimalRoughtimeToken(); var trust = new[] { new TimeTrustRoot("root1", new byte[32], "rsa") }; // Wrong algorithm var result = _verifier.Verify(token, trust, out var anchor); Assert.False(result.IsValid); // Should fail either on parsing or signature verification Assert.Contains("roughtime-", result.Reason); } [Fact] public void Verify_ReturnsFailure_WhenKeyLengthWrong() { var token = CreateMinimalRoughtimeToken(); var trust = new[] { new TimeTrustRoot("root1", new byte[16], "ed25519") }; // Wrong key length var result = _verifier.Verify(token, trust, out var anchor); Assert.False(result.IsValid); Assert.Contains("roughtime-", result.Reason); } [Fact] public void Verify_ProducesTokenDigest() { var token = new byte[] { 0xAA, 0xBB, 0xCC, 0xDD }; var trust = new[] { new TimeTrustRoot("root1", new byte[32], "ed25519") }; var result = _verifier.Verify(token, trust, out _); // Even on failure, we should get a deterministic result Assert.False(result.IsValid); } /// /// Creates a minimal Roughtime wire format token for testing parsing paths. /// Note: This will fail signature verification but tests the parsing logic. /// private static byte[] CreateMinimalRoughtimeToken() { // Roughtime wire format: // [num_tags:u32] [offsets:u32[n-1]] [tags:u32[n]] [values...] // We'll create 2 tags: SIG and SREP const uint TagSig = 0x00474953; // "SIG\0" const uint TagSrep = 0x50455253; // "SREP" var sigValue = new byte[64]; // Ed25519 signature var srepValue = CreateMinimalSrep(); // Header: num_tags=2, offset[0]=64 (sig length), tags=[SIG, SREP] var headerSize = 4 + 4 + 8; // num_tags + 1 offset + 2 tags = 16 bytes var token = new byte[headerSize + sigValue.Length + srepValue.Length]; BitConverter.TryWriteBytes(token.AsSpan(0, 4), (uint)2); // num_tags = 2 BitConverter.TryWriteBytes(token.AsSpan(4, 4), (uint)64); // offset[0] = 64 (sig length) BitConverter.TryWriteBytes(token.AsSpan(8, 4), TagSig); BitConverter.TryWriteBytes(token.AsSpan(12, 4), TagSrep); sigValue.CopyTo(token.AsSpan(16)); srepValue.CopyTo(token.AsSpan(16 + 64)); return token; } private static byte[] CreateMinimalSrep() { // SREP with MIDP tag containing 8-byte timestamp const uint TagMidp = 0x5044494D; // "MIDP" // Header: num_tags=1, tags=[MIDP] var headerSize = 4 + 4; // num_tags + 1 tag = 8 bytes var srepValue = new byte[headerSize + 8]; // + 8 bytes for MIDP value BitConverter.TryWriteBytes(srepValue.AsSpan(0, 4), (uint)1); // num_tags = 1 BitConverter.TryWriteBytes(srepValue.AsSpan(4, 4), TagMidp); // MIDP value: microseconds since Unix epoch (example: 2025-01-01 00:00:00 UTC) BitConverter.TryWriteBytes(srepValue.AsSpan(8, 8), 1735689600000000L); return srepValue; } }