Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions sitemap.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,8 @@
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://fosterstack.com/verify-a-release/</loc>
<lastmod>2026-10-08</lastmod>
</url>
</urlset>
235 changes: 235 additions & 0 deletions verify-a-release/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Verify a FosterStack Cache release yourself: signature, provenance, SBOM, VEX</title>
<meta name="description" content="Check a FosterStack Cache release with commands you run yourself: checksum signature, container image signature, build provenance, SBOM and VEX, each shown with real output from release 0.2.1.">
<link rel="canonical" href="https://fosterstack.com/verify-a-release/">
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<link rel="icon" href="/favicon.ico" sizes="any">
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
<style>
:root {
--bg: #ffffff; --fg: #16181d; --muted: #5b6472; --line: #e4e7ec;
--accent: #0d7a5f; --accent-fg: #ffffff; --card: #f6f8f9; --warn-bg: #fff7ed; --warn-line: #fdba74;
--mark: #0f4d66;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #101318; --fg: #e8eaf0; --muted: #98a2b3; --line: #262c36;
--accent: #2fbf95; --accent-fg: #08110e; --card: #171c23; --warn-bg: #241a10; --warn-line: #7c4a12;
--mark: #e8eaf0;
}
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: var(--bg); color: var(--fg);
font: 16px/1.6 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
-webkit-font-smoothing: antialiased;
}
main { max-width: 44rem; margin: 0 auto; padding: 0 1.25rem 4rem; }
header.site { max-width: 44rem; margin: 0 auto; padding: 1.25rem; display: flex; align-items: baseline; gap: .6rem; }
.logo { font-weight: 700; font-size: 1.05rem; letter-spacing: -.01em; }
.logo span { color: var(--accent); }
.logo .mark { width: 1.2em; height: 1.2em; vertical-align: -.24em; margin-right: .4em; }
.tag { color: var(--muted); font-size: .85rem; }
.notice {
background: var(--warn-bg); border: 1px solid var(--warn-line); border-radius: 8px;
padding: .7rem 1rem; font-size: .92rem; margin: 1.5rem 0 2.5rem;
}
h1 { font-size: 2rem; line-height: 1.2; letter-spacing: -.02em; margin-bottom: 1rem; }
.lede { font-size: 1.12rem; color: var(--muted); margin-bottom: 2rem; }
h2 { font-size: 1.25rem; margin: 3rem 0 .9rem; letter-spacing: -.01em; }
p + p { margin-top: .8rem; }
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); gap: .8rem; margin-top: 1rem; }
.card { background: var(--card); border: 1px solid var(--line); border-radius: 10px; padding: 1rem 1.1rem; }
.card h3 { font-size: .98rem; margin-bottom: .35rem; }
.card p { font-size: .9rem; color: var(--muted); }
ul.trust { list-style: none; margin-top: .5rem; }
ul.trust li { padding: .45rem 0 .45rem 1.6rem; position: relative; }
ul.trust li::before { content: "✓"; position: absolute; left: .2rem; color: var(--accent); font-weight: 700; }
code, pre { font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: .88em; }
code { background: var(--card); border: 1px solid var(--line); border-radius: 5px; padding: .1em .35em; }
pre code { background: none; border: 0; padding: 0; color: inherit; font-size: 1em; }
button {
padding: .7rem 1.3rem; font-size: 1rem; font-weight: 600; cursor: pointer;
background: var(--accent); color: var(--accent-fg); border: 0; border-radius: 8px;
}
button:disabled { opacity: .6; cursor: default; }
table { width: 100%; border-collapse: collapse; font-size: .92rem; margin-top: 1rem; }
th, td { text-align: left; padding: .55rem .6rem; border-bottom: 1px solid var(--line); vertical-align: top; }
th { font-size: .8rem; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); }
pre { background: #0f172a; color: #e2e8f0; padding: .9rem 1rem; border-radius: 8px;
overflow-x: auto; font-size: .9rem; line-height: 1.5; }
.cta-row { display: flex; gap: .6rem; flex-wrap: wrap; margin-top: 1.1rem; }
.btn { display: inline-block; padding: .55rem 1rem; border-radius: 8px; font-weight: 600;
text-decoration: none; background: var(--accent); color: #fff; }
.btn-secondary { background: transparent; color: inherit; border: 1px solid #cbd5e1; }
.muted-note { color: var(--muted); font-size: .93rem; margin-top: 1.1rem; }
footer { border-top: 1px solid var(--line); margin-top: 4rem; padding: 1.5rem 1.25rem 3rem; }
footer div { max-width: 44rem; margin: 0 auto; color: var(--muted); font-size: .82rem; }
a { color: var(--accent); }
</style>
</head>
<body>
<header class="site">
<div class="logo"><svg class="mark" viewBox="0 0 96 96" aria-hidden="true" focusable="false"><rect x="12" y="14" width="72" height="18" rx="5" fill="var(--mark)"/><rect x="12" y="39" width="48" height="18" rx="5" fill="var(--mark)"/><rect x="12" y="64" width="26" height="18" rx="5" fill="var(--accent)"/></svg>foster<span>stack</span></div>
<div class="tag">Infrastructure worth keeping — and kept patched.</div>
</header>
<main>
<h1>Verify a FosterStack Cache release yourself</h1>
<p class="lede">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.</p>

<h2>What you need</h2>
<p><code>cosign</code>, the <code>gh</code> tool (for the build-provenance steps), and <code>jq</code>. Set the version and your platform once:</p>
<pre><code>$ VER=0.2.1
PLATFORM=linux_arm64 # or linux_amd64, darwin_arm64, darwin_amd64</code></pre>

<h2>1. Download the release files</h2>
<pre><code>$ gh release download v${VER} --repo fosterstack/cache \
-p checksums.txt -p checksums.txt.bundle \
-p &quot;fscache_${VER}_${PLATFORM}.tar.gz&quot; \
-p &quot;fscache_${VER}_${PLATFORM}.tar.gz.sbom.json&quot; \
-p fosterstack-cache.openvex.json -p release-manifest.json</code></pre>
<p>Downloading from the <a href="https://github.com/fosterstack/cache/releases">releases page</a> in a browser works the same.</p>

<h2>2. Is the checksums file ours?</h2>
<p>This checks the signature on <code>checksums.txt</code>. It needs no GitHub account.</p>
<pre><code>$ cosign verify-blob \
--bundle checksums.txt.bundle \
--certificate-identity-regexp=&#x27;^https://github.com/fosterstack/cache/&#x27; \
--certificate-oidc-issuer=&#x27;https://token.actions.githubusercontent.com&#x27; \
checksums.txt
Verified OK</code></pre>

<h2>3. Does your download match it?</h2>
<pre><code>$ sha256sum -c &lt;(grep &quot;fscache_${VER}_${PLATFORM}.tar.gz&quot; checksums.txt | grep -v sbom)
fscache_0.2.1_linux_arm64.tar.gz: OK</code></pre>
<p>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.</p>

<h2>4. Is the container image signed by our release workflow?</h2>
<p>Pin the workflow and the release tag, not "any workflow in the repository". No account is needed.</p>
<pre><code>$ cosign verify ghcr.io/fosterstack/cache:${VER} \
--certificate-identity-regexp=&quot;^https://github.com/fosterstack/cache/.github/workflows/stage-promote.yml@refs/tags/v${VER}$&quot; \
--certificate-oidc-issuer=&#x27;https://token.actions.githubusercontent.com&#x27;
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

[{&quot;critical&quot;:{&quot;identity&quot;:{&quot;docker-reference&quot;:&quot;ghcr.io/fosterstack/cache:0.2.1&quot;},&quot;image&quot;:{&quot;docker-manifest-digest&quot;:&quot;sha256:8df991b5febdf5b4a177325b8af95b6bb6c321059e1715c06cb659c659f5a4f0&quot;},&quot;type&quot;:&quot;https://fosterstack.com/attestations/release-manifest/v1&quot;},&quot;optional&quot;:{}},{&quot;critical&quot;:{&quot;identity&quot;:{&quot;docker-reference&quot;:&quot;ghcr.io/fosterstack/cache:0.2.1&quot;},&quot;image&quot;:{&quot;docker-manifest-digest&quot;:&quot;sha256:8df991b5febdf5b4a177325b8af95b6bb6c321059e1715c06cb659c659f5a4f0&quot;},&quot;type&quot;:&quot;https://sigstore.dev/cosign/sign/v1&quot;},&quot;optional&quot;:{}}]</code></pre>
<p>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 <code>docker.io/fosterstack/cache:${VER}</code> returns the same digest, <code>sha256:8df991b5febdf5b4a177325b8af95b6bb6c321059e1715c06cb659c659f5a4f0</code>.</p>

<h2>5. Where was the image built?</h2>
<p>A signature says who signed. Build provenance says what produced the bytes. This step reads GitHub's attestation store, so it needs a token (<code>GH_TOKEN</code> or <code>gh auth login</code>), even though the repository is public.</p>
<pre><code>$ gh attestation verify oci://ghcr.io/fosterstack/cache:${VER} \
--repo fosterstack/cache \
--signer-workflow fosterstack/cache/.github/workflows/stage-image.yml \
--source-ref &quot;refs/tags/v${VER}&quot; \
--format json | jq &#x27;.[0].verificationResult | {statement: .statement.predicateType, image: .statement.subject[0].digest, signer: .signature.certificate.subjectAlternativeName, run: .signature.certificate.runInvocationURI}&#x27;
{
&quot;statement&quot;: &quot;https://slsa.dev/provenance/v1&quot;,
&quot;image&quot;: {
&quot;sha256&quot;: &quot;8df991b5febdf5b4a177325b8af95b6bb6c321059e1715c06cb659c659f5a4f0&quot;
},
&quot;signer&quot;: &quot;https://github.com/fosterstack/cache/.github/workflows/stage-image.yml@refs/tags/v0.2.1&quot;,
&quot;run&quot;: &quot;https://github.com/fosterstack/cache/actions/runs/35554556056/attempts/1&quot;
}</code></pre>
<p>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.</p>

<h2>6. Where was your download built?</h2>
<pre><code>$ gh attestation verify &quot;fscache_${VER}_${PLATFORM}.tar.gz&quot; --repo fosterstack/cache \
--format json | jq -r &#x27;.[0].verificationResult.statement.subject[].name, .[0].verificationResult.signature.certificate.subjectAlternativeName&#x27;
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</code></pre>
<p>One statement covers every archive, its SBOM and <code>checksums.txt</code>. Your archive is in the list.</p>

<h2>7. Did the whole release chain pass?</h2>
<p>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.</p>
<pre><code>$ 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 &#x27;.[0].verificationResult | {predicateType: .statement.predicateType, signer: .signature.certificate.subjectAlternativeName}&#x27;
{
&quot;predicateType&quot;: &quot;https://fosterstack.com/attestations/release-authorization/v1&quot;,
&quot;signer&quot;: &quot;https://github.com/fosterstack/cache/.github/workflows/stage-authorize.yml@refs/tags/v0.2.1&quot;
}</code></pre>

<h2>8. What is inside? The SBOM</h2>
<p>Each binary archive has a software bill of materials in SPDX format.</p>
<pre><code>$ jq &#x27;{spdxVersion, name, packages: (.packages | length)}&#x27; &quot;fscache_${VER}_${PLATFORM}.tar.gz.sbom.json&quot;
{
&quot;spdxVersion&quot;: &quot;SPDX-2.3&quot;,
&quot;name&quot;: &quot;fscache_0.2.1_linux_arm64.tar.gz&quot;,
&quot;packages&quot;: 13
}</code></pre>
<pre><code>$ jq -r &#x27;.packages[0:4][] | &quot;\(.name) \(.versionInfo)&quot;&#x27; &quot;fscache_${VER}_${PLATFORM}.tar.gz.sbom.json&quot;
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</code></pre>

<h2>9. Which known vulnerabilities do not apply? The VEX file</h2>
<p>The VEX (OpenVEX) file lists vulnerabilities that scanners may report and says why each does not affect this release.</p>
<pre><code>$ jq -r &#x27;.statements[] | &quot;\(.vulnerability.name) \(.status) \(.justification)&quot;&#x27; 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</code></pre>
<p>You do not have to take our word for those. Run your own scanner against the image and give it this file.</p>

<h2>10. The release manifest</h2>
<p><code>release-manifest.json</code> lists the evidence for the whole release. It names three image variants (the standard one, <code>debug</code> and <code>fips</code>), each with its digest. The standard one is the digest you verified in steps 4 and 5, at both registries:</p>
<pre><code>$ jq -r &#x27;.images[] | &quot;\(.variant) \(.digest)&quot;&#x27; release-manifest.json
production sha256:8df991b5febdf5b4a177325b8af95b6bb6c321059e1715c06cb659c659f5a4f0
debug sha256:e2e502f44d8bd9e6967cb160aabeb012e8520ebba0faa88e130143fe534f282e
fips sha256:e125fe3f6ed2c18c478453300ba2b787a42999edf66a39403877efb39865ab1b</code></pre>
<pre><code>$ jq -c &#x27;.images[0].refs&#x27; release-manifest.json
[&quot;ghcr.io/fosterstack/cache@sha256:8df991b5febdf5b4a177325b8af95b6bb6c321059e1715c06cb659c659f5a4f0&quot;,&quot;docker.io/fosterstack/cache@sha256:8df991b5febdf5b4a177325b8af95b6bb6c321059e1715c06cb659c659f5a4f0&quot;]</code></pre>
<pre><code>$ jq &#x27;.verified_statements | length&#x27; release-manifest.json
43</code></pre>
<p>That last number is the statements the authorization stage checked, each with its entry in the public transparency log.</p>

<h2>What these checks do and do not prove</h2>
<ul class="trust">
<li>A valid signature proves who signed these exact bytes.</li>
<li>Build provenance proves which repository, workflow and run produced them, which is a public CI run and not a developer machine.</li>
<li>The SBOM and VEX files are statements we publish. They are as trustworthy as the signature on the release that carries them.</li>
<li>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.</li>
</ul>
<p>The workflow names above belong to release 0.2.1. They are version-scoped, so for older releases use the <a href="https://github.com/fosterstack/cache/blob/main/docs/verify-images.md">verification guide</a>.</p>

<p class="muted-note">Related: <a href="/verify-container-image-signature-cosign/">verify a container image signature with cosign</a> and <a href="/how-we-build/">how releases are built and signed</a>.</p>
<div class="cta-row">
<a class="btn" href="https://github.com/fosterstack/cache/releases">Releases on GitHub</a>
<a class="btn btn-secondary" href="/">FosterStack home</a>
</div>
</main>
<footer>
<div>
<p>FosterStack is not affiliated with, endorsed by, or sponsored by Gradle Inc.
"Gradle" and "Develocity" are trademarks of Gradle Inc., used here only to identify
compatibility. FosterStack contains no Gradle Inc. code; it is a clean-room implementation
of the documented remote build cache HTTP protocol. Apache Maven is a trademark of the
Apache Software Foundation; the Maven Build Cache Extension is Apache's project, not
ours.</p>
<p style="margin-top:.6rem">© 2026 FosterStack · <a href="/regulated/">For regulated markets</a> · <a href="https://github.com/fosterstack/cache">github.com/fosterstack/cache</a> · <a href="mailto:hello@fosterstack.com">hello@fosterstack.com</a></p>
</div>
</footer>
</body>
</html>
Loading