Sign artefacts with minisign¶
Sign a binary release artefact so that a Rust consumer — cargo-binstall at
install time, rtb-update on self-update — can verify it. Those tools do not
parse OpenPGP, so the armoured output of openpgpkey is unreadable to them.
The minisign package emits what they do read.
This is a different job from signing a checksums
manifest: there, one OpenPGP signature covers a
manifest listing every asset. Here, each artefact gets its own .minisig file
alongside it. Many projects publish both. For why they coexist, see
Why there are two signature formats.
Get an Ed25519 signer¶
minisign is Ed25519-only. An RSA signer returns
minisign.ErrUnsupportedKeyType, so the RSA key that signs your OpenPGP
manifest cannot also sign artefacts — you need a second key.
Either key custody path works, and the calling code is identical:
# Local: an unencrypted PKCS#8 Ed25519 key the `local` backend can read.
openssl genpkey -algorithm ed25519 -out artefacts.pem
backend, err := signing.Get("local") // blank-import .../signing/local
if err != nil {
return err
}
signer, err := backend.NewSigner(ctx, "artefacts.pem")
For production custody, use an ECC_NIST_EDWARDS25519 key in AWS KMS through
the signing-aws-kms
backend. Everything below is unchanged — minisign.Sign takes any
crypto.Signer.
Sign an artefact¶
import "gitlab.com/phpboyscout/go/signing/minisign"
f, err := os.Open("tool_1.2.3_linux_amd64.tar.gz")
if err != nil {
return err
}
defer f.Close()
sigFile, err := minisign.Sign(signer, f, minisign.Options{
Filename: "tool_1.2.3_linux_amd64.tar.gz",
Project: "yourtool",
Time: releaseTime, // pin it — see below
})
if err != nil {
return err
}
// Conventionally written next to the artefact.
if err := os.WriteFile("tool_1.2.3_linux_amd64.tar.gz.minisig", sigFile, 0o644); err != nil {
return err
}
Sign streams the artefact through BLAKE2b-512, so memory stays constant
however large it is. It then makes exactly two calls to the signer: one over
the 64-byte digest, one over the global signature's message. Both are far below
the 4096-byte cap a KMS imposes on a signing message — which is precisely why
an HSM-held key can sign a multi-megabyte archive at all.
The .minisig suffix is load-bearing for rtb-update, which selects its
parser by extension. cargo-binstall's signature URL is configurable and can
be pointed at the same file.
Make the signature reproducible¶
Leave Options.Time at its zero value and the signature embeds time.Now(),
so re-signing identical input produces a different file every run. Pin it — to
the release tag's timestamp, or SOURCE_DATE_EPOCH — and re-signing identical
input yields byte-identical output:
epoch, err := strconv.ParseInt(os.Getenv("SOURCE_DATE_EPOCH"), 10, 64)
if err != nil {
return err
}
opts := minisign.Options{Filename: name, Time: time.Unix(epoch, 0).UTC()}
Nothing else in the file is time-dependent, so pinning Time is sufficient.
Publish the public key¶
Consumers need the public key as a base64 body. cargo-binstall pins that
string in the crate's [package.metadata.binstall.signing] table.
pub, ok := signer.Public().(ed25519.PublicKey)
if !ok {
return errors.New("signer is not Ed25519")
}
key, err := minisign.NewPublicKey(pub)
if err != nil {
return err
}
fmt.Println(key.Encode()) // the bare base64 body, for pinning
keyFile, err := key.File("yourtool release key") // the two-line .pub file
if err != nil {
return err
}
The key identifier is derived as SHA-256(public_key)[:8], so it is stable
across rebuilt signing hosts and can be re-derived from the KMS public half
alone. Nothing needs storing alongside the key.
Verify a signature¶
key, err := minisign.ParsePublicKey(pinnedPubKeyString) // bare body or whole file
if err != nil {
return err
}
f, err := os.Open("tool_1.2.3_linux_amd64.tar.gz")
if err != nil {
return err
}
defer f.Close()
if err := minisign.Verify(key, f, sigFile); err != nil {
return fmt.Errorf("refusing artefact: %w", err)
}
Verify runs the same checks as minisign-verify with allow_legacy = false,
in the same order: algorithm tag, key identifier, artefact signature, global
signature. A file this accepts is a file cargo-binstall accepts.
Use VerifyDigest when you already hold the BLAKE2b-512 digest, or VerifyFile
when the artefact is already in memory.
Choose the right comment for the right job¶
A signature file carries two comments, and they are not equivalent.
| Comment | Signed? | Use it for |
|---|---|---|
UntrustedComment |
No | Human orientation only. Never make a trust decision on it. |
TrustedComment |
Yes, by the global signature | Metadata you want tamper-evident: the filename, the timestamp, the project. |
Leave TrustedComment empty and it is built for you from Filename, Time
and Project. Set it explicitly and Filename, Project and Time are all
ignored.
Both comments are rejected if they contain a newline — which would corrupt the
line-oriented format and could forge extra fields — or if they exceed the
reference implementation's byte limits (1024 untrusted, 8192 trusted). Those
limits are enforced here because the Rust verifiers do not enforce them: an
over-long comment would verify in every test you can run and then fail against
upstream minisign -V.
What this will not do¶
- Sign with an RSA key.
ErrUnsupportedKeyType. Keep a separate Ed25519 key for artefacts. - Produce or accept legacy
"Ed"signatures. Only the prehashed"ED"variant is supported, in both directions. Verifying a legacy signature returnsErrUnsupportedAlgorithm— ascargo-binstallalso does. - Generate a key pair. Bring a
crypto.Signer. - Read or write minisign secret-key files. The private half stays wherever your backend keeps it.
- Express expiry or revocation. minisign has no such concept. Rotating a key means publishing a new public key and re-signing.
Related¶
- Why there are two signature formats — why OpenPGP and minisign coexist, and why prehashing is the point.
- File formats — the byte layout of both files.
- Errors — every failure mode of this package.
- Limits, defaults and tunables.