Verifying a release
Rivet reads your home directory and writes your configuration back. Before you run a downloaded binary with that reach, it is reasonable to want proof that it came from the source you can read, built by CI, and not modified afterwards.
This page is how you get that proof. Nothing here requires trusting the maintainer — every check is against a public transparency log.
For what Rivet defends against once it is running, see THREAT_MODEL.md.
What ships with each release
Section titled “What ships with each release”| Asset | What it is |
|---|---|
rivet-<tag>-<target>.tar.gz |
The binary and its licences |
….tar.gz.sha256 |
The archive’s SHA-256 digest |
….tar.gz.cosign.bundle |
A Sigstore signature over the archive, plus the certificate that proves which workflow produced it |
rivet-<tag>.sbom.cdx.json |
A CycloneDX software bill of materials for the release’s dependency tree |
<target> is one of x86_64-unknown-linux-musl,
aarch64-unknown-linux-musl, or universal-apple-darwin — the macOS archive
carries both an Apple Silicon and an Intel slice in one binary.
Plus a build provenance attestation stored by GitHub rather than attached
as a file, on v1.0.0 and later. See check 3 for why earlier releases have
none, and what that does and does not cost you.
The checks
Section titled “The checks”Three against the published assets, plus a fourth — rebuilding the archive yourself — for readers who would rather not take GitHub’s and Sigstore’s word for any of it.
1. The archive is intact
Section titled “1. The archive is intact”sha256sum -c rivet-v0.8.0-x86_64-unknown-linux-musl.tar.gz.sha256This only proves the download was not corrupted. It proves nothing about origin — whoever could replace the archive could replace the digest beside it. The next two checks are the ones that matter.
2. The signature was made by Rivet’s release workflow
Section titled “2. The signature was made by Rivet’s release workflow”Requires cosign.
cosign verify-blob rivet-v0.8.0-x86_64-unknown-linux-musl.tar.gz \ --bundle rivet-v0.8.0-x86_64-unknown-linux-musl.tar.gz.cosign.bundle \ --certificate-identity-regexp '^https://github\.com/fobiat/Rivet/\.github/workflows/release\.yml@' \ --certificate-oidc-issuer https://token.actions.githubusercontent.comVerified OK means the signature was made by that workflow file, in that
repository, using a short-lived certificate that Sigstore’s public log
recorded. There is no long-lived signing key involved, so there is none to
steal — and no way to add a log entry claiming a different origin after the
fact.
The two --certificate-* flags are not optional decoration. Without them
cosign will confirm that somebody signed the file, which is not a useful
statement.
3. The binary was built from a specific commit
Section titled “3. The binary was built from a specific commit”Requires the gh CLI.
gh attestation verify rivet-v0.8.0-x86_64-unknown-linux-musl.tar.gz \ --repo fobiat/RivetThis is the check that closes the loop between the source and the download. The attestation records the commit SHA, the workflow, and the runner that produced the archive, so you can take the SHA it reports, read that exact tree on GitHub, and know it is the code inside the tarball.
Releases up to and including v0.9.0-rc.2 carry no attestation, and the
command above will find none for them. That is not an oversight in the
release workflow: GitHub’s attestations API refuses to store one for a
user-owned private repository, answering Feature not available for user-owned private repositories. No permission or workflow setting changes it — the
repository has to be public, and Rivet’s was not. The release workflow skips
the step rather than failing the release over it.
From v1.0.0 onward this check works. Publishing the repository is decided
(ADR-0009) and the workflow’s guard
turns itself on when it happens, so v1.0 releases ship attested. Which side of
that line a given archive falls on is a property of the release, not of your
setup: if gh attestation verify reports nothing for a pre-1.0 archive, that
is expected.
What the gap costs you on those older releases, stated plainly: checks 1 and 2 still hold, so you can prove the archive is intact and that it was signed by Rivet’s release workflow running in this repository at a given ref — Sigstore records that in the public Rekor transparency log, and a private repository did not weaken it. What you cannot do is take a commit SHA from an attestation, read that tree, and confirm it is the code in the tarball. For those releases, treat the identity in check 2’s certificate as the strongest available statement about where a Rivet binary came from.
What the installer verifies for you
Section titled “What the installer verifies for you”install.sh — what curl -sSf https://rivet.fobiat.workers.dev/install | sh
runs — performs check 2 or check 1 on your behalf before it installs anything,
and tells you which one it managed.
| Level | When | What it proves |
|---|---|---|
| Signature | cosign is on your PATH and the release published a bundle |
Check 2, with the same identity and issuer pins. Detects tampering. |
| Checksum | No cosign, but a .sha256 is published and a hashing tool exists |
Check 1 only. Detects corruption, not tampering. |
| Refused | Neither is possible | Nothing — so it does not install. |
The installer fails closed. If it can verify neither a signature nor a
checksum it stops and tells you how to fix it, rather than installing and
warning. Earlier versions warned and continued; a warning scrolling past in a
curl | sh pipeline is not a decision anyone made.
Two behaviours worth knowing:
- A signature that fails is fatal and never falls back to the checksum. A bundle that exists but does not verify is a failed check, not a missing one, and the checksum beside a tampered archive would happily pass.
- The checksum level prints what it did not prove. Same origin as the archive means it is a corruption check; the installer says so rather than letting “verified” imply more than it should.
To verify by hand instead, install with --no-verify and run checks 1–3 above,
or download the assets and check them before running anything.
Reading the SBOM
Section titled “Reading the SBOM”jq -r '.components[] | "\(.name) \(.version)"' rivet-v0.8.0.sbom.cdx.json | sortEvery crate that ends up in the binary, with versions — enough to check
Rivet’s dependency tree against an advisory database without building
anything. Rivet’s own security-relevant surface is small and deliberately
isolated: the cryptography is confined to
crates/rivet-crypto, which is a few
hundred lines wrapping the age crate.
Reproducing the build yourself
Section titled “Reproducing the build yourself”The fourth check, and the only one that does not depend on GitHub or Sigstore being honest: build the archive yourself and compare digests.
The release build sets SOURCE_DATE_EPOCH from the tagged commit’s date,
remaps build paths out of the binary, and packages with a tar whose member
order, timestamps, ownership and permissions are fixed. Run the same script CI
runs:
git checkout v0.8.0SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct) \ scripts/package-release.sh v0.8.0 x86_64-unknown-linux-muslsha256sum -c rivet-v0.8.0-x86_64-unknown-linux-musl.tar.gz.sha256Compare the digest against the .sha256 published with the release. This
requires cross and Docker or Podman; set
RIVET_CARGO=cargo to build with plain cargo instead, which will not match
the published musl archive but still lets you check that two of your own builds
agree.
This is enforced, not merely intended.
.github/workflows/reproducibility.yml
builds the archive twice from the same commit, in two checkout directories with
different names, with no shared build cache, and fails if the SHA-256 digests
differ. It runs whenever the packaging changes and weekly against a fresh
toolchain and cross image.
If a check fails
Section titled “If a check fails”Do not run the binary. Report it through private vulnerability reporting rather than opening a public issue, and include the command you ran and its full output.