save progress
This commit is contained in:
@@ -98,6 +98,32 @@ public class FileSystemRootStoreTests : IDisposable
|
||||
roots.Should().HaveCount(2);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetFulcioRootsAsync_WithDirectory_OrdersByFileName()
|
||||
{
|
||||
// Arrange
|
||||
var fulcioDir = Path.Combine(_testRootPath, "fulcio-ordered");
|
||||
Directory.CreateDirectory(fulcioDir);
|
||||
|
||||
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);
|
||||
|
||||
var options = CreateOptions(fulcioPath: fulcioDir);
|
||||
var store = CreateStore(options);
|
||||
|
||||
// Act
|
||||
var roots = await store.GetFulcioRootsAsync();
|
||||
|
||||
// Assert
|
||||
roots.Should().HaveCount(2);
|
||||
roots[0].Subject.Should().Be("CN=Root A");
|
||||
roots[1].Subject.Should().Be("CN=Root B");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetFulcioRootsAsync_CachesCertificates_OnSecondCall()
|
||||
@@ -328,6 +354,33 @@ public class FileSystemRootStoreTests : IDisposable
|
||||
roots[0].Subject.Should().Be("CN=Offline Kit Root");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetFulcioRootsAsync_WithOfflineKitPath_Disabled_DoesNotLoad()
|
||||
{
|
||||
// Arrange
|
||||
var offlineKitPath = Path.Combine(_testRootPath, "offline-kit-disabled");
|
||||
var fulcioKitDir = Path.Combine(offlineKitPath, "roots", "fulcio");
|
||||
Directory.CreateDirectory(fulcioKitDir);
|
||||
|
||||
var cert = CreateTestCertificate("CN=Offline Kit Root");
|
||||
await WritePemFileAsync(Path.Combine(fulcioKitDir, "root.pem"), cert);
|
||||
|
||||
var options = Options.Create(new OfflineRootStoreOptions
|
||||
{
|
||||
BaseRootPath = _testRootPath,
|
||||
OfflineKitPath = offlineKitPath,
|
||||
UseOfflineKit = false
|
||||
});
|
||||
var store = CreateStore(options);
|
||||
|
||||
// Act
|
||||
var roots = await store.GetFulcioRootsAsync();
|
||||
|
||||
// Assert
|
||||
roots.Should().BeEmpty();
|
||||
}
|
||||
|
||||
private FileSystemRootStore CreateStore(IOptions<OfflineRootStoreOptions> options)
|
||||
{
|
||||
return new FileSystemRootStore(_loggerMock.Object, options);
|
||||
|
||||
@@ -5,17 +5,21 @@
|
||||
// Description: Unit tests for OfflineVerifier service
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moq;
|
||||
using StellaOps.Attestor.Bundling.Abstractions;
|
||||
using StellaOps.Attestor.Bundling.Models;
|
||||
using StellaOps.Attestor.Envelope;
|
||||
using StellaOps.Attestor.Offline.Abstractions;
|
||||
using StellaOps.Attestor.Offline.Models;
|
||||
using StellaOps.Attestor.Offline.Services;
|
||||
using StellaOps.Attestor.ProofChain.Merkle;
|
||||
using BundlingEnvelopeSignature = StellaOps.Attestor.Bundling.Models.EnvelopeSignature;
|
||||
|
||||
// Alias to resolve ambiguity
|
||||
using Severity = StellaOps.Attestor.Offline.Models.VerificationIssueSeverity;
|
||||
@@ -25,6 +29,7 @@ namespace StellaOps.Attestor.Offline.Tests;
|
||||
|
||||
public class OfflineVerifierTests
|
||||
{
|
||||
private static readonly DateTimeOffset FixedNow = new(2025, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
||||
private readonly Mock<IOfflineRootStore> _rootStoreMock;
|
||||
private readonly IMerkleTreeBuilder _merkleBuilder;
|
||||
private readonly Mock<IOrgKeySigner> _orgSignerMock;
|
||||
@@ -137,7 +142,7 @@ public class OfflineVerifierTests
|
||||
KeyId = "org-key-2025",
|
||||
Algorithm = "ECDSA_P256",
|
||||
Signature = Convert.ToBase64String(new byte[64]),
|
||||
SignedAt = DateTimeOffset.UtcNow,
|
||||
SignedAt = FixedNow,
|
||||
CertificateChain = null
|
||||
};
|
||||
|
||||
@@ -197,7 +202,7 @@ public class OfflineVerifierTests
|
||||
{
|
||||
Envelope = attestation.Envelope with
|
||||
{
|
||||
Signatures = new List<EnvelopeSignature>()
|
||||
Signatures = new List<BundlingEnvelopeSignature>()
|
||||
}
|
||||
};
|
||||
|
||||
@@ -255,7 +260,7 @@ public class OfflineVerifierTests
|
||||
Origin = "rekor.sigstore.dev",
|
||||
Size = 100000,
|
||||
RootHash = Convert.ToBase64String(new byte[32]),
|
||||
Timestamp = DateTimeOffset.UtcNow
|
||||
Timestamp = FixedNow
|
||||
},
|
||||
Path = new List<string>() // Empty path triggers warning
|
||||
}
|
||||
@@ -278,6 +283,85 @@ public class OfflineVerifierTests
|
||||
result.Issues.Should().Contain(i => i.Severity == Severity.Warning);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyBundleAsync_UsesConfigDefaults_WhenOptionsNull()
|
||||
{
|
||||
// Arrange
|
||||
var config = Options.Create(new OfflineVerificationConfig
|
||||
{
|
||||
RequireOrgSignatureDefault = true
|
||||
});
|
||||
var bundle = CreateTestBundle(1);
|
||||
var verifier = CreateVerifier(config);
|
||||
|
||||
// Act
|
||||
var result = await verifier.VerifyBundleAsync(bundle, options: null);
|
||||
|
||||
// Assert
|
||||
result.Valid.Should().BeFalse();
|
||||
result.Issues.Should().Contain(i => i.Code == "ORG_SIG_MISSING");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyAttestationAsync_UnbundledDisabled_ReturnsInvalid()
|
||||
{
|
||||
// Arrange
|
||||
var config = Options.Create(new OfflineVerificationConfig
|
||||
{
|
||||
AllowUnbundled = false
|
||||
});
|
||||
var attestation = CreateTestAttestation("entry-001");
|
||||
var verifier = CreateVerifier(config);
|
||||
|
||||
// Act
|
||||
var result = await verifier.VerifyAttestationAsync(attestation, options: null);
|
||||
|
||||
// Assert
|
||||
result.Valid.Should().BeFalse();
|
||||
result.Issues.Should().Contain(i => i.Code == "UNBUNDLED_NOT_ALLOWED");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyByArtifactAsync_BundleTooLarge_ReturnsInvalid()
|
||||
{
|
||||
// Arrange
|
||||
var tempPath = Path.Combine(Path.GetTempPath(), $"bundle-{Guid.NewGuid():N}.json");
|
||||
try
|
||||
{
|
||||
await File.WriteAllBytesAsync(tempPath, new byte[2 * 1024 * 1024]);
|
||||
|
||||
var config = Options.Create(new OfflineVerificationConfig
|
||||
{
|
||||
MaxCacheSizeMb = 1
|
||||
});
|
||||
var verifier = CreateVerifier(config);
|
||||
|
||||
// Act
|
||||
var result = await verifier.VerifyByArtifactAsync(
|
||||
"sha256:deadbeef",
|
||||
tempPath,
|
||||
new OfflineVerificationOptions(
|
||||
VerifyMerkleProof: false,
|
||||
VerifySignatures: false,
|
||||
VerifyCertificateChain: false,
|
||||
VerifyOrgSignature: false));
|
||||
|
||||
// Assert
|
||||
result.Valid.Should().BeFalse();
|
||||
result.Issues.Should().Contain(i => i.Code == "BUNDLE_TOO_LARGE");
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(tempPath))
|
||||
{
|
||||
File.Delete(tempPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyBundleAsync_DeterministicOrdering_SameMerkleValidation()
|
||||
@@ -306,16 +390,6 @@ public class OfflineVerifierTests
|
||||
result1.MerkleProofValid.Should().Be(result2.MerkleProofValid);
|
||||
}
|
||||
|
||||
private OfflineVerifier CreateVerifier()
|
||||
{
|
||||
return new OfflineVerifier(
|
||||
_rootStoreMock.Object,
|
||||
_merkleBuilder,
|
||||
_loggerMock.Object,
|
||||
_config,
|
||||
_orgSignerMock.Object);
|
||||
}
|
||||
|
||||
private AttestationBundle CreateTestBundle(int attestationCount)
|
||||
{
|
||||
var attestations = Enumerable.Range(0, attestationCount)
|
||||
@@ -346,9 +420,9 @@ public class OfflineVerifierTests
|
||||
{
|
||||
BundleId = merkleRootHex,
|
||||
Version = "1.0",
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
PeriodStart = DateTimeOffset.UtcNow.AddDays(-30),
|
||||
PeriodEnd = DateTimeOffset.UtcNow,
|
||||
CreatedAt = FixedNow,
|
||||
PeriodStart = FixedNow.AddDays(-30),
|
||||
PeriodEnd = FixedNow,
|
||||
AttestationCount = attestations.Length
|
||||
},
|
||||
Attestations = attestations,
|
||||
@@ -363,14 +437,28 @@ public class OfflineVerifierTests
|
||||
|
||||
private static BundledAttestation CreateTestAttestation(string entryId)
|
||||
{
|
||||
var payloadType = "application/vnd.in-toto+json";
|
||||
var payloadBytes = "{\"test\":true}"u8.ToArray();
|
||||
var payloadBase64 = Convert.ToBase64String(payloadBytes);
|
||||
|
||||
var (cert, key) = CreateTestKeyMaterial();
|
||||
var signatureService = new EnvelopeSignatureService();
|
||||
var signatureResult = signatureService.SignDsse(payloadType, payloadBytes, key);
|
||||
if (!signatureResult.IsSuccess)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to sign DSSE payload: {signatureResult.Error.Code}");
|
||||
}
|
||||
|
||||
var envelopeSignature = signatureResult.Value;
|
||||
|
||||
return new BundledAttestation
|
||||
{
|
||||
EntryId = entryId,
|
||||
RekorUuid = Guid.NewGuid().ToString("N"),
|
||||
RekorUuid = entryId,
|
||||
RekorLogIndex = 10000,
|
||||
ArtifactDigest = $"sha256:{entryId.PadRight(64, 'a')}",
|
||||
PredicateType = "verdict.stella/v1",
|
||||
SignedAt = DateTimeOffset.UtcNow,
|
||||
SignedAt = FixedNow,
|
||||
SigningMode = "keyless",
|
||||
SigningIdentity = new SigningIdentity
|
||||
{
|
||||
@@ -385,7 +473,7 @@ public class OfflineVerifierTests
|
||||
Origin = "rekor.sigstore.dev",
|
||||
Size = 100000,
|
||||
RootHash = Convert.ToBase64String(new byte[32]),
|
||||
Timestamp = DateTimeOffset.UtcNow
|
||||
Timestamp = FixedNow
|
||||
},
|
||||
Path = new List<string>
|
||||
{
|
||||
@@ -395,17 +483,53 @@ public class OfflineVerifierTests
|
||||
},
|
||||
Envelope = new DsseEnvelopeData
|
||||
{
|
||||
PayloadType = "application/vnd.in-toto+json",
|
||||
Payload = Convert.ToBase64String("{\"test\":true}"u8.ToArray()),
|
||||
Signatures = new List<EnvelopeSignature>
|
||||
PayloadType = payloadType,
|
||||
Payload = payloadBase64,
|
||||
Signatures = new List<BundlingEnvelopeSignature>
|
||||
{
|
||||
new() { KeyId = "key-1", Sig = Convert.ToBase64String(new byte[64]) }
|
||||
new()
|
||||
{
|
||||
KeyId = envelopeSignature.KeyId,
|
||||
Sig = Convert.ToBase64String(envelopeSignature.Value.ToArray())
|
||||
}
|
||||
},
|
||||
CertificateChain = new List<string>
|
||||
{
|
||||
"-----BEGIN CERTIFICATE-----\nMIIB...\n-----END CERTIFICATE-----"
|
||||
ToPem(cert)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private OfflineVerifier CreateVerifier(
|
||||
IOptions<OfflineVerificationConfig>? config = null,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
return new OfflineVerifier(
|
||||
_rootStoreMock.Object,
|
||||
_merkleBuilder,
|
||||
_loggerMock.Object,
|
||||
config ?? _config,
|
||||
_orgSignerMock.Object,
|
||||
timeProvider);
|
||||
}
|
||||
|
||||
private static (X509Certificate2 Cert, EnvelopeKey Key) CreateTestKeyMaterial()
|
||||
{
|
||||
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
|
||||
var request = new CertificateRequest("CN=Test Fulcio Root", ecdsa, HashAlgorithmName.SHA256);
|
||||
var cert = request.CreateSelfSigned(FixedNow.AddDays(-1), FixedNow.AddYears(1));
|
||||
var key = EnvelopeKey.CreateEcdsaSigner("ES256", ecdsa.ExportParameters(true));
|
||||
return (cert, key);
|
||||
}
|
||||
|
||||
private static string ToPem(X509Certificate2 cert)
|
||||
{
|
||||
var base64 = Convert.ToBase64String(cert.Export(X509ContentType.Cert), Base64FormattingOptions.InsertLineBreaks);
|
||||
var builder = new StringBuilder();
|
||||
builder.AppendLine("-----BEGIN CERTIFICATE-----");
|
||||
builder.AppendLine(base64);
|
||||
builder.AppendLine("-----END CERTIFICATE-----");
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user