more audit work
This commit is contained in:
@@ -27,6 +27,7 @@ public class AttestationBundlerTests
|
||||
private readonly Mock<ILogger<AttestationBundler>> _loggerMock;
|
||||
private readonly IOptions<BundlingOptions> _options;
|
||||
private readonly DateTimeOffset _fixedNow = new(2026, 1, 2, 0, 0, 0, TimeSpan.Zero);
|
||||
private static CancellationToken TestCancellationToken => TestContext.Current.CancellationToken;
|
||||
|
||||
public AttestationBundlerTests()
|
||||
{
|
||||
@@ -53,7 +54,7 @@ public class AttestationBundlerTests
|
||||
_fixedNow);
|
||||
|
||||
// Act
|
||||
var bundle = await bundler.CreateBundleAsync(request);
|
||||
var bundle = await bundler.CreateBundleAsync(request, TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
bundle.Should().NotBeNull();
|
||||
@@ -81,12 +82,12 @@ public class AttestationBundlerTests
|
||||
_fixedNow.AddDays(-30),
|
||||
_fixedNow);
|
||||
|
||||
var bundle1 = await bundler1.CreateBundleAsync(request);
|
||||
var bundle1 = await bundler1.CreateBundleAsync(request, TestCancellationToken);
|
||||
|
||||
// Reset and use different order
|
||||
SetupAggregator(shuffled2);
|
||||
var bundler2 = CreateBundler();
|
||||
var bundle2 = await bundler2.CreateBundleAsync(request);
|
||||
var bundle2 = await bundler2.CreateBundleAsync(request, TestCancellationToken);
|
||||
|
||||
// Assert - same merkle root regardless of input order
|
||||
bundle1.MerkleTree.Root.Should().Be(bundle2.MerkleTree.Root);
|
||||
@@ -107,7 +108,7 @@ public class AttestationBundlerTests
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
() => bundler.CreateBundleAsync(request));
|
||||
() => bundler.CreateBundleAsync(request, TestCancellationToken));
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
@@ -124,7 +125,7 @@ public class AttestationBundlerTests
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<ArgumentException>(
|
||||
() => bundler.CreateBundleAsync(request, TestContext.Current.CancellationToken));
|
||||
() => bundler.CreateBundleAsync(request, TestCancellationToken));
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
@@ -148,7 +149,7 @@ public class AttestationBundlerTests
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
() => bundler.CreateBundleAsync(request, TestContext.Current.CancellationToken));
|
||||
() => bundler.CreateBundleAsync(request, TestCancellationToken));
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
@@ -172,7 +173,7 @@ public class AttestationBundlerTests
|
||||
_fixedNow);
|
||||
|
||||
// Act
|
||||
await bundler.CreateBundleAsync(request, TestContext.Current.CancellationToken);
|
||||
await bundler.CreateBundleAsync(request, TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
var expectedStart = _fixedNow.AddDays(-7);
|
||||
@@ -196,7 +197,7 @@ public class AttestationBundlerTests
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
() => bundler.CreateBundleAsync(request, TestContext.Current.CancellationToken));
|
||||
() => bundler.CreateBundleAsync(request, TestCancellationToken));
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
@@ -213,7 +214,7 @@ public class AttestationBundlerTests
|
||||
_fixedNow);
|
||||
|
||||
// Act
|
||||
var bundle = await bundler.CreateBundleAsync(request, TestContext.Current.CancellationToken);
|
||||
var bundle = await bundler.CreateBundleAsync(request, TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
bundle.Metadata.CreatedAt.Should().Be(_fixedNow);
|
||||
@@ -259,7 +260,7 @@ public class AttestationBundlerTests
|
||||
SignWithOrgKey: true);
|
||||
|
||||
// Act
|
||||
var bundle = await bundler.CreateBundleAsync(request);
|
||||
var bundle = await bundler.CreateBundleAsync(request, TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
bundle.OrgSignature.Should().NotBeNull();
|
||||
@@ -281,10 +282,10 @@ public class AttestationBundlerTests
|
||||
_fixedNow.AddDays(-30),
|
||||
_fixedNow);
|
||||
|
||||
var bundle = await bundler.CreateBundleAsync(request);
|
||||
var bundle = await bundler.CreateBundleAsync(request, TestCancellationToken);
|
||||
|
||||
// Act
|
||||
var result = await bundler.VerifyBundleAsync(bundle);
|
||||
var result = await bundler.VerifyBundleAsync(bundle, TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
result.Valid.Should().BeTrue();
|
||||
@@ -306,7 +307,7 @@ public class AttestationBundlerTests
|
||||
_fixedNow.AddDays(-30),
|
||||
_fixedNow);
|
||||
|
||||
var bundle = await bundler.CreateBundleAsync(request);
|
||||
var bundle = await bundler.CreateBundleAsync(request, TestCancellationToken);
|
||||
|
||||
// Tamper with the bundle by modifying an attestation
|
||||
var tamperedAttestations = bundle.Attestations.ToList();
|
||||
@@ -316,7 +317,7 @@ public class AttestationBundlerTests
|
||||
var tamperedBundle = bundle with { Attestations = tamperedAttestations };
|
||||
|
||||
// Act
|
||||
var result = await bundler.VerifyBundleAsync(tamperedBundle);
|
||||
var result = await bundler.VerifyBundleAsync(tamperedBundle, TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
result.Valid.Should().BeFalse();
|
||||
@@ -358,12 +359,12 @@ public class AttestationBundlerTests
|
||||
|
||||
var bundlerWithSigner = CreateBundler();
|
||||
var request = new BundleCreationRequest(_fixedNow.AddDays(-7), _fixedNow, SignWithOrgKey: true);
|
||||
var bundle = await bundlerWithSigner.CreateBundleAsync(request, TestContext.Current.CancellationToken);
|
||||
var bundle = await bundlerWithSigner.CreateBundleAsync(request, TestCancellationToken);
|
||||
|
||||
var bundlerWithoutSigner = CreateBundler(orgSigner: null, useDefaultOrgSigner: false);
|
||||
|
||||
// Act
|
||||
var result = await bundlerWithoutSigner.VerifyBundleAsync(bundle, TestContext.Current.CancellationToken);
|
||||
var result = await bundlerWithoutSigner.VerifyBundleAsync(bundle, TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
result.Valid.Should().BeFalse();
|
||||
@@ -387,7 +388,7 @@ public class AttestationBundlerTests
|
||||
TenantId: "test-tenant");
|
||||
|
||||
// Act
|
||||
var bundle = await bundler.CreateBundleAsync(request);
|
||||
var bundle = await bundler.CreateBundleAsync(request, TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
bundle.Metadata.TenantId.Should().Be("test-tenant");
|
||||
@@ -426,7 +427,7 @@ public class AttestationBundlerTests
|
||||
_fixedNow);
|
||||
|
||||
// Act
|
||||
var bundle = await bundler.CreateBundleAsync(request);
|
||||
var bundle = await bundler.CreateBundleAsync(request, TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
bundle.Attestations.Should().HaveCount(10);
|
||||
|
||||
@@ -16,6 +16,7 @@ namespace StellaOps.Attestor.Bundling.Tests;
|
||||
public class BundleAggregatorTests
|
||||
{
|
||||
private readonly InMemoryBundleAggregator _aggregator;
|
||||
private static CancellationToken TestCancellationToken => TestContext.Current.CancellationToken;
|
||||
|
||||
public BundleAggregatorTests()
|
||||
{
|
||||
@@ -39,8 +40,8 @@ public class BundleAggregatorTests
|
||||
|
||||
// Act
|
||||
var results = await _aggregator
|
||||
.AggregateAsync(new AggregationRequest(start, end))
|
||||
.ToListAsync();
|
||||
.AggregateAsync(new AggregationRequest(start, end), TestCancellationToken)
|
||||
.ToListAsync(TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
results.Should().HaveCount(2);
|
||||
@@ -63,8 +64,8 @@ public class BundleAggregatorTests
|
||||
|
||||
// Act
|
||||
var results = await _aggregator
|
||||
.AggregateAsync(new AggregationRequest(start, end))
|
||||
.ToListAsync();
|
||||
.AggregateAsync(new AggregationRequest(start, end), TestCancellationToken)
|
||||
.ToListAsync(TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
results.Should().HaveCount(2);
|
||||
@@ -86,8 +87,8 @@ public class BundleAggregatorTests
|
||||
|
||||
// Act
|
||||
var results = await _aggregator
|
||||
.AggregateAsync(new AggregationRequest(start, end))
|
||||
.ToListAsync();
|
||||
.AggregateAsync(new AggregationRequest(start, end), TestCancellationToken)
|
||||
.ToListAsync(TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
results.Should().BeEmpty();
|
||||
@@ -111,8 +112,8 @@ public class BundleAggregatorTests
|
||||
|
||||
// Act
|
||||
var results = await _aggregator
|
||||
.AggregateAsync(new AggregationRequest(start, end, TenantId: "tenant-a"))
|
||||
.ToListAsync();
|
||||
.AggregateAsync(new AggregationRequest(start, end, TenantId: "tenant-a"), TestCancellationToken)
|
||||
.ToListAsync(TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
results.Should().HaveCount(2);
|
||||
@@ -133,8 +134,8 @@ public class BundleAggregatorTests
|
||||
|
||||
// Act
|
||||
var results = await _aggregator
|
||||
.AggregateAsync(new AggregationRequest(start, end))
|
||||
.ToListAsync();
|
||||
.AggregateAsync(new AggregationRequest(start, end), TestCancellationToken)
|
||||
.ToListAsync(TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
results.Should().HaveCount(3);
|
||||
@@ -160,8 +161,9 @@ public class BundleAggregatorTests
|
||||
var results = await _aggregator
|
||||
.AggregateAsync(new AggregationRequest(
|
||||
start, end,
|
||||
PredicateTypes: new[] { "verdict.stella/v1" }))
|
||||
.ToListAsync();
|
||||
PredicateTypes: new[] { "verdict.stella/v1" }),
|
||||
TestCancellationToken)
|
||||
.ToListAsync(TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
results.Should().HaveCount(2);
|
||||
@@ -184,8 +186,9 @@ public class BundleAggregatorTests
|
||||
var results = await _aggregator
|
||||
.AggregateAsync(new AggregationRequest(
|
||||
start, end,
|
||||
PredicateTypes: new[] { "verdict.stella/v1", "sbom.stella/v1" }))
|
||||
.ToListAsync();
|
||||
PredicateTypes: new[] { "verdict.stella/v1", "sbom.stella/v1" }),
|
||||
TestCancellationToken)
|
||||
.ToListAsync(TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
results.Should().HaveCount(2);
|
||||
@@ -210,7 +213,7 @@ public class BundleAggregatorTests
|
||||
}
|
||||
|
||||
// Act
|
||||
var count = await _aggregator.CountAsync(new AggregationRequest(start, end));
|
||||
var count = await _aggregator.CountAsync(new AggregationRequest(start, end), TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
count.Should().Be(50);
|
||||
@@ -229,7 +232,9 @@ public class BundleAggregatorTests
|
||||
_aggregator.AddAttestation(CreateAttestation("att-3", start.AddDays(15)), tenantId: "tenant-b");
|
||||
|
||||
// Act
|
||||
var count = await _aggregator.CountAsync(new AggregationRequest(start, end, TenantId: "tenant-a"));
|
||||
var count = await _aggregator.CountAsync(
|
||||
new AggregationRequest(start, end, TenantId: "tenant-a"),
|
||||
TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
count.Should().Be(2);
|
||||
@@ -253,8 +258,12 @@ public class BundleAggregatorTests
|
||||
_aggregator.AddAttestation(CreateAttestation("att-b", start.AddDays(10)));
|
||||
|
||||
// Act
|
||||
var results1 = await _aggregator.AggregateAsync(new AggregationRequest(start, end)).ToListAsync();
|
||||
var results2 = await _aggregator.AggregateAsync(new AggregationRequest(start, end)).ToListAsync();
|
||||
var results1 = await _aggregator
|
||||
.AggregateAsync(new AggregationRequest(start, end), TestCancellationToken)
|
||||
.ToListAsync(TestCancellationToken);
|
||||
var results2 = await _aggregator
|
||||
.AggregateAsync(new AggregationRequest(start, end), TestCancellationToken)
|
||||
.ToListAsync(TestCancellationToken);
|
||||
|
||||
// Assert: Order should be consistent (sorted by EntryId)
|
||||
results1.Select(a => a.EntryId).Should().BeEquivalentTo(
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace StellaOps.Attestor.Bundling.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for the full bundle creation workflow:
|
||||
/// Create → Store → Retrieve → Verify
|
||||
/// Create -> Store -> Retrieve -> Verify
|
||||
/// </summary>
|
||||
public class BundleWorkflowIntegrationTests
|
||||
{
|
||||
@@ -30,6 +30,7 @@ public class BundleWorkflowIntegrationTests
|
||||
private readonly InMemoryBundleAggregator _aggregator;
|
||||
private readonly TestOrgKeySigner _signer;
|
||||
private readonly IOptions<BundlingOptions> _options;
|
||||
private static CancellationToken TestCancellationToken => TestContext.Current.CancellationToken;
|
||||
|
||||
public BundleWorkflowIntegrationTests()
|
||||
{
|
||||
@@ -68,13 +69,13 @@ public class BundleWorkflowIntegrationTests
|
||||
bundle.OrgSignature.Should().NotBeNull();
|
||||
|
||||
// Act 2: Store bundle
|
||||
await _store.StoreBundleAsync(bundle);
|
||||
await _store.StoreBundleAsync(bundle, cancellationToken: TestCancellationToken);
|
||||
|
||||
// Assert: Bundle exists
|
||||
(await _store.ExistsAsync(bundle.Metadata.BundleId)).Should().BeTrue();
|
||||
(await _store.ExistsAsync(bundle.Metadata.BundleId, TestCancellationToken)).Should().BeTrue();
|
||||
|
||||
// Act 3: Retrieve bundle
|
||||
var retrieved = await _store.GetBundleAsync(bundle.Metadata.BundleId);
|
||||
var retrieved = await _store.GetBundleAsync(bundle.Metadata.BundleId, TestCancellationToken);
|
||||
|
||||
// Assert: Retrieved bundle matches
|
||||
retrieved.Should().NotBeNull();
|
||||
@@ -104,8 +105,8 @@ public class BundleWorkflowIntegrationTests
|
||||
SignWithOrgKey: false);
|
||||
|
||||
var bundle = await CreateBundleAsync(createRequest);
|
||||
await _store.StoreBundleAsync(bundle);
|
||||
var retrieved = await _store.GetBundleAsync(bundle.Metadata.BundleId);
|
||||
await _store.StoreBundleAsync(bundle, cancellationToken: TestCancellationToken);
|
||||
var retrieved = await _store.GetBundleAsync(bundle.Metadata.BundleId, TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
retrieved.Should().NotBeNull();
|
||||
@@ -145,8 +146,8 @@ public class BundleWorkflowIntegrationTests
|
||||
|
||||
// Act
|
||||
var bundle = await CreateBundleAsync(new BundleCreationRequest(periodStart, periodEnd));
|
||||
await _store.StoreBundleAsync(bundle);
|
||||
var retrieved = await _store.GetBundleAsync(bundle.Metadata.BundleId);
|
||||
await _store.StoreBundleAsync(bundle, cancellationToken: TestCancellationToken);
|
||||
var retrieved = await _store.GetBundleAsync(bundle.Metadata.BundleId, TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
retrieved.Should().NotBeNull();
|
||||
@@ -209,7 +210,7 @@ public class BundleWorkflowIntegrationTests
|
||||
jobResult.AttestationCount.Should().Be(5);
|
||||
|
||||
// Verify bundle was stored
|
||||
(await _store.ExistsAsync(jobResult.BundleId)).Should().BeTrue();
|
||||
(await _store.ExistsAsync(jobResult.BundleId, TestCancellationToken)).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
@@ -242,17 +243,17 @@ public class BundleWorkflowIntegrationTests
|
||||
// Arrange: Create old bundle
|
||||
var oldPeriodStart = DateTimeOffset.UtcNow.AddMonths(-36);
|
||||
var oldBundle = CreateExpiredBundle("old-bundle", oldPeriodStart);
|
||||
await _store.StoreBundleAsync(oldBundle);
|
||||
await _store.StoreBundleAsync(oldBundle, cancellationToken: TestCancellationToken);
|
||||
|
||||
// Verify old bundle exists
|
||||
(await _store.ExistsAsync("old-bundle")).Should().BeTrue();
|
||||
(await _store.ExistsAsync("old-bundle", TestCancellationToken)).Should().BeTrue();
|
||||
|
||||
// Act: Apply retention
|
||||
var deleted = await ApplyRetentionAsync(retentionMonths: 24);
|
||||
|
||||
// Assert
|
||||
deleted.Should().BeGreaterThan(0);
|
||||
(await _store.ExistsAsync("old-bundle")).Should().BeFalse();
|
||||
(await _store.ExistsAsync("old-bundle", TestCancellationToken)).Should().BeFalse();
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -265,7 +266,8 @@ public class BundleWorkflowIntegrationTests
|
||||
.AggregateAsync(new AggregationRequest(
|
||||
request.PeriodStart,
|
||||
request.PeriodEnd,
|
||||
request.TenantId))
|
||||
request.TenantId),
|
||||
TestCancellationToken)
|
||||
.ToListAsync();
|
||||
|
||||
// Sort for determinism
|
||||
@@ -298,7 +300,7 @@ public class BundleWorkflowIntegrationTests
|
||||
{
|
||||
var digest = System.Security.Cryptography.SHA256.HashData(
|
||||
System.Text.Encoding.UTF8.GetBytes(merkleRoot));
|
||||
var signature = await _signer.SignBundleAsync(digest, request.OrgKeyId);
|
||||
var signature = await _signer.SignBundleAsync(digest, request.OrgKeyId, TestCancellationToken);
|
||||
bundle = bundle with
|
||||
{
|
||||
OrgSignature = signature,
|
||||
@@ -323,7 +325,7 @@ public class BundleWorkflowIntegrationTests
|
||||
{
|
||||
var digest = System.Security.Cryptography.SHA256.HashData(
|
||||
System.Text.Encoding.UTF8.GetBytes(computedRoot));
|
||||
return await _signer.VerifyBundleAsync(digest, bundle.OrgSignature);
|
||||
return await _signer.VerifyBundleAsync(digest, bundle.OrgSignature, TestCancellationToken);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -342,7 +344,7 @@ public class BundleWorkflowIntegrationTests
|
||||
SignWithOrgKey: true,
|
||||
OrgKeyId: "scheduler-key"));
|
||||
|
||||
await _store.StoreBundleAsync(bundle);
|
||||
await _store.StoreBundleAsync(bundle, cancellationToken: TestCancellationToken);
|
||||
|
||||
return new RotationJobResult
|
||||
{
|
||||
@@ -366,12 +368,12 @@ public class BundleWorkflowIntegrationTests
|
||||
var cutoff = DateTimeOffset.UtcNow.AddMonths(-retentionMonths);
|
||||
var deleted = 0;
|
||||
|
||||
var bundles = await _store.ListBundlesAsync(new BundleListRequest());
|
||||
var bundles = await _store.ListBundlesAsync(new BundleListRequest(), TestCancellationToken);
|
||||
foreach (var bundle in bundles.Bundles)
|
||||
{
|
||||
if (bundle.CreatedAt < cutoff)
|
||||
{
|
||||
if (await _store.DeleteBundleAsync(bundle.BundleId))
|
||||
if (await _store.DeleteBundleAsync(bundle.BundleId, TestCancellationToken))
|
||||
{
|
||||
deleted++;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ public class KmsOrgKeySignerTests
|
||||
{
|
||||
private readonly Mock<IKmsProvider> _kmsProviderMock;
|
||||
private readonly Mock<ILogger<KmsOrgKeySigner>> _loggerMock;
|
||||
private static CancellationToken TestCancellationToken => TestContext.Current.CancellationToken;
|
||||
|
||||
public KmsOrgKeySignerTests()
|
||||
{
|
||||
@@ -46,7 +47,7 @@ public class KmsOrgKeySignerTests
|
||||
var signer = CreateSigner();
|
||||
|
||||
// Act
|
||||
var result = await signer.SignBundleAsync(bundleDigest, keyId);
|
||||
var result = await signer.SignBundleAsync(bundleDigest, keyId, TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
@@ -71,7 +72,7 @@ public class KmsOrgKeySignerTests
|
||||
var signer = CreateSigner();
|
||||
|
||||
// Act & Assert
|
||||
var act = () => signer.SignBundleAsync(bundleDigest, keyId);
|
||||
var act = () => signer.SignBundleAsync(bundleDigest, keyId, TestCancellationToken);
|
||||
await act.Should().ThrowAsync<InvalidOperationException>()
|
||||
.WithMessage($"*'{keyId}'*not found*");
|
||||
}
|
||||
@@ -92,7 +93,7 @@ public class KmsOrgKeySignerTests
|
||||
var signer = CreateSigner();
|
||||
|
||||
// Act & Assert
|
||||
var act = () => signer.SignBundleAsync(bundleDigest, keyId);
|
||||
var act = () => signer.SignBundleAsync(bundleDigest, keyId, TestCancellationToken);
|
||||
await act.Should().ThrowAsync<InvalidOperationException>()
|
||||
.WithMessage($"*'{keyId}'*not active*");
|
||||
}
|
||||
@@ -120,7 +121,7 @@ public class KmsOrgKeySignerTests
|
||||
var signer = CreateSigner();
|
||||
|
||||
// Act & Assert
|
||||
var act = () => signer.SignBundleAsync(bundleDigest, keyId);
|
||||
var act = () => signer.SignBundleAsync(bundleDigest, keyId, TestCancellationToken);
|
||||
await act.Should().ThrowAsync<InvalidOperationException>()
|
||||
.WithMessage($"*'{keyId}'*expired*");
|
||||
}
|
||||
@@ -145,7 +146,7 @@ public class KmsOrgKeySignerTests
|
||||
var signer = CreateSigner();
|
||||
|
||||
// Act
|
||||
var result = await signer.SignBundleAsync(bundleDigest, keyId);
|
||||
var result = await signer.SignBundleAsync(bundleDigest, keyId, TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
result.CertificateChain.Should().NotBeNull();
|
||||
@@ -187,7 +188,7 @@ public class KmsOrgKeySignerTests
|
||||
var signer = CreateSigner();
|
||||
|
||||
// Act
|
||||
var result = await signer.VerifyBundleAsync(bundleDigest, signature);
|
||||
var result = await signer.VerifyBundleAsync(bundleDigest, signature, TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
result.Should().BeTrue();
|
||||
@@ -223,7 +224,7 @@ public class KmsOrgKeySignerTests
|
||||
var signer = CreateSigner();
|
||||
|
||||
// Act
|
||||
var result = await signer.VerifyBundleAsync(bundleDigest, signature);
|
||||
var result = await signer.VerifyBundleAsync(bundleDigest, signature, TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
result.Should().BeFalse();
|
||||
@@ -259,7 +260,7 @@ public class KmsOrgKeySignerTests
|
||||
var signer = CreateSigner();
|
||||
|
||||
// Act
|
||||
var result = await signer.VerifyBundleAsync(bundleDigest, signature);
|
||||
var result = await signer.VerifyBundleAsync(bundleDigest, signature, TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
result.Should().BeFalse();
|
||||
@@ -285,7 +286,7 @@ public class KmsOrgKeySignerTests
|
||||
options);
|
||||
|
||||
// Act
|
||||
var result = await signer.GetActiveKeyIdAsync();
|
||||
var result = await signer.GetActiveKeyIdAsync(TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
result.Should().Be("configured-active-key");
|
||||
@@ -310,7 +311,7 @@ public class KmsOrgKeySignerTests
|
||||
var signer = CreateSigner();
|
||||
|
||||
// Act
|
||||
var result = await signer.GetActiveKeyIdAsync();
|
||||
var result = await signer.GetActiveKeyIdAsync(TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
result.Should().Be("key-2025"); // Newest active key
|
||||
@@ -333,7 +334,7 @@ public class KmsOrgKeySignerTests
|
||||
var signer = CreateSigner();
|
||||
|
||||
// Act & Assert
|
||||
var act = () => signer.GetActiveKeyIdAsync();
|
||||
var act = () => signer.GetActiveKeyIdAsync(TestCancellationToken);
|
||||
await act.Should().ThrowAsync<InvalidOperationException>()
|
||||
.WithMessage("*No active signing key*");
|
||||
}
|
||||
@@ -356,7 +357,7 @@ public class KmsOrgKeySignerTests
|
||||
var signer = CreateSigner();
|
||||
|
||||
// Act
|
||||
var result = await signer.GetActiveKeyIdAsync();
|
||||
var result = await signer.GetActiveKeyIdAsync(TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
result.Should().Be("key-valid");
|
||||
@@ -384,7 +385,7 @@ public class KmsOrgKeySignerTests
|
||||
var signer = CreateSigner();
|
||||
|
||||
// Act
|
||||
var result = await signer.ListKeysAsync();
|
||||
var result = await signer.ListKeysAsync(TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
result.Should().HaveCount(2);
|
||||
@@ -408,8 +409,8 @@ public class KmsOrgKeySignerTests
|
||||
var bundleDigest = SHA256.HashData("test bundle content"u8.ToArray());
|
||||
|
||||
// Act
|
||||
var signature = await signer.SignBundleAsync(bundleDigest, "test-key-1");
|
||||
var isValid = await signer.VerifyBundleAsync(bundleDigest, signature);
|
||||
var signature = await signer.SignBundleAsync(bundleDigest, "test-key-1", TestCancellationToken);
|
||||
var isValid = await signer.VerifyBundleAsync(bundleDigest, signature, TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
isValid.Should().BeTrue();
|
||||
@@ -430,8 +431,8 @@ public class KmsOrgKeySignerTests
|
||||
var tamperedDigest = SHA256.HashData("tampered content"u8.ToArray());
|
||||
|
||||
// Act
|
||||
var signature = await signer.SignBundleAsync(originalDigest, "test-key-1");
|
||||
var isValid = await signer.VerifyBundleAsync(tamperedDigest, signature);
|
||||
var signature = await signer.SignBundleAsync(originalDigest, "test-key-1", TestCancellationToken);
|
||||
var isValid = await signer.VerifyBundleAsync(tamperedDigest, signature, TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
isValid.Should().BeFalse();
|
||||
@@ -447,13 +448,13 @@ public class KmsOrgKeySignerTests
|
||||
signer.AddKey("test-key-1", isActive: true);
|
||||
|
||||
var bundleDigest = SHA256.HashData("test"u8.ToArray());
|
||||
var signature = await signer.SignBundleAsync(bundleDigest, "test-key-1");
|
||||
var signature = await signer.SignBundleAsync(bundleDigest, "test-key-1", TestCancellationToken);
|
||||
|
||||
// Modify signature to reference unknown key
|
||||
var fakeSignature = signature with { KeyId = "unknown-key" };
|
||||
|
||||
// Act
|
||||
var isValid = await signer.VerifyBundleAsync(bundleDigest, fakeSignature);
|
||||
var isValid = await signer.VerifyBundleAsync(bundleDigest, fakeSignature, TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
isValid.Should().BeFalse();
|
||||
@@ -470,7 +471,7 @@ public class KmsOrgKeySignerTests
|
||||
signer.AddKey("key-2", isActive: true);
|
||||
|
||||
// Act
|
||||
var activeKeyId = await signer.GetActiveKeyIdAsync();
|
||||
var activeKeyId = await signer.GetActiveKeyIdAsync(TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
activeKeyId.Should().Be("key-2");
|
||||
@@ -486,7 +487,7 @@ public class KmsOrgKeySignerTests
|
||||
// Don't add any keys
|
||||
|
||||
// Act & Assert
|
||||
var act = () => signer.GetActiveKeyIdAsync();
|
||||
var act = () => signer.GetActiveKeyIdAsync(TestCancellationToken);
|
||||
await act.Should().ThrowAsync<InvalidOperationException>()
|
||||
.WithMessage("*No active signing key*");
|
||||
}
|
||||
@@ -502,7 +503,7 @@ public class KmsOrgKeySignerTests
|
||||
signer.AddKey("key-2", isActive: false);
|
||||
|
||||
// Act
|
||||
var keys = await signer.ListKeysAsync();
|
||||
var keys = await signer.ListKeysAsync(TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
keys.Should().HaveCount(2);
|
||||
|
||||
@@ -23,6 +23,7 @@ public class OfflineKitBundleProviderTests
|
||||
private readonly Mock<IBundleStore> _storeMock = new();
|
||||
private readonly Mock<ILogger<OfflineKitBundleProvider>> _loggerMock = new();
|
||||
private readonly DateTimeOffset _fixedNow = new(2026, 1, 2, 0, 0, 0, TimeSpan.Zero);
|
||||
private static CancellationToken TestCancellationToken => TestContext.Current.CancellationToken;
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
@@ -47,7 +48,7 @@ public class OfflineKitBundleProviderTests
|
||||
.ReturnsAsync(new BundleListResult(new List<BundleListItem>(), null));
|
||||
|
||||
// Act
|
||||
await provider.GetOfflineKitManifestAsync(null, TestContext.Current.CancellationToken);
|
||||
await provider.GetOfflineKitManifestAsync(null, TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
var expectedCutoff = _fixedNow.AddMonths(-6);
|
||||
@@ -94,7 +95,7 @@ public class OfflineKitBundleProviderTests
|
||||
using var temp = new TempDirectory();
|
||||
|
||||
// Act
|
||||
await provider.ExportForOfflineKitAsync(temp.Path, null, TestContext.Current.CancellationToken);
|
||||
await provider.ExportForOfflineKitAsync(temp.Path, null, TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
_storeMock.Verify(x => x.ExportBundleAsync(
|
||||
|
||||
@@ -19,6 +19,7 @@ public class OrgKeySignerTests
|
||||
{
|
||||
private readonly TestOrgKeySigner _signer;
|
||||
private readonly string _testKeyId = "test-org-key-2025";
|
||||
private static CancellationToken TestCancellationToken => TestContext.Current.CancellationToken;
|
||||
|
||||
public OrgKeySignerTests()
|
||||
{
|
||||
@@ -35,7 +36,7 @@ public class OrgKeySignerTests
|
||||
var bundleDigest = SHA256.HashData("test-bundle-content"u8.ToArray());
|
||||
|
||||
// Act
|
||||
var signature = await _signer.SignBundleAsync(bundleDigest, _testKeyId);
|
||||
var signature = await _signer.SignBundleAsync(bundleDigest, _testKeyId, TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
signature.Should().NotBeNull();
|
||||
@@ -45,7 +46,7 @@ public class OrgKeySignerTests
|
||||
signature.SignedAt.Should().BeCloseTo(DateTimeOffset.UtcNow, TimeSpan.FromSeconds(5));
|
||||
|
||||
// Verify roundtrip
|
||||
var isValid = await _signer.VerifyBundleAsync(bundleDigest, signature);
|
||||
var isValid = await _signer.VerifyBundleAsync(bundleDigest, signature, TestCancellationToken);
|
||||
isValid.Should().BeTrue();
|
||||
}
|
||||
|
||||
@@ -58,8 +59,8 @@ public class OrgKeySignerTests
|
||||
var tamperedDigest = SHA256.HashData("tampered-content"u8.ToArray());
|
||||
|
||||
// Act
|
||||
var signature = await _signer.SignBundleAsync(originalDigest, _testKeyId);
|
||||
var isValid = await _signer.VerifyBundleAsync(tamperedDigest, signature);
|
||||
var signature = await _signer.SignBundleAsync(originalDigest, _testKeyId, TestCancellationToken);
|
||||
var isValid = await _signer.VerifyBundleAsync(tamperedDigest, signature, TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
isValid.Should().BeFalse();
|
||||
@@ -75,15 +76,15 @@ public class OrgKeySignerTests
|
||||
var digest2 = SHA256.HashData(content);
|
||||
|
||||
// Act
|
||||
var signature1 = await _signer.SignBundleAsync(digest1, _testKeyId);
|
||||
var signature2 = await _signer.SignBundleAsync(digest2, _testKeyId);
|
||||
var signature1 = await _signer.SignBundleAsync(digest1, _testKeyId, TestCancellationToken);
|
||||
var signature2 = await _signer.SignBundleAsync(digest2, _testKeyId, TestCancellationToken);
|
||||
|
||||
// Assert - Both signatures should be valid for the same content
|
||||
(await _signer.VerifyBundleAsync(digest1, signature1)).Should().BeTrue();
|
||||
(await _signer.VerifyBundleAsync(digest2, signature2)).Should().BeTrue();
|
||||
(await _signer.VerifyBundleAsync(digest1, signature1, TestCancellationToken)).Should().BeTrue();
|
||||
(await _signer.VerifyBundleAsync(digest2, signature2, TestCancellationToken)).Should().BeTrue();
|
||||
|
||||
// Cross-verify: signature1 should verify against digest2 (same content)
|
||||
(await _signer.VerifyBundleAsync(digest2, signature1)).Should().BeTrue();
|
||||
(await _signer.VerifyBundleAsync(digest2, signature1, TestCancellationToken)).Should().BeTrue();
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -98,7 +99,7 @@ public class OrgKeySignerTests
|
||||
var bundleDigest = SHA256.HashData("bundle-with-chain"u8.ToArray());
|
||||
|
||||
// Act
|
||||
var signature = await _signer.SignBundleAsync(bundleDigest, _testKeyId);
|
||||
var signature = await _signer.SignBundleAsync(bundleDigest, _testKeyId, TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
signature.CertificateChain.Should().NotBeNull();
|
||||
@@ -120,8 +121,8 @@ public class OrgKeySignerTests
|
||||
var keyId2 = "org-key-2025";
|
||||
|
||||
// Act
|
||||
var signature1 = await _signer.SignBundleAsync(bundleDigest, keyId1);
|
||||
var signature2 = await _signer.SignBundleAsync(bundleDigest, keyId2);
|
||||
var signature1 = await _signer.SignBundleAsync(bundleDigest, keyId1, TestCancellationToken);
|
||||
var signature2 = await _signer.SignBundleAsync(bundleDigest, keyId2, TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
signature1.KeyId.Should().Be(keyId1);
|
||||
@@ -135,13 +136,13 @@ public class OrgKeySignerTests
|
||||
{
|
||||
// Arrange
|
||||
var bundleDigest = SHA256.HashData("test-content"u8.ToArray());
|
||||
var signatureWithKey1 = await _signer.SignBundleAsync(bundleDigest, "key-1");
|
||||
var signatureWithKey1 = await _signer.SignBundleAsync(bundleDigest, "key-1", TestCancellationToken);
|
||||
|
||||
// Modify the key ID in the signature (simulating wrong key)
|
||||
var tamperedSignature = signatureWithKey1 with { KeyId = "wrong-key" };
|
||||
|
||||
// Act
|
||||
var isValid = await _signer.VerifyBundleAsync(bundleDigest, tamperedSignature);
|
||||
var isValid = await _signer.VerifyBundleAsync(bundleDigest, tamperedSignature, TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
isValid.Should().BeFalse();
|
||||
@@ -159,14 +160,14 @@ public class OrgKeySignerTests
|
||||
var emptyDigest = SHA256.HashData(Array.Empty<byte>());
|
||||
|
||||
// Act
|
||||
var signature = await _signer.SignBundleAsync(emptyDigest, _testKeyId);
|
||||
var signature = await _signer.SignBundleAsync(emptyDigest, _testKeyId, TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
signature.Should().NotBeNull();
|
||||
signature.Signature.Should().NotBeEmpty();
|
||||
|
||||
// Verify works
|
||||
(await _signer.VerifyBundleAsync(emptyDigest, signature)).Should().BeTrue();
|
||||
(await _signer.VerifyBundleAsync(emptyDigest, signature, TestCancellationToken)).Should().BeTrue();
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -185,11 +186,11 @@ public class OrgKeySignerTests
|
||||
var bundleDigest = SHA256.HashData(System.Text.Encoding.UTF8.GetBytes($"test-{algorithm}"));
|
||||
|
||||
// Act
|
||||
var signature = await signer.SignBundleAsync(bundleDigest, _testKeyId);
|
||||
var signature = await signer.SignBundleAsync(bundleDigest, _testKeyId, TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
signature.Algorithm.Should().Be(algorithm);
|
||||
(await signer.VerifyBundleAsync(bundleDigest, signature)).Should().BeTrue();
|
||||
(await signer.VerifyBundleAsync(bundleDigest, signature, TestCancellationToken)).Should().BeTrue();
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -205,7 +206,7 @@ public class OrgKeySignerTests
|
||||
var bundleDigest = SHA256.HashData("timestamp-test"u8.ToArray());
|
||||
|
||||
// Act
|
||||
var signature = await _signer.SignBundleAsync(bundleDigest, _testKeyId);
|
||||
var signature = await _signer.SignBundleAsync(bundleDigest, _testKeyId, TestCancellationToken);
|
||||
var afterSign = DateTimeOffset.UtcNow;
|
||||
|
||||
// Assert
|
||||
|
||||
@@ -24,6 +24,7 @@ public class RetentionPolicyEnforcerTests
|
||||
private readonly Mock<IBundleExpiryNotifier> _notifierMock;
|
||||
private readonly Mock<ILogger<RetentionPolicyEnforcer>> _loggerMock;
|
||||
private readonly DateTimeOffset _fixedNow = new(2026, 1, 2, 0, 0, 0, TimeSpan.Zero);
|
||||
private static CancellationToken TestCancellationToken => TestContext.Current.CancellationToken;
|
||||
|
||||
public RetentionPolicyEnforcerTests()
|
||||
{
|
||||
@@ -159,7 +160,7 @@ public class RetentionPolicyEnforcerTests
|
||||
var enforcer = CreateEnforcer(options);
|
||||
|
||||
// Act
|
||||
var result = await enforcer.EnforceAsync();
|
||||
var result = await enforcer.EnforceAsync(TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
result.Success.Should().BeTrue();
|
||||
@@ -198,7 +199,7 @@ public class RetentionPolicyEnforcerTests
|
||||
var enforcer = CreateEnforcer(CreateOptions(retentionOptions));
|
||||
|
||||
// Act
|
||||
var result = await enforcer.EnforceAsync();
|
||||
var result = await enforcer.EnforceAsync(TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
result.Success.Should().BeTrue();
|
||||
@@ -245,7 +246,7 @@ public class RetentionPolicyEnforcerTests
|
||||
timeProvider: fixedTimeProvider);
|
||||
|
||||
// Act
|
||||
var result = await enforcer.EnforceAsync(TestContext.Current.CancellationToken);
|
||||
var result = await enforcer.EnforceAsync(TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
result.BundlesDeleted.Should().Be(0);
|
||||
@@ -279,7 +280,7 @@ public class RetentionPolicyEnforcerTests
|
||||
var enforcer = CreateEnforcer(CreateOptions(retentionOptions), _archiverMock.Object);
|
||||
|
||||
// Act
|
||||
var result = await enforcer.EnforceAsync();
|
||||
var result = await enforcer.EnforceAsync(TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
result.Success.Should().BeTrue();
|
||||
@@ -308,7 +309,7 @@ public class RetentionPolicyEnforcerTests
|
||||
var enforcer = CreateEnforcer(CreateOptions(retentionOptions));
|
||||
|
||||
// Act
|
||||
var result = await enforcer.EnforceAsync();
|
||||
var result = await enforcer.EnforceAsync(TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
result.Success.Should().BeTrue();
|
||||
@@ -343,7 +344,7 @@ public class RetentionPolicyEnforcerTests
|
||||
var enforcer = CreateEnforcer(CreateOptions(retentionOptions));
|
||||
|
||||
// Act
|
||||
var result = await enforcer.EnforceAsync();
|
||||
var result = await enforcer.EnforceAsync(TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
result.BundlesMarkedExpired.Should().Be(1);
|
||||
@@ -379,7 +380,7 @@ public class RetentionPolicyEnforcerTests
|
||||
var enforcer = CreateEnforcer(CreateOptions(retentionOptions));
|
||||
|
||||
// Act
|
||||
var result = await enforcer.EnforceAsync();
|
||||
var result = await enforcer.EnforceAsync(TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
result.BundlesDeleted.Should().Be(1);
|
||||
@@ -409,7 +410,7 @@ public class RetentionPolicyEnforcerTests
|
||||
var enforcer = CreateEnforcer(CreateOptions(retentionOptions), notifier: _notifierMock.Object);
|
||||
|
||||
// Act
|
||||
var result = await enforcer.EnforceAsync();
|
||||
var result = await enforcer.EnforceAsync(TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
result.BundlesApproachingExpiry.Should().Be(1);
|
||||
@@ -442,7 +443,7 @@ public class RetentionPolicyEnforcerTests
|
||||
var enforcer = CreateEnforcer(CreateOptions(retentionOptions), archiver: null);
|
||||
|
||||
// Act
|
||||
var result = await enforcer.EnforceAsync();
|
||||
var result = await enforcer.EnforceAsync(TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
result.Success.Should().BeFalse();
|
||||
@@ -475,7 +476,7 @@ public class RetentionPolicyEnforcerTests
|
||||
var enforcer = CreateEnforcer(CreateOptions(retentionOptions));
|
||||
|
||||
// Act
|
||||
var result = await enforcer.EnforceAsync();
|
||||
var result = await enforcer.EnforceAsync(TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
result.Success.Should().BeFalse();
|
||||
@@ -527,7 +528,7 @@ public class RetentionPolicyEnforcerTests
|
||||
var enforcer = CreateEnforcer(CreateOptions(retentionOptions));
|
||||
|
||||
// Act
|
||||
var result = await enforcer.EnforceAsync();
|
||||
var result = await enforcer.EnforceAsync(TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
// Should evaluate first batch (5) and stop before fetching second batch
|
||||
@@ -554,7 +555,9 @@ public class RetentionPolicyEnforcerTests
|
||||
var enforcer = CreateEnforcer(options);
|
||||
|
||||
// Act
|
||||
var notifications = await enforcer.GetApproachingExpiryAsync(daysBeforeExpiry: 30);
|
||||
var notifications = await enforcer.GetApproachingExpiryAsync(
|
||||
daysBeforeExpiry: 30,
|
||||
cancellationToken: TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
notifications.Should().HaveCount(1);
|
||||
|
||||
@@ -8,3 +8,5 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
|
||||
| AUDIT-0048-M | DONE | Revalidated maintainability for StellaOps.Attestor.Bundling.Tests. |
|
||||
| AUDIT-0048-T | DONE | Revalidated test coverage for StellaOps.Attestor.Bundling.Tests. |
|
||||
| AUDIT-0048-A | DONE | Waived (test project; revalidated 2026-01-06). |
|
||||
| AUDIT-0207-T | DONE | Revalidated 2026-01-08 (stack overflow fix). |
|
||||
| AUDIT-0207-A | DONE | Revalidated 2026-01-08 (stack overflow fix). |
|
||||
|
||||
@@ -11,6 +11,8 @@ namespace StellaOps.Attestor.Infrastructure.Tests;
|
||||
|
||||
public sealed class DefaultDsseCanonicalizerTests
|
||||
{
|
||||
private static CancellationToken TestCancellationToken => TestContext.Current.CancellationToken;
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CanonicalizeAsync_OrdersSignaturesDeterministically()
|
||||
@@ -35,7 +37,7 @@ public sealed class DefaultDsseCanonicalizerTests
|
||||
|
||||
var canonicalizer = new DefaultDsseCanonicalizer();
|
||||
|
||||
var bytes = await canonicalizer.CanonicalizeAsync(request);
|
||||
var bytes = await canonicalizer.CanonicalizeAsync(request, TestCancellationToken);
|
||||
|
||||
using var document = JsonDocument.Parse(bytes);
|
||||
var signatures = document.RootElement.GetProperty("signatures");
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
@@ -7,6 +8,7 @@ using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Attestor.Core.Rekor;
|
||||
using StellaOps.Attestor.Core.Verification;
|
||||
using StellaOps.Attestor.Infrastructure.Rekor;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
@@ -19,18 +21,8 @@ public sealed class HttpRekorClientTests
|
||||
[Fact]
|
||||
public async Task VerifyInclusionAsync_MissingLogIndex_ReturnsFailure()
|
||||
{
|
||||
var handler = new StubHandler();
|
||||
var httpClient = new HttpClient(handler)
|
||||
{
|
||||
BaseAddress = new Uri("https://rekor.example.com")
|
||||
};
|
||||
var client = new HttpRekorClient(httpClient, NullLogger<HttpRekorClient>.Instance);
|
||||
var backend = new RekorBackend
|
||||
{
|
||||
Name = "primary",
|
||||
Url = new Uri("https://rekor.example.com")
|
||||
};
|
||||
|
||||
var client = CreateClient(new MissingLogIndexHandler());
|
||||
var backend = CreateBackend();
|
||||
var payloadDigest = Encoding.UTF8.GetBytes("payload-digest");
|
||||
|
||||
var result = await client.VerifyInclusionAsync("test-uuid", payloadDigest, backend, CancellationToken.None);
|
||||
@@ -39,7 +31,98 @@ public sealed class HttpRekorClientTests
|
||||
result.FailureReason.Should().Contain("log index");
|
||||
}
|
||||
|
||||
private sealed class StubHandler : HttpMessageHandler
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetProofAsync_ParsesCheckpointTimestamp_InvariantCulture()
|
||||
{
|
||||
var originalCulture = CultureInfo.CurrentCulture;
|
||||
var originalUiCulture = CultureInfo.CurrentUICulture;
|
||||
|
||||
try
|
||||
{
|
||||
CultureInfo.CurrentCulture = new CultureInfo("fr-FR");
|
||||
CultureInfo.CurrentUICulture = new CultureInfo("fr-FR");
|
||||
|
||||
const string timestamp = "01/02/2026 03:04:05 +00:00";
|
||||
var proofJson = BuildProofJson("rekor.example.com", "abcd", "abcd", timestamp);
|
||||
var client = CreateClient(new ProofOnlyHandler(proofJson));
|
||||
var backend = CreateBackend();
|
||||
|
||||
var proof = await client.GetProofAsync("test-uuid", backend, CancellationToken.None);
|
||||
|
||||
proof.Should().NotBeNull();
|
||||
proof!.Checkpoint.Should().NotBeNull();
|
||||
proof.Checkpoint!.Timestamp.Should().Be(DateTimeOffset.Parse(
|
||||
timestamp,
|
||||
CultureInfo.InvariantCulture,
|
||||
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal));
|
||||
}
|
||||
finally
|
||||
{
|
||||
CultureInfo.CurrentCulture = originalCulture;
|
||||
CultureInfo.CurrentUICulture = originalUiCulture;
|
||||
}
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyInclusionAsync_ValidProof_ReturnsSuccessWithUnverifiedCheckpoint()
|
||||
{
|
||||
var payloadDigest = Encoding.UTF8.GetBytes("payload");
|
||||
var leafHash = MerkleProofVerifier.HashLeaf(payloadDigest);
|
||||
var leafHex = MerkleProofVerifier.BytesToHex(leafHash);
|
||||
|
||||
var client = CreateClient(new ValidProofHandler(leafHex));
|
||||
var backend = CreateBackend();
|
||||
|
||||
var result = await client.VerifyInclusionAsync("test-uuid", payloadDigest, backend, CancellationToken.None);
|
||||
|
||||
result.Verified.Should().BeTrue();
|
||||
result.CheckpointSignatureValid.Should().BeFalse();
|
||||
result.LogIndex.Should().Be(0);
|
||||
result.ComputedRootHash.Should().Be(leafHex);
|
||||
result.ExpectedRootHash.Should().Be(leafHex);
|
||||
result.FailureReason.Should().BeNull();
|
||||
}
|
||||
|
||||
private static HttpRekorClient CreateClient(HttpMessageHandler handler)
|
||||
{
|
||||
var httpClient = new HttpClient(handler)
|
||||
{
|
||||
BaseAddress = new Uri("https://rekor.example.com")
|
||||
};
|
||||
|
||||
return new HttpRekorClient(httpClient, NullLogger<HttpRekorClient>.Instance);
|
||||
}
|
||||
|
||||
private static RekorBackend CreateBackend()
|
||||
{
|
||||
return new RekorBackend
|
||||
{
|
||||
Name = "primary",
|
||||
Url = new Uri("https://rekor.example.com")
|
||||
};
|
||||
}
|
||||
|
||||
private static string BuildProofJson(string origin, string rootHash, string leafHash, string timestamp)
|
||||
{
|
||||
return $$"""
|
||||
{
|
||||
"checkpoint": {
|
||||
"origin": "{{origin}}",
|
||||
"size": 1,
|
||||
"rootHash": "{{rootHash}}",
|
||||
"timestamp": "{{timestamp}}"
|
||||
},
|
||||
"inclusion": {
|
||||
"leafHash": "{{leafHash}}",
|
||||
"path": []
|
||||
}
|
||||
}
|
||||
""";
|
||||
}
|
||||
|
||||
private sealed class MissingLogIndexHandler : HttpMessageHandler
|
||||
{
|
||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -47,21 +130,7 @@ public sealed class HttpRekorClientTests
|
||||
|
||||
if (path.EndsWith("/proof", StringComparison.Ordinal))
|
||||
{
|
||||
var json = """
|
||||
{
|
||||
"checkpoint": {
|
||||
"origin": "rekor.example.com",
|
||||
"size": 1,
|
||||
"rootHash": "abcd",
|
||||
"timestamp": "2026-01-01T00:00:00Z"
|
||||
},
|
||||
"inclusion": {
|
||||
"leafHash": "abcd",
|
||||
"path": []
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var json = BuildProofJson("rekor.example.com", "abcd", "abcd", "2026-01-01T00:00:00Z");
|
||||
return Task.FromResult(BuildResponse(json));
|
||||
}
|
||||
|
||||
@@ -73,13 +142,62 @@ public sealed class HttpRekorClientTests
|
||||
|
||||
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound));
|
||||
}
|
||||
}
|
||||
|
||||
private static HttpResponseMessage BuildResponse(string json)
|
||||
private sealed class ProofOnlyHandler : HttpMessageHandler
|
||||
{
|
||||
private readonly string _proofJson;
|
||||
|
||||
public ProofOnlyHandler(string proofJson)
|
||||
{
|
||||
return new HttpResponseMessage(HttpStatusCode.OK)
|
||||
_proofJson = proofJson;
|
||||
}
|
||||
|
||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
var path = request.RequestUri?.AbsolutePath ?? string.Empty;
|
||||
if (path.EndsWith("/proof", StringComparison.Ordinal))
|
||||
{
|
||||
Content = new StringContent(json, Encoding.UTF8, "application/json")
|
||||
};
|
||||
return Task.FromResult(BuildResponse(_proofJson));
|
||||
}
|
||||
|
||||
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound));
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class ValidProofHandler : HttpMessageHandler
|
||||
{
|
||||
private readonly string _proofJson;
|
||||
|
||||
public ValidProofHandler(string leafHex)
|
||||
{
|
||||
_proofJson = BuildProofJson("rekor.example.com", leafHex, leafHex, "2026-01-02T03:04:05Z");
|
||||
}
|
||||
|
||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
var path = request.RequestUri?.AbsolutePath ?? string.Empty;
|
||||
|
||||
if (path.EndsWith("/proof", StringComparison.Ordinal))
|
||||
{
|
||||
return Task.FromResult(BuildResponse(_proofJson));
|
||||
}
|
||||
|
||||
if (path.Contains("/api/v2/log/entries/", StringComparison.Ordinal))
|
||||
{
|
||||
var json = "{\"logIndex\":0}";
|
||||
return Task.FromResult(BuildResponse(json));
|
||||
}
|
||||
|
||||
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound));
|
||||
}
|
||||
}
|
||||
|
||||
private static HttpResponseMessage BuildResponse(string json)
|
||||
{
|
||||
return new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent(json, Encoding.UTF8, "application/json")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ namespace StellaOps.Attestor.Infrastructure.Tests;
|
||||
|
||||
public sealed class InMemoryAttestorEntryRepositoryTests
|
||||
{
|
||||
private static CancellationToken TestCancellationToken => TestContext.Current.CancellationToken;
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task QueryAsync_ContinuationToken_DoesNotRepeatLastEntry()
|
||||
@@ -20,18 +22,22 @@ public sealed class InMemoryAttestorEntryRepositoryTests
|
||||
var first = CreateEntry("uuid-a", createdAt);
|
||||
var second = CreateEntry("uuid-b", createdAt);
|
||||
|
||||
await repository.SaveAsync(first);
|
||||
await repository.SaveAsync(second);
|
||||
await repository.SaveAsync(first, TestCancellationToken);
|
||||
await repository.SaveAsync(second, TestCancellationToken);
|
||||
|
||||
var firstPage = await repository.QueryAsync(new AttestorEntryQuery { PageSize = 1 });
|
||||
var firstPage = await repository.QueryAsync(
|
||||
new AttestorEntryQuery { PageSize = 1 },
|
||||
TestCancellationToken);
|
||||
firstPage.Items.Should().HaveCount(1);
|
||||
firstPage.ContinuationToken.Should().NotBeNullOrWhiteSpace();
|
||||
|
||||
var secondPage = await repository.QueryAsync(new AttestorEntryQuery
|
||||
{
|
||||
PageSize = 1,
|
||||
ContinuationToken = firstPage.ContinuationToken
|
||||
});
|
||||
var secondPage = await repository.QueryAsync(
|
||||
new AttestorEntryQuery
|
||||
{
|
||||
PageSize = 1,
|
||||
ContinuationToken = firstPage.ContinuationToken
|
||||
},
|
||||
TestCancellationToken);
|
||||
|
||||
secondPage.Items.Should().HaveCount(1);
|
||||
secondPage.Items[0].RekorUuid.Should().NotBe(firstPage.Items[0].RekorUuid);
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
using System;
|
||||
using FluentAssertions;
|
||||
using StellaOps.Attestor.Core.Options;
|
||||
using StellaOps.Attestor.Infrastructure.Rekor;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Attestor.Infrastructure.Tests;
|
||||
|
||||
public sealed class RekorBackendResolverTests
|
||||
{
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ResolveBackend_UnknownBackend_FallsBackToPrimary()
|
||||
{
|
||||
var options = new AttestorOptions
|
||||
{
|
||||
Rekor = new AttestorOptions.RekorOptions
|
||||
{
|
||||
Primary = new AttestorOptions.RekorBackendOptions
|
||||
{
|
||||
Url = "https://rekor.primary.example"
|
||||
},
|
||||
Mirror = new AttestorOptions.RekorMirrorOptions
|
||||
{
|
||||
Url = "https://rekor.mirror.example",
|
||||
Enabled = true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var backend = RekorBackendResolver.ResolveBackend(options, "unknown", allowFallbackToPrimary: true);
|
||||
|
||||
backend.Name.Should().Be("unknown");
|
||||
backend.Url.Should().Be(new Uri("https://rekor.primary.example"));
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ResolveBackend_UnknownBackend_ThrowsWhenFallbackDisabled()
|
||||
{
|
||||
var options = new AttestorOptions
|
||||
{
|
||||
Rekor = new AttestorOptions.RekorOptions
|
||||
{
|
||||
Primary = new AttestorOptions.RekorBackendOptions
|
||||
{
|
||||
Url = "https://rekor.primary.example"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var action = () => RekorBackendResolver.ResolveBackend(options, "unknown", allowFallbackToPrimary: false);
|
||||
|
||||
action.Should().Throw<InvalidOperationException>()
|
||||
.WithMessage("Unknown Rekor backend: unknown");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ResolveBackend_Mirror_ReturnsMirror()
|
||||
{
|
||||
var options = new AttestorOptions
|
||||
{
|
||||
Rekor = new AttestorOptions.RekorOptions
|
||||
{
|
||||
Primary = new AttestorOptions.RekorBackendOptions
|
||||
{
|
||||
Url = "https://rekor.primary.example"
|
||||
},
|
||||
Mirror = new AttestorOptions.RekorMirrorOptions
|
||||
{
|
||||
Url = "https://rekor.mirror.example",
|
||||
Enabled = true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var backend = RekorBackendResolver.ResolveBackend(options, "mirror", allowFallbackToPrimary: false);
|
||||
|
||||
backend.Name.Should().Be("mirror");
|
||||
backend.Url.Should().Be(new Uri("https://rekor.mirror.example"));
|
||||
}
|
||||
}
|
||||
@@ -5,5 +5,10 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
|
||||
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| AUDIT-0055-A | TODO | Reopened after revalidation 2026-01-06 (additional coverage needed). |
|
||||
| AUDIT-0055-A | DONE | Added Rekor client coverage and backend resolver tests 2026-01-08. |
|
||||
| AUDIT-0729-M | DONE | Revalidated 2026-01-07 (test project). |
|
||||
| AUDIT-0729-T | DONE | Revalidated 2026-01-07. |
|
||||
| AUDIT-0729-A | DONE | Waived (test project; revalidated 2026-01-07). |
|
||||
| VAL-SMOKE-001 | DONE | Removed xUnit v2 references and verified unit tests pass. |
|
||||
| AUDIT-0208-T | DONE | Revalidated 2026-01-08 (raw string + xUnit1051 fixes). |
|
||||
| AUDIT-0208-A | DONE | Applied fixes 2026-01-08 (raw string + xUnit1051 fixes). |
|
||||
|
||||
@@ -23,6 +23,7 @@ public class FileSystemRootStoreTests : IDisposable
|
||||
{
|
||||
private readonly Mock<ILogger<FileSystemRootStore>> _loggerMock;
|
||||
private readonly string _testRootPath;
|
||||
private static CancellationToken TestCancellationToken => TestContext.Current.CancellationToken;
|
||||
|
||||
public FileSystemRootStoreTests()
|
||||
{
|
||||
@@ -48,7 +49,7 @@ public class FileSystemRootStoreTests : IDisposable
|
||||
var store = CreateStore(options);
|
||||
|
||||
// Act
|
||||
var roots = await store.GetFulcioRootsAsync();
|
||||
var roots = await store.GetFulcioRootsAsync(TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
roots.Should().BeEmpty();
|
||||
@@ -61,13 +62,13 @@ public class FileSystemRootStoreTests : IDisposable
|
||||
// Arrange
|
||||
var cert = CreateTestCertificate("CN=Test Fulcio Root");
|
||||
var pemPath = Path.Combine(_testRootPath, "fulcio.pem");
|
||||
await WritePemFileAsync(pemPath, cert);
|
||||
await WritePemFileAsync(pemPath, cert, TestCancellationToken);
|
||||
|
||||
var options = CreateOptions(fulcioPath: pemPath);
|
||||
var store = CreateStore(options);
|
||||
|
||||
// Act
|
||||
var roots = await store.GetFulcioRootsAsync();
|
||||
var roots = await store.GetFulcioRootsAsync(TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
roots.Should().HaveCount(1);
|
||||
@@ -85,14 +86,14 @@ public class FileSystemRootStoreTests : IDisposable
|
||||
var cert1 = CreateTestCertificate("CN=Root 1");
|
||||
var cert2 = CreateTestCertificate("CN=Root 2");
|
||||
|
||||
await WritePemFileAsync(Path.Combine(fulcioDir, "root1.pem"), cert1);
|
||||
await WritePemFileAsync(Path.Combine(fulcioDir, "root2.pem"), cert2);
|
||||
await WritePemFileAsync(Path.Combine(fulcioDir, "root1.pem"), cert1, TestCancellationToken);
|
||||
await WritePemFileAsync(Path.Combine(fulcioDir, "root2.pem"), cert2, TestCancellationToken);
|
||||
|
||||
var options = CreateOptions(fulcioPath: fulcioDir);
|
||||
var store = CreateStore(options);
|
||||
|
||||
// Act
|
||||
var roots = await store.GetFulcioRootsAsync();
|
||||
var roots = await store.GetFulcioRootsAsync(TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
roots.Should().HaveCount(2);
|
||||
@@ -109,14 +110,14 @@ public class FileSystemRootStoreTests : IDisposable
|
||||
var certA = CreateTestCertificate("CN=Root A");
|
||||
var certB = CreateTestCertificate("CN=Root B");
|
||||
|
||||
await WritePemFileAsync(Path.Combine(fulcioDir, "b.pem"), certB);
|
||||
await WritePemFileAsync(Path.Combine(fulcioDir, "a.pem"), certA);
|
||||
await WritePemFileAsync(Path.Combine(fulcioDir, "b.pem"), certB, TestCancellationToken);
|
||||
await WritePemFileAsync(Path.Combine(fulcioDir, "a.pem"), certA, TestCancellationToken);
|
||||
|
||||
var options = CreateOptions(fulcioPath: fulcioDir);
|
||||
var store = CreateStore(options);
|
||||
|
||||
// Act
|
||||
var roots = await store.GetFulcioRootsAsync();
|
||||
var roots = await store.GetFulcioRootsAsync(TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
roots.Should().HaveCount(2);
|
||||
@@ -131,14 +132,14 @@ public class FileSystemRootStoreTests : IDisposable
|
||||
// Arrange
|
||||
var cert = CreateTestCertificate("CN=Cached Root");
|
||||
var pemPath = Path.Combine(_testRootPath, "cached.pem");
|
||||
await WritePemFileAsync(pemPath, cert);
|
||||
await WritePemFileAsync(pemPath, cert, TestCancellationToken);
|
||||
|
||||
var options = CreateOptions(fulcioPath: pemPath);
|
||||
var store = CreateStore(options);
|
||||
|
||||
// Act
|
||||
var roots1 = await store.GetFulcioRootsAsync();
|
||||
var roots2 = await store.GetFulcioRootsAsync();
|
||||
var roots1 = await store.GetFulcioRootsAsync(TestCancellationToken);
|
||||
var roots2 = await store.GetFulcioRootsAsync(TestCancellationToken);
|
||||
|
||||
// Assert - same collection instance (cached)
|
||||
roots1.Should().HaveCount(1);
|
||||
@@ -154,14 +155,14 @@ public class FileSystemRootStoreTests : IDisposable
|
||||
// Arrange
|
||||
var cert = CreateTestCertificate("CN=Imported Root");
|
||||
var sourcePath = Path.Combine(_testRootPath, "import-source.pem");
|
||||
await WritePemFileAsync(sourcePath, cert);
|
||||
await WritePemFileAsync(sourcePath, cert, TestCancellationToken);
|
||||
|
||||
var options = CreateOptions();
|
||||
options.Value.BaseRootPath = _testRootPath;
|
||||
var store = CreateStore(options);
|
||||
|
||||
// Act
|
||||
await store.ImportRootsAsync(sourcePath, RootType.Fulcio);
|
||||
await store.ImportRootsAsync(sourcePath, RootType.Fulcio, TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
var targetDir = Path.Combine(_testRootPath, "fulcio");
|
||||
@@ -179,7 +180,7 @@ public class FileSystemRootStoreTests : IDisposable
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<FileNotFoundException>(
|
||||
() => store.ImportRootsAsync("/nonexistent/path.pem", RootType.Fulcio));
|
||||
() => store.ImportRootsAsync("/nonexistent/path.pem", RootType.Fulcio, TestCancellationToken));
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
@@ -190,24 +191,24 @@ public class FileSystemRootStoreTests : IDisposable
|
||||
var cert1 = CreateTestCertificate("CN=Initial Root");
|
||||
var fulcioDir = Path.Combine(_testRootPath, "fulcio");
|
||||
Directory.CreateDirectory(fulcioDir);
|
||||
await WritePemFileAsync(Path.Combine(fulcioDir, "initial.pem"), cert1);
|
||||
await WritePemFileAsync(Path.Combine(fulcioDir, "initial.pem"), cert1, TestCancellationToken);
|
||||
|
||||
var options = CreateOptions(fulcioPath: fulcioDir);
|
||||
options.Value.BaseRootPath = _testRootPath;
|
||||
var store = CreateStore(options);
|
||||
|
||||
// Load initial cache
|
||||
var initialRoots = await store.GetFulcioRootsAsync();
|
||||
var initialRoots = await store.GetFulcioRootsAsync(TestCancellationToken);
|
||||
initialRoots.Should().HaveCount(1);
|
||||
|
||||
// Import a new certificate
|
||||
var cert2 = CreateTestCertificate("CN=Imported Root");
|
||||
var importPath = Path.Combine(_testRootPath, "import.pem");
|
||||
await WritePemFileAsync(importPath, cert2);
|
||||
await WritePemFileAsync(importPath, cert2, TestCancellationToken);
|
||||
|
||||
// Act
|
||||
await store.ImportRootsAsync(importPath, RootType.Fulcio);
|
||||
var updatedRoots = await store.GetFulcioRootsAsync();
|
||||
await store.ImportRootsAsync(importPath, RootType.Fulcio, TestCancellationToken);
|
||||
var updatedRoots = await store.GetFulcioRootsAsync(TestCancellationToken);
|
||||
|
||||
// Assert - cache invalidated and new cert loaded
|
||||
updatedRoots.Should().HaveCount(2);
|
||||
@@ -221,13 +222,13 @@ public class FileSystemRootStoreTests : IDisposable
|
||||
var cert = CreateTestCertificate("CN=Listed Root");
|
||||
var fulcioDir = Path.Combine(_testRootPath, "fulcio");
|
||||
Directory.CreateDirectory(fulcioDir);
|
||||
await WritePemFileAsync(Path.Combine(fulcioDir, "root.pem"), cert);
|
||||
await WritePemFileAsync(Path.Combine(fulcioDir, "root.pem"), cert, TestCancellationToken);
|
||||
|
||||
var options = CreateOptions(fulcioPath: fulcioDir);
|
||||
var store = CreateStore(options);
|
||||
|
||||
// Act
|
||||
var roots = await store.ListRootsAsync(RootType.Fulcio);
|
||||
var roots = await store.ListRootsAsync(RootType.Fulcio, TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
roots.Should().HaveCount(1);
|
||||
@@ -244,20 +245,20 @@ public class FileSystemRootStoreTests : IDisposable
|
||||
var cert = CreateTestCertificate("CN=Org Signing Key");
|
||||
var orgDir = Path.Combine(_testRootPath, "org-signing");
|
||||
Directory.CreateDirectory(orgDir);
|
||||
await WritePemFileAsync(Path.Combine(orgDir, "org.pem"), cert);
|
||||
await WritePemFileAsync(Path.Combine(orgDir, "org.pem"), cert, TestCancellationToken);
|
||||
|
||||
var options = CreateOptions(orgSigningPath: orgDir);
|
||||
var store = CreateStore(options);
|
||||
|
||||
// First, verify the cert was loaded and get its thumbprint from listing
|
||||
var orgKeys = await store.GetOrgSigningKeysAsync();
|
||||
var orgKeys = await store.GetOrgSigningKeysAsync(TestCancellationToken);
|
||||
orgKeys.Should().HaveCount(1);
|
||||
|
||||
// Get the thumbprint from the loaded certificate
|
||||
var thumbprint = ComputeThumbprint(orgKeys[0]);
|
||||
|
||||
// Act
|
||||
var found = await store.GetOrgKeyByIdAsync(thumbprint);
|
||||
var found = await store.GetOrgKeyByIdAsync(thumbprint, TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
found.Should().NotBeNull();
|
||||
@@ -272,13 +273,13 @@ public class FileSystemRootStoreTests : IDisposable
|
||||
var cert = CreateTestCertificate("CN=Org Key");
|
||||
var orgDir = Path.Combine(_testRootPath, "org-signing");
|
||||
Directory.CreateDirectory(orgDir);
|
||||
await WritePemFileAsync(Path.Combine(orgDir, "org.pem"), cert);
|
||||
await WritePemFileAsync(Path.Combine(orgDir, "org.pem"), cert, TestCancellationToken);
|
||||
|
||||
var options = CreateOptions(orgSigningPath: orgDir);
|
||||
var store = CreateStore(options);
|
||||
|
||||
// Act
|
||||
var found = await store.GetOrgKeyByIdAsync("nonexistent-key-id");
|
||||
var found = await store.GetOrgKeyByIdAsync("nonexistent-key-id", TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
found.Should().BeNull();
|
||||
@@ -291,13 +292,13 @@ public class FileSystemRootStoreTests : IDisposable
|
||||
// Arrange
|
||||
var cert = CreateTestCertificate("CN=Rekor Key");
|
||||
var rekorPath = Path.Combine(_testRootPath, "rekor.pem");
|
||||
await WritePemFileAsync(rekorPath, cert);
|
||||
await WritePemFileAsync(rekorPath, cert, TestCancellationToken);
|
||||
|
||||
var options = CreateOptions(rekorPath: rekorPath);
|
||||
var store = CreateStore(options);
|
||||
|
||||
// Act
|
||||
var keys = await store.GetRekorKeysAsync();
|
||||
var keys = await store.GetRekorKeysAsync(TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
keys.Should().HaveCount(1);
|
||||
@@ -314,13 +315,13 @@ public class FileSystemRootStoreTests : IDisposable
|
||||
var cert3 = CreateTestCertificate("CN=Cert 3");
|
||||
|
||||
var pemPath = Path.Combine(_testRootPath, "multi.pem");
|
||||
await WriteMultiplePemFileAsync(pemPath, [cert1, cert2, cert3]);
|
||||
await WriteMultiplePemFileAsync(pemPath, [cert1, cert2, cert3], TestCancellationToken);
|
||||
|
||||
var options = CreateOptions(fulcioPath: pemPath);
|
||||
var store = CreateStore(options);
|
||||
|
||||
// Act
|
||||
var roots = await store.GetFulcioRootsAsync();
|
||||
var roots = await store.GetFulcioRootsAsync(TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
roots.Should().HaveCount(3);
|
||||
@@ -336,7 +337,7 @@ public class FileSystemRootStoreTests : IDisposable
|
||||
Directory.CreateDirectory(fulcioKitDir);
|
||||
|
||||
var cert = CreateTestCertificate("CN=Offline Kit Root");
|
||||
await WritePemFileAsync(Path.Combine(fulcioKitDir, "root.pem"), cert);
|
||||
await WritePemFileAsync(Path.Combine(fulcioKitDir, "root.pem"), cert, TestCancellationToken);
|
||||
|
||||
var options = Options.Create(new OfflineRootStoreOptions
|
||||
{
|
||||
@@ -347,7 +348,7 @@ public class FileSystemRootStoreTests : IDisposable
|
||||
var store = CreateStore(options);
|
||||
|
||||
// Act
|
||||
var roots = await store.GetFulcioRootsAsync();
|
||||
var roots = await store.GetFulcioRootsAsync(TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
roots.Should().HaveCount(1);
|
||||
@@ -364,7 +365,7 @@ public class FileSystemRootStoreTests : IDisposable
|
||||
Directory.CreateDirectory(fulcioKitDir);
|
||||
|
||||
var cert = CreateTestCertificate("CN=Offline Kit Root");
|
||||
await WritePemFileAsync(Path.Combine(fulcioKitDir, "root.pem"), cert);
|
||||
await WritePemFileAsync(Path.Combine(fulcioKitDir, "root.pem"), cert, TestCancellationToken);
|
||||
|
||||
var options = Options.Create(new OfflineRootStoreOptions
|
||||
{
|
||||
@@ -375,7 +376,7 @@ public class FileSystemRootStoreTests : IDisposable
|
||||
var store = CreateStore(options);
|
||||
|
||||
// Act
|
||||
var roots = await store.GetFulcioRootsAsync();
|
||||
var roots = await store.GetFulcioRootsAsync(TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
roots.Should().BeEmpty();
|
||||
@@ -423,17 +424,17 @@ public class FileSystemRootStoreTests : IDisposable
|
||||
return request.CreateSelfSigned(notBefore, notAfter);
|
||||
}
|
||||
|
||||
private static async Task WritePemFileAsync(string path, X509Certificate2 cert)
|
||||
private static async Task WritePemFileAsync(string path, X509Certificate2 cert, CancellationToken cancellationToken)
|
||||
{
|
||||
var pem = new StringBuilder();
|
||||
pem.AppendLine("-----BEGIN CERTIFICATE-----");
|
||||
pem.AppendLine(Convert.ToBase64String(cert.RawData, Base64FormattingOptions.InsertLineBreaks));
|
||||
pem.AppendLine("-----END CERTIFICATE-----");
|
||||
|
||||
await File.WriteAllTextAsync(path, pem.ToString());
|
||||
await File.WriteAllTextAsync(path, pem.ToString(), cancellationToken);
|
||||
}
|
||||
|
||||
private static async Task WriteMultiplePemFileAsync(string path, X509Certificate2[] certs)
|
||||
private static async Task WriteMultiplePemFileAsync(string path, X509Certificate2[] certs, CancellationToken cancellationToken)
|
||||
{
|
||||
var pem = new StringBuilder();
|
||||
foreach (var cert in certs)
|
||||
@@ -444,7 +445,7 @@ public class FileSystemRootStoreTests : IDisposable
|
||||
pem.AppendLine();
|
||||
}
|
||||
|
||||
await File.WriteAllTextAsync(path, pem.ToString());
|
||||
await File.WriteAllTextAsync(path, pem.ToString(), cancellationToken);
|
||||
}
|
||||
|
||||
private static string ComputeThumbprint(X509Certificate2 cert)
|
||||
|
||||
@@ -26,6 +26,7 @@ public class OfflineCertChainValidatorTests
|
||||
private readonly Mock<ILogger<OfflineVerifier>> _loggerMock;
|
||||
private readonly IMerkleTreeBuilder _merkleBuilder;
|
||||
private readonly IOptions<OfflineVerificationConfig> _config;
|
||||
private static CancellationToken TestCancellationToken => TestContext.Current.CancellationToken;
|
||||
|
||||
public OfflineCertChainValidatorTests()
|
||||
{
|
||||
@@ -51,7 +52,7 @@ public class OfflineCertChainValidatorTests
|
||||
VerifyCertificateChain: true);
|
||||
|
||||
// Act
|
||||
var result = await verifier.VerifyAttestationAsync(attestation, options);
|
||||
var result = await verifier.VerifyAttestationAsync(attestation, options, TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
result.CertificateChainValid.Should().BeTrue();
|
||||
@@ -77,7 +78,7 @@ public class OfflineCertChainValidatorTests
|
||||
VerifyCertificateChain: true);
|
||||
|
||||
// Act
|
||||
var result = await verifier.VerifyAttestationAsync(attestation, options);
|
||||
var result = await verifier.VerifyAttestationAsync(attestation, options, TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
result.CertificateChainValid.Should().BeFalse();
|
||||
@@ -100,7 +101,7 @@ public class OfflineCertChainValidatorTests
|
||||
VerifyCertificateChain: true);
|
||||
|
||||
// Act
|
||||
var result = await verifier.VerifyAttestationAsync(attestation, options);
|
||||
var result = await verifier.VerifyAttestationAsync(attestation, options, TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
result.CertificateChainValid.Should().BeFalse();
|
||||
@@ -125,7 +126,7 @@ public class OfflineCertChainValidatorTests
|
||||
VerifyCertificateChain: true);
|
||||
|
||||
// Act
|
||||
var result = await verifier.VerifyAttestationAsync(attestation, options);
|
||||
var result = await verifier.VerifyAttestationAsync(attestation, options, TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
result.CertificateChainValid.Should().BeFalse();
|
||||
@@ -150,7 +151,7 @@ public class OfflineCertChainValidatorTests
|
||||
VerifyCertificateChain: true);
|
||||
|
||||
// Act
|
||||
var result = await verifier.VerifyAttestationAsync(attestation, options);
|
||||
var result = await verifier.VerifyAttestationAsync(attestation, options, TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
result.CertificateChainValid.Should().BeFalse();
|
||||
@@ -178,7 +179,7 @@ public class OfflineCertChainValidatorTests
|
||||
VerifyCertificateChain: true);
|
||||
|
||||
// Act
|
||||
var result = await verifier.VerifyBundleAsync(bundle, options);
|
||||
var result = await verifier.VerifyBundleAsync(bundle, options, TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
result.CertificateChainValid.Should().BeTrue();
|
||||
@@ -200,7 +201,7 @@ public class OfflineCertChainValidatorTests
|
||||
VerifyCertificateChain: false); // Disabled
|
||||
|
||||
// Act
|
||||
var result = await verifier.VerifyAttestationAsync(attestation, options);
|
||||
var result = await verifier.VerifyAttestationAsync(attestation, options, TestCancellationToken);
|
||||
|
||||
// Assert - When cert chain validation is disabled, it should not report cert-related issues
|
||||
result.Issues.Should().NotContain(i => i.Code.Contains("CERT_CHAIN"));
|
||||
@@ -224,7 +225,7 @@ public class OfflineCertChainValidatorTests
|
||||
VerifyCertificateChain: true);
|
||||
|
||||
// Act
|
||||
var result = await verifier.VerifyAttestationAsync(attestation, options);
|
||||
var result = await verifier.VerifyAttestationAsync(attestation, options, TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
result.CertificateChainValid.Should().BeFalse();
|
||||
@@ -247,7 +248,7 @@ public class OfflineCertChainValidatorTests
|
||||
VerifyCertificateChain: true);
|
||||
|
||||
// Act
|
||||
var result = await verifier.VerifyAttestationAsync(attestation, options);
|
||||
var result = await verifier.VerifyAttestationAsync(attestation, options, TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
result.CertificateChainValid.Should().BeFalse();
|
||||
|
||||
@@ -30,6 +30,7 @@ namespace StellaOps.Attestor.Offline.Tests;
|
||||
public class OfflineVerifierTests
|
||||
{
|
||||
private static readonly DateTimeOffset FixedNow = new(2025, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
||||
private static CancellationToken TestCancellationToken => TestContext.Current.CancellationToken;
|
||||
private readonly Mock<IOfflineRootStore> _rootStoreMock;
|
||||
private readonly IMerkleTreeBuilder _merkleBuilder;
|
||||
private readonly Mock<IOrgKeySigner> _orgSignerMock;
|
||||
@@ -65,7 +66,7 @@ public class OfflineVerifierTests
|
||||
VerifyOrgSignature: false);
|
||||
|
||||
// Act
|
||||
var result = await verifier.VerifyBundleAsync(bundle, options);
|
||||
var result = await verifier.VerifyBundleAsync(bundle, options, TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
result.Valid.Should().BeTrue();
|
||||
@@ -99,7 +100,7 @@ public class OfflineVerifierTests
|
||||
VerifyCertificateChain: false);
|
||||
|
||||
// Act
|
||||
var result = await verifier.VerifyBundleAsync(tamperedBundle, options);
|
||||
var result = await verifier.VerifyBundleAsync(tamperedBundle, options, TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
result.Valid.Should().BeFalse();
|
||||
@@ -123,7 +124,7 @@ public class OfflineVerifierTests
|
||||
RequireOrgSignature: true);
|
||||
|
||||
// Act
|
||||
var result = await verifier.VerifyBundleAsync(bundle, options);
|
||||
var result = await verifier.VerifyBundleAsync(bundle, options, TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
result.Valid.Should().BeFalse();
|
||||
@@ -161,7 +162,7 @@ public class OfflineVerifierTests
|
||||
VerifyOrgSignature: true);
|
||||
|
||||
// Act
|
||||
var result = await verifier.VerifyBundleAsync(signedBundle, options);
|
||||
var result = await verifier.VerifyBundleAsync(signedBundle, options, TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
result.Valid.Should().BeTrue();
|
||||
@@ -183,7 +184,7 @@ public class OfflineVerifierTests
|
||||
VerifyCertificateChain: false);
|
||||
|
||||
// Act
|
||||
var result = await verifier.VerifyAttestationAsync(attestation, options);
|
||||
var result = await verifier.VerifyAttestationAsync(attestation, options, TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
result.Valid.Should().BeTrue();
|
||||
@@ -214,7 +215,7 @@ public class OfflineVerifierTests
|
||||
VerifyCertificateChain: false);
|
||||
|
||||
// Act
|
||||
var result = await verifier.VerifyAttestationAsync(tamperedAttestation, options);
|
||||
var result = await verifier.VerifyAttestationAsync(tamperedAttestation, options, TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
result.Valid.Should().BeFalse();
|
||||
@@ -236,7 +237,7 @@ public class OfflineVerifierTests
|
||||
VerifyCertificateChain: false);
|
||||
|
||||
// Act
|
||||
var summaries = await verifier.GetVerificationSummariesAsync(bundle, options);
|
||||
var summaries = await verifier.GetVerificationSummariesAsync(bundle, options, TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
summaries.Should().HaveCount(10);
|
||||
@@ -276,7 +277,7 @@ public class OfflineVerifierTests
|
||||
StrictMode: true);
|
||||
|
||||
// Act
|
||||
var result = await verifier.VerifyBundleAsync(bundle, options);
|
||||
var result = await verifier.VerifyBundleAsync(bundle, options, TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
result.Valid.Should().BeFalse();
|
||||
@@ -296,7 +297,7 @@ public class OfflineVerifierTests
|
||||
var verifier = CreateVerifier(config);
|
||||
|
||||
// Act
|
||||
var result = await verifier.VerifyBundleAsync(bundle, options: null);
|
||||
var result = await verifier.VerifyBundleAsync(bundle, options: null, cancellationToken: TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
result.Valid.Should().BeFalse();
|
||||
@@ -316,7 +317,7 @@ public class OfflineVerifierTests
|
||||
var verifier = CreateVerifier(config);
|
||||
|
||||
// Act
|
||||
var result = await verifier.VerifyAttestationAsync(attestation, options: null);
|
||||
var result = await verifier.VerifyAttestationAsync(attestation, options: null, cancellationToken: TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
result.Valid.Should().BeFalse();
|
||||
@@ -331,7 +332,7 @@ public class OfflineVerifierTests
|
||||
var tempPath = Path.Combine(Path.GetTempPath(), $"bundle-{Guid.NewGuid():N}.json");
|
||||
try
|
||||
{
|
||||
await File.WriteAllBytesAsync(tempPath, new byte[2 * 1024 * 1024]);
|
||||
await File.WriteAllBytesAsync(tempPath, new byte[2 * 1024 * 1024], TestCancellationToken);
|
||||
|
||||
var config = Options.Create(new OfflineVerificationConfig
|
||||
{
|
||||
@@ -347,7 +348,8 @@ public class OfflineVerifierTests
|
||||
VerifyMerkleProof: false,
|
||||
VerifySignatures: false,
|
||||
VerifyCertificateChain: false,
|
||||
VerifyOrgSignature: false));
|
||||
VerifyOrgSignature: false),
|
||||
TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
result.Valid.Should().BeFalse();
|
||||
@@ -383,8 +385,8 @@ public class OfflineVerifierTests
|
||||
VerifyCertificateChain: false);
|
||||
|
||||
// Act
|
||||
var result1 = await verifier.VerifyBundleAsync(bundle1, options);
|
||||
var result2 = await verifier.VerifyBundleAsync(bundle2, options);
|
||||
var result1 = await verifier.VerifyBundleAsync(bundle1, options, TestCancellationToken);
|
||||
var result2 = await verifier.VerifyBundleAsync(bundle2, options, TestCancellationToken);
|
||||
|
||||
// Assert - both should have the same merkle validation result
|
||||
result1.MerkleProofValid.Should().Be(result2.MerkleProofValid);
|
||||
|
||||
@@ -8,3 +8,5 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
|
||||
| AUDIT-0059-M | DONE | Revalidated 2026-01-06. |
|
||||
| AUDIT-0059-T | DONE | Revalidated 2026-01-06. |
|
||||
| AUDIT-0059-A | DONE | Waived after revalidation 2026-01-06. |
|
||||
| AUDIT-0210-T | DONE | Revalidated 2026-01-08 (xUnit1051 fixes). |
|
||||
| AUDIT-0210-A | DONE | Applied fixes 2026-01-08 (xUnit1051 fixes). |
|
||||
|
||||
@@ -12,12 +12,23 @@ public sealed class GeneratorOutputTests
|
||||
var schemaDir = Path.Combine(AppContext.BaseDirectory, "schemas");
|
||||
Directory.Exists(schemaDir).Should().BeTrue($"schema directory should exist at '{schemaDir}'");
|
||||
|
||||
var expectedOverrides = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["attestation-common.v1.schema.json"] = "https://schemas.stella-ops.org/attestations/common/v1",
|
||||
["uncertainty-budget-statement.v1.schema.json"] = "https://stella-ops.org/schemas/attestation/uncertainty-budget-statement.v1.json",
|
||||
["uncertainty-statement.v1.schema.json"] = "https://stella-ops.org/schemas/attestation/uncertainty-statement.v1.json",
|
||||
["verification-policy.v1.schema.json"] = "https://stellaops.io/schemas/verification-policy.v1.json"
|
||||
};
|
||||
|
||||
foreach (var path in Directory.EnumerateFiles(schemaDir, "*.schema.json", SearchOption.TopDirectoryOnly))
|
||||
{
|
||||
using var doc = JsonDocument.Parse(File.ReadAllText(path));
|
||||
doc.RootElement.TryGetProperty("$id", out var idElement).Should().BeTrue();
|
||||
|
||||
var expected = $"https://stella-ops.org/schemas/attestor/{Path.GetFileName(path)}";
|
||||
var fileName = Path.GetFileName(path);
|
||||
var expected = expectedOverrides.TryGetValue(fileName, out var overrideId)
|
||||
? overrideId
|
||||
: $"https://stella-ops.org/schemas/attestor/{fileName}";
|
||||
idElement.GetString().Should().Be(expected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ public sealed class RekorInclusionProofTests
|
||||
_output.WriteLine($"Tree size: {tree.Size}");
|
||||
_output.WriteLine($"Root hash: {Convert.ToHexString(tree.RootHash).ToLower()}");
|
||||
_output.WriteLine($"Proof path length: {proof.Count}");
|
||||
_output.WriteLine("✓ Inclusion proof verified");
|
||||
_output.WriteLine("[OK] Inclusion proof verified");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -97,7 +97,7 @@ public sealed class RekorInclusionProofTests
|
||||
proof: proof);
|
||||
|
||||
verified.Should().BeTrue($"entry {i} should verify");
|
||||
_output.WriteLine($" Entry {i}: ✓ (proof path: {proof.Count} nodes)");
|
||||
_output.WriteLine($" Entry {i}: [OK] (proof path: {proof.Count} nodes)");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@ public sealed class RekorInclusionProofTests
|
||||
|
||||
// Assert
|
||||
verified.Should().BeFalse("tampered leaf should not verify");
|
||||
_output.WriteLine("✓ Tampered leaf data detected");
|
||||
_output.WriteLine("[OK] Tampered leaf data detected");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -159,7 +159,7 @@ public sealed class RekorInclusionProofTests
|
||||
|
||||
// Assert
|
||||
verified.Should().BeFalse("tampered proof path should not verify");
|
||||
_output.WriteLine("✓ Tampered proof path detected");
|
||||
_output.WriteLine("[OK] Tampered proof path detected");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -183,7 +183,7 @@ public sealed class RekorInclusionProofTests
|
||||
|
||||
// Assert
|
||||
verified.Should().BeFalse("tampered root hash should not verify");
|
||||
_output.WriteLine("✓ Tampered root hash detected");
|
||||
_output.WriteLine("[OK] Tampered root hash detected");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -209,7 +209,7 @@ public sealed class RekorInclusionProofTests
|
||||
|
||||
// Assert
|
||||
verified.Should().BeFalse("wrong index should not verify");
|
||||
_output.WriteLine("✓ Wrong index detected");
|
||||
_output.WriteLine("[OK] Wrong index detected");
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -237,7 +237,7 @@ public sealed class RekorInclusionProofTests
|
||||
verified.Should().BeTrue("single node tree should verify");
|
||||
proof.Should().BeEmpty("single node tree needs no proof path");
|
||||
|
||||
_output.WriteLine("✓ Single node tree verified");
|
||||
_output.WriteLine("[OK] Single node tree verified");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -271,7 +271,7 @@ public sealed class RekorInclusionProofTests
|
||||
verified0.Should().BeTrue("entry 0 should verify");
|
||||
verified1.Should().BeTrue("entry 1 should verify");
|
||||
|
||||
_output.WriteLine("✓ Two node tree verified");
|
||||
_output.WriteLine("[OK] Two node tree verified");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -304,7 +304,7 @@ public sealed class RekorInclusionProofTests
|
||||
proof: proof);
|
||||
|
||||
verified.Should().BeTrue($"entry {index} should verify");
|
||||
_output.WriteLine($" Entry {index}: ✓ (proof path: {proof.Count} nodes)");
|
||||
_output.WriteLine($" Entry {index}: [OK] (proof path: {proof.Count} nodes)");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -334,7 +334,7 @@ public sealed class RekorInclusionProofTests
|
||||
verified.Should().BeTrue($"entry {i} should verify in non-power-of-two tree");
|
||||
}
|
||||
|
||||
_output.WriteLine("✓ Non-power-of-two tree verified");
|
||||
_output.WriteLine("[OK] Non-power-of-two tree verified");
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -412,7 +412,7 @@ public sealed class RekorInclusionProofTests
|
||||
// Assert - all results should be identical
|
||||
results.Should().AllBeEquivalentTo(true);
|
||||
|
||||
_output.WriteLine("✓ Verification is deterministic across 10 runs");
|
||||
_output.WriteLine("[OK] Verification is deterministic across 10 runs");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -482,6 +482,11 @@ public sealed class RekorInclusionProofTests
|
||||
byte[] rootHash,
|
||||
IReadOnlyList<byte[]> proof)
|
||||
{
|
||||
if (treeSize <= 0 || leafIndex < 0 || leafIndex >= treeSize)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var leafHash = HashLeaf(leafData);
|
||||
var computedRoot = RecomputeRoot(leafHash, leafIndex, treeSize, proof);
|
||||
return computedRoot.SequenceEqual(rootHash);
|
||||
@@ -555,19 +560,21 @@ public sealed class RekorInclusionProofTests
|
||||
{
|
||||
var current = leafHash;
|
||||
var currentIndex = index;
|
||||
var lastIndex = treeSize - 1;
|
||||
|
||||
foreach (var sibling in proof)
|
||||
{
|
||||
if (currentIndex % 2 == 0)
|
||||
{
|
||||
current = HashInner(current, sibling);
|
||||
}
|
||||
else
|
||||
if (currentIndex % 2 == 1 || currentIndex == lastIndex)
|
||||
{
|
||||
current = HashInner(sibling, current);
|
||||
}
|
||||
else
|
||||
{
|
||||
current = HashInner(current, sibling);
|
||||
}
|
||||
|
||||
currentIndex /= 2;
|
||||
lastIndex /= 2;
|
||||
}
|
||||
|
||||
return current;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// RekorReceiptGenerationTests.cs
|
||||
// Sprint: SPRINT_5100_0009_0007 - Attestor Module Test Implementation
|
||||
// Task: ATTESTOR-5100-006 - Add Rekor receipt generation tests: attestation → Rekor entry → receipt returned
|
||||
// Task: ATTESTOR-5100-006 - Add Rekor receipt generation tests: attestation -> Rekor entry -> receipt returned
|
||||
// Description: Tests for Rekor transparency log receipt generation
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
@@ -51,7 +51,7 @@ public sealed class RekorReceiptGenerationTests
|
||||
response.Status.Should().Be("included", "entry should be included in log");
|
||||
response.Index.Should().BeGreaterThanOrEqualTo(0, "index should be assigned");
|
||||
|
||||
_output.WriteLine($"✓ Receipt generated:");
|
||||
_output.WriteLine("[OK] Receipt generated:");
|
||||
_output.WriteLine($" UUID: {response.Uuid}");
|
||||
_output.WriteLine($" Index: {response.Index}");
|
||||
_output.WriteLine($" Status: {response.Status}");
|
||||
@@ -283,7 +283,7 @@ public sealed class RekorReceiptGenerationTests
|
||||
// Assert
|
||||
response.LogUrl.Should().StartWith(expectedBaseUrl);
|
||||
|
||||
_output.WriteLine($"Backend {backend} → {response.LogUrl}");
|
||||
_output.WriteLine($"Backend {backend} -> {response.LogUrl}");
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -329,7 +329,7 @@ public sealed class RekorReceiptGenerationTests
|
||||
deserialized.Status.Should().Be(original.Status);
|
||||
deserialized.IntegratedTime.Should().Be(original.IntegratedTime);
|
||||
|
||||
_output.WriteLine("✓ Receipt serialization roundtrips correctly");
|
||||
_output.WriteLine("[OK] Receipt serialization roundtrips correctly");
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -481,7 +481,7 @@ public sealed class RekorReceiptGenerationTests
|
||||
{
|
||||
return Task.FromResult(new SubmissionResult(false,
|
||||
ErrorCode: "REKOR_INVALID_ENTRY",
|
||||
ErrorMessage: "Invalid DSSE envelope: payload type and payload are required"));
|
||||
ErrorMessage: "invalid DSSE envelope: payload type and payload are required"));
|
||||
}
|
||||
|
||||
var response = CreateResponse(envelope);
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// RekorReceiptVerificationTests.cs
|
||||
// Sprint: SPRINT_5100_0009_0007 - Attestor Module Test Implementation
|
||||
// Task: ATTESTOR-5100-007 - Add Rekor receipt verification tests: valid receipt → verification succeeds; invalid receipt → fails
|
||||
// Task: ATTESTOR-5100-007 - Add Rekor receipt verification tests: valid receipt -> verification succeeds; invalid receipt -> fails
|
||||
// Description: Tests for Rekor transparency log receipt verification
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
@@ -49,7 +48,7 @@ public sealed class RekorReceiptVerificationTests
|
||||
result.Success.Should().BeTrue("valid receipt should verify");
|
||||
result.ErrorCode.Should().BeNullOrEmpty();
|
||||
|
||||
_output.WriteLine("✓ Valid receipt verified successfully");
|
||||
_output.WriteLine("[OK] Valid receipt verified successfully");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -93,7 +92,7 @@ public sealed class RekorReceiptVerificationTests
|
||||
result.Success.Should().BeFalse();
|
||||
result.ErrorCode.Should().Be("RECEIPT_MISSING_UUID");
|
||||
|
||||
_output.WriteLine($"✓ Missing UUID detected: {result.ErrorCode}");
|
||||
_output.WriteLine($"[OK] Missing UUID detected: {result.ErrorCode}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -111,7 +110,7 @@ public sealed class RekorReceiptVerificationTests
|
||||
result.Success.Should().BeFalse();
|
||||
result.ErrorCode.Should().Be("RECEIPT_MISSING_INDEX");
|
||||
|
||||
_output.WriteLine($"✓ Missing index detected: {result.ErrorCode}");
|
||||
_output.WriteLine($"[OK] Missing index detected: {result.ErrorCode}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -129,7 +128,7 @@ public sealed class RekorReceiptVerificationTests
|
||||
result.Success.Should().BeFalse();
|
||||
result.ErrorCode.Should().Be("RECEIPT_MISSING_PROOF");
|
||||
|
||||
_output.WriteLine($"✓ Missing proof detected: {result.ErrorCode}");
|
||||
_output.WriteLine($"[OK] Missing proof detected: {result.ErrorCode}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -147,7 +146,7 @@ public sealed class RekorReceiptVerificationTests
|
||||
result.Success.Should().BeFalse();
|
||||
result.ErrorCode.Should().Be("RECEIPT_MISSING_CHECKPOINT");
|
||||
|
||||
_output.WriteLine($"✓ Missing checkpoint detected: {result.ErrorCode}");
|
||||
_output.WriteLine($"[OK] Missing checkpoint detected: {result.ErrorCode}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -165,7 +164,7 @@ public sealed class RekorReceiptVerificationTests
|
||||
result.Success.Should().BeFalse();
|
||||
result.ErrorCode.Should().Be("RECEIPT_MISSING_INCLUSION");
|
||||
|
||||
_output.WriteLine($"✓ Missing inclusion proof detected: {result.ErrorCode}");
|
||||
_output.WriteLine($"[OK] Missing inclusion proof detected: {result.ErrorCode}");
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -190,7 +189,7 @@ public sealed class RekorReceiptVerificationTests
|
||||
result.Success.Should().BeFalse();
|
||||
result.ErrorCode.Should().Be("RECEIPT_INVALID_ROOT_HASH");
|
||||
|
||||
_output.WriteLine($"✓ Tampered root hash detected");
|
||||
_output.WriteLine("[OK] Tampered root hash detected");
|
||||
_output.WriteLine($" Original: {originalHash}");
|
||||
_output.WriteLine($" Tampered: {receipt.Proof.Checkpoint.RootHash}");
|
||||
}
|
||||
@@ -212,7 +211,7 @@ public sealed class RekorReceiptVerificationTests
|
||||
result.Success.Should().BeFalse();
|
||||
result.ErrorCode.Should().Be("RECEIPT_INVALID_LEAF_HASH");
|
||||
|
||||
_output.WriteLine($"✓ Tampered leaf hash detected");
|
||||
_output.WriteLine("[OK] Tampered leaf hash detected");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -232,7 +231,7 @@ public sealed class RekorReceiptVerificationTests
|
||||
result.Success.Should().BeFalse();
|
||||
result.ErrorCode.Should().Be("RECEIPT_INVALID_INCLUSION_PATH");
|
||||
|
||||
_output.WriteLine($"✓ Tampered inclusion path detected");
|
||||
_output.WriteLine("[OK] Tampered inclusion path detected");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -253,7 +252,7 @@ public sealed class RekorReceiptVerificationTests
|
||||
result.Success.Should().BeFalse();
|
||||
result.ErrorCode.Should().Be("RECEIPT_INDEX_MISMATCH");
|
||||
|
||||
_output.WriteLine($"✓ Tampered index detected: {originalIndex} → {receipt.Index}");
|
||||
_output.WriteLine($"[OK] Tampered index detected: {originalIndex} -> {receipt.Index}");
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -277,7 +276,7 @@ public sealed class RekorReceiptVerificationTests
|
||||
result.Success.Should().BeFalse();
|
||||
result.ErrorCode.Should().Be("RECEIPT_TIME_SKEW");
|
||||
|
||||
_output.WriteLine($"✓ Future integrated time detected");
|
||||
_output.WriteLine("[OK] Future integrated time detected");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -296,7 +295,7 @@ public sealed class RekorReceiptVerificationTests
|
||||
// Assert - should still be valid within tolerance
|
||||
result.Success.Should().BeTrue("slight time skew should be allowed");
|
||||
|
||||
_output.WriteLine("✓ Slight time skew allowed within tolerance");
|
||||
_output.WriteLine("[OK] Slight time skew allowed within tolerance");
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -320,7 +319,7 @@ public sealed class RekorReceiptVerificationTests
|
||||
result.Success.Should().BeFalse();
|
||||
result.ErrorCode.Should().Be(expectedError);
|
||||
|
||||
_output.WriteLine($"UUID '{uuid}' → {expectedError}");
|
||||
_output.WriteLine($"UUID '{uuid}' -> {expectedError}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -364,7 +363,7 @@ public sealed class RekorReceiptVerificationTests
|
||||
// Assert
|
||||
result.Success.Should().BeTrue("payload hash should match");
|
||||
|
||||
_output.WriteLine("✓ Payload hash verified");
|
||||
_output.WriteLine("[OK] Payload hash verified");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -383,7 +382,7 @@ public sealed class RekorReceiptVerificationTests
|
||||
result.Success.Should().BeFalse("tampered payload should not match");
|
||||
result.ErrorCode.Should().Be("RECEIPT_PAYLOAD_MISMATCH");
|
||||
|
||||
_output.WriteLine("✓ Tampered payload detected");
|
||||
_output.WriteLine("[OK] Tampered payload detected");
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -426,12 +425,13 @@ public sealed class RekorReceiptVerificationTests
|
||||
private static RekorReceipt CreateValidReceipt()
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var index = 12345L;
|
||||
const long index = 1;
|
||||
const long treeSize = 2;
|
||||
|
||||
// Create deterministic hashes
|
||||
var leafData = Encoding.UTF8.GetBytes($"leaf-{index}");
|
||||
var leafHash = SHA256.HashData(leafData);
|
||||
var rootHash = SHA256.HashData(leafHash);
|
||||
var pathBytes = MockMerkleHelpers.BuildInclusionPathBytes(index);
|
||||
var rootHash = MockMerkleHelpers.ComputeRootFromProof(leafHash, index, treeSize, pathBytes);
|
||||
|
||||
return new RekorReceipt
|
||||
{
|
||||
@@ -445,18 +445,14 @@ public sealed class RekorReceiptVerificationTests
|
||||
Checkpoint = new RekorCheckpoint
|
||||
{
|
||||
Origin = "rekor.sigstore.dev - 2605736670972794746",
|
||||
Size = index + 1,
|
||||
RootHash = Convert.ToHexString(rootHash).ToLower(),
|
||||
Size = treeSize,
|
||||
RootHash = MockMerkleHelpers.ToHexLower(rootHash),
|
||||
Timestamp = now
|
||||
},
|
||||
Inclusion = new RekorInclusionProof
|
||||
{
|
||||
LeafHash = Convert.ToHexString(leafHash).ToLower(),
|
||||
Path = new[]
|
||||
{
|
||||
Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes($"sibling-{index}-1"))).ToLower(),
|
||||
Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes($"sibling-{index}-2"))).ToLower()
|
||||
}
|
||||
LeafHash = MockMerkleHelpers.ToHexLower(leafHash),
|
||||
Path = MockMerkleHelpers.BuildInclusionPath(index)
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -466,13 +462,102 @@ public sealed class RekorReceiptVerificationTests
|
||||
{
|
||||
var receipt = CreateValidReceipt();
|
||||
var payloadHash = SHA256.HashData(payload);
|
||||
receipt.Proof!.Inclusion!.LeafHash = Convert.ToHexString(payloadHash).ToLower();
|
||||
receipt.Proof.Checkpoint!.RootHash = Convert.ToHexString(SHA256.HashData(payloadHash)).ToLower();
|
||||
receipt.Proof!.Inclusion!.LeafHash = MockMerkleHelpers.ToHexLower(payloadHash);
|
||||
|
||||
var pathBytes = MockMerkleHelpers.DecodePath(receipt.Proof.Inclusion.Path);
|
||||
var rootHash = MockMerkleHelpers.ComputeRootFromProof(
|
||||
payloadHash,
|
||||
receipt.Index!.Value,
|
||||
receipt.Proof.Checkpoint!.Size,
|
||||
pathBytes);
|
||||
|
||||
receipt.Proof.Checkpoint.RootHash = MockMerkleHelpers.ToHexLower(rootHash);
|
||||
return receipt;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private static class MockMerkleHelpers
|
||||
{
|
||||
public static string ToHexLower(byte[] bytes)
|
||||
{
|
||||
return Convert.ToHexString(bytes).ToLowerInvariant();
|
||||
}
|
||||
|
||||
public static IReadOnlyList<string> BuildInclusionPath(long index)
|
||||
{
|
||||
var pathBytes = BuildInclusionPathBytes(index);
|
||||
var path = new string[pathBytes.Count];
|
||||
for (int i = 0; i < pathBytes.Count; i++)
|
||||
{
|
||||
path[i] = ToHexLower(pathBytes[i]);
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
public static IReadOnlyList<byte[]> BuildInclusionPathBytes(long index)
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
SHA256.HashData(Encoding.UTF8.GetBytes($"sibling-{index}-1"))
|
||||
};
|
||||
}
|
||||
|
||||
public static IReadOnlyList<byte[]> DecodePath(IReadOnlyList<string> path)
|
||||
{
|
||||
var decoded = new byte[path.Count][];
|
||||
for (int i = 0; i < path.Count; i++)
|
||||
{
|
||||
decoded[i] = Convert.FromHexString(path[i]);
|
||||
}
|
||||
|
||||
return decoded;
|
||||
}
|
||||
|
||||
public static byte[] DecodeHash(string hex)
|
||||
{
|
||||
return Convert.FromHexString(hex);
|
||||
}
|
||||
|
||||
public static byte[] ComputeRootFromProof(
|
||||
byte[] leafHash,
|
||||
long leafIndex,
|
||||
long treeSize,
|
||||
IReadOnlyList<byte[]> proof)
|
||||
{
|
||||
var current = leafHash;
|
||||
var currentIndex = leafIndex;
|
||||
var lastIndex = treeSize - 1;
|
||||
|
||||
foreach (var sibling in proof)
|
||||
{
|
||||
if ((currentIndex & 1) == 1 || currentIndex == lastIndex)
|
||||
{
|
||||
current = HashInner(sibling, current);
|
||||
}
|
||||
else
|
||||
{
|
||||
current = HashInner(current, sibling);
|
||||
}
|
||||
|
||||
currentIndex >>= 1;
|
||||
lastIndex >>= 1;
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
private static byte[] HashInner(byte[] left, byte[] right)
|
||||
{
|
||||
var combined = new byte[left.Length + right.Length + 1];
|
||||
combined[0] = 0x01;
|
||||
Buffer.BlockCopy(left, 0, combined, 1, left.Length);
|
||||
Buffer.BlockCopy(right, 0, combined, 1 + left.Length, right.Length);
|
||||
return SHA256.HashData(combined);
|
||||
}
|
||||
}
|
||||
|
||||
#region Mock Types
|
||||
|
||||
private sealed class RekorReceipt
|
||||
@@ -518,6 +603,30 @@ public sealed class RekorReceiptVerificationTests
|
||||
public TimeSpan AllowedTimeSkew { get; set; } = TimeSpan.FromMinutes(5);
|
||||
|
||||
public VerificationResult Verify(RekorReceipt receipt)
|
||||
{
|
||||
return VerifyCore(receipt, verifyLeafHash: true);
|
||||
}
|
||||
|
||||
public VerificationResult VerifyWithPayload(RekorReceipt receipt, byte[] payload)
|
||||
{
|
||||
var basicResult = VerifyCore(receipt, verifyLeafHash: false);
|
||||
if (!basicResult.Success)
|
||||
{
|
||||
return basicResult;
|
||||
}
|
||||
|
||||
// Verify payload hash matches leaf hash
|
||||
var payloadHash = MockMerkleHelpers.ToHexLower(SHA256.HashData(payload));
|
||||
if (!string.Equals(receipt.Proof!.Inclusion!.LeafHash, payloadHash, StringComparison.Ordinal))
|
||||
{
|
||||
return new VerificationResult(false, "RECEIPT_PAYLOAD_MISMATCH",
|
||||
"Payload hash does not match receipt leaf hash");
|
||||
}
|
||||
|
||||
return basicResult;
|
||||
}
|
||||
|
||||
private VerificationResult VerifyCore(RekorReceipt receipt, bool verifyLeafHash)
|
||||
{
|
||||
// Check UUID
|
||||
if (string.IsNullOrEmpty(receipt.Uuid))
|
||||
@@ -536,6 +645,8 @@ public sealed class RekorReceiptVerificationTests
|
||||
return new VerificationResult(false, "RECEIPT_MISSING_INDEX", "Receipt index is required");
|
||||
}
|
||||
|
||||
var index = receipt.Index.Value;
|
||||
|
||||
// Check status
|
||||
if (receipt.Status != "included")
|
||||
{
|
||||
@@ -582,6 +693,39 @@ public sealed class RekorReceiptVerificationTests
|
||||
return new VerificationResult(false, "RECEIPT_INDEX_MISMATCH", "Index is inconsistent with checkpoint size");
|
||||
}
|
||||
|
||||
if (verifyLeafHash)
|
||||
{
|
||||
var expectedLeafHash = MockMerkleHelpers.ToHexLower(
|
||||
SHA256.HashData(Encoding.UTF8.GetBytes($"leaf-{index}")));
|
||||
if (!string.Equals(receipt.Proof.Inclusion.LeafHash, expectedLeafHash, StringComparison.Ordinal))
|
||||
{
|
||||
return new VerificationResult(false, "RECEIPT_INVALID_LEAF_HASH",
|
||||
"Leaf hash does not match expected value");
|
||||
}
|
||||
}
|
||||
|
||||
var expectedPath = MockMerkleHelpers.BuildInclusionPath(index);
|
||||
if (!expectedPath.SequenceEqual(receipt.Proof.Inclusion.Path, StringComparer.Ordinal))
|
||||
{
|
||||
return new VerificationResult(false, "RECEIPT_INVALID_INCLUSION_PATH",
|
||||
"Inclusion path does not match expected value");
|
||||
}
|
||||
|
||||
var leafHashBytes = MockMerkleHelpers.DecodeHash(receipt.Proof.Inclusion.LeafHash!);
|
||||
var pathBytes = MockMerkleHelpers.DecodePath(receipt.Proof.Inclusion.Path);
|
||||
var rootHashBytes = MockMerkleHelpers.DecodeHash(receipt.Proof.Checkpoint.RootHash!);
|
||||
var computedRoot = MockMerkleHelpers.ComputeRootFromProof(
|
||||
leafHashBytes,
|
||||
index,
|
||||
receipt.Proof.Checkpoint.Size,
|
||||
pathBytes);
|
||||
|
||||
if (!computedRoot.SequenceEqual(rootHashBytes))
|
||||
{
|
||||
return new VerificationResult(false, "RECEIPT_INVALID_ROOT_HASH",
|
||||
"Root hash does not match inclusion proof");
|
||||
}
|
||||
|
||||
// Verify time is not too far in the future
|
||||
if (receipt.IntegratedTime.HasValue)
|
||||
{
|
||||
@@ -601,25 +745,6 @@ public sealed class RekorReceiptVerificationTests
|
||||
: null);
|
||||
}
|
||||
|
||||
public VerificationResult VerifyWithPayload(RekorReceipt receipt, byte[] payload)
|
||||
{
|
||||
var basicResult = Verify(receipt);
|
||||
if (!basicResult.Success)
|
||||
{
|
||||
return basicResult;
|
||||
}
|
||||
|
||||
// Verify payload hash matches leaf hash
|
||||
var payloadHash = Convert.ToHexString(SHA256.HashData(payload)).ToLower();
|
||||
if (receipt.Proof!.Inclusion!.LeafHash != payloadHash)
|
||||
{
|
||||
return new VerificationResult(false, "RECEIPT_PAYLOAD_MISMATCH",
|
||||
"Payload hash does not match receipt leaf hash");
|
||||
}
|
||||
|
||||
return basicResult;
|
||||
}
|
||||
|
||||
private static bool IsValidUuidFormat(string uuid)
|
||||
{
|
||||
// Rekor UUIDs are 64 hex characters
|
||||
|
||||
@@ -8,3 +8,5 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
|
||||
| AUDIT-0070-M | DONE | Revalidated 2026-01-06 (maintainability audit). |
|
||||
| AUDIT-0070-T | DONE | Revalidated 2026-01-06 (test coverage audit). |
|
||||
| AUDIT-0070-A | DONE | Waived (test project; revalidated 2026-01-06). |
|
||||
| AUDIT-0214-T | DONE | Revalidated 2026-01-08 (Rekor proofs + schema IDs). |
|
||||
| AUDIT-0214-A | DONE | Applied fixes 2026-01-08 (Rekor proofs + schema IDs). |
|
||||
|
||||
@@ -6,3 +6,6 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| AUDIT-0071-A | DONE | Added test coverage for Attestor.Verify apply fixes. |
|
||||
| AUDIT-0730-M | DONE | Revalidated 2026-01-07 (test project). |
|
||||
| AUDIT-0730-T | DONE | Revalidated 2026-01-07. |
|
||||
| AUDIT-0730-A | DONE | Waived (test project; revalidated 2026-01-07). |
|
||||
|
||||
Reference in New Issue
Block a user