Skip to content

Secure releases & self-update

rtb-update will only replace the running binary with an artefact it can verify. That guarantee depends on how you build and publish releases.

Build artefacts the updater trusts

For every release archive, publish a minisign detached signature alongside it, named <archive>.minisig.

The signature must be minisign's prehashed ED variant — ed25519(BLAKE2b-512(archive)). The legacy pure-Ed variant is rejected. That is deliberate: cargo-binstall requires the prehashed variant, so accepting only ED means the signature rtb-update trusts is byte-for-byte the one cargo-binstall trusts. Verification is delegated to [minisign-verify], the same crate cargo-binstall uses, rather than reimplemented — two implementations of one check is how the two consumers would silently drift apart.

Prehashing is also what allows an HSM- or KMS-held key to sign an artefact of any size: the signer only ever sees a 64-byte digest.

A SHA-256 checksums file is optional. Set ToolMetadata::update_checksums_asset to the asset's name and the updater will additionally verify the archive against it, in sha256sum format. Leave it unset — as rtb itself does — and the signature is the sole authority. It already covers the archive bytes, so a checksums file adds transport-error detection, not security.

Before swapping, the updater self-tests the staged binary: it runs it and checks it reports the expected version. Verification failure, a checksum mismatch, or a failed self-test all abort the swap and leave the running binary untouched — the binary on disk is always either the old version or the fully-verified new one, never a partial write.

Signing keys are pinned in ToolMetadata::update_public_keys, which is any-one-verifies: a rotation ships as {old, new} and spans the change with no dual-signing window.

How rtb's own releases are built

Tagging rtb-cli-bin-v* runs the dist stage in .gitlab-ci.yml: it builds the binary per target, signs each archive with sigillum against an AWS KMS-held Ed25519 key, uploads both to the generic package registry, and attaches them to the GitLab release as asset links. No private key material exists outside KMS; CI authenticates by OIDC and is permitted only to sign.

Versions and tags are produced by release-plz, not cargo-release.

Supported targets

rtb currently ships x86_64-unknown-linux-gnu and x86_64-unknown-linux-musl. macOS and Windows targets are not yet built — the eventual target set is darwin-{aarch64,x86_64}, linux-{aarch64,x86_64,musl}, windows-{aarch64,x86_64}.

Verify a download yourself

Every rtb archive is signed under this key, which is also pinned in the binary and published at keys.phpboyscout.uk:

RWSkmyHppi7WSWp8Mii3RJByLf0eLckGPVajCrpX+zRD/3WWhXCibJ7y

Download an archive and its .minisig from the releases page, then:

$ minisign -Vm rtb-<version>-<target>.tar.gz \
    -P 'RWSkmyHppi7WSWp8Mii3RJByLf0eLckGPVajCrpX+zRD/3WWhXCibJ7y'
Signature and comment signature verified

minisign verifies the trusted comment as well as the archive, so the comment cannot be altered without detection. Compare the key above against the published copy before trusting a first download — pinning it in the binary only protects upgrades, not the initial install.

Install with cargo binstall

rtb-cli-bin carries [package.metadata.binstall], so cargo-binstall fetches the signed release binary and verifies the signature before installing — the same .minisig, checked against the same key:

$ cargo binstall rtb-cli-bin
INFO Verified signature for package 'rtb-cli-bin': … key:A49B21E9A62ED649

If verification fails, cargo-binstall refuses the binary and falls back to building from source. That fallback is deliberate and load-bearing: prebuilt binaries currently exist only for the two Linux targets above, so every other platform reaches rtb through a source build. Disabling the fallback would turn "slower install" into "no install".

cargo-binstall and rtb-update verify the same file with the same crate ([minisign-verify]). Neither has its own opinion about what a valid signature is, which is what keeps the two from drifting apart.

Wire the updater

use rtb_update::{CheckOutcome, RunOptions, Updater};

# async fn check(app: &rtb_app::app::App, provider: std::sync::Arc<dyn rtb_forge::ReleaseProvider>) -> Result<(), rtb_update::UpdateError> {
let updater = Updater::builder().app(app).provider(provider).build();

match updater.check().await? {
    CheckOutcome::Newer { latest, .. } => {
        println!("updating to {latest}");
        updater.run(RunOptions::default()).await?;
    }
    CheckOutcome::UpToDate { .. } => println!("already current"),
    CheckOutcome::Older { .. } => println!("local build ahead of upstream; not downgrading"),
}
# Ok(())
# }

The ReleaseProvider is selected by ToolMetadata::release_source and wrapped in Arc<dyn ReleaseProvider> — downstream tools never import octocrab or gitlab directly. See rtb-forge (formerly rtb-vcs).

Airgapped updates

Updater::run_from_file verifies and swaps from a locally-staged archive, for environments with no network path to the release host. The same signature verification and self-test apply — staging a file by hand does not bypass them.

Dry run

Set RunOptions { dry_run: true, .. } to verify and stage the new binary into the cache dir without swapping — useful for validating a release pipeline end to end.