Skip to content

sign

sign <input-file> --backend <type>[:<instance>[?<settings>]] --key-id <id> [--format openpgp|minisign] [flags]

Constructor: signingcli.NewCmdSign(log Logger) *cobra.Command.

Produce a detached signature over a single input file using a crypto.Signer resolved from a compiled-in go/signing backend. Signatures are written with mode 0644, because they are public artefacts distributed alongside the signed file.

The private key never leaves the backend. Signing is one round-trip (two for minisign) through the crypto.Signer interface.

sign overwrites an existing output file without asking. There is no --force flag and no prompt; the only path it refuses is one that resolves to the input file itself.

Two formats, two audiences

--format Output Signs For
openpgp (default) <input>.sig ASCII-armored OpenPGP detached signature checksum manifests, Go self-update
minisign <input>.minisig prehashed ED minisign signature release artefacts, verified by cargo-binstall and rtb-update

The key type decides the format, not the flag

The two formats need two different key types, and neither will accept the other's:

  • --format openpgp requires an RSA key. An Ed25519 key fails with DetachSign: ed25519.PublicKey: unsupported key type: only RSA is supported. The restriction comes from go/signing's openpgpkey package and is structural: go-crypto's OpenPGP Ed25519 path cannot be driven through a crypto.Signer, so a KMS-held Ed25519 key could not work here regardless.
  • --format minisign requires an Ed25519 key, which for AWS KMS means an ECC_NIST_EDWARDS25519 key. An RSA key is refused before any signing happens.

A project signing both a checksum manifest and release artefacts therefore needs two keys. See What signing-cli does not do.

How the minisign signature is produced

--format minisign hashes the artefact with BLAKE2b-512 and signs the 64-byte digest, so an HSM-held key can sign an artefact of any size despite the KMS 4096-byte message cap, and the key never leaves the HSM to do it. It takes no --public-key: a minisign signature carries its own key identifier, so the identity comes from the signer itself. Publish the matching public key with keys minisign.

The .minisig extension matters, because rtb-update selects its parser by extension. cargo-binstall's signature URL is configurable, so one file serves both.

Recording project identity

--project appends project:<name> and key:<KEYID> to the trusted comment, after minisign's conventional timestamp/file/hashed fields so anything parsing the standard format still finds what it expects:

timestamp:1785326400    file:tool_1.2.3_linux_amd64.tar.gz  hashed  project:rtb-cli-bin key:56475AA75463474C

The trusted comment is covered by the signature's global signature, so these fields are tamper-evident: altering the project name makes verification fail. The key: field duplicates the key ID already in the signature body. The body is authoritative; the copy exists so a signature file is greppable and self-describing under review.

Comment lengths are validated against the reference minisign implementation's limits (1024 bytes untrusted, 8192 trusted). No Rust verifier enforces these, so an over-long comment would verify in cargo-binstall and then fail against upstream minisign -V. sign refuses to emit one.

Arguments

Argument Required Description
<input-file> yes (exactly one) Path to the file to sign.

Flags

Flag Required Default Description
--backend yes — Backend selector: a backend type compiled into the binary, or a named instance of one with inline settings. See Selecting a backend. --help lists the compiled-in types.
--key-id yes — Backend-specific key identifier. For aws-kms: a KMS key ID, ARN, or alias. For local: a PEM file path.
--format no openpgp openpgp or minisign. Any other value is an error listing the valid ones.
--project minisign only — Project this artefact belongs to, recorded in the signature's signed trusted comment as project:<name> alongside key:<KEYID>. Omit to emit minisign's exact default comment. Rejected for --format openpgp, which has no trusted comment to record it in.
--public-key openpgp only — Path to the armored OpenPGP public-key file (.asc, produced by keys mint). Must contain the public half of the key resolved by --backend / --key-id. Rejected for --format minisign.
--output no <input>.sig, or <input>.minisig Output file path for the detached signature. Refuses to equal the input path.
--created no now Signature creation time (RFC3339), truncated to whole seconds. Pin it to produce byte-identical signatures across re-runs. Must not be earlier than the signing key's creation time (see below). For --format minisign this sets the trusted comment's timestamp, and SOURCE_DATE_EPOCH is honoured when the flag is unset. --format openpgp ignores SOURCE_DATE_EPOCH.
--append openpgp only false Merge the new signature into an existing armored signature at --output instead of overwriting, producing one armored block with multiple signature packets. No-op when --output does not exist yet. Rejected for --format minisign, which has no equivalent.

Backends may register additional flags. The aws-kms backend, for example, adds --kms-region (default eu-west-2). These appear in --help only when a backend that declares them is compiled in, and they configure that backend's registered default and nothing else (see the next section).

Selecting a backend

--backend takes one of three forms. The same grammar applies to keys mint and keys minisign, which share the resolver.

Form Example Resolves to
<type> aws-kms The backend's registered default, configured by its top-level flags such as --kms-region.
<type>:<instance> aws-kms:prod A fresh instance minted through signing.NewInstance, with every backend flag at its default.
<type>:<instance>?<settings> 'aws-kms:prod?kms-region=eu-west-1' The same, with the settings applied to that instance only.

A named instance lets one binary sign through two differently configured copies of the same backend, say a production and a staging KMS account, without the two sharing flag values. The canonical name (aws-kms:prod) is what appears in the backend field of the log line. The settings never do.

sign --backend 'aws-kms:prod?kms-region=eu-west-1' \
    --key-id alias/release-signing-v1 --public-key release.asc checksums.txt

The rules, each enforced before anything is read or signed:

  • Instance names match ^[a-z0-9][a-z0-9-]*$: lowercase letters, digits and hyphens, starting with a letter or digit. aws-kms:Prod and aws-kms: are both refused.
  • Settings are only valid on a named instance. local?x=1 is refused; give the instance a name first.
  • Settings are url-encoded key=value pairs joined by &. Percent-encode a value that contains &, =, + or %.
  • Each key must be a flag the backend declares, written without the leading --. For aws-kms that is kms-region. An unknown key is refused, and so is any setting at all for a backend that declares no flags.
  • No duplicates, no empty keys or values, no stray &. kms-region=a&kms-region=b, kms-region=, =eu-west-1, a bare kms-region, and a leading, trailing or doubled & are each refused.
  • A top-level backend flag cannot be combined with a named instance. --backend aws-kms:prod --kms-region eu-west-1 fails, because --kms-region is bound to the registered default and would otherwise be silently ignored. Put the value in the selector instead.
  • The type must offer named instances. A backend opts in by registering a factory with signing.RegisterFactory. local does from go/signing v0.8.0, which this module requires, and aws-kms does from signing-aws-kms v0.6.0. A type compiled in without a factory gets an error telling you to use the bare <type> form or update the backend module.

Quote the whole value in a shell. An unquoted & ends the command there and runs it in the background, and ? is a glob character that zsh, by default, refuses to pass through when nothing matches it.

Validation runs in two phases. Every setting is checked before any is applied, so a bad selector never leaves an instance half-configured. The errors name the setting key rather than its value. A setting with an empty key (?=value), no = at all (?value) or a malformed %-escape in its key is named by its position instead, counting from 1. The exact messages are listed under A named instance was refused.

--created must not predate the key

--created is used as go-crypto's notion of "now" while the signature is built, so a timestamp from before the signing key was created makes the key look not-yet-valid and the run fails:

Error: computing signature: computing detached signature: openpgp: invalid argument: no valid signing keys

The message does not mention the timestamp. If you see it while pinning --created, compare the value against the key's creation date: gpg --list-packets release.asc prints it as created <unix-seconds>.

Pinning --created to a valid instant does give byte-identical output: signing the same file twice with the same key and the same --created produces two identical .sig files.

--append and key rotation

--append is the dual-sign primitive for a key-rotation overlap window. Sign once with the old key, then --append the new key's signature into the same armored file:

sign --backend aws-kms --key-id alias/release-signing-v1 \
    --public-key v1.asc --output checksums.txt.sig checksums.txt
sign --backend aws-kms --key-id alias/release-signing-v2 \
    --public-key v2.asc --output checksums.txt.sig --append checksums.txt

Verifiers skip signature packets from issuers they do not know, so one armored file carrying both packets verifies for binaries that trust either key.

Examples

# Sign checksums.txt with a production KMS key
sign \
    --backend aws-kms \
    --kms-region eu-west-2 \
    --key-id alias/gtb-release-signing-v1 \
    --public-key release.asc \
    checksums.txt

# The same key through a named instance, configured inline
sign \
    --backend 'aws-kms:prod?kms-region=eu-west-2' \
    --key-id alias/gtb-release-signing-v1 \
    --public-key release.asc \
    checksums.txt

# Reproducible signature: pin --created to a fixed instant
sign \
    --backend local \
    --key-id ./release.pem \
    --public-key ./release.asc \
    --created 2026-01-01T00:00:00Z \
    --output build.txt.sig \
    build.txt

Output

With --format openpgp, sign writes the armored .sig file and logs a Signed file INFO line carrying backend, key_id, public_key, input, output, sig_creation_time and the primary-key fingerprint, so operators can confirm the signing identity without a follow-up gpg --verify.

With --format minisign it writes the .minisig file and logs a Signed artefact INFO line carrying format, backend, key_id, minisign_key_id, input, output, project and trusted_comment_time.

See also