part #2
This commit is contained in:
@@ -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. |
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -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>
|
||||
@@ -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). |
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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. |
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user