- Implement `SbomVexOrderingDeterminismProperties` for testing component list and vulnerability metadata hash consistency. - Create `UnicodeNormalizationDeterminismProperties` to validate NFC normalization and Unicode string handling. - Add project file for `StellaOps.Testing.Determinism.Properties` with necessary dependencies. - Introduce CI/CD template validation tests including YAML syntax checks and documentation content verification. - Create validation script for CI/CD templates ensuring all required files and structures are present.
13 KiB
Sigstore Bundle Format
This document describes the Sigstore Bundle v0.3 format implementation in StellaOps for offline DSSE envelope verification.
Overview
A Sigstore bundle is a self-contained package that includes all verification material needed to verify a DSSE envelope without network access. This enables offline verification scenarios critical for air-gapped environments.
Bundle Structure
{
"mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json",
"verificationMaterial": {
"certificate": { ... },
"tlogEntries": [ ... ],
"timestampVerificationData": { ... }
},
"dsseEnvelope": {
"payloadType": "application/vnd.in-toto+json",
"payload": "<base64-encoded-payload>",
"signatures": [
{
"sig": "<base64-encoded-signature>",
"keyid": "<optional-key-id>"
}
]
}
}
Components
Media Type
- v0.3:
application/vnd.dev.sigstore.bundle.v0.3+json(current) - v0.2:
application/vnd.dev.sigstore.bundle.v0.2+json(legacy)
Verification Material
Contains cryptographic material for verification:
| Field | Type | Description |
|---|---|---|
certificate |
Object | X.509 signing certificate (keyless signing) |
publicKey |
Object | Public key (keyful signing, alternative to certificate) |
tlogEntries |
Array | Transparency log entries from Rekor |
timestampVerificationData |
Object | RFC 3161 timestamps |
Certificate
{
"certificate": {
"rawBytes": "<base64-DER-certificate>"
}
}
Public Key (alternative to certificate)
{
"publicKey": {
"hint": "<optional-key-hint>",
"rawBytes": "<base64-public-key>"
}
}
Transparency Log Entry
{
"logIndex": "12345",
"logId": {
"keyId": "<base64-log-key-id>"
},
"kindVersion": {
"kind": "dsse",
"version": "0.0.1"
},
"integratedTime": "1703500000",
"inclusionPromise": {
"signedEntryTimestamp": "<base64-SET>"
},
"inclusionProof": {
"logIndex": "12345",
"rootHash": "<base64-merkle-root>",
"treeSize": "100000",
"hashes": ["<base64-hash>", ...],
"checkpoint": {
"envelope": "<checkpoint-note>"
}
},
"canonicalizedBody": "<base64-canonical-body>"
}
DSSE Envelope
Standard DSSE envelope format:
{
"payloadType": "application/vnd.in-toto+json",
"payload": "<base64-encoded-attestation>",
"signatures": [
{
"sig": "<base64-signature>",
"keyid": ""
}
]
}
Usage
Building a Bundle
using StellaOps.Attestor.Bundle.Builder;
using StellaOps.Attestor.Bundle.Models;
var bundle = new SigstoreBundleBuilder()
.WithDsseEnvelope(payloadType, payload, signatures)
.WithCertificate(certificateBytes)
.WithRekorEntry(
logIndex: "12345",
logIdKeyId: logKeyId,
integratedTime: "1703500000",
canonicalizedBody: body)
.WithInclusionProof(inclusionProof)
.Build();
// Serialize to JSON
var json = bundle.BuildJson();
File.WriteAllText("attestation.bundle", json);
Deserializing a Bundle
using StellaOps.Attestor.Bundle.Serialization;
var json = File.ReadAllText("attestation.bundle");
var bundle = SigstoreBundleSerializer.Deserialize(json);
// Or with error handling
if (SigstoreBundleSerializer.TryDeserialize(json, out var bundle))
{
// Use bundle
}
Verifying a Bundle
using StellaOps.Attestor.Bundle.Verification;
var verifier = new SigstoreBundleVerifier();
var result = await verifier.VerifyAsync(bundle);
if (result.IsValid)
{
Console.WriteLine("Bundle verified successfully");
Console.WriteLine($"DSSE Signature: {result.Checks.DsseSignature}");
Console.WriteLine($"Certificate Chain: {result.Checks.CertificateChain}");
Console.WriteLine($"Inclusion Proof: {result.Checks.InclusionProof}");
}
else
{
foreach (var error in result.Errors)
{
Console.WriteLine($"Error: {error.Code} - {error.Message}");
}
}
Verification Options
var options = new BundleVerificationOptions
{
VerifyInclusionProof = true,
VerifyTimestamps = false,
VerificationTime = DateTimeOffset.UtcNow,
TrustedRoots = trustedCertificates
};
var result = await verifier.VerifyAsync(bundle, options);
Verification Checks
| Check | Description |
|---|---|
DsseSignature |
Verifies DSSE signature using PAE encoding |
CertificateChain |
Validates certificate validity period |
InclusionProof |
Verifies Merkle inclusion proof (RFC 6962) |
TransparencyLog |
Validates transparency log entry |
Timestamp |
Verifies RFC 3161 timestamps (optional) |
Error Codes
| Code | Description |
|---|---|
InvalidBundleStructure |
Bundle JSON structure is invalid |
MissingDsseEnvelope |
DSSE envelope is required |
DsseSignatureInvalid |
Signature verification failed |
MissingCertificate |
No certificate or public key |
CertificateChainInvalid |
Certificate chain validation failed |
CertificateExpired |
Certificate has expired |
CertificateNotYetValid |
Certificate not yet valid |
InclusionProofInvalid |
Merkle proof verification failed |
RootHashMismatch |
Computed root doesn't match expected |
Cosign Compatibility
Bundles created by StellaOps are compatible with cosign verification:
# Verify bundle with cosign
cosign verify-attestation \
--bundle attestation.bundle \
--certificate-identity="subject@example.com" \
--certificate-oidc-issuer="https://issuer.example.com" \
--type=slsaprovenance \
registry.example.com/image:tag
See Cosign Verification Examples for more details.
Aggregated Attestation Bundle Format
This section describes the StellaOps Attestation Bundle format for aggregating multiple attestations for long-term verification.
Overview
Aggregated attestation bundles collect multiple attestations from a time period into a single verifiable package. This enables:
- Long-term verification of keyless-signed artifacts after certificate expiry
- Organizational endorsement via optional org-key signature
- Offline verification with bundled Rekor inclusion proofs
- Regulatory compliance with audit-ready evidence packages
Bundle Structure
{
"metadata": {
"bundleId": "sha256:abc123...",
"version": "1.0",
"createdAt": "2025-12-26T02:00:00Z",
"periodStart": "2025-12-01T00:00:00Z",
"periodEnd": "2025-12-31T23:59:59Z",
"attestationCount": 1542,
"tenantId": "tenant-1",
"orgKeyFingerprint": "sha256:def456..."
},
"attestations": [
{
"entryId": "uuid-1",
"rekorUuid": "24296fb2...",
"rekorLogIndex": 12345678,
"artifactDigest": "sha256:...",
"predicateType": "verdict.stella/v1",
"signedAt": "2025-12-15T10:30:00Z",
"signingMode": "keyless",
"signingIdentity": {
"issuer": "https://token.actions.githubusercontent.com",
"subject": "repo:org/repo:ref:refs/heads/main",
"san": "https://github.com/org/repo/.github/workflows/release.yml@refs/heads/main"
},
"inclusionProof": {
"checkpoint": {
"origin": "rekor.sigstore.dev - ...",
"size": 12000000,
"rootHash": "base64...",
"timestamp": "2025-12-15T10:30:05Z"
},
"path": ["base64hash1", "base64hash2", ...]
},
"envelope": {
"payloadType": "application/vnd.in-toto+json",
"payload": "base64...",
"signatures": [{"sig": "base64...", "keyid": ""}],
"certificateChain": ["base64cert1", ...]
}
}
],
"merkleTree": {
"algorithm": "SHA256",
"root": "sha256:abc123...",
"leafCount": 1542
},
"orgSignature": {
"keyId": "org-signing-key-2025",
"algorithm": "ECDSA_P256",
"signature": "base64...",
"signedAt": "2025-12-26T02:05:00Z",
"certificateChain": ["base64cert1", ...]
}
}
Components
Metadata
| Field | Type | Description |
|---|---|---|
bundleId |
string | Content-addressed ID: sha256:<merkle_root> |
version |
string | Bundle schema version (currently "1.0") |
createdAt |
ISO 8601 | Bundle creation timestamp (UTC) |
periodStart |
ISO 8601 | Start of attestation collection period |
periodEnd |
ISO 8601 | End of attestation collection period |
attestationCount |
int | Number of attestations in bundle |
tenantId |
string | Optional tenant identifier |
orgKeyFingerprint |
string | Fingerprint of org signing key (if signed) |
Attestations
Each attestation entry contains:
| Field | Type | Description |
|---|---|---|
entryId |
string | Unique entry identifier |
rekorUuid |
string | Rekor transparency log UUID |
rekorLogIndex |
long | Rekor log index |
artifactDigest |
string | SHA256 digest of attested artifact |
predicateType |
string | In-toto predicate type |
signedAt |
ISO 8601 | When attestation was signed |
signingMode |
string | keyless, kms, hsm, or fido2 |
signingIdentity |
object | Signer identity information |
inclusionProof |
object | Rekor Merkle inclusion proof |
envelope |
object | DSSE envelope with signatures and certificates |
Merkle Tree
Deterministic Merkle tree over attestation hashes:
| Field | Type | Description |
|---|---|---|
algorithm |
string | Hash algorithm (always "SHA256") |
root |
string | Merkle root: sha256:<64-hex> |
leafCount |
int | Number of leaves (= attestation count) |
Org Signature
Optional organizational endorsement:
| Field | Type | Description |
|---|---|---|
keyId |
string | Signing key identifier |
algorithm |
string | ECDSA_P256, Ed25519, or RSA_PSS_SHA256 |
signature |
string | Base64-encoded signature |
signedAt |
ISO 8601 | Signature timestamp |
certificateChain |
array | PEM-encoded certificate chain |
Determinism
Bundles are deterministic - same attestations produce same bundle:
- Attestation ordering: Sorted by
entryIdlexicographically - Merkle tree: Leaves computed as
SHA256(canonicalized_attestation_json) - Bundle ID: Derived from Merkle root:
sha256:<merkle_root> - JSON serialization: Canonical ordering (sorted keys, no whitespace)
Verification
Full Bundle Verification
using StellaOps.Attestor.Bundling.Verification;
var verifier = new AttestationBundleVerifier();
var result = await verifier.VerifyAsync(bundle);
if (result.Valid)
{
Console.WriteLine($"Merkle root verified: {result.MerkleRootVerified}");
Console.WriteLine($"Org signature verified: {result.OrgSignatureVerified}");
Console.WriteLine($"Attestations verified: {result.AttestationsVerified}");
}
Individual Attestation Verification
// Extract single attestation for verification
var attestation = bundle.Attestations.First(a => a.ArtifactDigest == targetDigest);
// Verify inclusion proof against Rekor
var proofValid = await RekorVerifier.VerifyInclusionAsync(
attestation.RekorLogIndex,
attestation.InclusionProof);
// Verify DSSE envelope signature
var sigValid = await DsseVerifier.VerifyAsync(
attestation.Envelope,
attestation.SigningIdentity);
Storage
S3/Object Storage
attestor:
bundling:
storage:
backend: s3
s3:
bucket: stellaops-attestor
prefix: bundles/
objectLock: governance # WORM protection
storageClass: STANDARD
Filesystem
attestor:
bundling:
storage:
backend: filesystem
filesystem:
path: /var/lib/stellaops/attestor/bundles
directoryPermissions: "0750"
filePermissions: "0640"
Retention
Bundles follow configurable retention policies:
| Setting | Default | Description |
|---|---|---|
defaultMonths |
24 | Standard retention period |
minimumMonths |
6 | Cannot be reduced below this |
maximumMonths |
120 | Maximum allowed retention |
Tenant Overrides
attestor:
bundling:
retention:
defaultMonths: 24
tenantOverrides:
tenant-gov: 84 # 7 years
tenant-finance: 120 # 10 years
Export Formats
JSON (Default)
Human-readable, suitable for debugging and audit:
stella attestor bundle export --format json bundle-sha256-abc.json
CBOR
Compact binary format (~40% smaller):
stella attestor bundle export --format cbor bundle-sha256-abc.cbor
Compression
Both formats support compression:
attestor:
bundling:
export:
compression: zstd # none | gzip | zstd
compressionLevel: 3