The quickest way to understand release signing is to do it once, by hand, with
nothing but a key on disk. No cloud account, no CI, no cost. This first part of
the signing series
walks the whole loop on your laptop: make a key, sign a file, and verify the
signature, including with plain gpg so you can see it isn’t locked to anything
of ours. Everything you learn here maps one-to-one onto the AWS KMS workflow in
the later parts; only where the key lives changes.
You’ll need the gtb CLI (installation docs).
Make a scratch directory to work in, because we’re going to create a few files.
Make a signing key
gtb keys generate creates a keypair entirely inside the process and writes
both halves to disk:
gtb keys generate \
--algorithm rsa \
--name "Acme Releases" \
--email "[email protected]" \
--created "2026-06-01T00:00:00Z" \
--output signing.asc
INFO Generated OpenPGP keypair algorithm=rsa public_output=signing.asc private_output=signing.pem creation_time=2026-06-01T00:00:00Z fingerprint=...
WARN Move the private-half file to offline storage now. private_output=signing.pem
Two files come out: signing.asc (the public half) and signing.pem (the
private half, a PKCS#1 PEM). The private half is the thing you guard. There’s no
on-disk passphrase in this version of gtb, so keep it under filesystem
encryption (LUKS, FileVault, or wrap it with age) rather than leaving it lying
about.
One flag is doing quiet but important work: --created. An OpenPGP key’s
fingerprint is derived partly from its creation time, so if you let it default
to “now”, every run produces a different fingerprint. Pin it to a fixed
instant and the key is reproducible, which matters the moment you start
embedding it in a binary. Get in the habit now.
Mint the public key you’ll actually publish
You could hand signing.asc around as-is, but we’re going to produce the public
key a slightly different way, with gtb keys mint:
gtb keys mint \
--backend local \
--key-id signing.pem \
--name "Acme Releases" \
--email "[email protected]" \
--created "2026-06-01T00:00:00Z" \
--output release.asc
INFO Minted OpenPGP key backend=local key_id=signing.pem output=release.asc creation_time=2026-06-01T00:00:00Z fingerprint=...
mint wraps a signing backend in OpenPGP framing and writes out the armored
public key. Here the backend is local (a PEM file on disk), but in production
it’ll be aws-kms pointing at a key you can’t hold. Minting the public key from
the backend is the one habit worth forming early: it’s the only way to get the
public half of a KMS key, so doing it the same way locally means the rest of the
series is identical bar one flag. release.asc is the key you publish and embed
from here on. (Because we pinned the same --created, its fingerprint matches
the generated one exactly.)
Sign something
A real release signs its checksums.txt, so make a stand-in and sign it:
printf 'abc123 acme_linux_amd64\ndef456 acme_darwin_arm64\n' > checksums.txt
gtb sign \
--backend local \
--key-id signing.pem \
--public-key release.asc \
checksums.txt
INFO Signed file backend=local key_id=signing.pem public_key=release.asc input=checksums.txt output=checksums.txt.sig ...
That writes checksums.txt.sig, a detached, ASCII-armored OpenPGP signature.
Note gtb sign takes --public-key: it cross-checks that the backend key
matches the public key you claim to be signing as, and refuses if they diverge,
so you can’t accidentally sign with the wrong key.

Verify it, two ways
First, the way your tool will do it on every self-update: against the public
key. That path is the subject of a signature the platform can’t
forge and we
wire it into a real binary in part 5. For now, prove the signature is sound with
something every machine already has, gpg:
gpg --import release.asc
gpg --verify checksums.txt.sig checksums.txt
gpg: Signature made ...
gpg: using RSA key ...
gpg: Good signature from "Acme Releases <[email protected]>"
Good signature is the whole point. The signature gtb sign produced is an
ordinary OpenPGP detached signature, so anyone can verify it with the standard
tool, no go-tool-base required. (gpg will warn the key isn’t certified in its
web of trust; that’s expected and unrelated to whether the signature is valid.)
Now change a byte of checksums.txt and run the verify again. gpg reports
BAD signature. That failure is the entire reason any of this exists: a tampered
manifest no longer matches the signature, and a tool that requires a valid
signature will refuse the update.
Where this leaves you
You’ve signed a file with a key you made and verified it independently. That’s
the complete trust loop in miniature, and the shape never changes: a private key
signs, a public key verifies, and the two are produced and checked the same way
whether the private half is a .pem on your laptop or an HSM-held key in AWS.
The local key was the easy bit, and also the weakest: it’s a file, and files get
copied. Part 2
moves the private key somewhere it can’t be copied at all, AWS KMS, and the only
command that changes is the --backend flag.
