This commit is contained in:
master
2026-02-04 19:59:20 +02:00
parent 557feefdc3
commit 5548cf83bf
1479 changed files with 53557 additions and 40339 deletions

View File

@@ -8,3 +8,4 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
| AUDIT-0087-M | DONE | Revalidated 2026-01-06. |
| AUDIT-0087-T | DONE | Revalidated 2026-01-06 (coverage reviewed). |
| AUDIT-0087-A | DONE | Waived (test project; revalidated 2026-01-06). |
| REMED-05 | DONE | Unit coverage expanded for verdict manifest remediation. |

View File

@@ -77,6 +77,30 @@ public sealed class InMemoryVerdictManifestStoreTests
page3.NextPageToken.Should().BeNull();
}
[Fact]
public async Task ListByAsset_Paginates()
{
var assetDigest = "sha256:asset";
var first = CreateManifest("m0", "t", assetDigest: assetDigest, evaluatedAt: BaseTime);
var second = CreateManifest("m1", "t", assetDigest: assetDigest, evaluatedAt: BaseTime.AddMinutes(-1));
var third = CreateManifest("m2", "t", assetDigest: assetDigest, evaluatedAt: BaseTime.AddMinutes(-2));
await _store.StoreAsync(first);
await _store.StoreAsync(second);
await _store.StoreAsync(third);
var page1 = await _store.ListByAssetAsync("t", assetDigest, limit: 2);
page1.Manifests.Should().HaveCount(2);
page1.Manifests[0].ManifestId.Should().Be("m0");
page1.Manifests[1].ManifestId.Should().Be("m1");
page1.NextPageToken.Should().NotBeNull();
var page2 = await _store.ListByAssetAsync("t", assetDigest, limit: 2, pageToken: page1.NextPageToken);
page2.Manifests.Should().HaveCount(1);
page2.Manifests[0].ManifestId.Should().Be("m2");
page2.NextPageToken.Should().BeNull();
}
[Fact]
public async Task Delete_RemovesManifest()
{

View File

@@ -34,6 +34,16 @@ public sealed class VerdictManifestSerializerTests
deserialized.Result.Confidence.Should().Be(manifest.Result.Confidence);
}
[Theory]
[InlineData("")]
[InlineData(" ")]
public void Deserialize_ReturnsNull_ForEmptyJson(string json)
{
var deserialized = VerdictManifestSerializer.Deserialize(json);
deserialized.Should().BeNull();
}
[Fact]
public void ComputeDigest_IsDeterministic()
{

View File

@@ -33,9 +33,31 @@ public sealed class VerdictReplayVerifierTests
result.Error.Should().Contain("Signature verification failed");
}
private static VerdictManifest CreateManifest()
[Fact]
public async Task VerifyAsync_ReturnsDifferences_WhenReplayDiffers()
{
return new VerdictManifest
var replayResult = new VerdictResult
{
Status = VexStatus.Affected,
Confidence = 0.2,
Explanations = ImmutableArray<VerdictExplanation>.Empty,
EvidenceRefs = ImmutableArray<string>.Empty,
};
var verifier = new VerdictReplayVerifier(new NullStore(), new NullVerdictManifestSigner(), new FixedEvaluator(replayResult));
var manifest = CreateManifest(includeSignature: false);
var result = await verifier.VerifyAsync(manifest, CancellationToken.None);
result.Success.Should().BeFalse();
result.SignatureValid.Should().BeTrue();
result.Differences.Should().NotBeNull();
result.Differences!.Should().NotBeEmpty();
}
private static VerdictManifest CreateManifest(VerdictResult? result = null, bool includeSignature = true)
{
var manifest = new VerdictManifest
{
ManifestId = "manifest-1",
Tenant = "tenant-a",
@@ -49,7 +71,7 @@ public sealed class VerdictReplayVerifierTests
ReachabilityGraphIds = ImmutableArray<string>.Empty,
ClockCutoff = DateTimeOffset.Parse("2025-01-01T00:00:00Z"),
},
Result = new VerdictResult
Result = result ?? new VerdictResult
{
Status = VexStatus.NotAffected,
Confidence = 0.5,
@@ -59,9 +81,12 @@ public sealed class VerdictReplayVerifierTests
PolicyHash = "sha256:policy",
LatticeVersion = "1.0.0",
EvaluatedAt = DateTimeOffset.Parse("2025-01-01T00:00:00Z"),
ManifestDigest = "sha256:manifest",
SignatureBase64 = "invalid"
ManifestDigest = string.Empty,
SignatureBase64 = includeSignature ? "invalid" : null
};
var digest = VerdictManifestSerializer.ComputeDigest(manifest);
return manifest with { ManifestDigest = digest };
}
private sealed class NullStore : IVerdictManifestStore
@@ -122,4 +147,21 @@ public sealed class VerdictReplayVerifierTests
});
}
}
private sealed class FixedEvaluator(VerdictResult result) : IVerdictEvaluator
{
private readonly VerdictResult _result = result;
public Task<VerdictResult> EvaluateAsync(
string tenant,
string assetDigest,
string vulnerabilityId,
VerdictInputs inputs,
string policyHash,
string latticeVersion,
CancellationToken ct = default)
{
return Task.FromResult(_result);
}
}
}

View File

@@ -0,0 +1,22 @@
# Authority Timestamping Abstractions Tests AGENTS
## Purpose & Scope
- Working directory: `src/Authority/__Tests/StellaOps.Authority.Timestamping.Abstractions.Tests/`.
- Roles: QA automation, backend engineer.
- Focus: timestamp request/response models, verification options, and deterministic helpers.
## Required Reading (treat as read before DOING)
- `docs/README.md`
- `docs/07_HIGH_LEVEL_ARCHITECTURE.md`
- `docs/modules/platform/architecture-overview.md`
- `docs/modules/authority/architecture.md`
- Relevant sprint files.
## Working Agreements
- Keep tests deterministic (fixed inputs, stable ordering).
- Avoid live network calls; use in-memory data only.
- Update `docs/implplan/SPRINT_*.md` and local `TASKS.md` when starting or completing work.
## Testing
- Use xUnit assertions.
- Cover factory helpers, option defaults, and validation mappings.

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" ?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<RootNamespace>StellaOps.Authority.Timestamping.Abstractions.Tests</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\__Libraries\StellaOps.Authority.Timestamping.Abstractions\StellaOps.Authority.Timestamping.Abstractions.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,8 @@
# Authority Timestamping Abstractions Tests Task Board
This board mirrors active sprint tasks for this module.
Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_solid_review.md`.
| Task ID | Status | Notes |
| --- | --- | --- |
| REMED-05 | DONE | Unit test coverage added to remediate Timestamping.Abstractions test gap (2026-02-04). |

View File

@@ -0,0 +1,39 @@
using System.Security.Cryptography;
namespace StellaOps.Authority.Timestamping.Abstractions.Tests;
public sealed class TimeStampRequestTests
{
[Fact]
public void CreateFromHash_UsesProvidedHashAndNonceToggle()
{
var hash = new byte[] { 0x01, 0x02, 0x03 };
var request = TimeStampRequest.CreateFromHash(hash, HashAlgorithmName.SHA256, includeNonce: false);
Assert.Equal(hash, request.MessageImprint.ToArray());
Assert.Null(request.Nonce);
}
[Fact]
public void Create_ComputesHashAndNonceWhenRequested()
{
var data = new byte[] { 0x10, 0x20, 0x30 };
var expectedHash = SHA256.HashData(data);
var request = TimeStampRequest.Create(data, HashAlgorithmName.SHA256, includeNonce: true);
Assert.Equal(expectedHash, request.MessageImprint.ToArray());
Assert.NotNull(request.Nonce);
Assert.Equal(8, request.Nonce!.Value.Length);
}
[Fact]
public void Create_ThrowsForUnsupportedHashAlgorithm()
{
var act = () => TimeStampRequest.Create([0x01], new HashAlgorithmName("MD5"));
var exception = Assert.Throws<ArgumentException>(act);
Assert.Equal("algorithm", exception.ParamName);
}
}

View File

@@ -0,0 +1,28 @@
namespace StellaOps.Authority.Timestamping.Abstractions.Tests;
public sealed class TimeStampResponseTests
{
[Fact]
public void Success_PopulatesTokenAndProvider()
{
var token = TimestampingTestData.CreateToken();
var response = TimeStampResponse.Success(token, "tsa-A");
Assert.True(response.IsSuccess);
Assert.Equal(PkiStatus.Granted, response.Status);
Assert.Same(token, response.Token);
Assert.Equal("tsa-A", response.ProviderName);
}
[Fact]
public void Failure_PopulatesStatusAndErrorFields()
{
var response = TimeStampResponse.Failure(PkiStatus.Rejection, PkiFailureInfo.BadAlg, "bad algo");
Assert.False(response.IsSuccess);
Assert.Equal(PkiStatus.Rejection, response.Status);
Assert.Equal(PkiFailureInfo.BadAlg, response.FailureInfo);
Assert.Equal("bad algo", response.StatusString);
}
}

View File

@@ -0,0 +1,18 @@
using System.Security.Cryptography;
namespace StellaOps.Authority.Timestamping.Abstractions.Tests;
public sealed class TimeStampTokenTests
{
[Fact]
public void TstInfoDigest_UsesLowercaseSha256()
{
var encoded = new byte[] { 0x10, 0x20, 0x30 };
var info = TimestampingTestData.CreateTstInfo(encoded: encoded);
var token = TimestampingTestData.CreateToken(info);
var expected = Convert.ToHexString(SHA256.HashData(encoded)).ToLowerInvariant();
Assert.Equal(expected, token.TstInfoDigest);
}
}

View File

@@ -0,0 +1,36 @@
using System.Security.Cryptography.X509Certificates;
namespace StellaOps.Authority.Timestamping.Abstractions.Tests;
public sealed class TimeStampVerificationOptionsTests
{
[Fact]
public void Defaults_AreStable()
{
var options = TimeStampVerificationOptions.Default;
Assert.True(options.VerifyCertificateChain);
Assert.True(options.CheckRevocation);
Assert.False(options.AllowWeakHashAlgorithms);
}
[Fact]
public void Strict_EnablesChecks()
{
var options = TimeStampVerificationOptions.Strict;
Assert.True(options.VerifyCertificateChain);
Assert.True(options.CheckRevocation);
Assert.False(options.AllowWeakHashAlgorithms);
Assert.Equal(60, options.MaxAccuracySeconds);
}
[Fact]
public void Offline_DisablesRevocation()
{
var options = TimeStampVerificationOptions.Offline;
Assert.False(options.CheckRevocation);
Assert.Equal(X509RevocationMode.NoCheck, options.RevocationMode);
}
}

View File

@@ -0,0 +1,31 @@
namespace StellaOps.Authority.Timestamping.Abstractions.Tests;
public sealed class TimeStampVerificationResultTests
{
[Fact]
public void Success_SetsValidStatusAndData()
{
var verifiedTime = DateTimeOffset.UnixEpoch;
var timeRange = (verifiedTime, verifiedTime.AddSeconds(1));
var result = TimeStampVerificationResult.Success(verifiedTime, timeRange, policyOid: "1.2.3");
Assert.True(result.IsValid);
Assert.Equal(VerificationStatus.Valid, result.Status);
Assert.Equal(verifiedTime, result.VerifiedTime);
Assert.Equal(timeRange, result.TimeRange);
Assert.Equal("1.2.3", result.PolicyOid);
}
[Fact]
public void Failure_MapsErrorCodeToStatus()
{
var error = new VerificationError(VerificationErrorCode.SignatureInvalid, "bad signature");
var result = TimeStampVerificationResult.Failure(error);
Assert.Equal(VerificationStatus.SignatureInvalid, result.Status);
Assert.Same(error, result.Error);
Assert.False(result.IsValid);
}
}

View File

@@ -0,0 +1,33 @@
using System.Security.Cryptography;
namespace StellaOps.Authority.Timestamping.Abstractions.Tests;
internal static class TimestampingTestData
{
internal static TstInfo CreateTstInfo(
ReadOnlyMemory<byte>? encoded = null,
DateTimeOffset? genTime = null,
TstAccuracy? accuracy = null)
{
var encodedValue = encoded ?? new byte[] { 0x01, 0x02, 0x03 };
return new TstInfo
{
EncodedTstInfo = encodedValue,
PolicyOid = "1.2.3",
HashAlgorithm = HashAlgorithmName.SHA256,
MessageImprint = new byte[] { 0xAA },
SerialNumber = new byte[] { 0xBB },
GenTime = genTime ?? DateTimeOffset.UnixEpoch,
Accuracy = accuracy
};
}
internal static TimeStampToken CreateToken(TstInfo? info = null)
{
return new TimeStampToken
{
EncodedToken = new byte[] { 0x10 },
TstInfo = info ?? CreateTstInfo()
};
}
}

View File

@@ -0,0 +1,36 @@
namespace StellaOps.Authority.Timestamping.Abstractions.Tests;
public sealed class TsaClientOptionsTests
{
[Fact]
public void Defaults_AreStable()
{
var options = new TsaClientOptions();
Assert.Equal(FailoverStrategy.Priority, options.FailoverStrategy);
Assert.True(options.EnableCaching);
Assert.Equal(TimeSpan.FromHours(24), options.CacheDuration);
Assert.Equal("SHA256", options.DefaultHashAlgorithm);
Assert.True(options.IncludeNonceByDefault);
Assert.True(options.RequestCertificatesByDefault);
Assert.Same(TimeStampVerificationOptions.Default, options.DefaultVerificationOptions);
Assert.Empty(options.Providers);
}
[Fact]
public void ProviderDefaults_AreStable()
{
var provider = new TsaProviderOptions
{
Name = "tsa-A",
Url = new Uri("https://tsa.example")
};
Assert.Equal(100, provider.Priority);
Assert.Equal(TimeSpan.FromSeconds(30), provider.Timeout);
Assert.Equal(3, provider.RetryCount);
Assert.Equal(TimeSpan.FromSeconds(1), provider.RetryBaseDelay);
Assert.True(provider.Enabled);
Assert.Empty(provider.Headers);
}
}

View File

@@ -0,0 +1,12 @@
namespace StellaOps.Authority.Timestamping.Abstractions.Tests;
public sealed class TstAccuracyTests
{
[Fact]
public void ToTimeSpan_ConvertsMicrosAndMillis()
{
var accuracy = new TstAccuracy { Seconds = 2, Millis = 3, Micros = 4 };
Assert.Equal(TimeSpan.FromMicroseconds(2_003_004), accuracy.ToTimeSpan());
}
}

View File

@@ -0,0 +1,29 @@
namespace StellaOps.Authority.Timestamping.Abstractions.Tests;
public sealed class TstInfoTests
{
[Fact]
public void GetTimeRange_UsesAccuracyWhenPresent()
{
var genTime = new DateTimeOffset(2026, 2, 4, 0, 0, 0, TimeSpan.Zero);
var accuracy = new TstAccuracy { Seconds = 1, Millis = 500 };
var info = TimestampingTestData.CreateTstInfo(genTime: genTime, accuracy: accuracy);
var (earliest, latest) = info.GetTimeRange();
Assert.Equal(genTime - TimeSpan.FromSeconds(1.5), earliest);
Assert.Equal(genTime + TimeSpan.FromSeconds(1.5), latest);
}
[Fact]
public void GetTimeRange_DefaultsToGenTimeWithoutAccuracy()
{
var genTime = new DateTimeOffset(2026, 2, 4, 1, 0, 0, TimeSpan.Zero);
var info = TimestampingTestData.CreateTstInfo(genTime: genTime, accuracy: null);
var (earliest, latest) = info.GetTimeRange();
Assert.Equal(genTime, earliest);
Assert.Equal(genTime, latest);
}
}

View File

@@ -0,0 +1,21 @@
# Authority Timestamping Tests AGENTS
## Purpose & Scope
- Working directory: `src/Authority/__Tests/StellaOps.Authority.Timestamping.Tests/`.
- Roles: QA automation, backend engineer.
- Focus: timestamping client helpers, registry/cache behavior, and ASN.1 encoding/decoding.
## Required Reading (treat as read before DOING)
- `docs/README.md`
- `docs/07_HIGH_LEVEL_ARCHITECTURE.md`
- `docs/modules/platform/architecture-overview.md`
- `docs/modules/authority/architecture.md`
- Relevant sprint files.
## Working Agreements
- Keep tests deterministic (fixed inputs, stable ordering).
- Avoid live network calls; use in-memory handlers only.
- Update `docs/implplan/SPRINT_*.md` and local `TASKS.md` when starting or completing work.
## Testing
- Use xUnit assertions.

View File

@@ -0,0 +1,41 @@
using StellaOps.Authority.Timestamping.Caching;
namespace StellaOps.Authority.Timestamping.Tests;
public sealed class InMemoryTsaCacheStoreTests
{
[Fact]
public async Task GetAsync_ReturnsToken_WhenPresent()
{
using var store = new InMemoryTsaCacheStore(TimeSpan.FromHours(1));
var token = TimestampingTestData.CreateToken();
await store.SetAsync(token.TstInfo.MessageImprint, token, TimeSpan.FromMinutes(5));
var result = await store.GetAsync(token.TstInfo.MessageImprint);
Assert.Same(token, result);
var stats = store.GetStats();
Assert.Equal(1, stats.ItemCount);
Assert.Equal(1, stats.HitCount);
Assert.Equal(0, stats.MissCount);
Assert.Equal(token.EncodedToken.Length, stats.ApproximateSizeBytes);
}
[Fact]
public async Task GetAsync_ReturnsNull_WhenExpired()
{
using var store = new InMemoryTsaCacheStore(TimeSpan.FromHours(1));
var token = TimestampingTestData.CreateToken();
await store.SetAsync(token.TstInfo.MessageImprint, token, TimeSpan.FromSeconds(-1));
var result = await store.GetAsync(token.TstInfo.MessageImprint);
Assert.Null(result);
var stats = store.GetStats();
Assert.Equal(0, stats.ItemCount);
Assert.Equal(0, stats.HitCount);
Assert.Equal(1, stats.MissCount);
}
}

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" ?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<RootNamespace>StellaOps.Authority.Timestamping.Tests</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\__Libraries\StellaOps.Authority.Timestamping\StellaOps.Authority.Timestamping.csproj" />
<ProjectReference Include="..\..\__Libraries\StellaOps.Authority.Timestamping.Abstractions\StellaOps.Authority.Timestamping.Abstractions.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,8 @@
# Authority Timestamping Tests Task Board
This board mirrors active sprint tasks for this module.
Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_solid_review.md`.
| Task ID | Status | Notes |
| --- | --- | --- |
| REMED-05 | DONE | Added unit tests for Timestamping library remediation gaps. |

View File

@@ -0,0 +1,58 @@
using StellaOps.Authority.Timestamping.Abstractions;
using StellaOps.Authority.Timestamping.Asn1;
using System.Formats.Asn1;
using System.Security.Cryptography;
namespace StellaOps.Authority.Timestamping.Tests;
public sealed class TimeStampReqEncoderTests
{
[Fact]
public void GetHashAlgorithmOid_RoundTrips()
{
var oid = TimeStampReqEncoder.GetHashAlgorithmOid(HashAlgorithmName.SHA256);
var roundTrip = TimeStampReqEncoder.GetHashAlgorithmFromOid(oid);
Assert.Equal(HashAlgorithmName.SHA256, roundTrip);
}
[Fact]
public void Encode_WritesExpectedFields()
{
var request = new TimeStampRequest
{
HashAlgorithm = HashAlgorithmName.SHA256,
MessageImprint = new byte[] { 0x10, 0x20 },
PolicyOid = "1.2.3.4.5",
Nonce = new byte[] { 0x01, 0x02 },
CertificateRequired = true,
Extensions = new[]
{
new TimeStampExtension("1.2.3.4.5.6", true, new byte[] { 0xAA })
}
};
var encoded = TimeStampReqEncoder.Encode(request);
var reader = new AsnReader(encoded, AsnEncodingRules.DER);
var sequence = reader.ReadSequence();
Assert.Equal(1, (int)sequence.ReadInteger());
var messageImprint = sequence.ReadSequence();
var algId = messageImprint.ReadSequence();
Assert.Equal(TimeStampReqEncoder.GetHashAlgorithmOid(request.HashAlgorithm), algId.ReadObjectIdentifier());
algId.ReadNull();
Assert.Equal(request.MessageImprint.Span.ToArray(), messageImprint.ReadOctetString());
Assert.Equal(request.PolicyOid, sequence.ReadObjectIdentifier());
Assert.Equal(request.Nonce!.Value.Span.ToArray(), sequence.ReadIntegerBytes().ToArray());
Assert.True(sequence.ReadBoolean());
var extSeq = sequence.ReadSequence(new Asn1Tag(TagClass.ContextSpecific, 0));
var extension = extSeq.ReadSequence();
Assert.Equal("1.2.3.4.5.6", extension.ReadObjectIdentifier());
Assert.True(extension.ReadBoolean());
Assert.Equal(new byte[] { 0xAA }, extension.ReadOctetString());
Assert.False(sequence.HasData);
}
}

View File

@@ -0,0 +1,42 @@
using StellaOps.Authority.Timestamping.Abstractions;
using StellaOps.Authority.Timestamping.Asn1;
using System.Formats.Asn1;
namespace StellaOps.Authority.Timestamping.Tests;
public sealed class TimeStampRespDecoderTests
{
[Fact]
public void Decode_ParsesStatusAndFailureInfo()
{
var encoded = BuildResponse(PkiStatus.Rejection, "bad request");
var response = TimeStampRespDecoder.Decode(encoded);
Assert.Equal(PkiStatus.Rejection, response.Status);
Assert.Equal("bad request", response.StatusString);
Assert.Equal(PkiFailureInfo.BadAlg, response.FailureInfo);
Assert.Null(response.Token);
}
private static byte[] BuildResponse(PkiStatus status, string statusString)
{
var writer = new AsnWriter(AsnEncodingRules.DER);
using (writer.PushSequence())
{
using (writer.PushSequence())
{
writer.WriteInteger((int)status);
using (writer.PushSequence())
{
writer.WriteCharacterString(UniversalTagNumber.UTF8String, statusString);
}
writer.WriteBitString(new byte[] { 0x80 }, 7);
}
}
return writer.Encode();
}
}

View File

@@ -0,0 +1,37 @@
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Authority.Timestamping.Abstractions;
namespace StellaOps.Authority.Timestamping.Tests;
public sealed class TimeStampTokenVerifierTests
{
[Fact]
public async Task VerifyAsync_ReturnsImprintMismatch_WhenHashDiffers()
{
var verifier = new TimeStampTokenVerifier(NullLogger<TimeStampTokenVerifier>.Instance);
var info = TimestampingTestData.CreateTstInfo(messageImprint: new byte[] { 0x01 });
var token = TimestampingTestData.CreateToken(info);
var result = await verifier.VerifyAsync(
token,
new byte[] { 0x02 },
new TimeStampVerificationOptions());
Assert.Equal(VerificationErrorCode.MessageImprintMismatch, result.Error?.Code);
}
[Fact]
public async Task VerifyAsync_ReturnsNonceMismatch_WhenExpectedNonceMissing()
{
var verifier = new TimeStampTokenVerifier(NullLogger<TimeStampTokenVerifier>.Instance);
var info = TimestampingTestData.CreateTstInfo(messageImprint: new byte[] { 0x0A });
var token = TimestampingTestData.CreateToken(info);
var result = await verifier.VerifyAsync(
token,
info.MessageImprint,
new TimeStampVerificationOptions { ExpectedNonce = new byte[] { 0xFF } });
Assert.Equal(VerificationErrorCode.NonceMismatch, result.Error?.Code);
}
}

View File

@@ -0,0 +1,38 @@
using StellaOps.Authority.Timestamping.Abstractions;
using System.Security.Cryptography;
namespace StellaOps.Authority.Timestamping.Tests;
internal static class TimestampingTestData
{
internal static TstInfo CreateTstInfo(
ReadOnlyMemory<byte>? messageImprint = null,
HashAlgorithmName? algorithm = null,
ReadOnlyMemory<byte>? nonce = null,
TstAccuracy? accuracy = null,
DateTimeOffset? genTime = null)
{
return new TstInfo
{
EncodedTstInfo = new byte[] { 0x01, 0x02 },
PolicyOid = "1.2.3.4.5",
HashAlgorithm = algorithm ?? HashAlgorithmName.SHA256,
MessageImprint = messageImprint ?? new byte[] { 0x10, 0x20 },
SerialNumber = new byte[] { 0x0A },
GenTime = genTime ?? new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero),
Nonce = nonce,
Accuracy = accuracy
};
}
internal static TimeStampToken CreateToken(
TstInfo? info = null,
ReadOnlyMemory<byte>? encodedToken = null)
{
return new TimeStampToken
{
EncodedToken = encodedToken ?? new byte[] { 0x30, 0x00 },
TstInfo = info ?? CreateTstInfo()
};
}
}

View File

@@ -0,0 +1,89 @@
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Authority.Timestamping.Abstractions;
using System.Net;
using System.Net.Http;
namespace StellaOps.Authority.Timestamping.Tests;
public sealed class TsaProviderRegistryTests
{
[Fact]
public void GetOrderedProviders_RespectsPriority()
{
var registry = CreateRegistry(
new TestMessageHandler(HttpStatusCode.OK),
new TsaProviderOptions { Name = "slow", Url = new Uri("https://tsa.slow"), Priority = 200 },
new TsaProviderOptions { Name = "fast", Url = new Uri("https://tsa.fast"), Priority = 100 });
var ordered = registry.GetOrderedProviders().Select(p => p.Name).ToArray();
Assert.Equal(new[] { "fast", "slow" }, ordered);
}
[Fact]
public async Task CheckHealthAsync_ReturnsHealthy_OnAnyResponse()
{
var handler = new TestMessageHandler(HttpStatusCode.ServiceUnavailable);
var registry = CreateRegistry(handler, new TsaProviderOptions
{
Name = "primary",
Url = new Uri("https://tsa.primary")
});
var health = await registry.CheckHealthAsync("primary");
Assert.True(health.IsHealthy);
Assert.Equal(HttpMethod.Head, handler.LastRequest?.Method);
}
[Fact]
public void ReportSuccess_UpdatesStatsAndHealth()
{
var registry = CreateRegistry(
new TestMessageHandler(HttpStatusCode.OK),
new TsaProviderOptions { Name = "primary", Url = new Uri("https://tsa.primary") });
registry.ReportSuccess("primary", TimeSpan.FromMilliseconds(12));
var provider = registry.GetProviders().Single();
Assert.Equal(1, provider.Stats.TotalRequests);
Assert.Equal(1, provider.Stats.SuccessCount);
Assert.Equal(0, provider.Stats.FailureCount);
Assert.Equal(TsaHealthStatus.Healthy, provider.Health.Status);
}
private static TsaProviderRegistry CreateRegistry(HttpMessageHandler handler, params TsaProviderOptions[] providers)
{
var options = new TsaClientOptions
{
FailoverStrategy = FailoverStrategy.Priority,
Providers = providers.ToList()
};
return new TsaProviderRegistry(
Options.Create(options),
new TestHttpClientFactory(handler),
NullLogger<TsaProviderRegistry>.Instance);
}
private sealed class TestHttpClientFactory(HttpMessageHandler handler) : IHttpClientFactory
{
private readonly HttpClient _client = new(handler);
public HttpClient CreateClient(string name) => _client;
}
private sealed class TestMessageHandler(HttpStatusCode statusCode) : HttpMessageHandler
{
public HttpRequestMessage? LastRequest { get; private set; }
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
LastRequest = request;
return Task.FromResult(new HttpResponseMessage(statusCode));
}
}
}