forgejo-mcp/openspec/specs/release-tools-image/signed-sbom-attestation.demo.md
2026-06-02 15:16:33 +02:00

188 lines
9 KiB
Markdown

# Signed SBOM attestation (release-tools-image)
*2026-06-02T13:12:23Z by Showboat 0.6.1*
<!-- showboat-id: e2ed831c-a5c6-485c-be1b-f6b795173611 -->
*Captured: 2026-06-02 via Showboat 0.6.1*
<!-- captured-for: PR #193 -->
<!-- captured-at: 2026-06-02 -->
<!-- captured-against: 7b85ba4 (openspec/archive-signed-sbom-attestation) -->
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 <ref> --output cyclonedx-json` then `cosign attest --predicate <sbom> --type=cyclonedx --key <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 <pub> <image-ref>`
> **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-<digest>/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-<digest>.sig
└── 🍒 sha256:6cad25d7569532c30ea9cd5eda16e44d34329ce64292ffd25ebe8dbc77d6f355
└── 📦 SBOMs for an image tag: codeberg.org/operate-first/release-tools:sha256-<digest>.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`,
```