Featured image of post Sign your own binaries with go-tool-base, part 1: sign and verify on your laptop

Sign your own binaries with go-tool-base, part 1: sign and verify on your laptop

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 "release@acme.dev" \
    --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 "release@acme.dev" \
    --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.

Signing a checksums file and verifying it, then a tampered copy failing

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 <release@acme.dev>"

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.

Built with Hugo
Theme Stack designed by Jimmy