Skip to content

Why there are two signature formats

The module signs the same release twice, in two unrelated formats, with two different keys. That looks like indecision. It is not — the two formats answer to two different audiences, and neither can be dropped without losing one of them.

Two audiences that cannot read each other's signatures

The OpenPGP path signs a checksums manifest: one file listing a hash per asset, covered by one detached signature. That suits a consumer that downloads the manifest, verifies it once, and then checks each asset against its line. It is also the format gpg --verify speaks, so a suspicious user can check a release with tools they already trust and nothing of ours installed.

The minisign path signs each artefact individually. That suits a consumer that fetches exactly one binary and wants to verify precisely that binary, without first fetching and parsing a manifest.

The forcing constraint is that the second kind of consumer here is written in Rust. cargo-binstall verifies at install time and rtb-update verifies on self-update, and neither parses OpenPGP. An armoured OpenPGP signature is not merely inconvenient for them; it is unreadable. Publishing only OpenPGP would mean those installs verify nothing at all.

So the answer is not "pick the better format". Each audience can read exactly one of the two.

Why the prehashed variant, and not the obvious one

minisign has two on-wire variants, distinguished by a two-byte tag:

  • "Ed" — legacy, "pure": ed25519(<whole file bytes>).
  • "ED" — prehashed: ed25519(BLAKE2b-512(<whole file bytes>)).

This module produces and verifies "ED" only. Two independent reasons point the same way, which is why there is nothing to weigh up.

The first is consumer mandate. cargo-binstall runs minisign-verify with allow_legacy = false and rejects "Ed" outright. A legacy signature would simply fail at the point it mattered.

The second is key custody, and it is the more interesting one. An HSM-backed crypto.Signer such as AWS KMS caps its signing message at 4096 bytes. A multi-megabyte release archive can never be that message, so a pure signature over the whole file is impossible for an HSM-held key — you would have to hold a software signing key instead, which is the thing the whole design exists to avoid.

Prehashing moves the hard part outside the signer. The caller collapses the artefact to a 64-byte digest, which is trivially within the cap, and the HSM signs that. Full HSM custody, no software key, arbitrary artefact size. The variant the consumers demand turns out to be the variant that makes custody possible.

Worth being precise about what "ED" is not: it is not Ed25519ph / HashEdDSA. It is a plain hash-then-sign construction — the caller hashes, and an ordinary Ed25519 signing operation signs that digest as its message. Driving a KMS with its ED25519_PH_SHA_512 algorithm produces something neither consumer can verify, and the failure would surface a long way from its cause.

Why the two paths need two keys

OpenPGP signing here is RSA-only, and minisign is Ed25519-only, so a single key cannot do both.

That is not an accident of implementation. The OpenPGP entity this module mints binds to the key algorithm, and RSA is what a KMS reliably offers for the OpenPGP path. minisign is Ed25519 by construction — the format has no other algorithm. Handing minisign.Sign an RSA signer returns ErrUnsupportedKeyType, and handing openpgpkey.ArmoredPublicKey an Ed25519 signer returns its own ErrUnsupportedKeyType.

A project publishing both therefore holds two release keys, ideally with the same custody posture. The AWS KMS backend supports both key specs — RSA_4096 for OpenPGP and ECC_NIST_EDWARDS25519 for minisign — so "two keys" does not mean "two custody stories".

The trust anchors are not shared either

This is the sharper edge of publishing both, and it is worth stating plainly rather than discovering.

The OpenPGP path has a full trust model behind it: a trust set, a minimum strength policy, embedded keys cross-checked against a key published over WKD, and expiry and revocation enforced by go-crypto at verification time. See The trust model.

The minisign path has none of that. Verification takes one public key, supplied by the caller, and checks a signature against it. There is no resolver, no cross-check, no strength policy — Ed25519 has no weak sizes to police — and no expiry or revocation, because the format has no way to express either.

So a consumer's confidence in a minisign signature rests entirely on how the pinned public key reached them. For cargo-binstall that is the pubkey string in the crate metadata, which is as trustworthy as the crate publishing process. Rotating such a key means publishing a new one and re-signing; there is no revocation to broadcast.

That asymmetry is a property of the formats, not a gap in this module. It is the reason a project that can use the OpenPGP path should, and the reason the minisign path exists anyway: some consumers have no other option.