ADR 0012 — Release signing and trust-on-first-use update verification
- Status: Accepted
- Date: 2026-07-01
- Tracking: legacy tracker 1266 (release signing key); release/install overhaul epic (ADR 0010)
Context
madtea update downloads a release and replaces the running binary. Without
integrity verification, a tampered release or a man-in-the-middle on the download
runs arbitrary code as the user. madtea depends on no third-party update service
and publishes to a self-hosted forge, so the trust model must be self-contained
in the binary plus the release artifacts.
Decision
Releases are GPG-signed, and madtea update verifies that signature against a
key embedded in the running binary — a trust-on-first-use (TOFU) model.
- At release, GoReleaser signs
checksums.txtwith the release signing key (fingerprint4CF168E8EF9E40DC9173505F07D6FBD4B0D2A065, RSA-4096), producingchecksums.txt.asc;release.shverifies that signature locally before declaring the release done. madtea updatedownloads the latest release, verifieschecksums.txt.ascagainst the public key embedded in the running binary (not fetched over the wire), then verifies each artifact’s SHA-256 against the now-trustedchecksums.txt, then atomically replaces the executable — signature checked before the large artifact is trusted.- TOFU boundary: the first install (
go install, a package manager, or a downloaded binary) is trusted on first use — you trust the transport (TLS / Homebrew / Scoop), the same as anygo installorcurl | sh. The embedded key only protects subsequent updates. - Key rotation ships a new release: the running binary trusts only the key it shipped with, so rotating the signing key requires publishing a release built with the new embedded key; users must update (over TLS) to a release carrying the new key before signatures from it will verify.
Consequences
- Self-contained integrity without a PKI or update service — no CA, no key server, no third-party updater; the binary carries its own trust root.
- The signing key is a release-critical secret kept on the maintainer’s machine. ADR 0010 keeps signing local for exactly this reason — the key never enters CI. Loss or compromise requires a key-rotation release.
- First-install trust is explicitly out of scope (documented in
SECURITY.md); package managers narrow that window in practice (Homebrew/Scoop verify their own download). - Version comparison uses semver (
golang.org/x/mod/semver): if the running binary’s version is already >= the latest release tag the update is skipped as already up to date. This handles pseudo-versions likev0.14.5-devthat may be newer than the current release. Free-form dev builds (version string starting with"dev", e.g."dev (abc1234)") are never considered up to date and are always replaced by a real release — an unparseable version on either side also allows the update rather than blocking it.
Alternatives considered
- No signing (SHA-256 only). Rejected: a checksum an attacker can also replace verifies nothing against a compromised release host.
- Fetch the public key over the network at update time. Rejected: a network-fetched key is a MITM target; embedding the key in the binary is the whole point of TOFU.
- Full PKI / OS notarization as the primary trust root. Deferred: Apple notarization + Windows Authenticode improve first-install trust (a launch follow-up under ADR 0010) but don’t replace the embedded-key model for updates.
Canonical source: docs/adr/0012-release-signing-and-tofu-updates.md in the madtea repo.