Skip to content

Verify a release — operator runbook

Audience: operator or auditor who just pulled a TLSStress.Art release and wants to verify it came from us, hasn't been tampered with, and matches the SBOM we published.

Outcome: in ~5 minutes you confirm (a) every container image carries a valid Cosign signature linked to this GitHub repo, (b) the SHA-256 of the SBOM matches what the release page advertises, and (c) the SBOM's package set matches what's actually inside the running image.

Prerequisites

  • cosign ≥ 2.0 — install: https://docs.sigstore.dev/cosign/installation
  • syft ≥ 1.0 — install: curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
  • jq — most distros: apt install jq / brew install jq
  • The release tag you're verifying (e.g. v3.7.0)

Step 1 — Pull the image you intend to verify

Pick any image you actually run. Example: the dashboard.

TAG=v3.7.0
IMG=ghcr.io/nollagluiz/web-agent-dashboard:${TAG}
docker pull "${IMG}"

Step 2 — Verify the Cosign signature is from this repo

Cosign keyless OIDC sigs encode the workflow + repo + ref that signed the image. We check the certificate identity matches our release workflow exactly.

cosign verify "${IMG}" \
  --certificate-identity-regexp "^https://github.com/nollagluiz/AI_forSE/\\.github/workflows/release\\.yml@refs/tags/${TAG}\$" \
  --certificate-oidc-issuer "https://token.actions.githubusercontent.com"

Expected: JSON output ending in Verification successful. If you get an error, do not run the image — it may be a typosquat or a tampered re-push.

Common identity-regexp gotchas:

  • The refs/tags/${TAG} part — must match the release tag you pulled. If you pulled :latest substitute the resolved tag from docker inspect.
  • The --certificate-oidc-issuer is always the GitHub Actions issuer for keyless OIDC sigs — never change this.

Step 3 — Verify the GHCR attestation

GitHub Actions native attestation (via actions/attest-build-provenance) gives us a second independent check.

gh attestation verify "oci://${IMG}" \
  --repo nollagluiz/AI_forSE \
  --predicate-type "https://spdx.dev/Document"

Expected: Loaded digest sha256:... for oci://... then Verification successful!. If this fails but Cosign succeeded, the most likely cause is the slim-SBOM attestation hitting the 16 MiB API cap — that's tolerated in the release pipeline.

Step 4 — Pull the full SPDX SBOM from the release page

gh release download "${TAG}" \
  --repo nollagluiz/AI_forSE \
  --pattern "dashboard-sbom.spdx.json"

Or via web: https://github.com/nollagluiz/AI_forSE/releases/tag/v3.7.0Assets section → click the <image>-sbom.spdx.json file.

Step 5 — Verify the SBOM matches the running image

Generate an SBOM yourself from the image you pulled, then diff the package set against ours.

syft "${IMG}" -o spdx-json > /tmp/local-sbom.spdx.json

# Compare the package list (name + version). Output should be empty
# — meaning every package we listed is in the image, and vice versa.
diff \
  <(jq -r '.packages[] | "\(.name)@\(.versionInfo)"' dashboard-sbom.spdx.json | sort -u) \
  <(jq -r '.packages[] | "\(.name)@\(.versionInfo)"' /tmp/local-sbom.spdx.json | sort -u)

Expected: empty diff. A non-empty diff means either we added / removed something between SBOM-generation time and image-build time, or the image you pulled is not the one we shipped. Open a bug report if you see this.

Step 6 — (Optional) Scan the SBOM for known vulnerabilities

Trivy + Grype both accept SPDX SBOMs and tell you which packages have known CVEs.

# Grype
grype sbom:dashboard-sbom.spdx.json -o table

# Trivy
trivy sbom dashboard-sbom.spdx.json

If a CVE appears that you believe doesn't apply to your usage, file a VEX statement following the VEX policy.

Step 7 — (Optional) Verify the release-feed manifest

The release-feed is the signed channel the self-upgrade flow consumes (ADR 0013).

git clone --branch release-feed \
  https://github.com/nollagluiz/AI_forSE.git \
  /tmp/release-feed
cat /tmp/release-feed/v3.7.0/manifest.json | jq .

Each image entry has its digest (immutable) — verify the digest you pulled in Step 1 matches the digest in the manifest:

PULLED_DIGEST=$(docker inspect "${IMG}" --format='{{index .RepoDigests 0}}' | awk -F@ '{print $2}')
MANIFEST_DIGEST=$(jq -r '.images.dashboard.digest' /tmp/release-feed/v3.7.0/manifest.json)
[ "${PULLED_DIGEST}" = "${MANIFEST_DIGEST}" ] && echo "✅ Digest match" || echo "❌ DIGEST MISMATCH"

Step 8 — (Optional) Verify the Tier B obfuscation receipt

Tier B binaries are garble-obfuscated in CI. The release ships a SHA-256 of the tier-policy YAML so an auditor can confirm the build used the same partition policy as the public repo.

# What the release shipped:
gh release download "${TAG}" \
  --repo nollagluiz/AI_forSE \
  --pattern "tier-policy.yaml.sha256"
cat tier-policy.yaml.sha256

# What the current repo says:
sha256sum platform/ztp-prem/tier-policy.yaml

Both values must match.

Troubleshooting

Symptom Likely cause Fix
Cosign verify fails with no matching signatures You pulled :latest but the tag has been moved Re-pull by digest or by explicit version tag
Cosign verify fails with Certificate not found Image was pushed but never signed (impossible for a real release) Stop. Re-pull from a known-good source.
gh attestation verify fails but Cosign succeeded Slim-SBOM attestation hit the 16 MiB API cap Tolerated. Full SBOM on release page is authoritative.
Step 5 diff is non-empty SBOM generation drift between CI and your local Syft version Check Syft versions match. If they do, file a bug.
Step 7 digest mismatch Image tag was re-pushed after release-feed manifest was signed Stop. File a security advisory — this should never happen on a maintained release.

What "✅ Verified" actually proves

After all 8 steps:

  • The image you pulled was built by this GitHub Actions workflow in this repository, signed at tag time by a Sigstore certificate that GitHub vouches for
  • The SBOM the release advertises matches the contents of the image you ran
  • The image digest matches the release-feed manifest the self-upgrade flow consumes
  • The Tier B partition that defined moat-closed code at build time was the same policy file that ships in the public repo

That's the supply-chain audit floor for any TLSStress.Art release. The release-prep checklist in docs/governance/RELEASE_CADENCE.md ensures we produce every one of those artefacts before tagging.


Last verified against shipping code: v3.7.0 (2026-05-12).