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.
Related¶
- Sign artefacts with minisign — the task version.
- File formats — the byte layout of both.
- The trust model — what the OpenPGP path anchors on.
- What this module does not do.