What a self-update trusts, and why¶
A tool that replaces its own binary is asking a user to run code that arrived over
the network, unattended. This page explains what rtb-update checks before it does
that, and why each check is shaped the way it is.
The trust anchor is compiled in¶
ToolMetadata::update_public_keys holds base64 minisign public keys, set at build
time. Nothing at run time can add to that set — not a config file, not an
environment variable, not a flag. An empty set means updates are refused outright
rather than performed unverified.
That is the whole trust model in one sentence: the binary you already have decides what the next binary must be signed by. An attacker who can serve you a release cannot also tell you which key to trust, because the answer was baked in before the download existed.
Multiple keys are accepted, and any one verifying is enough. That is what makes key
rotation possible without a dual-signing window: ship a release trusting
{old, new}, rotate, and every already-deployed binary spans the change.
Why only prehashed ED signatures¶
minisign has two signature formats: legacy pure Ed (ed25519 over the file) and
prehashed ED (ed25519(BLAKE2b-512(file))). rtb-update accepts only the
prehashed one, and refuses a legacy signature even when the trusted key could
verify it.
Two reasons, and neither is cryptographic strength.
One published signature has to serve two consumers. The same release artefacts
are installed by cargo-binstall, which requires the prehashed variant and
rejects legacy outright. Accepting only ED means what rtb-update trusts is
exactly what cargo-binstall trusts. Accepting both would create a class of release
that one installer verifies and the other refuses — discovered by a user, at
install time.
Prehashing is what lets an HSM sign. The signer only ever sees a 64-byte digest, which fits comfortably inside limits like the AWS KMS 4096-byte message cap. A pure signature over a 40 MB archive does not.
Why verification is delegated, not implemented here¶
Parsing and checking are done by
minisign-verify — the same crate
cargo-binstall uses.
Two independent implementations of one check is how two consumers of the same release silently drift apart: a signature one accepts and the other does not, with neither side wrong on its own terms. Sharing the implementation makes that impossible. The crate vendors its own BLAKE2b and has no dependencies, so this costs nothing in tree weight.
It also checks more than the artefact signature: the algorithm tag, the key id, the
signature itself, and the global signature over signature ‖ trusted_comment. That
last one matters because producers record the signing project in the trusted
comment; without it, the comment could be rewritten undetected.
The test suite signs its fixtures with jedisct1's own minisign crate and verifies
them with minisign-verify, so the fixtures are real minisign output rather than
this project's idea of the format.
Why a checksum as well, and why it is optional¶
ToolMetadata::update_checksums_asset names a sha256sum-format asset. When set,
the archive's hash is cross-checked against it after signature verification. When
unset, the signature is the only integrity gate.
The signature already covers the bytes, so the checksum is not a second security
boundary — it is a compatibility one. Publishing a .sum file is a widespread
release convention, and cross-checking the file you already publish catches a
pipeline that signed one artefact and uploaded another. Making it optional is
honest about that: it is a build-pipeline check, not a defence against an attacker
who can already produce a valid signature.
Why the swap is last, and why there is a self-test¶
The flow orders every fallible step before the only irreversible one:
resolve → select asset → download → verify signature → verify checksum → extract → self-test → mark executable → swap.
A failure anywhere leaves the installed binary untouched, because the installed
binary is not touched until self-replace runs. There is no partial state to
recover from — the disk holds either the old version or the fully verified new one.
The self-test is the step that catches what cryptography cannot. A correctly signed
binary can still be built for the wrong architecture, linked against a library the
host lacks, or be the wrong release entirely. Running <staged> --version and
requiring the release tag in its output is a cheap end-to-end check that the thing
about to replace your tool actually runs and is what it claims to be.
It is also the step most likely to surprise you, because rtb-cli does not give a
tool a root --version flag — see
the limitation.
Why automatic updates default to off¶
UpdatePolicy::Disabled is the default, and a user cannot override whatever the
tool author compiled in.
An automatic check is a network call on every invocation, on a throttle. An
automatic update under UpdatePolicy::Enabled is worse: the user asked to run one
command and got a binary replacement first, and any failure in that replacement
fails the command they actually wanted. Prompt exists so the common case — tell
me, let me decide — does not require choosing between silence and surprise.
Since users have no override, Enabled is a decision made on their behalf every
time they run the tool. Default off is the only defensible starting point.
Related¶
updatereference — the flags, the steps, the failure codes.- Automatic update checks — the policy and its throttle.
- Ship self-update — the release-side work.