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
