release: reproducible builds and signed assets #180

Merged
sirius merged 1 commit from repro-builds-and-signing into main 2026-10-11 17:21:13 +00:00
Owner

Summary

"The source is public" is not a check anyone can run. Against a Tor + Monero tool
the objection that survives is that the released binary might not be that source —
and until now nothing in this repo could answer it: no reproducible build, and no
signed releases.

This adds both, plus the verifier-facing document that tells someone how to use them.

What is in it

  • scripts/reproducible-build.sh — the single definition of a release build.
    Digest-pinned builder image, fixed in-container paths, SOURCE_DATE_EPOCH from
    the released commit, --locked, its own apt build deps, and RUSTFLAGS replaced
    rather than extended.
  • release.yml — pins the image by digest, resolves SOURCE_DATE_EPOCH from the
    tag's commit, runs that recipe, uploads build-info-<target>.txt beside the binary,
    and creates the cargo target volume per run and removes it afterwards.
  • scripts/verify-reproducible.sh — rebuilds a tag in the same pinned image and
    compares bytes against the release asset. One command, docker only.
  • scripts/sign-release.sh + a signing step that runs before publish and
    fails closed without MINISIGN_SECRET_KEY.
  • docs/VERIFY.md — the walkthrough a sceptic can actually run, ending with an
    explicit list of what none of these checks prove.
  • build-release-local.sh now runs the same container and recipe; Cargo.toml
    repository points at Forgejo instead of GitHub.

The fixed paths are load-bearing and worth a reviewer's attention: rustc's
--remap-path-prefix cannot reach absolute paths compiled in by a C toolchain, and
this graph always has one — rusqlite compiles OpenSSL from vendored sources, and
the default cuprate-daemon adds RandomX and cmake. Hence "build in the pinned image
at fixed paths" rather than "trust the flags".

Verified, not assumed

  • Reproducibility: two fresh builds of 1031092 in the pinned image, each from an
    empty target dir, produced identical
    sha256 8d7af032ec6aac867d7a5e65f8f7772397aed620b31d80e28092639675a01201.
  • Signing, with a throwaway keypair: signature accepted by an independent
    minisign -V; a tampered file rejected; a run with no key refused; a run with no
    public key refused.
  • bash -n on all four scripts, YAML parse, and bash -n on all 15 workflow run-blocks.

Action required before the next release

MINISIGN_SECRET_KEY must exist as an Actions secret and
scripts/onionwire-signing.pub must be committed. Until then the publish job
stops by design: an unsigned release is indistinguishable from a signed one to
anyone who is not checking, which makes it worse than no release at all. Procedure is
in docs/RELEASING.md, "Reproducibility and signing".

Honest limits (also written into VERIFY.md)

  • Reproducibility pins the build, not the intent: it proves the binary is this source,
    not that the source is benign.
  • The apt build deps (gcc, cmake) are not digest-pinned the way the image is; a Debian
    security update can move the bytes. build-info records their versions so such a
    mismatch is diagnosable rather than mysterious.
  • Still no third-party audit.
  • The wallet's seed-read capability is by design (it drives the backup screen). It is
    already in THREAT_MODEL.md; VERIFY.md now spells it out rather than leaving a critic
    to discover it.

Notes

  • ci.yml is untouched; this is build/release plumbing plus docs.
  • The image digest appears in three places (release.yml, verify-reproducible.sh,
    build-release-local.sh) and must be bumped in all three together — called out in
    both RELEASING.md and VERIFY.md.
## Summary "The source is public" is not a check anyone can run. Against a Tor + Monero tool the objection that survives is that the released binary might not be that source — and until now nothing in this repo could answer it: no reproducible build, and no signed releases. This adds both, plus the verifier-facing document that tells someone how to use them. ## What is in it - **`scripts/reproducible-build.sh`** — the single definition of a release build. Digest-pinned builder image, fixed in-container paths, `SOURCE_DATE_EPOCH` from the released commit, `--locked`, its own apt build deps, and `RUSTFLAGS` replaced rather than extended. - **`release.yml`** — pins the image by digest, resolves `SOURCE_DATE_EPOCH` from the tag's commit, runs that recipe, uploads `build-info-<target>.txt` beside the binary, and creates the cargo target volume per run and removes it afterwards. - **`scripts/verify-reproducible.sh`** — rebuilds a tag in the same pinned image and compares bytes against the release asset. One command, docker only. - **`scripts/sign-release.sh`** + a signing step that runs **before** publish and fails closed without `MINISIGN_SECRET_KEY`. - **`docs/VERIFY.md`** — the walkthrough a sceptic can actually run, ending with an explicit list of what none of these checks prove. - `build-release-local.sh` now runs the same container and recipe; `Cargo.toml` `repository` points at Forgejo instead of GitHub. The fixed paths are load-bearing and worth a reviewer's attention: rustc's `--remap-path-prefix` cannot reach absolute paths compiled in by a C toolchain, and this graph always has one — `rusqlite` compiles OpenSSL from vendored sources, and the default `cuprate-daemon` adds RandomX and cmake. Hence "build in the pinned image at fixed paths" rather than "trust the flags". ## Verified, not assumed - **Reproducibility**: two fresh builds of `1031092` in the pinned image, each from an empty target dir, produced identical `sha256 8d7af032ec6aac867d7a5e65f8f7772397aed620b31d80e28092639675a01201`. - **Signing**, with a throwaway keypair: signature accepted by an independent `minisign -V`; a tampered file rejected; a run with no key refused; a run with no public key refused. - `bash -n` on all four scripts, YAML parse, and `bash -n` on all 15 workflow run-blocks. ## Action required before the next release `MINISIGN_SECRET_KEY` must exist as an Actions secret and `scripts/onionwire-signing.pub` must be committed. Until then the publish job **stops by design**: an unsigned release is indistinguishable from a signed one to anyone who is not checking, which makes it worse than no release at all. Procedure is in `docs/RELEASING.md`, "Reproducibility and signing". ## Honest limits (also written into VERIFY.md) - Reproducibility pins the build, not the intent: it proves the binary is this source, not that the source is benign. - The apt build deps (gcc, cmake) are not digest-pinned the way the image is; a Debian security update can move the bytes. `build-info` records their versions so such a mismatch is diagnosable rather than mysterious. - Still no third-party audit. - The wallet's seed-read capability is by design (it drives the backup screen). It is already in `THREAT_MODEL.md`; VERIFY.md now spells it out rather than leaving a critic to discover it. ## Notes - `ci.yml` is untouched; this is build/release plumbing plus docs. - The image digest appears in three places (`release.yml`, `verify-reproducible.sh`, `build-release-local.sh`) and must be bumped in all three together — called out in both `RELEASING.md` and `VERIFY.md`.
release: reproducible builds, and sign every asset
Some checks failed
ci / test (pull_request) Failing after 11m16s
ci / sdk (pull_request) Successful in 1m18s
android-ci / android (pull_request) Successful in 12m47s
fffa1d48ed
"The source is public" is not a check anyone can run, and against a Tor +
Monero tool the surviving objection is that the released binary might not be
that source. This makes the build checkable instead of trusted.

- scripts/reproducible-build.sh is the single definition of a release build:
  digest-pinned builder image, fixed in-container paths, SOURCE_DATE_EPOCH
  from the released commit, --locked, its own apt build deps, and RUSTFLAGS
  replaced rather than extended. The fixed paths are load-bearing: rustc's
  --remap-path-prefix cannot reach absolute paths compiled in by a C
  toolchain, and this graph always has one (rusqlite compiles OpenSSL from
  vendored sources; the default cuprate-daemon adds RandomX and cmake). It
  also records toolchain versions in build-info, because the apt build deps
  are not digest-pinned the way the image is.
- release.yml pins the image by digest, resolves SOURCE_DATE_EPOCH from the
  tag's commit, runs that recipe, and uploads build-info-<target>.txt beside
  the binary. The cargo target volume is created per run and removed, so the
  published artifact cannot come out of a cache that a verifier does not have.
- scripts/verify-reproducible.sh rebuilds a tag in the same pinned image and
  compares bytes against the release asset. Two fresh builds of the same tree
  in that image produce identical sha256.
- scripts/sign-release.sh signs with minisign and verifies its own signature
  against the committed public key. The publish job signs BEFORE publishing
  and fails closed without MINISIGN_SECRET_KEY, so nothing unsigned reaches
  the release page: an unsigned release is indistinguishable from a signed
  one to anyone who is not checking. Checked: signature accepted by an
  independent minisign -V, a tampered file rejected, a missing key refused,
  a missing public key refused.
- build-release-local.sh runs the same container and the same recipe. A host
  cargo build would publish a hash nobody could reproduce, which is worse
  than having no fallback at all.
- docs/VERIFY.md is the verifier-facing walkthrough: the dependency graph and
  where its HTTP actually comes from, the wallet's real seed-read exposure,
  the rebuild check, the signature check, and an explicit list of what none
  of it proves.
- Cargo.toml repository: point at the Forgejo instance that hosts this and
  builds its releases.

Needs a MINISIGN_SECRET_KEY secret before the next release; without it the
publish job stops by design. docs/RELEASING.md has the keygen procedure.
sirius force-pushed repro-builds-and-signing from fffa1d48ed
Some checks failed
ci / test (pull_request) Failing after 11m16s
ci / sdk (pull_request) Successful in 1m18s
android-ci / android (pull_request) Successful in 12m47s
to 9bedcc957e
Some checks failed
ci / test (pull_request) Failing after 11m51s
android-ci / android (pull_request) Successful in 12m22s
ci / sdk (pull_request) Successful in 1m21s
2026-10-11 16:40:55 +00:00
Compare
sirius force-pushed repro-builds-and-signing from 9bedcc957e
Some checks failed
ci / test (pull_request) Failing after 11m51s
android-ci / android (pull_request) Successful in 12m22s
ci / sdk (pull_request) Successful in 1m21s
to 601f640f78
All checks were successful
android-ci / android (pull_request) Successful in 12m34s
ci / test (pull_request) Successful in 13m28s
ci / sdk (pull_request) Successful in 1m13s
2026-10-11 16:55:40 +00:00
Compare
sirius merged commit a0b79d457d into main 2026-10-11 17:21:13 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
sirius/onionwire!180
No description provided.