Skip to content

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.

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.

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.

Terminal window
sha256sum -c rivet-v0.8.0-x86_64-unknown-linux-musl.tar.gz.sha256

This 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.

Terminal window
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.com

Verified 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.

Terminal window
gh attestation verify rivet-v0.8.0-x86_64-unknown-linux-musl.tar.gz \
--repo fobiat/Rivet

This 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.

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.

Terminal window
jq -r '.components[] | "\(.name) \(.version)"' rivet-v0.8.0.sbom.cdx.json | sort

Every 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.

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:

Terminal window
git checkout v0.8.0
SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct) \
scripts/package-release.sh v0.8.0 x86_64-unknown-linux-musl
sha256sum -c rivet-v0.8.0-x86_64-unknown-linux-musl.tar.gz.sha256

Compare 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.

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.