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.txt with the release signing key (fingerprint 4CF168E8EF9E40DC9173505F07D6FBD4B0D2A065, RSA-4096), producing checksums.txt.asc; release.sh verifies that signature locally before declaring the release done.
  • madtea update downloads the latest release, verifies checksums.txt.asc against the public key embedded in the running binary (not fetched over the wire), then verifies each artifact’s SHA-256 against the now-trusted checksums.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 any go install or curl | 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 like v0.14.5-dev that 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.