Skip to content

keys

keys <mint | generate | wkd | minisign | publish> [flags]

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

keys mint --backend <name> --key-id <id> --name <name> --email <email> [flags]

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

keys generate --algorithm <ed25519|rsa> --name <name> --email <email> [flags]

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 format gpg --export-secret-keys uses). Intended for rotation-authority keys.
  • --algorithm rsa — public half: armored OpenPGP; private half: PKCS#1 PEM. Pairs with keys mint --backend local for 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 mint so 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

keys wkd <public-key.asc> [<public-key.asc>...] --domain <domain> [flags]

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

keys minisign --backend <name> --key-id <id> [--output <file>] [--comment <text>] [--force]

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 pubkey field of the crate's [package.metadata.binstall.signing] table. Use the base64 string as-is.
  • rtb-updateToolMetadata::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:

untrusted comment: phpboyscout <project> artefact signing v<N> (minisign key <KEYID>)

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