Verify SBOM signatures (cosign)
Every source scan signs its CycloneDX SBOM with cosign and builds an in-toto / SLSA provenance attestation. This page shows a consumer how to download the signing artifacts and verify them outside the portal, and shows an operator how to configure the signing key.
Two readers:
- Verifiers (sections 1–4) — anyone who consumes a TRUSCA SBOM and wants to prove it is intact and was signed by a known deployment. Assumes a shell and the ability to install a CLI binary.
- Operators (section 5) — the person who deploys the portal and owns the signing key. Assumes Linux + Docker Compose proficiency and familiarity with environment variables.
Prerequisites
For verification:
- A TRUSCA account with at least the Developer role on the project's team (the signature endpoints reuse the same access control as the SBOM export — an outsider sees
404). - The project has at least one succeeded scan, and signing was configured on the deployment that ran it (see section 5). A scan that was never signed has no signature artifacts.
- cosign installed on the machine that verifies (see section 2).
1. Why sign an SBOM?
An SBOM tells a consumer what is inside a release. A signature answers two further questions the SBOM alone cannot:
- Integrity — were the SBOM bytes altered after the deployment produced them? A signature over the exact bytes detects any tampering.
- Provenance — how was the SBOM produced, and by whom? The in-toto / SLSA provenance attestation records the build platform identity and version.
This is the supply-chain-security expectation set by Executive Order 14028, the CISA 2025 SBOM minimum elements, and the NTIA minimum elements: a consumer should be able to verify an artifact's authenticity without trusting the channel it arrived over.
Key-based vs keyless
cosign supports two trust models. TRUSCA supports both; key-based is the default for self-hosted, on-prem, and air-gapped deployments.
| Model | How the deployment signs | What the verifier needs | When |
|---|---|---|---|
| Key-based (default) | A cosign key pair; the private key signs, the public key is published | cosign.pub (the public key) | Self-hosted / on-prem / air-gapped — no internet dependency |
| Keyless (opt-in) | A short-lived Fulcio certificate bound to an OIDC identity; the signature is logged in Rekor | the Fulcio certificate plus the expected identity and OIDC issuer | CI-driven deployments with an OIDC provider and outbound access to a Sigstore instance |
The verification command differs between the two — both are covered in section 4.
Signing is best-effort. If the worker has no cosign binary, no key is configured, or cosign fails, the scan still succeeds but the SBOM is left unsigned (a structured warning is logged). An unsigned scan has no signature artifacts, so the download endpoints return 404 — see Troubleshooting.
2. Install cosign
Install cosign on the machine that performs verification. cosign v2.x is recommended (the commands below assume the v2 CLI).
# macOS (Homebrew)
brew install cosign
# Linux (binary)
curl -sSfL -o cosign \
"https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64"
chmod +x cosign && sudo mv cosign /usr/local/bin/
# Verify the install
cosign version
See the cosign installation guide for other platforms.
3. Download the signing artifacts
The signature surface always describes the project's latest succeeded scan — the same scan the SBOM export serves, so the signed bytes and the exported SBOM match exactly.
The bundle (recommended)
The signature bundle is a single zip containing everything needed to verify offline, so prefer it over the individual files. The zip contains:
| File | Always present? | Purpose |
|---|---|---|
sbom-<slug>.cdx.json | yes | the CycloneDX SBOM — the signed bytes |
sbom-<slug>.cdx.json.sig | yes | the detached cosign signature over the SBOM |
cosign.pub | key-based only | the cosign public key (key-based verification) |
sbom-<slug>.cdx.json.cert.pem | keyless only | the Fulcio signing certificate (keyless verification) |
sbom-<slug>.intoto.jsonl | when attestation succeeded | the in-toto / SLSA provenance attestation |
sbom-<slug>.attest.cert.pem | keyless + attestation | the Fulcio certificate for the attestation |
VERIFY.md | yes | verification instructions tailored to what the bundle contains |
<slug> is the project's URL slug; the zip itself is named sbom-signature-<slug>.zip.
Download it from the API (replace the placeholders):
curl -sS -L -OJ \
-H "Authorization: Bearer ${TRUSTEDOSS_API_KEY}" \
"https://<your-domain>/v1/projects/${PROJECT_ID}/sbom/signature-bundle"
-OJ saves the file under the server-supplied name (sbom-signature-<slug>.zip). Substitute <your-domain> with your deployment's host and ${PROJECT_ID} / ${TRUSTEDOSS_API_KEY} with your values.
Each bundle ships a VERIFY.md whose commands name the exact files in that bundle (key-based vs keyless, attestation present or not). When the bundle's contents and this page disagree, trust VERIFY.md — it is generated from the artifacts you actually received.
Individual artifacts
If you only need one file, each is also a discrete endpoint. All require the same Developer access and return the latest succeeded scan's artifact (or 404 if it does not exist).
| Endpoint | Returns | Notes |
|---|---|---|
GET /v1/projects/{project_id}/sbom/signature | sbom-<slug>.cdx.json.sig | the detached signature |
GET /v1/projects/{project_id}/sbom/public-key | cosign.pub | key-based deployments only; 404 when keyless |
GET /v1/projects/{project_id}/sbom/certificate | sbom-<slug>.cdx.json.cert.pem | keyless deployments only; 404 when key-based |
GET /v1/projects/{project_id}/sbom/attestation | sbom-<slug>.intoto.jsonl | the SLSA provenance attestation |
GET /v1/projects/{project_id}/sbom/attestation-certificate | sbom-<slug>.attest.cert.pem | keyless attestation only |
The SBOM itself comes from the existing SBOM export endpoint (GET /v1/projects/{project_id}/sbom?format=cyclonedx-json).
These endpoints serve only public artifacts — the SBOM, the signature, the Fulcio certificate, the attestation, and the cosign public key. The private signing key and its password are never read, returned, or logged by the portal. The public-key endpoint additionally refuses to serve anything that looks like a private key.
4. Verify
Unzip the bundle, then run the command that matches your deployment's signing model.
unzip sbom-signature-<slug>.zip -d sbom-verify
cd sbom-verify
Key-based (default)
cosign verify-blob \
--key cosign.pub \
--signature sbom-<slug>.cdx.json.sig \
sbom-<slug>.cdx.json
A successful run prints:
Verified OK
Keyless (Fulcio)
Keyless verification additionally pins the signer's identity and OIDC issuer — substitute the values your operator published (for example, a CI workflow identity and its issuer URL).
cosign verify-blob \
--certificate sbom-<slug>.cdx.json.cert.pem \
--certificate-identity <expected-identity> \
--certificate-oidc-issuer <expected-issuer> \
--signature sbom-<slug>.cdx.json.sig \
sbom-<slug>.cdx.json
A successful run prints Verified OK. A mismatch — altered SBOM bytes, the wrong key/certificate, or an identity that does not match — exits non-zero with an error such as:
Error: verifying blob: invalid signature when validating ASN.1 encoded signature
Verified OK means the SBOM bytes are intact and were signed by this deployment's signing identity.
Inspect the provenance attestation
When the bundle contains sbom-<slug>.intoto.jsonl, decode its payload to read how the SBOM was produced — the in-toto Statement carries the build platform builder.id / builder.version and the SBOM-generation context:
jq -r '.payload' sbom-<slug>.intoto.jsonl | base64 -d | jq .
To cryptographically verify the attestation (not just decode it):
# Key-based
cosign verify-blob-attestation \
--key cosign.pub \
--bundle sbom-<slug>.intoto.jsonl \
sbom-<slug>.cdx.json
# Keyless
cosign verify-blob-attestation \
--certificate sbom-<slug>.attest.cert.pem \
--certificate-identity <expected-identity> \
--certificate-oidc-issuer <expected-issuer> \
--bundle sbom-<slug>.intoto.jsonl \
sbom-<slug>.cdx.json
Verify it worked
-
cosign verify-blobprintsVerified OKand exits0. -
jqdecodes the attestation payload and thepredicate.builder.idmatches the builder your operator configured (SLSA_BUILDER_ID). -
Re-downloading the SBOM produces a byte-identical file (the export is byte-stable), so the same signature verifies it again:
sha256sum sbom-<slug>.cdx.json# → matches the digest cosign reported during verify-blob
5. Operator key setup
This section is for the person deploying the portal. Pick one model.
Key-based (default)
-
Generate a cosign key pair with the bundled helper. It writes
cosign.key(encrypted private key) andcosign.pub(public key), and prints the.envwiring:bash scripts/cosign-keygen.sh --out ./secrets/cosigncosign prompts for a password to encrypt the private key at rest (or reads
COSIGN_PASSWORDif exported). If cosign is not on your PATH, run the helper inside the worker container, which ships cosign:docker-compose run --rm worker bash scripts/cosign-keygen.sh -
Encrypt the private-key password with the app's Fernet key so it can live in
.envas ciphertext (never plaintext). Run it inside the worker so it uses the same key the app decrypts with at signing time:docker-compose run --rm worker \python -c "import sys;from core.crypto import encrypt_secret;print(encrypt_secret(sys.argv[1]))" 'YOUR_KEY_PASSWORD'A passwordless key is allowed — leave
COSIGN_KEY_PASSWORD_ENCRYPTEDunset. -
Wire the keys into
.env(the worker mountsCOSIGN_KEYS_HOST_PATHread-only at/cosign):COSIGN_KEYLESS=falseCOSIGN_KEY_PATH=/cosign/cosign.keyCOSIGN_KEY_PASSWORD_ENCRYPTED=<paste the ciphertext from step 2>COSIGN_KEYS_HOST_PATH=./secrets/cosign -
Distribute
cosign.pubto your verifiers — publish it next to your releases or hand it out via a trusted channel. Verifiers can also pull it from the public-key endpoint, but an out-of-band copy lets them verify without portal access.
cosign.key is the deployment's signing authority. Keep it out of version control (the bundled .gitignore excludes secrets/), mount it read-only into the worker, and back it up securely. The portal never reads, returns, or logs the private key or its password — and neither should your operational tooling.
Keyless (opt-in)
Keyless signing needs no key pair. cosign drives its own OIDC identity (an ambient CI token or a configured provider) and logs the signature to Rekor. Set:
COSIGN_KEYLESS=true
For a private Sigstore deployment, also set COSIGN_OIDC_ISSUER, SIGSTORE_FULCIO_URL, and SIGSTORE_REKOR_URL in the worker environment. Publish the expected identity and OIDC issuer to your verifiers so they can pass --certificate-identity / --certificate-oidc-issuer to cosign verify-blob.
Provenance builder identity
The attestation stamps a builder identity into the provenance. Set these so a verifier can pin provenance to a known builder:
| Key | Default | Purpose |
|---|---|---|
SLSA_BUILDER_ID | a vendor-neutral URI | URI naming this build platform in the provenance builder.id |
TRUSTEDOSS_VERSION | bundled portal version | stamped into builder.version and the SBOM-generation context |
See Environment variables → cosign signing for the full key list and runtime semantics.
Troubleshooting
404 on a signature endpoint — the scan was not signed
The signature endpoints return 404 when the project's latest succeeded scan has no artifact of that kind. The usual causes:
- Signing was never configured — the deployment ran with no cosign key (
COSIGN_KEY_PATHunset) andCOSIGN_KEYLESS=false. Configure a key (section 5) and re-run the scan. - The worker has no cosign binary — only the worker image ships cosign; a custom worker may have dropped it. Check the worker logs for a
cosign_not_found/ signing warning. - No succeeded scan yet — run a scan and wait for it to reach
Completed.
The 404 body is an RFC 7807 application/problem+json envelope whose detail names the actionable reason.
404 on /public-key but /certificate works (or vice versa)
This is expected and tells you which model the deployment uses:
- Key-based →
/sbom/public-keyreturnscosign.pub;/sbom/certificatereturns404. Verify with--key cosign.pub. - Keyless →
/sbom/certificatereturns the Fulcio cert;/sbom/public-keyreturns404. Verify with--certificate ….
The bundle picks the right file automatically, which is another reason to prefer it.
cosign verify-blob fails with "invalid signature"
The SBOM bytes do not match the signature. Re-download both from the same bundle (do not mix a freshly exported SBOM with an older signature), and confirm you did not re-format or re-save the SBOM (the export is byte-stable; any edit breaks the signature). For keyless, also confirm --certificate-identity / --certificate-oidc-issuer match the values your operator published.
413 on a download
The artifact or bundle exceeds the deployment's configured download size cap. This is a server-side guard, not a client-fixable error — contact the operator.
See also
- SBOM — export the SBOM the signature is over
- Glossary — SBOM, SCA, VEX, and RBAC role definitions
- Environment variables — the
COSIGN_*andSLSA_*keys - API reference (Redoc) — the generated endpoint contract
- Report an issue — if verification fails unexpectedly