Verify a FosterStack Cache release yourself
- Check our signature, build provenance, SBOM and vulnerability statements with commands you run yourself, using cosign and the gh tool. You can confirm that our release workflow signed a container image and that GitHub's CI built it. No account is needed, except for the gh attestation steps (5 to 7), which need a GitHub login or token.
-
- What you need
- cosign, the gh tool (for the build-provenance steps), and jq. Get them from their own pages: cosign, gh and jq. In our timed run we used cosign 3.1.3, gh 2.102.0 and jq 1.6; we make no claim about other versions. Set the version and your platform once:
- $ VER=0.2.2
-PLATFORM=linux_arm64 # or linux_amd64, darwin_arm64, darwin_amd64
- To check another release, set VER to its number, for example VER=0.2.1. To check the latest release, set VER from GitHub with this command in place of the first line above:
+ You should not have to take our word for what you run. Signed at every stage, verified together at release. These steps check the image signatures, the signed file lists, the digest, how the release was built and its parts list, using only cosign, gh, jq and sha256sum. They are for v0.3.0 and later. For v0.2.x, see the image verification guide.
+ You need no account to pull the images or to run cosign. Downloading release files with gh needs gh auth login; you can also download them from the release page in a browser.
+ Set the version
VER=$(curl -fsSL https://api.github.com/repos/fosterstack/cache/releases/latest \
- | sed -n 's/.*"tag_name": *"v\([^"]*\)".*/\1/p')
-
- 1. Download the release files
- $ gh release download v${VER} --repo fosterstack/cache \
+ | sed -n 's/.*"tag_name": *"v\([^"]*\)".*/\1/p')
+echo "$VER"
+IDENT="https://github.com/fosterstack/cache/.github/workflows"
+ISSUER=https://token.actions.githubusercontent.com
+ There are two images, production and -fips. Run step 1 for each.
+ 1. The signature and who signed it
+ cosign verify ghcr.io/fosterstack/cache:${VER} \
+ --certificate-identity "${IDENT}/stage-promote.yml@refs/tags/v${VER}" \
+ --certificate-oidc-issuer "$ISSUER"
+cosign verify ghcr.io/fosterstack/cache:${VER}-fips \
+ --certificate-identity "${IDENT}/stage-promote.yml@refs/tags/v${VER}" \
+ --certificate-oidc-issuer "$ISSUER"
+ This shows that the release stage of our public pipeline, at this release tag, signed these exact images. The signing certificate lasts minutes and is recorded in Rekor, the public transparency log. There is no signing key to steal.
+ 2. The file lists are ours, and your files match them
+ gh release download v${VER} --repo fosterstack/cache \
-p checksums.txt -p checksums.txt.bundle \
- -p "fscache_${VER}_${PLATFORM}.tar.gz" \
- -p "fscache_${VER}_${PLATFORM}.tar.gz.sbom.json" \
- -p fosterstack-cache.openvex.json -p release-manifest.json
- Downloading from the releases page in a browser works the same.
-
- 2. Is the checksums file ours?
- This checks the signature on checksums.txt. It needs no GitHub account.
- $ cosign verify-blob \
- --bundle checksums.txt.bundle \
- --certificate-identity-regexp='^https://github.com/fosterstack/cache/' \
- --certificate-oidc-issuer='https://token.actions.githubusercontent.com' \
- checksums.txt
-Verified OK
-
- 3. Does your download match it?
- $ sha256sum -c <(grep "fscache_${VER}_${PLATFORM}.tar.gz" checksums.txt | grep -v sbom)
-fscache_0.2.2_linux_arm64.tar.gz: OK
- Steps 2 and 3 together are the important pair. The checksum alone only shows the archive matches a file you downloaded beside it. The signature is what ties that file to us.
-
- 4. Verify a container image signature with cosign
- This answers one question: did our release workflow sign these exact bytes? Pin the workflow and the release tag, not "any workflow in the repository". No account is needed.
- $ cosign verify ghcr.io/fosterstack/cache:${VER} \
- --certificate-identity-regexp="^https://github.com/fosterstack/cache/.github/workflows/stage-promote.yml@refs/tags/v${VER}$" \
- --certificate-oidc-issuer='https://token.actions.githubusercontent.com'
-Verification for ghcr.io/fosterstack/cache:0.2.2 --
-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 code-signing certificate was verified using trusted certificate authority certificates
-
-[{"critical":{"identity":{"docker-reference":"ghcr.io/fosterstack/cache:0.2.2"},"image":{"docker-manifest-digest":"sha256:f2b330cf27b3814405230cc001a771909ae5bbf3b1e223a90ee7a9ee5d0e53dd"},"type":"https://sigstore.dev/cosign/sign/v1"},"optional":{}},{"critical":{"identity":{"docker-reference":"ghcr.io/fosterstack/cache:0.2.2"},"image":{"docker-manifest-digest":"sha256:f2b330cf27b3814405230cc001a771909ae5bbf3b1e223a90ee7a9ee5d0e53dd"},"type":"https://fosterstack.com/attestations/release-manifest/v1"},"optional":{}}]
- There is no signing key to steal. The certificate is short-lived and was issued when the workflow ran, and the signature is in a public transparency log. The same command against docker.io/fosterstack/cache:${VER} returns the same digest, sha256:f2b330cf27b3814405230cc001a771909ae5bbf3b1e223a90ee7a9ee5d0e53dd.
-
- 5. Where was the image built?
- A signature says who signed. Build provenance says what produced the bytes. This step reads GitHub's attestation store, so it needs a token (GH_TOKEN or gh auth login), even though the repository is public.
- $ gh attestation verify oci://ghcr.io/fosterstack/cache:${VER} \
- --repo fosterstack/cache \
- --signer-workflow fosterstack/cache/.github/workflows/stage-image.yml \
- --source-ref "refs/tags/v${VER}" \
- --format json | jq '.[0].verificationResult | {statement: .statement.predicateType, image: .statement.subject[0].digest, signer: .signature.certificate.subjectAlternativeName, run: .signature.certificate.runInvocationURI}'
-{
- "statement": "https://slsa.dev/provenance/v1",
- "image": {
- "sha256": "f2b330cf27b3814405230cc001a771909ae5bbf3b1e223a90ee7a9ee5d0e53dd"
- },
- "signer": "https://github.com/fosterstack/cache/.github/workflows/stage-image.yml@refs/tags/v0.2.2",
- "run": "https://github.com/fosterstack/cache/actions/runs/37957120550/attempts/2"
-}
- The command exits with status 0 on success. The image digest is the one from step 4, and the run link is the public build that produced it. The link opens attempt 2 of the release run. That attempt built and authorized the image. Attempt 3 re-ran only the promotion job, after a registry login was rejected in attempt 2. It published and signed the images, so the public signatures come from attempt 3. The image digests did not change.
-
- 6. Where was your download built?
- $ gh attestation verify "fscache_${VER}_${PLATFORM}.tar.gz" --repo fosterstack/cache \
- --format json | jq -r '.[0].verificationResult.statement.subject[].name, .[0].verificationResult.signature.certificate.subjectAlternativeName'
-fscache-fips_0.2.2_linux_amd64.tar.gz
-fscache-fips_0.2.2_linux_amd64.tar.gz.sbom.json
-fscache-fips_0.2.2_linux_arm64.tar.gz
-fscache-fips_0.2.2_linux_arm64.tar.gz.sbom.json
-fscache_0.2.2_darwin_amd64.tar.gz
-fscache_0.2.2_darwin_amd64.tar.gz.sbom.json
-fscache_0.2.2_darwin_arm64.tar.gz
-fscache_0.2.2_darwin_arm64.tar.gz.sbom.json
-fscache_0.2.2_linux_amd64.tar.gz
-fscache_0.2.2_linux_amd64.tar.gz.sbom.json
-fscache_0.2.2_linux_arm64.tar.gz
-fscache_0.2.2_linux_arm64.tar.gz.sbom.json
-checksums.txt
-https://github.com/fosterstack/cache/.github/workflows/stage-build.yml@refs/tags/v0.2.2
- One statement covers every archive, its SBOM and checksums.txt. Your archive is in the list.
-
- 7. Did the whole release chain pass?
- Each stage of the release signs its own statement about the exact image. One of them exists only if the final authorization stage checked all the others for this image: the source check, the build, the image assembly, the reproducibility check, each scanner's verdict, and the acceptance tests.
- $ gh attestation verify oci://ghcr.io/fosterstack/cache:${VER} \
- --repo fosterstack/cache \
- --signer-workflow fosterstack/cache/.github/workflows/stage-authorize.yml \
- --predicate-type https://fosterstack.com/attestations/release-authorization/v1 \
- --format json | jq '.[0].verificationResult | {predicateType: .statement.predicateType, signer: .signature.certificate.subjectAlternativeName}'
-{
- "predicateType": "https://fosterstack.com/attestations/release-authorization/v1",
- "signer": "https://github.com/fosterstack/cache/.github/workflows/stage-authorize.yml@refs/tags/v0.2.2"
-}
-
- 8. What is inside? The SBOM
- Each binary archive has a software bill of materials in SPDX format.
- $ jq '{spdxVersion, name, packages: (.packages | length)}' "fscache_${VER}_${PLATFORM}.tar.gz.sbom.json"
-{
- "spdxVersion": "SPDX-2.3",
- "name": "fscache_0.2.2_linux_arm64.tar.gz",
- "packages": 13
-}
- $ jq -r '.packages[0:4][] | "\(.name) \(.versionInfo)"' "fscache_${VER}_${PLATFORM}.tar.gz.sbom.json"
-github.com/beorn7/perks v1.0.1
-github.com/cespare/xxhash/v2 v2.3.0
-github.com/fosterstack/cache v0.2.2
-github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822
-
- 9. Which known vulnerabilities do not apply? The VEX file
- The VEX (OpenVEX) file lists vulnerabilities that scanners may report and says why each does not affect this release.
- $ jq -r '.statements[] | "\(.vulnerability.name) \(.status) \(.justification)"' fosterstack-cache.openvex.json
-CVE-2024-51744 not_affected component_not_present
-CVE-2025-60876 not_affected vulnerable_code_not_in_execute_path
-CVE-2025-46394 not_affected vulnerable_code_not_in_execute_path
-CVE-2024-58251 not_affected vulnerable_code_not_in_execute_path
- You do not have to take our word for those. Run your own scanner against the image and give it this file.
-
- 10. The release manifest
- release-manifest.json lists the evidence for the whole release. It names three image variants (the standard one, debug and fips), each with its digest. The standard one is the digest you verified in steps 4 and 5, at both registries:
- $ jq -r '.images[] | "\(.variant) \(.digest)"' release-manifest.json
-production sha256:f2b330cf27b3814405230cc001a771909ae5bbf3b1e223a90ee7a9ee5d0e53dd
-debug sha256:87887f55ffa41cc63d4aa6f6d9459fd0dbf919491b83f1d3c8b7fc5444b5a081
-fips sha256:92fae7f20616e553c5fe24e9b92f186c3d3f28ad89cee5c24864911032e6c05e
- $ jq -c '.images[0].refs' release-manifest.json
-["ghcr.io/fosterstack/cache@sha256:f2b330cf27b3814405230cc001a771909ae5bbf3b1e223a90ee7a9ee5d0e53dd","docker.io/fosterstack/cache@sha256:f2b330cf27b3814405230cc001a771909ae5bbf3b1e223a90ee7a9ee5d0e53dd"]
- $ jq '.verified_statements | length' release-manifest.json
-43
- That last number is the statements the authorization stage checked, each with its entry in the public transparency log.
-
- What these checks do and do not prove
-
- - A valid signature proves who signed these exact bytes.
- - Build provenance proves which repository, workflow and run produced them, which is a public CI run and not a developer machine.
- - Neither says the image was scanned or tested. Those are separate signed statements attached to the same image digest, and you can verify them the same way.
- - The SBOM and VEX files are statements we publish. They are as trustworthy as the signature on the release that carries them.
- - None of this proves the software has no bugs or that no vulnerability exists. It proves the release is the one we built, in the way we describe.
+ -p release-files.sha256 -p release-files.sha256.bundle \
+ -p release-manifest.json -p "fscache_${VER}_linux_amd64.tar.gz"
+for f in checksums.txt release-files.sha256; do
+ cosign verify-blob "$f" --bundle "$f.bundle" \
+ --certificate-identity "${IDENT}/stage-promote.yml@refs/tags/v${VER}" \
+ --certificate-oidc-issuer "$ISSUER"
+done
+sha256sum -c <(grep "fscache_${VER}_linux_amd64.tar.gz" checksums.txt)
+sha256sum -c <(grep release-manifest.json release-files.sha256)
+ Do the signature check first. Without it, a list only proves that your files match a file anyone could have written. The archives are covered by the signed checksums.txt, and the manifest by the signed release-files.sha256.
+ 3. The digest is the one in the release
+
jq -r '.images[] | "\(.variant) \(.digest)"' release-manifest.json
+ Every statement names an image by digest, never by tag. The digest you pulled should be in this list.
+ 4. How it was built
+ gh release download v${VER} --repo fosterstack/cache \
+ -p provenance.json -p provenance.bundle.json
+cosign verify-blob-attestation --bundle provenance.bundle.json \
+ --type https://slsa.dev/provenance/v1 --check-claims=false \
+ --certificate-identity "${IDENT}/stage-sign.yml@refs/tags/v${VER}" \
+ --certificate-oidc-issuer "$ISSUER"
+jq -r '.dsseEnvelope.payload | @base64d | fromjson | .subject[] | .name + " sha256:" + .digest.sha256' provenance.bundle.json
+ The SLSA provenance is signed by a separate job with its own identity, stage-sign.yml. It receives only the list of digests the build made and runs no code from the build. The second command lists each item it names, with its digest.
+ 5. The parts list and what it was assembled from
+ gh release download v${VER} --repo fosterstack/cache \
+ -p production.sbom.json -p production.sbom.json.bundle \
+ -p production.full.lock.json -p production.full.lock.json.bundle
+for f in production.sbom.json production.full.lock.json; do
+ cosign verify-blob "$f" --bundle "$f.bundle" \
+ --certificate-identity "${IDENT}/stage-promote.yml@refs/tags/v${VER}" \
+ --certificate-oidc-issuer "$ISSUER"
+done
+ The SBOM lists what is in the image. The lock file lists every package the image was assembled from, by checksum. Repeat with fips.sbom.json and fips.full.lock.json for the -fips image.
+ What this proves
+
+ - A valid signature proves who signed these exact files, and that the signing was recorded publicly.
+ - Provenance proves which workflow, at which tag, produced the digests it names.
+ - It does not prove the software has no bugs. Scan results and our statements about findings are separate: see the security policy.
- How we checked
- Every command above was run against release 0.2.2, and the output shown is what it printed. We ran the same commands against release 0.2.1 too: they worked the same, and only the values differ. The workflow names in the commands belong to these two releases. They are version-scoped, so for older releases use the verification guide.
- How long it takes. We followed this page word for word in a new container on a GitHub-hosted runner, with only the tools it names installed (cosign, gh and jq, plus curl, ca-certificates and tar from the system). Steps 1 to 7 took 12.6 to 13.1 seconds, and all ten steps 13.9 to 14.5 seconds, in two runs for release 0.2.1 and two for release 0.2.2. Installing the tools took about 5 seconds more. Reading and typing are not in those numbers. Steps 1 to 4 ran with no GitHub login; only steps 5 to 7 were given a token. Every step passed. In the 0.2.1 runs we also compared the values this page used to print for 0.2.1, and the digests, the build-run link, the package count, a VEX line and the statement count all matched. The run and its log are public, and the script is bin/bench-verify-release.sh.
-
- Related: how releases are built and signed and installing on an air-gapped network.
-
+ Each stage of the release also writes a record that anyone with the Witness tool can check. Those records are extra evidence outside the steps above, and are attached to the release.
+ Related: how releases are built and signed, how the pipeline maps to NIST and SLSA and scan the image yourself.
+ Moving off the free Build Cache Node? See what the end of life means and the migration guide.