Signed SBOM Archive Specification
Version: 1.0.0 Status: Draft Last Updated: 2026-04-29
Overview
This specification defines a self-contained, cryptographically signed SBOM archive format that bundles:
- The SBOM document (SPDX or CycloneDX)
- DSSE signature envelope
- Verification materials (certificates, transparency proofs)
- Metadata (tool versions, timestamps)
- Offline verification resources
Archive Structure
signed-sbom-{digest_short}-{timestamp}.tar.gz
|
+-- sbom.spdx.json # OR sbom.cdx.json (CycloneDX)
+-- sbom.dsse.json # DSSE envelope containing signature
+-- manifest.json # Archive inventory with hashes
+-- metadata.json # Generation metadata
+-- certs/
| +-- signing-cert.pem # Signing certificate
| +-- signing-chain.pem # Full certificate chain
| +-- fulcio-root.pem # Fulcio root CA (for keyless)
| +-- revocation-bundle.json # Optional local certificate revocation snapshot
+-- rekor-proof/ # Optional: transparency log proof
| +-- inclusion-proof.json
| +-- checkpoint.sig
| +-- rekor-public.pem
| +-- rekor-tile-snapshot.json # Optional local tile/root consistency snapshot
| +-- rekor-sparse-proof.json # Optional bounded sparse proof for large logs
+-- schemas/ # Bundled validation schemas
| +-- README.md
| +-- manifest.v1.schema.json
| +-- metadata.v1.schema.json
+-- VERIFY.md # Human-readable verification guide
File Specifications
sbom.spdx.json / sbom.cdx.json
The primary SBOM document in either:
- SPDX: Versions 2.3 or 3.0.1 (JSON format)
- CycloneDX: Versions 1.4, 1.5, 1.6, or 1.7 (JSON format)
Requirements:
- UTF-8 encoding without BOM
- Canonical JSON formatting (RFC 8785 compliant)
- No trailing whitespace or newlines
sbom.dsse.json
DSSE envelope containing the SBOM signature:
{
"payloadType": "application/vnd.stellaops.sbom+json",
"payload": "<base64-encoded-sbom>",
"signatures": [
{
"keyid": "SHA256:abc123...",
"sig": "<base64-encoded-signature>"
}
]
}
manifest.json
Archive inventory with integrity hashes:
{
"schemaVersion": "1.0.0",
"archiveId": "signed-sbom-abc123-20260115T123456Z",
"generatedAt": "2026-01-15T12:34:56Z",
"files": [
{
"path": "sbom.spdx.json",
"sha256": "abc123...",
"size": 45678,
"mediaType": "application/spdx+json"
},
{
"path": "sbom.dsse.json",
"sha256": "def456...",
"size": 1234,
"mediaType": "application/vnd.dsse+json"
}
],
"merkleRoot": "sha256:789abc...",
"totalFiles": 12,
"totalSize": 98765
}
metadata.json
Generation and tool metadata:
{
"schemaVersion": "1.0.0",
"stellaOps": {
"suiteVersion": "1.0.0-alpha1",
"scannerVersion": "1.0.0-alpha1",
"scannerDigest": "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
},
"generation": {
"timestamp": "2026-01-15T12:34:56Z",
"hlcTimestamp": "1737000000000000000",
"operator": "build@company.com"
},
"input": {
"imageRef": "registry.company.com/app:v1.0.0",
"imageDigest": "sha256:image-digest-here",
"platform": "linux/amd64"
},
"sbom": {
"format": "spdx-2.3",
"componentCount": 142,
"packageCount": 89,
"fileCount": 1247
},
"signature": {
"type": "keyless",
"issuer": "https://accounts.google.com",
"subject": "build@company.com",
"signedAt": "2026-01-15T12:34:57Z"
},
"reproducibility": {
"deterministic": true,
"expectedDigest": "sha256:expected-sbom-digest"
}
}
VERIFY.md
Human-readable verification instructions:
# SBOM Archive Verification
## Quick Verification
```bash
# Verify archive integrity
sha256sum -c <<EOF
abc123... sbom.spdx.json
def456... sbom.dsse.json
EOF
# Verify signature using cosign
cosign verify-blob \
--signature sbom.dsse.json \
--certificate certs/signing-cert.pem \
--certificate-chain certs/signing-chain.pem \
sbom.spdx.json
Offline Verification
# Using bundled Fulcio root
cosign verify-blob \
--signature sbom.dsse.json \
--certificate certs/signing-cert.pem \
--certificate-chain certs/signing-chain.pem \
--certificate-oidc-issuer https://accounts.google.com \
--offline \
sbom.spdx.json
Rekor Inclusion Proof
# Verify transparency log inclusion
rekor-cli verify \
--artifact sbom.spdx.json \
--signature sbom.dsse.json \
--public-key certs/signing-cert.pem \
--rekor-server https://rekor.sigstore.dev
## Cryptographic Requirements
### Current implementation boundary
Scanner only emits or tests a positive signed SBOM archive when the DSSE payload
exactly matches the SBOM bytes and at least one DSSE signature verifies against
the bundled signing certificate/public key. Signed-looking JSON, JWS-looking
tokens, placeholder certificates, and payload/signature mismatches are invalid.
Scanner.WebService resolves export material from durable table
`scanner.scan_signed_sbom_materials` by `(scan_id, sbom_sha256, sbom_format)`.
The lookup digest is computed from the SBOM bytes that will be packaged. Missing
material returns `409 signed_material_unavailable`; persisted material that does
not verify locally returns `422 signed_material_invalid`. The endpoint does not
generate placeholder DSSE envelopes, certificates, Rekor receipts, checkpoints,
or tile material during export.
For `SignatureType=keyless` or any bundled Fulcio material, Scanner validates
the signing certificate chain before packaging the archive. The request must
include local Fulcio root certificate material; any signing chain PEM must parse
as X.509 certificates; the chain must terminate at the configured local Fulcio
root; the signing leaf must not be a CA and must carry `digitalSignature`; and
the Fulcio OIDC issuer extension must match `metadata.signature.issuer` when an
issuer is supplied. Missing roots, malformed or broken chains, untrusted roots,
issuer mismatch, and issuer strings without cryptographic chain validation fail
closed.
When `IncludeRekorProof=true`, Scanner verifies the bundled Rekor receipt with
the local Rekor verifier before emitting a positive signed SBOM archive. Scanner
also performs the same verification whenever any Rekor receipt/checkpoint/key
field, tile snapshot field, sparse proof field, or tile/sparse consistency flag
is supplied, even if `IncludeRekorProof=false`. `IncludeRekorProof=false` is a
packaging choice only: it may omit `rekor-proof/**` files from the emitted
archive, but it cannot be used to store or export unverified transparency
material. The receipt must bind to the DSSE envelope bytes, the local Rekor
public key must verify the signed checkpoint note, and the RFC 6962 Merkle path
must recompute the checkpoint root. A UUID, log index, Fulcio issuer string, JWS
header, public key, or unsigned/signed-note-shaped checkpoint is not by itself
transparency verification. Archives emitted without bundled Rekor proof material
do not expose a Rekor log-index claim.
When `CheckRekorTileConsistency=true`, or when `rekor-tile-snapshot.json`
material is supplied, Scanner also validates the local tile snapshot through
Attestor Core's strict `stellaops.rekor.tile-snapshot.v1` verifier. The snapshot
must include contiguous full levels from leaf hashes to the root, its derived
root must match the signed checkpoint, and the snapshot-derived inclusion path
must match the Rekor receipt. Missing, malformed, metadata-only, root-mismatched,
or receipt-path-mismatched tile material fails closed. When no tile snapshot is
supplied and tile consistency is not required, the archive can still be
transparency-positive for signed-checkpoint inclusion only.
Scanner resolves tile snapshot material before calling the Attestor-backed tile
verifier and before adding `rekor-proof/rekor-tile-snapshot.json` to the archive.
Resolution order is:
1. `RekorTileSnapshotBytes` supplied on the signed-SBOM archive request.
2. `RekorTileSnapshotUri` supplied on the request. Absolute `file:`, `http:`, and
`https:` URIs are supported; relative URIs require
`RekorTileSnapshotRootDirectory` and are resolved under that root.
3. Offline Rekor log metadata supplied through builder-level
`RekorLogMetadataBytes` or a local `RekorLogMetadataUri`. Metadata-derived
locations are resolved only as local file paths under
`RekorTileSnapshotRootDirectory`; metadata-derived HTTP(S) URLs fail closed.
4. `RekorTileSnapshotRootDirectory` alone, using the default file name
`rekor-tile-snapshot.json` under that root.
Supplying any tile snapshot bytes, URI, root, or log metadata makes the material
required. If `CheckRekorTileConsistency=true`, material is required even when no
URI/root/metadata is configured. Empty, missing, oversized, unsupported-scheme,
root-escaping, unmatched metadata, or malformed material fails closed and the
archive is not emitted. The default resolver bounds snapshot and metadata reads
to 1 MiB and bounds explicit HTTP(S) snapshot retrieval by a 10-second timeout.
HTTP(S) retrieval remains an explicit caller-configured snapshot URI operation;
Scanner does not derive live Rekor tile URLs from receipts, checkpoints, UUIDs,
or log indexes.
When `CheckRekorSparseConsistency=true`, or when `rekor-sparse-proof.json`
material is supplied, Scanner validates bounded sparse proof nodes through
Attestor Core's strict `stellaops.rekor.sparse-tile-proof.v1` verifier. The
sparse proof origin, tree size, root hash, log index, payload-bound leaf hash,
expected audit-path coordinates, and every receipt path hash must agree with the
same receipt and signed checkpoint. Missing required sparse material,
metadata-only sparse material, malformed hashes, missing/reordered proof nodes,
unexpected coordinates, or a recomputed-root mismatch fails closed. Verified
sparse proof bytes are bundled at `rekor-proof/rekor-sparse-proof.json` and
persisted with the signed-SBOM material for later re-verification.
Offline log metadata uses this fail-closed schema:
```json
{
"schemaVersion": "stellaops.rekor.tile-resolver.v1",
"logs": [
{
"origin": "rekor.sigstore.dev - local mirror",
"treeSize": 1234,
"rootHash": "lowercase-hex-sha256-root",
"tileSnapshotPathTemplate": "mirrors/{originHash}/{treeSize}/{logIndex}/rekor-tile-snapshot.json"
}
]
}
stellaOps.scannerDigest is optional and is emitted only when the Scanner WebService runtime is configured with a real image digest through scanner:archiveMetadata:scannerImageDigest (environment form for the prefixed WebService host: SCANNER_SCANNER__ARCHIVEMETADATA__SCANNERIMAGEDIGEST). The configured value must be sha256:<64 lowercase-or-uppercase hex characters>; startup validation normalizes it to lowercase. If the deployment pipeline cannot provide the Scanner runtime image digest, the field is omitted rather than filled with an unknown, placeholder, or synthetic digest.
suiteVersion, scannerVersion, signerVersion, and sbomServiceVersion may also be supplied through scanner:archiveMetadata:*. When suite or scanner version is not configured, Scanner uses the WebService assembly informational version. Unconfigured signer and SBOM-service versions are omitted.
origin must match the signed checkpoint origin. Optional treeSize and rootHash entries, when present, must match the signed checkpoint. Supported template tokens are {originHash}, {treeSize}, {rootHash}, and {logIndex}. The derived location must stay under RekorTileSnapshotRootDirectory and must resolve to a local file. Public producer endpoint and durable-store propagation for these metadata fields is not part of this slice; producers still use the existing explicit tile fields until that wiring is added.
When certificate revocation checking is requested, Scanner requires local revocation bundle JSON bytes and validates them only after DSSE signature verification and Fulcio chain validation have succeeded. The bundle reuses the VexLens local revocation schema (schemaVersion: stellaops.vexlens.revocation.v1) for revokedCertificates entries: certificateSha256, certificateThumbprintSha1, or issuer plus serialNumber may identify the revoked leaf. Optional revokedAt entries are time-scoped; a proof with deterministic signing timestamp material before revokedAt remains valid, while a proof signed at or after revokedAt fails closed. Without signing timestamp material, a matching revocation entry is treated as active.
Missing local revocation bundle material, malformed JSON, unsupported schema versions, invalid revoked-certificate entries, and a matching active revocation entry all fail closed. An empty revocation bundle is only a local “no known revocations in this snapshot” statement for an already verified DSSE/Fulcio path; it is not issuer trust proof.
Scanner does not perform automatic online Rekor lookup, online Fulcio discovery, OCSP, or CRL fetching for archive verification. The only online tile retrieval path is the explicit RekorTileSnapshotUri request field, which is bounded and still feeds the same fail-closed local verifier. Offline metadata can derive local paths only. Remaining gaps for this slice are live Rekor tile URL derivation, sparse proof URI/log-metadata derivation, public producer/store wiring for metadata fields, and online Rekor consistency/tile fetching.
Cross-module fixture coverage now combines Scanner-shaped DSSE/Fulcio/Rekor material with VexLens compact JWS/JWKS/revocation material without conflating proof shapes. The fixture verifies Scanner material through the local DSSE trust-root path and VexLens material through the compact JWS JWKS/revocation path, then asserts each verifier rejects the other proof shape. VexLens has a separate local compact JWS JWKS/kid verifier for signed VEX inputs; that proof shape is not treated as signed-SBOM archive proof.
When includeSchemas=true, Scanner bundles signed-SBOM archive contract schemas from embedded WebService resources. Current production bundles include schemas/manifest.v1.schema.json for manifest.json and schemas/metadata.v1.schema.json for metadata.json, plus a local README. The bundle is deterministic and does not require network access or later downloads for archive-level validation.
Hash Algorithms
| Purpose | Algorithm | Format |
|---|---|---|
| File hashes | SHA-256 | Lowercase hex |
| Merkle tree | SHA-256 | Lowercase hex with sha256: prefix |
| Certificate fingerprint | SHA-256 | Uppercase hex with colons |
Signature Algorithms
Supported signature algorithms:
- ECDSA-P256: Recommended for keyless (Fulcio)
- ECDSA-P384: High-security environments
- RSA-PSS-4096: Legacy compatibility
- Ed25519: High-performance signing
DSSE Envelope
DSSE (Dead Simple Signing Envelope) per specification:
- PAE (Pre-Authentication Encoding) for signing
- Base64 encoding for payload and signatures
- Multiple signatures supported for threshold signing
Verification Process
Step 1: Archive Integrity
# Verify tar.gz integrity
import tarfile
import hashlib
with tarfile.open("signed-sbom.tar.gz", "r:gz") as tar:
manifest = json.load(tar.extractfile("manifest.json"))
for file_entry in manifest["files"]:
content = tar.extractfile(file_entry["path"]).read()
actual_hash = hashlib.sha256(content).hexdigest()
assert actual_hash == file_entry["sha256"]
Step 2: Signature Verification
# Verify DSSE signature
from sigstore.verify import Verifier
verifier = Verifier.production()
result = verifier.verify(
artifact=sbom_content,
signature=dsse_envelope,
certificate=signing_cert
)
assert result.success
Step 3: Certificate Chain Validation
# Validate certificate chain
from cryptography import x509
chain = load_certificate_chain("certs/signing-chain.pem")
root = load_certificate("certs/fulcio-root.pem")
validate_chain(chain, root)
Step 4: Transparency Log (Optional)
# Verify Rekor inclusion
from rekor_client import verify_inclusion
result = verify_inclusion(
artifact_hash=sbom_hash,
proof=inclusion_proof,
checkpoint=checkpoint
)
assert result.verified
Compatibility
SBOM Formats
| Format | Version | Status |
|---|---|---|
| SPDX | 2.3 | Supported |
| SPDX | 3.0.1 | Supported |
| CycloneDX | 1.4 | Supported |
| CycloneDX | 1.5 | Supported |
| CycloneDX | 1.6 | Supported |
| CycloneDX | 1.7 | Supported |
Compression
| Format | Extension | Status |
|---|---|---|
| gzip | .tar.gz | Default |
| zstd | .tar.zst | Recommended (smaller) |
| none | .tar | Not emitted by the Scanner.WebService export endpoint |
API Endpoint
GET /scans/{scanId}/exports/signed-sbom-archive
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
| format | string | spdx-2.3 | SBOM format |
| compression | string | gzip | Compression type |
| includeRekor | bool | true | Bundle Rekor proof files when verified. This is not a verification bypass; persisted Rekor/tile/sparse material is still re-verified before export. |
| includeSchemas | bool | true | Bundle JSON schemas |
Response Headers:
| Header | Description |
|---|---|
| Content-Type | application/gzip or application/zstd |
| Content-Disposition | attachment; filename=“signed-sbom-{digest}.tar.gz” or signed-sbom-{digest}.tar.zst |
| X-SBOM-Digest | SHA-256 of SBOM content |
| X-Archive-Merkle-Root | Merkle root of archive |
| X-Rekor-Log-Index | Rekor log index (if applicable) |
Status behavior:
| Status | Reason | Meaning |
|---|---|---|
| 200 | n/a | Archive emitted after persisted material verifies locally. |
| 404 | scan_not_found or no_sbom_data | The scan or exportable SBOM bytes are unavailable. |
| 409 | signed_material_unavailable | No persisted signed material matches the scan, SBOM SHA-256, and SBOM format. |
| 422 | signed_material_invalid | Persisted signed material exists but fails local cryptographic verification. |
Producer handoff endpoint
POST /scans/{scanId}/exports/signed-sbom-material
This endpoint is for Scanner signing/attestation producers, not archive consumers. It requires Authority scope scanner.signed-sbom-material.write.
Request JSON:
| Field | Type | Required | Description |
|---|---|---|---|
| format or sbomFormat | string | no | SBOM format to export and bind, default spdx-2.3. |
| profile | string | no | SPDX 3 profile when format=spdx-3.0.1. |
| dsseEnvelopeBytes | base64 bytes | yes | DSSE envelope over Scanner’s exact exported SBOM bytes. |
| signingCertPem | string | yes | Signing certificate/public certificate PEM. |
| signingChainPem | string | no | X.509 intermediate chain PEM. |
| fulcioRootPem | string | no | Local Fulcio root PEM for keyless verification. |
| signatureType | string | no | keyless or key-based; default keyless. |
| signatureIssuer, signatureSubject, signatureSignedAt | string/date | no | Signature metadata stored in archive metadata. |
| rekorInclusionProofBytes, rekorCheckpointBytes, rekorPublicKeyPem, rekorLogIndex | mixed | no | Local Rekor proof material. If any Rekor proof field is supplied, the full local proof is required and verified even when includeRekorProof=false. |
| checkRekorTileConsistency, rekorTileSnapshotBytes, rekorTileSnapshotUri, rekorTileSnapshotRootDirectory | mixed | no | Local tile consistency inputs. Supplying tile material makes local tile verification required even when includeRekorProof=false. |
| checkRekorSparseConsistency, rekorSparseProofBytes | mixed | no | Local sparse proof inputs. Supplying sparse proof bytes makes sparse proof verification required even when includeRekorProof=false. |
| checkCertificateRevocation, certificateRevocationBundleBytes | mixed | no | Local revocation bundle gate. |
| packageCount, fileCount, platform, operator | mixed | no | Archive metadata. |
Scanner does not trust the producer’s digest claim. It exports the current SBOM for the scan and format, computes sbomSha256, validates the submitted material through SignedSbomArchiveBuilder, and only then upserts scanner.scan_signed_sbom_materials keyed by (scan_id, sbom_sha256, sbom_format). The endpoint never creates DSSE envelopes, certificates, Rekor receipts, checkpoints, tile snapshots, or revocation bundles on behalf of the producer. If the producer supplies any Rekor/tile/sparse field or requires tile/sparse consistency, the endpoint forces local Rekor verification regardless of includeRekorProof; unverified material returns 422 signed_material_invalid and is not persisted.
Successful response:
{
"scanId": "scan_...",
"sbomSha256": "...",
"sbomFormat": "spdx-2.3",
"stored": true,
"signatureType": "key-based",
"rekorProofStored": false,
"rekorTileConsistencyRequired": false,
"rekorSparseConsistencyRequired": false,
"certificateRevocationRequired": false
}
rekorTileConsistencyRequired and rekorSparseConsistencyRequired report the effective local verification gates. They are true when the corresponding explicit flag is true or when local tile/sparse material was supplied, not only when the explicit flag was set.
Status behavior:
| Status | Reason | Meaning |
|---|---|---|
| 200 | n/a | Material verified locally and was upserted. |
| 400 | validation | Required request fields are missing or malformed. |
| 404 | scan_not_found or no_sbom_data | The scan or exportable SBOM bytes are unavailable. |
| 422 | signed_material_invalid | Submitted material failed local DSSE/Fulcio/Rekor/tile/sparse/revocation verification and was not persisted. |
Security Considerations
- Determinism: All outputs must be reproducible given same inputs
- Canonicalization: JSON must be RFC 8785 canonical before signing
- Time sources: Use injected TimeProvider, not system clock
- Key material: Never include private keys in archives
- Offline verification: Bundle all necessary verification materials
