save progress

This commit is contained in:
StellaOps Bot
2026-01-03 00:47:24 +02:00
parent 3f197814c5
commit ca578801fd
319 changed files with 32478 additions and 2202 deletions

View File

@@ -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);

View File

@@ -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();
}
}