# Signed SBOM attestation (release-tools-image) *2026-06-02T13:12:23Z by Showboat 0.6.1* *Captured: 2026-06-02 via Showboat 0.6.1* Proves the OpenSpec change [`signed-sbom-attestation`](../../changes/archive/2026-06-02-signed-sbom-attestation/) — spec [`release-tools-image/spec.md`](./spec.md), requirement **"SBOM attached as registry artifact"**. Tracks issue `forgejo-mcp-3y1`; code landed under `forgejo-mcp-aa6` (commit `b2619fc`). The pipeline command under proof lives in [`.tekton/release-tools/tasks/cosign-attach-sbom.yaml`](../../../.tekton/release-tools/tasks/cosign-attach-sbom.yaml): `syft --output cyclonedx-json` then `cosign attest --predicate --type=cyclonedx --key `. ## Replay setup ```bash # Tooling: cosign, syft, podman, python3 (all real, no project binary needed). # Roundtrip blocks run against an ephemeral ttl.sh registry with a THROWAWAY # cosign key — they prove the *mechanism* the pipeline runs, not the prod key. # State (image ref, keypair, SBOM) persists in the workdir between blocks. export DEMO_WORK="${DEMO_WORK:-/tmp/sbom-demo-work}" # roundtrip blocks export SHOWBOAT_REPO="${SHOWBOAT_REPO:-$(git rev-parse --show-toplevel)}" # scenario-B block ``` > **Why a throwaway key + ttl.sh, not the published image?** No tagged > release-tools image has been published *since* the `attach sbom` → > `attest` migration (aa6) landed, so the registry carries no CycloneDX > attestation to verify yet (see the probe at the end). The roundtrip below > runs the **identical cosign/syft commands** the pipeline runs, so it proves > the contract the spec asserts. First post-aa6 tag will reproduce it against > the real key `cosign-signing-key-images.pub`. ## Scenario: Signed SBOM attestation verifiable alongside the image > **WHEN** a consumer runs `cosign verify-attestation --type cyclonedx --key ` > **THEN** the command succeeds, confirming the attestation signature against the key > **AND** the verified payload carries a CycloneDX 1.x JSON document as its predicate ### Setup: push a small image to an ephemeral registry + mint a throwaway key ```bash set -e podman pull -q alpine:3.20 >/dev/null REF="ttl.sh/fjmcp-sbom-demo-${RANDOM}${RANDOM}:1h" echo "$REF" > ref.txt podman tag alpine:3.20 "$REF" podman push -q "$REF" >/dev/null COSIGN_PASSWORD="" cosign generate-key-pair >/dev/null echo "image ref : $REF" echo "keypair : $(ls cosign.key cosign.pub | tr "\n" " ")" ``` ```output Private key written to cosign.key Public key written to cosign.pub image ref : ttl.sh/fjmcp-sbom-demo-298194484:1h keypair : cosign.key cosign.pub ``` ### Step 1 — generate the CycloneDX SBOM (same as the pipeline `syft` step) ```bash REF=$(cat ref.txt) syft -q "$REF" --output cyclonedx-json=sbom.cdx.json python3 -c "import json;d=json.load(open(\"sbom.cdx.json\"));print(\"bomFormat :\",d[\"bomFormat\"]);print(\"specVersion :\",d[\"specVersion\"]);print(\"components :\",len(d.get(\"components\",[])))" ``` ```output bomFormat : CycloneDX specVersion : 1.6 components : 15 ``` ### Step 2 — bind the SBOM as a **signed** in-toto attestation (`cosign attest --type=cyclonedx`) ```bash REF=$(cat ref.txt) COSIGN_PASSWORD="" cosign attest --predicate=sbom.cdx.json --type=cyclonedx --key=cosign.key --yes "$REF" 2>&1 ``` ```output WARNING: Image reference ttl.sh/fjmcp-sbom-demo-298194484:1h uses a tag, not a digest, to identify the image to sign. This can lead you to sign a different image than the intended one. Please use a digest (example.com/ubuntu@sha256:abc123...) rather than tag (example.com/ubuntu:latest) for the input to cosign. The ability to refer to images by tag will be removed in a future release. Using payload from: sbom.cdx.json Signing artifact... ``` ### Step 3 — consumer verifies the attestation (the spec scenario) ```bash REF=$(cat ref.txt) COSIGN_PASSWORD="" cosign verify-attestation --type cyclonedx --key cosign.pub "$REF" > att.json 2>verify.txt cat verify.txt echo "verify-attestation exit: $?" ``` ```output Verification for ttl.sh/fjmcp-sbom-demo-298194484:1h -- The following checks were performed on each of these signatures: - The cosign claims were validated - Existence of the claims in the transparency log was verified offline - The signatures were verified against the specified public key verify-attestation exit: 0 ``` ### Step 4 — confirm the verified predicate IS a CycloneDX document ```bash python3 -c "import json,base64;e=json.load(open(\"att.json\"));s=json.loads(base64.b64decode(e[\"payload\"]));print(\"predicateType :\",s[\"predicateType\"]);print(\"predicate.bomFormat :\",s[\"predicate\"][\"bomFormat\"]);print(\"predicate.specVersion :\",s[\"predicate\"][\"specVersion\"])" ``` ```output predicateType : https://cyclonedx.org/bom predicate.bomFormat : CycloneDX predicate.specVersion : 1.6 ``` ### Published-image state (current) The image-signing key the spec names — `cosign-signing-key-images.pub` — is published in the GitOps repo (`op1st-pipelines-tokens`), distinct from the artifact-signing key. The currently published `release-tools:latest` predates both the key-split and the `attest` migration: it verifies against the legacy **artifacts** key and still carries only the **old tag-based `.sbom`** referrer, **no CycloneDX attestation**. This block documents that gap — the one piece the spec scenario cannot yet show against prod until the first post-migration tag is cut (then it reproduces against `cosign-signing-key-images.pub`). ```bash BASE=https://codeberg.org/operate-first/op1st-emea-b4mad/raw/branch/main/manifests/applications/op1st-pipelines-tokens curl -sSfL --retry 6 --retry-all-errors --retry-delay 2 -o images.pub "$BASE/cosign-signing-key-images.pub" curl -sSfL --retry 6 --retry-all-errors --retry-delay 2 -o artifacts.pub "$BASE/cosign-signing-key-artifacts.pub" echo "== the image-signing key the spec names is published ==" printf "cosign-signing-key-images.pub: %s bytes (distinct from artifacts key: %s)\n" \ "$(wc -c < images.pub)" "$(cmp -s images.pub artifacts.pub && echo SAME || echo DIFFERENT)" echo echo "== current latest verifies with the legacy artifacts key (signed pre-split) ==" cosign verify --key artifacts.pub codeberg.org/operate-first/release-tools:latest 2>&1 | grep -iE "claims|transparency|verified against" echo echo "== referrers present today (cosign tree): signature + old tag-based .sbom, no cyclonedx attestation ==" cosign tree codeberg.org/operate-first/release-tools:latest 2>&1 | sed "s/sha256-[0-9a-f]\{12\}[0-9a-f]*/sha256-/g" ``` ```output == the image-signing key the spec names is published == cosign-signing-key-images.pub: 178 bytes (distinct from artifacts key: DIFFERENT) == current latest verifies with the legacy artifacts key (signed pre-split) == - The cosign claims were validated - Existence of the claims in the transparency log was verified offline - The signatures were verified against the specified public key == referrers present today (cosign tree): signature + old tag-based .sbom, no cyclonedx attestation == 📦 Supply Chain Security Related artifacts for an image: codeberg.org/operate-first/release-tools:latest └── 🔐 Signatures for an image tag: codeberg.org/operate-first/release-tools:sha256-.sig └── 🍒 sha256:6cad25d7569532c30ea9cd5eda16e44d34329ce64292ffd25ebe8dbc77d6f355 └── 📦 SBOMs for an image tag: codeberg.org/operate-first/release-tools:sha256-.sbom └── 🍒 sha256:ff4be5015dbb4fb6f89dc57ed5218555b84ee0fe4fc1134162e4b0ec92dfe54a ``` ## Scenario: Deprecated unsigned attach path is not used > **WHEN** the publish pipeline binds the SBOM to the published image > **THEN** it SHALL use `cosign attest` (signed) > **AND** it SHALL NOT use `cosign attach sbom` (deprecated, unsigned) ```bash TASK="${SHOWBOAT_REPO:-$(git rev-parse --show-toplevel)}/.tekton/release-tools/tasks/cosign-attach-sbom.yaml" echo "== active command in the Tekton task ==" grep -nE "cosign attest" "$TASK" echo echo "== any ACTIVE (non-comment) use of the deprecated path? ==" if grep -nE "^[[:space:]]*cosign attach sbom" "$TASK"; then echo "FOUND active attach sbom (FAIL)"; else echo "none — only historical references in comments:"; grep -nE "attach sbom" "$TASK" | sed "s/^/ /"; fi ``` ```output == active command in the Tekton task == 6: tekton.dev/displayName: "syft scan + cosign attest (signed CycloneDX SBOM) for release-tools image" 10: manifest as a SIGNED in-toto attestation using `cosign attest --type 46: path. cosign attest pushes the signed SBOM attestation as an OCI 113: # `cosign attest` pushes the signed SBOM attestation as an OCI referrer, 162: cosign attest \ == any ACTIVE (non-comment) use of the deprecated path? == none — only historical references in comments: 15: Migrated from `cosign attach sbom` (forgejo-mcp-aa6): `attach sbom` is 160: # attestation OCI referrer. Replaces deprecated `cosign attach sbom`, ```