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:
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 \
-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.
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.