keys¶
Constructor: signingcli.NewCmdKeys(log Logger) *cobra.Command — the parent
group with all five subcommands attached. Each subcommand also has its own
exported constructor (NewCmdKeysMint, NewCmdKeysGenerate, NewCmdKeysWKD,
NewCmdKeysMinisign, NewCmdKeysPublish) if you want to attach them
individually.
keys groups the cryptographic operations a tool author runs while setting up
release-binary signing:
| Subcommand | Purpose |
|---|---|
mint |
Wrap an existing signer (KMS or local PEM) in OpenPGP framing and emit the armored public half. |
generate |
Generate a fresh keypair locally (Ed25519 or RSA) and emit both halves. |
wkd |
Generate a Web Key Directory tree from one or more public keys. |
minisign |
Emit the minisign public key for an Ed25519 signer — the string release consumers pin. |
publish |
Stage a minisign public key into a keys site, with a machine-readable manifest. |
Key output files use 0644 for public (armored) halves — they are meant to be
distributed — and 0600 for private halves. mint, generate and minisign
write with O_EXCL and refuse to clobber an existing file unless --force is
passed, so a private key is never silently overwritten. wkd and publish behave
differently: wkd has no --force and overwrites, and publish is add-only and
refuses a changed key outright.
Two of the five need a signing backend and three do not:
| Subcommand | Backend | Key type |
|---|---|---|
mint |
required | RSA only |
minisign |
required | Ed25519 only |
generate |
none — generates in-process | RSA or Ed25519 |
wkd |
none — reads armored public keys from disk | any |
publish |
none — reads a minisign public-key file | Ed25519 (minisign) |
keys mint¶
Constructor: signingcli.NewCmdKeysMint(log Logger) *cobra.Command.
Wrap an existing signer — an HSM/KMS-held key or an on-disk PEM private key — in OpenPGP framing and write the armored public half to a file. The private half never touches this tool; every signing operation flows through the backend.
mint handles RSA keys only. Pointing --key-id at an Ed25519 key fails with
minting armored public key: got ed25519.PublicKey: unsupported key type: only RSA
is supported. The Ed25519 counterpart is keys minisign, which
emits a minisign public key rather than an OpenPGP one.
The minted key is a single primary key with sign and certify capability, one User ID, and no expiry. No subkey and no revocation certificate are produced.
Flags¶
| Flag | Required | Default | Description |
|---|---|---|---|
--backend |
yes | — | Signing backend name (compiled-in; --help lists them). |
--key-id |
yes | — | Backend-specific key identifier. aws-kms: KMS key ID/ARN/alias. local: PEM file path. |
--name |
yes | — | OpenPGP user-id real name (e.g. "GTB Release"). |
--email |
yes | — | OpenPGP user-id email (e.g. release@example.com). |
--output |
no | release.asc |
Output file path for the armored public key. Stdout (-) is not supported. |
--created |
no | now | Creation time (RFC3339). Pin only when re-minting an existing key — a different creation time yields a different fingerprint. |
--force |
no | false |
Overwrite an existing output file. |
Backend-specific flags (e.g. --kms-region) are added by the backend when
compiled in.
Output¶
Writes the armored public key and logs an INFO line with backend, key_id,
output, creation_time, and the fingerprint. Publish the resulting .asc via
keys wkd and/or embed it in your tool's trust set; pass it to
sign --public-key.
keys generate¶
Constructor: signingcli.NewCmdKeysGenerate(log Logger) *cobra.Command.
Generate a fresh keypair entirely in-process (no shell-out, no backend) and write both halves to disk. Used during onboarding for a rotation-authority key and for the tutorial / local signing key.
The on-disk format of the private half depends on the algorithm:
--algorithm ed25519— public half: armored OpenPGP; private half: armored OpenPGP secret-key block (the formatgpg --export-secret-keysuses). Intended for rotation-authority keys.--algorithm rsa— public half: armored OpenPGP; private half: PKCS#1 PEM. Pairs withkeys mint --backend localfor the tutorial path.
The two algorithms do not produce the same shape of key. --algorithm rsa builds
the entity from a generated RSA key and emits a primary key only. --algorithm
ed25519 hands the whole job to go-crypto's NewEntity, which additionally
creates a Curve25519 encryption subkey you did not ask for and cannot
suppress. Neither key carries an expiry date.
The private half is always written unencrypted, whichever format you choose, and there is no passphrase flag. Use filesystem-level encryption (LUKS, FileVault, age) and move the private half to offline storage immediately — or use a KMS-held key with
keys mintso no private half exists on disk at all.
Flags¶
| Flag | Required | Default | Description |
|---|---|---|---|
--algorithm |
yes | — | ed25519 or rsa. |
--private-format |
no | algorithm default | openpgp (armored secret key — the Ed25519 default) or pem. For ed25519, pem writes an unencrypted PKCS#8 PEM. RSA always writes PKCS#1 PEM, so openpgp is rejected for it. |
--rsa-bits |
no | 4096 |
RSA modulus size; one of 2048, 3072, 4096. Ignored for ed25519. |
--name |
yes | — | OpenPGP user-id real name. |
--email |
yes | — | OpenPGP user-id email. |
--output |
no | <algorithm>.asc |
Output path for the armored public key. |
--private-output |
no | derived from --output |
Private-half path: .asc → .priv.asc for Ed25519 (or .pem with --private-format pem), .asc → .pem for RSA. Must differ from --output. |
--created |
no | now | Creation time (RFC3339). Pin only when re-deriving an existing key. |
--force |
no | false |
Overwrite existing output files. |
Generating a key for minisign artefact signing¶
--algorithm ed25519 --private-format pem produces the one thing that makes the
artefact path usable without an HSM: a PKCS#8 PEM that
go/signing's local backend reads.
keys generate --algorithm ed25519 --private-format pem \
--name "Demo Artefact Signing" --email demo@example.com \
--output demo.asc --private-output demo.pem
sign --format minisign --backend local --key-id ./demo.pem \
--project demo tool.tar.gz
The default armored OpenPGP private half cannot be used this way — the local
backend reads PEM. Note the key is written to disk unencrypted, so this is for
tutorials, tests and projects with no KMS; production artefact signing uses a
KMS-held key, where the private half never materialises.
The written PEM is checked against the public half before being emitted: a
private key that disagreed with the published .asc would produce signatures
that fail against the very key you distributed.
This subcommand takes no backend and no backend-specific flags.
Output¶
Writes both halves, logs an INFO line with algorithm, public_output,
private_output, creation_time, and fingerprint, then a WARN line reminding
you to move the private half to offline storage.
The private half is written first, so a failure on the public half — most
often the file already existing without --force — exits non-zero with the
private half already on disk. Check for a stray .pem or .priv.asc after any
failed run.
keys wkd¶
Constructor: signingcli.NewCmdKeysWKD(log Logger) *cobra.Command.
Read one or more armored OpenPGP public keys, group them by email, and write a
Web Key Directory
tree (per draft-koch §3.1) under --output, ready to upload to a static host.
For each requested email, keys whose UIDs contain that email are concatenated —
in lexicographic fingerprint order for reproducible output — into a single
hu/<z-base-32-hash> bucket. A key with multiple matching UIDs lands under every
matching email's bucket (standard WKD behaviour).
Arguments¶
| Argument | Required | Description |
|---|---|---|
<public-key.asc>… |
yes (one or more) | Armored OpenPGP public-key files. A file holding a key ring contributes each entity individually. |
Flags¶
| Flag | Required | Default | Description |
|---|---|---|---|
--domain |
yes | — | DNS domain serving the WKD endpoint (e.g. phpboyscout.uk). Appears in the URL path for the advanced method. |
--email |
no | all emails found | Email address(es) to publish under. Repeatable. If omitted, every distinct email across the input keys is published. |
--output |
no | ./wkd-staging |
Output directory; receives a .well-known/openpgpkey/… tree. |
--method |
no | advanced |
URL layout: advanced (served from openpgpkey.<domain>) or direct (served from <domain> itself). |
--submission-address |
no | (none) | Address to write to the WKD submission-address file. Empty means do not emit the file; auto uses the first resolved email — the first --email when given, otherwise the first discovered across the input keys; any other value is used verbatim. |
Output¶
Writes the tree and logs a WKD bucket INFO line per email (with its hash and
key count), a wrote line per file, and a final WKD tree complete summary.
Serve the .well-known/openpgpkey/… tree over HTTPS.
wkd overwrites and never prunes. There is no --force flag: an existing
hu/<hash> bucket, policy or submission-address file is rewritten silently,
and a bucket for an email you have stopped publishing is left in place and will be
deployed again. Build into a fresh directory when the tree needs to be exact.
--domain is required even for --method direct, which does not put it in the
path. It is validated as a hostname — letters, digits, hyphens and dots only, no
leading or trailing dot, no .., no path separators.
keys minisign¶
Constructor: signingcli.NewCmdKeysMinisign(log Logger) *cobra.Command.
The counterpart to mint for the artefact-signing path. Resolves
an Ed25519 key through a backend and emits its minisign public key — the
single base64 string release consumers pin. A non-Ed25519 key is refused with an
error naming the format it belongs to.
Nothing secret is involved: only the public half is read, which for a KMS-held
key is one GetPublicKey call. The key identifier is derived from the public key
itself, so re-running against the same key always produces the same output, on
any machine — a rebuilt signing host needs no saved state.
Flags¶
| Flag | Required | Default | Description |
|---|---|---|---|
--backend |
yes | — | Signing backend name. |
--key-id |
yes | — | Backend-specific key identifier. |
--output |
no | — | Write the two-line minisign public-key file here instead of printing the bare key. |
--comment |
no | minisign public key <id> |
Untrusted comment for the public-key file. Nothing signs it. |
--force |
no | false |
Overwrite --output if it exists. |
Output¶
Without --output, the bare base64 key on stdout, ready to pipe into whatever
pins it. With --output, the two-line public-key file, mode 0644.
Where it goes:
- cargo-binstall — the
pubkeyfield of the crate's[package.metadata.binstall.signing]table. Use the base64 string as-is. - rtb-update —
ToolMetadata::update_public_keys, which wants the raw 32-byte Ed25519 key: the last 32 bytes of the decoded body, not the base64 string itself.
keys publish¶
keys publish <public-key.pub> --project <name> [--output <dir>] [--generation N]
[--status active|retired|revoked] [--purpose <text>] [--valid-from YYYY-MM-DD]
Constructor: signingcli.NewCmdKeysPublish(log Logger) *cobra.Command.
Stages a minisign public key into a static keys site: writes it to
minisign/<project>/v<N>.pub and records it in a keys.json manifest at the
site root.
The published file is not a copy of the input¶
Only the key material is carried across. The untrusted comment line is regenerated:
So the published .pub differs byte-for-byte from the file
keys minisign --output produced, and the phpboyscout prefix
is fixed in the code rather than configurable. Nothing signs an untrusted comment,
so this is presentation only — but it means you cannot diff the two files to check
they match. Compare the base64 key line, or the pubkey field in keys.json.
The minisign counterpart to wkd, composing the same way — both
write into one staging directory you then deploy by hand:
keys wkd --output ./keys-staging --domain phpboyscout.uk --email … release.asc
keys publish --output ./keys-staging --project mytool mytool.pub
wrangler pages deploy ./keys-staging
Why it reads a file, not a backend¶
Minting a key touches your KMS; publishing it touches the trust anchor. Keeping
those apart means the publish step needs no cloud credentials, so a
compromised runner cannot rewrite what the world believes your keys are — the
same air gap wkd preserves for OpenPGP. Produce the input with
keys minisign --output.
Publishing is add-only¶
A key already published at the same path with different bytes is refused: consumers pin these keys, so changing one under them is precisely what must never happen. Rotate by publishing a new generation and marking the old one retired — never by replacing it.
Re-running with identical content is a no-op, so the command is safe to repeat and can be used to confirm a staging directory still matches production.
What can change on a re-run is the rest of the manifest entry: --status,
--purpose and --valid-from are overwritten in place as long as the key itself
is the same. That is how a generation moves from active to retired.
Retired and revoked keys stay published: a signature must remain checkable long after its key's private half is destroyed.
Flags¶
| Flag | Required | Default | Description |
|---|---|---|---|
--project |
yes | — | Project whose artefacts this key signs. Becomes a path segment, so it must match ^[a-z0-9][a-z0-9._-]{0,63}$ — start with a lowercase letter or digit, then lowercase letters, digits, dots, dashes or underscores, up to 64 characters total. |
--output |
no | ./keys-staging |
Site root. |
--generation |
no | 1 |
Key generation, incrementing on rotation. Must be 1 or greater. |
--status |
no | active |
active, retired or revoked. |
--purpose |
no | artefact |
What this key signs; recorded in the manifest. |
--valid-from |
no | today | Date the key started signing. Pin it for reproducible output. |
The manifest¶
keys.json is an audit and discovery record, never a trust input — consumers
verify against a key compiled into them or pinned in crate metadata. Its value is
that a pinned value can be checked against what was actually published:
{
"keys": [
{
"id": "56475AA75463474C",
"project": "rtb-cli-bin",
"generation": 1,
"algorithm": "minisign-ED",
"purpose": "artefact",
"status": "active",
"valid_from": "2026-08-01",
"pubkey": "RWRWR1qnVGNHTAOhB7/zzhC+HXDdGOdLwJln5NYwm6UNXx3chmQSVTG4",
"path": "minisign/rtb-cli-bin/v1.pub"
}
]
}
pubkey is the exact string pinned as cargo-binstall's pubkey, so the two can
be compared directly. Entries are sorted by project then generation, so the file
does not churn between runs.
Nothing else in the output directory is touched — a hand-maintained index.html
and a wkd-written tree both survive untouched.
See also¶
sign— consumes the public key minted here.- Attach the commands to a CLI — expose these under your own root.
- What signing-cli does not do — key types, encryption, expiry and the other boundaries these commands sit inside.
- Errors and what they mean — every message the
keyssubcommands can print. - Configuration and environment — default output paths and file modes.
- Publishing/serving detail:
go/signing— Publish keys via WKD.