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:

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:

Requirements:

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

PurposeAlgorithmFormat
File hashesSHA-256Lowercase hex
Merkle treeSHA-256Lowercase hex with sha256: prefix
Certificate fingerprintSHA-256Uppercase hex with colons

Signature Algorithms

Supported signature algorithms:

DSSE Envelope

DSSE (Dead Simple Signing Envelope) per specification:

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

FormatVersionStatus
SPDX2.3Supported
SPDX3.0.1Supported
CycloneDX1.4Supported
CycloneDX1.5Supported
CycloneDX1.6Supported
CycloneDX1.7Supported

Compression

FormatExtensionStatus
gzip.tar.gzDefault
zstd.tar.zstRecommended (smaller)
none.tarNot emitted by the Scanner.WebService export endpoint

API Endpoint

GET /scans/{scanId}/exports/signed-sbom-archive

Query Parameters:

ParameterTypeDefaultDescription
formatstringspdx-2.3SBOM format
compressionstringgzipCompression type
includeRekorbooltrueBundle Rekor proof files when verified. This is not a verification bypass; persisted Rekor/tile/sparse material is still re-verified before export.
includeSchemasbooltrueBundle JSON schemas

Response Headers:

HeaderDescription
Content-Typeapplication/gzip or application/zstd
Content-Dispositionattachment; filename=“signed-sbom-{digest}.tar.gz” or signed-sbom-{digest}.tar.zst
X-SBOM-DigestSHA-256 of SBOM content
X-Archive-Merkle-RootMerkle root of archive
X-Rekor-Log-IndexRekor log index (if applicable)

Status behavior:

StatusReasonMeaning
200n/aArchive emitted after persisted material verifies locally.
404scan_not_found or no_sbom_dataThe scan or exportable SBOM bytes are unavailable.
409signed_material_unavailableNo persisted signed material matches the scan, SBOM SHA-256, and SBOM format.
422signed_material_invalidPersisted 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:

FieldTypeRequiredDescription
format or sbomFormatstringnoSBOM format to export and bind, default spdx-2.3.
profilestringnoSPDX 3 profile when format=spdx-3.0.1.
dsseEnvelopeBytesbase64 bytesyesDSSE envelope over Scanner’s exact exported SBOM bytes.
signingCertPemstringyesSigning certificate/public certificate PEM.
signingChainPemstringnoX.509 intermediate chain PEM.
fulcioRootPemstringnoLocal Fulcio root PEM for keyless verification.
signatureTypestringnokeyless or key-based; default keyless.
signatureIssuer, signatureSubject, signatureSignedAtstring/datenoSignature metadata stored in archive metadata.
rekorInclusionProofBytes, rekorCheckpointBytes, rekorPublicKeyPem, rekorLogIndexmixednoLocal 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, rekorTileSnapshotRootDirectorymixednoLocal tile consistency inputs. Supplying tile material makes local tile verification required even when includeRekorProof=false.
checkRekorSparseConsistency, rekorSparseProofBytesmixednoLocal sparse proof inputs. Supplying sparse proof bytes makes sparse proof verification required even when includeRekorProof=false.
checkCertificateRevocation, certificateRevocationBundleBytesmixednoLocal revocation bundle gate.
packageCount, fileCount, platform, operatormixednoArchive 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:

StatusReasonMeaning
200n/aMaterial verified locally and was upserted.
400validationRequired request fields are missing or malformed.
404scan_not_found or no_sbom_dataThe scan or exportable SBOM bytes are unavailable.
422signed_material_invalidSubmitted material failed local DSSE/Fulcio/Rekor/tile/sparse/revocation verification and was not persisted.

Security Considerations

  1. Determinism: All outputs must be reproducible given same inputs
  2. Canonicalization: JSON must be RFC 8785 canonical before signing
  3. Time sources: Use injected TimeProvider, not system clock
  4. Key material: Never include private keys in archives
  5. Offline verification: Bundle all necessary verification materials