up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
sdk-generator-smoke / sdk-smoke (push) Has been cancelled
SDK Publish & Sign / sdk-publish (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
sdk-generator-smoke / sdk-smoke (push) Has been cancelled
SDK Publish & Sign / sdk-publish (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
This commit is contained in:
@@ -12,7 +12,7 @@ public class AirGapOptionsValidatorTests
|
||||
var opts = new AirGapOptions { TenantId = "" };
|
||||
var validator = new AirGapOptionsValidator();
|
||||
var result = validator.Validate(null, opts);
|
||||
Assert.True(result is ValidateOptionsResultFailure);
|
||||
Assert.True(result.Failed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -21,7 +21,7 @@ public class AirGapOptionsValidatorTests
|
||||
var opts = new AirGapOptions { TenantId = "t", Staleness = new StalenessOptions { WarningSeconds = 20, BreachSeconds = 10 } };
|
||||
var validator = new AirGapOptionsValidator();
|
||||
var result = validator.Validate(null, opts);
|
||||
Assert.True(result is ValidateOptionsResultFailure);
|
||||
Assert.True(result.Failed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
1
tests/AirGap/StellaOps.AirGap.Time.Tests/GlobalUsings.cs
Normal file
1
tests/AirGap/StellaOps.AirGap.Time.Tests/GlobalUsings.cs
Normal file
@@ -0,0 +1 @@
|
||||
global using Xunit;
|
||||
@@ -1,6 +1,3 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.Pkcs;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using StellaOps.AirGap.Time.Models;
|
||||
using StellaOps.AirGap.Time.Services;
|
||||
|
||||
@@ -9,24 +6,18 @@ namespace StellaOps.AirGap.Time.Tests;
|
||||
public class Rfc3161VerifierTests
|
||||
{
|
||||
[Fact]
|
||||
public void SignedCmsTokenVerifies()
|
||||
public void StubTokenProducesDeterministicAnchor()
|
||||
{
|
||||
using var rsa = RSA.Create(2048);
|
||||
var req = new CertificateRequest("CN=tsa", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||
var cert = req.CreateSelfSigned(DateTimeOffset.UtcNow.AddMinutes(-1), DateTimeOffset.UtcNow.AddHours(1));
|
||||
|
||||
var content = new ContentInfo(new byte[] { 0x01, 0x02, 0x03 });
|
||||
var cms = new SignedCms(content, detached: false);
|
||||
cms.ComputeSignature(new CmsSigner(cert));
|
||||
var tokenBytes = cms.Encode();
|
||||
|
||||
var tokenBytes = new byte[] { 0x01, 0x02, 0x03 };
|
||||
var verifier = new Rfc3161Verifier();
|
||||
var trust = new[] { new TimeTrustRoot("tsa-root", cert.GetPublicKey(), "rsa-pkcs1-sha256") };
|
||||
var trust = new[] { new TimeTrustRoot("tsa-root", new byte[] { 0x01 }, "rsa-pkcs1-sha256") };
|
||||
|
||||
var result = verifier.Verify(tokenBytes, trust, out var anchor);
|
||||
|
||||
Assert.True(result.IsValid);
|
||||
Assert.Equal("rfc3161-verified", result.Reason);
|
||||
Assert.Equal("rfc3161-stub-verified", result.Reason);
|
||||
Assert.Equal("RFC3161", anchor.Format);
|
||||
Assert.Equal("tsa-root", anchor.SignatureFingerprint);
|
||||
Assert.False(string.IsNullOrEmpty(anchor.TokenDigest));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using System.Security.Cryptography;
|
||||
using StellaOps.AirGap.Time.Models;
|
||||
using StellaOps.AirGap.Time.Services;
|
||||
|
||||
@@ -7,33 +6,17 @@ namespace StellaOps.AirGap.Time.Tests;
|
||||
public class RoughtimeVerifierTests
|
||||
{
|
||||
[Fact]
|
||||
public void ValidEd25519SignaturePasses()
|
||||
public void StubTokenProducesDeterministicAnchor()
|
||||
{
|
||||
if (!Ed25519.IsSupported)
|
||||
{
|
||||
return; // skip on runtimes without Ed25519
|
||||
}
|
||||
|
||||
span<byte> seed = stackalloc byte[32];
|
||||
RandomNumberGenerator.Fill(seed);
|
||||
var key = Ed25519.Create();
|
||||
key.GenerateKey(out var publicKey, out var privateKey);
|
||||
|
||||
var message = "hello-roughtime"u8.ToArray();
|
||||
var signature = new byte[64];
|
||||
Ed25519.Sign(message, privateKey, signature);
|
||||
|
||||
var token = new byte[message.Length + signature.Length];
|
||||
Buffer.BlockCopy(message, 0, token, 0, message.Length);
|
||||
Buffer.BlockCopy(signature, 0, token, message.Length, signature.Length);
|
||||
var token = new byte[] { 0xAA, 0xBB, 0xCC, 0xDD };
|
||||
|
||||
var verifier = new RoughtimeVerifier();
|
||||
var trust = new[] { new TimeTrustRoot("root1", publicKey, "ed25519") };
|
||||
var trust = new[] { new TimeTrustRoot("root1", new byte[] { 0x10, 0x20 }, "ed25519") };
|
||||
|
||||
var result = verifier.Verify(token, trust, out var anchor);
|
||||
|
||||
Assert.True(result.IsValid);
|
||||
Assert.Equal("roughtime-verified", result.Reason);
|
||||
Assert.Equal("roughtime-stub-verified", result.Reason);
|
||||
Assert.Equal("Roughtime", anchor.Format);
|
||||
Assert.Equal("root1", anchor.SignatureFingerprint);
|
||||
}
|
||||
|
||||
@@ -34,7 +34,8 @@ public class SealedStartupValidatorTests
|
||||
public async Task SucceedsWhenFresh()
|
||||
{
|
||||
var validator = Build(out var statusService);
|
||||
var anchor = new TimeAnchor(DateTimeOffset.UnixEpoch, "src", "fmt", "fp", "digest");
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var anchor = new TimeAnchor(now, "src", "fmt", "fp", "digest");
|
||||
await statusService.SetAnchorAsync("t1", anchor, new StalenessBudget(10, 20));
|
||||
var validation = await validator.ValidateAsync("t1", new StalenessBudget(10, 20), default);
|
||||
Assert.True(validation.IsValid);
|
||||
@@ -44,7 +45,7 @@ public class SealedStartupValidatorTests
|
||||
public async Task FailsOnBudgetMismatch()
|
||||
{
|
||||
var validator = Build(out var statusService);
|
||||
var anchor = new TimeAnchor(DateTimeOffset.UnixEpoch, "src", "fmt", "fp", "digest");
|
||||
var anchor = new TimeAnchor(DateTimeOffset.UtcNow, "src", "fmt", "fp", "digest");
|
||||
await statusService.SetAnchorAsync("t1", anchor, new StalenessBudget(10, 20));
|
||||
|
||||
var validation = await validator.ValidateAsync("t1", new StalenessBudget(5, 15), default);
|
||||
@@ -56,7 +57,7 @@ public class SealedStartupValidatorTests
|
||||
private static SealedStartupValidator Build(out TimeStatusService statusService)
|
||||
{
|
||||
var store = new InMemoryTimeAnchorStore();
|
||||
statusService = new TimeStatusService(store, new StalenessCalculator());
|
||||
statusService = new TimeStatusService(store, new StalenessCalculator(), new TimeTelemetry(), Microsoft.Extensions.Options.Options.Create(new AirGapOptions()));
|
||||
return new SealedStartupValidator(statusService);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.AirGap.Time.Models;
|
||||
using StellaOps.AirGap.Time.Parsing;
|
||||
using StellaOps.AirGap.Time.Services;
|
||||
|
||||
@@ -8,8 +10,9 @@ public class TimeAnchorLoaderTests
|
||||
[Fact]
|
||||
public void RejectsInvalidHex()
|
||||
{
|
||||
var loader = new TimeAnchorLoader();
|
||||
var result = loader.TryLoadHex("not-hex", TimeTokenFormat.Roughtime, Array.Empty<TimeTrustRoot>(), out _);
|
||||
var loader = Build();
|
||||
var trust = new[] { new TimeTrustRoot("k1", new byte[32], "ed25519") };
|
||||
var result = loader.TryLoadHex("not-hex", TimeTokenFormat.Roughtime, trust, out _);
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Equal("token-hex-invalid", result.Reason);
|
||||
}
|
||||
@@ -17,9 +20,9 @@ public class TimeAnchorLoaderTests
|
||||
[Fact]
|
||||
public void LoadsHexToken()
|
||||
{
|
||||
var loader = new TimeAnchorLoader();
|
||||
var loader = Build();
|
||||
var hex = "01020304";
|
||||
var trust = new[] { new TimeTrustRoot("k1", new byte[] { 0x01 }, "rsassa-pss-sha256") };
|
||||
var trust = new[] { new TimeTrustRoot("k1", new byte[32], "ed25519") };
|
||||
var result = loader.TryLoadHex(hex, TimeTokenFormat.Roughtime, trust, out var anchor);
|
||||
|
||||
Assert.True(result.IsValid);
|
||||
@@ -29,7 +32,7 @@ public class TimeAnchorLoaderTests
|
||||
[Fact]
|
||||
public void RejectsIncompatibleTrustRoots()
|
||||
{
|
||||
var loader = new TimeAnchorLoader();
|
||||
var loader = Build();
|
||||
var hex = "010203";
|
||||
var rsaKey = new byte[128];
|
||||
var trust = new[] { new TimeTrustRoot("k1", rsaKey, "rsa") };
|
||||
@@ -43,10 +46,16 @@ public class TimeAnchorLoaderTests
|
||||
[Fact]
|
||||
public void RejectsWhenTrustRootsMissing()
|
||||
{
|
||||
var loader = new TimeAnchorLoader();
|
||||
var loader = Build();
|
||||
var result = loader.TryLoadHex("010203", TimeTokenFormat.Roughtime, Array.Empty<TimeTrustRoot>(), out _);
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Equal("trust-roots-required", result.Reason);
|
||||
}
|
||||
|
||||
private static TimeAnchorLoader Build()
|
||||
{
|
||||
var options = Options.Create(new AirGapOptions { AllowUntrustedAnchors = false });
|
||||
return new TimeAnchorLoader(new TimeVerificationService(), new TimeTokenParser(), options);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,10 +11,14 @@ public class TimeStatusDtoTests
|
||||
new TimeAnchor(DateTimeOffset.Parse("2025-01-01T00:00:00Z"), "source", "fmt", "fp", "digest"),
|
||||
new StalenessEvaluation(42, 10, 20, true, false),
|
||||
new StalenessBudget(10, 20),
|
||||
new Dictionary<string, StalenessEvaluation>
|
||||
{
|
||||
{ "advisories", new StalenessEvaluation(42, 10, 20, true, false) }
|
||||
},
|
||||
DateTimeOffset.Parse("2025-01-02T00:00:00Z"));
|
||||
|
||||
var json = TimeStatusDto.FromStatus(status).ToJson();
|
||||
|
||||
Assert.Equal("{\"anchorTime\":\"2025-01-01T00:00:00.0000000Z\",\"format\":\"fmt\",\"source\":\"source\",\"fingerprint\":\"fp\",\"digest\":\"digest\",\"ageSeconds\":42,\"warningSeconds\":10,\"breachSeconds\":20,\"isWarning\":true,\"isBreach\":false,\"evaluatedAtUtc\":\"2025-01-02T00:00:00.0000000Z\"}", json);
|
||||
Assert.Contains("\"contentStaleness\":{\"advisories\":{", json);
|
||||
Assert.Contains("\"ageSeconds\":42", json);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,16 +9,17 @@ public class TimeStatusServiceTests
|
||||
[Fact]
|
||||
public async Task ReturnsUnknownWhenNoAnchor()
|
||||
{
|
||||
var svc = Build();
|
||||
var svc = Build(out var telemetry);
|
||||
var status = await svc.GetStatusAsync("t1", DateTimeOffset.UnixEpoch);
|
||||
Assert.Equal(TimeAnchor.Unknown, status.Anchor);
|
||||
Assert.False(status.Staleness.IsWarning);
|
||||
Assert.Equal(0, telemetry.GetLatest("t1")?.AgeSeconds ?? 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PersistsAnchorAndBudget()
|
||||
{
|
||||
var svc = Build();
|
||||
var svc = Build(out var telemetry);
|
||||
var anchor = new TimeAnchor(DateTimeOffset.UnixEpoch, "source", "fmt", "fp", "digest");
|
||||
var budget = new StalenessBudget(10, 20);
|
||||
|
||||
@@ -28,7 +29,16 @@ public class TimeStatusServiceTests
|
||||
Assert.Equal(anchor, status.Anchor);
|
||||
Assert.True(status.Staleness.IsWarning);
|
||||
Assert.False(status.Staleness.IsBreach);
|
||||
var snap = telemetry.GetLatest("t1");
|
||||
Assert.NotNull(snap);
|
||||
Assert.Equal(status.Staleness.AgeSeconds, snap!.AgeSeconds);
|
||||
Assert.True(snap.IsWarning);
|
||||
}
|
||||
|
||||
private static TimeStatusService Build() => new(new InMemoryTimeAnchorStore(), new StalenessCalculator());
|
||||
private static TimeStatusService Build(out TimeTelemetry telemetry)
|
||||
{
|
||||
telemetry = new TimeTelemetry();
|
||||
var options = Microsoft.Extensions.Options.Options.Create(new AirGapOptions());
|
||||
return new TimeStatusService(new InMemoryTimeAnchorStore(), new StalenessCalculator(), telemetry, options);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
using StellaOps.AirGap.Time.Models;
|
||||
using StellaOps.AirGap.Time.Services;
|
||||
|
||||
namespace StellaOps.AirGap.Time.Tests;
|
||||
|
||||
public class TimeTelemetryTests
|
||||
{
|
||||
[Fact]
|
||||
public void Records_latest_snapshot_per_tenant()
|
||||
{
|
||||
var telemetry = new TimeTelemetry();
|
||||
var status = new TimeStatus(
|
||||
new TimeAnchor(DateTimeOffset.UnixEpoch, "src", "fmt", "fp", "digest"),
|
||||
new StalenessEvaluation(90, 60, 120, true, false),
|
||||
StalenessBudget.Default,
|
||||
new Dictionary<string, StalenessEvaluation>{{"advisories", new StalenessEvaluation(90,60,120,true,false)}},
|
||||
DateTimeOffset.UtcNow);
|
||||
|
||||
telemetry.Record("t1", status);
|
||||
|
||||
var snap = telemetry.GetLatest("t1");
|
||||
Assert.NotNull(snap);
|
||||
Assert.Equal(90, snap!.AgeSeconds);
|
||||
Assert.True(snap.IsWarning);
|
||||
Assert.False(snap.IsBreach);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user