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;
}
}