Infrastructure worth keeping — and kept patched.

Verify a FosterStack Cache release yourself

Check our signature, build provenance, SBOM and vulnerability statements with commands you run yourself. Every command below was run against release 0.2.1, and the output shown is what it printed. No account is needed except where we say so.

What you need

cosign, the gh tool (for the build-provenance steps), and jq. Set the version and your platform once:

$ VER=0.2.1
PLATFORM=linux_arm64   # or linux_amd64, darwin_arm64, darwin_amd64

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.1_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. Is the container image signed by our release workflow?

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.1 --
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.1"},"image":{"docker-manifest-digest":"sha256:8df991b5febdf5b4a177325b8af95b6bb6c321059e1715c06cb659c659f5a4f0"},"type":"https://fosterstack.com/attestations/release-manifest/v1"},"optional":{}},{"critical":{"identity":{"docker-reference":"ghcr.io/fosterstack/cache:0.2.1"},"image":{"docker-manifest-digest":"sha256:8df991b5febdf5b4a177325b8af95b6bb6c321059e1715c06cb659c659f5a4f0"},"type":"https://sigstore.dev/cosign/sign/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:8df991b5febdf5b4a177325b8af95b6bb6c321059e1715c06cb659c659f5a4f0.

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": "8df991b5febdf5b4a177325b8af95b6bb6c321059e1715c06cb659c659f5a4f0"
  },
  "signer": "https://github.com/fosterstack/cache/.github/workflows/stage-image.yml@refs/tags/v0.2.1",
  "run": "https://github.com/fosterstack/cache/actions/runs/35554556056/attempts/1"
}

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.

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.1_linux_amd64.tar.gz
fscache-fips_0.2.1_linux_amd64.tar.gz.sbom.json
fscache-fips_0.2.1_linux_arm64.tar.gz
fscache-fips_0.2.1_linux_arm64.tar.gz.sbom.json
fscache_0.2.1_darwin_amd64.tar.gz
fscache_0.2.1_darwin_amd64.tar.gz.sbom.json
fscache_0.2.1_darwin_arm64.tar.gz
fscache_0.2.1_darwin_arm64.tar.gz.sbom.json
fscache_0.2.1_linux_amd64.tar.gz
fscache_0.2.1_linux_amd64.tar.gz.sbom.json
fscache_0.2.1_linux_arm64.tar.gz
fscache_0.2.1_linux_arm64.tar.gz.sbom.json
checksums.txt
https://github.com/fosterstack/cache/.github/workflows/stage-build.yml@refs/tags/v0.2.1

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.1"
}

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.1_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.1
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:8df991b5febdf5b4a177325b8af95b6bb6c321059e1715c06cb659c659f5a4f0
debug sha256:e2e502f44d8bd9e6967cb160aabeb012e8520ebba0faa88e130143fe534f282e
fips sha256:e125fe3f6ed2c18c478453300ba2b787a42999edf66a39403877efb39865ab1b
$ jq -c '.images[0].refs' release-manifest.json
["ghcr.io/fosterstack/cache@sha256:8df991b5febdf5b4a177325b8af95b6bb6c321059e1715c06cb659c659f5a4f0","docker.io/fosterstack/cache@sha256:8df991b5febdf5b4a177325b8af95b6bb6c321059e1715c06cb659c659f5a4f0"]
$ 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

The workflow names above belong to release 0.2.1. They are version-scoped, so for older releases use the verification guide.

Related: verify a container image signature with cosign and how releases are built and signed.

Releases on GitHub FosterStack home