Featured image of post A good signature in ten minutes

A good signature in ten minutes

Ten minutes, a throwaway directory and no cloud account, to find out what a release signature actually is before you decide whether you want one in your pipeline.

Signing releases is one of those things that sits on the list for years looking bigger than it is. There’s key management in it, and key management has a reputation.

So before any of that, it’s worth spending ten minutes finding out what a signature actually is, with a throwaway key on your own machine, where the worst thing that can happen is you delete a directory afterwards.

That’s what this walkthrough does. A keypair on disk, a signature over a file, and gpg saying Good signature about something a tool you have never used before produced. Then you break the file and watch it say BAD signature, which is what proves the first bit meant anything.

It uses the local backend, which signs with a private key file sitting on your disk. That keeps the whole thing free of cloud accounts, and it is emphatically the wrong choice for a real release: a key on disk is a key that can be copied. Production wants KMS, where the private half never leaves the service. But you don’t need to have solved that to find out whether any of this is for you.

This is the canonical walkthrough from the sigillum docs, reproduced here so the two don’t drift: https://sigillum.phpboyscout.uk/tutorials/sign-and-verify-your-first-release/. Every command and output below was validated by running it against v0.5.3 with GnuPG 2.4.4.

What you need

  • Go 1.27.2 or newer, if you install from source. That’s the module’s go directive and an older toolchain refuses to build.
  • gpg, so the verification is done by an implementation that is not sigillum. Any GnuPG 2.2 or later.
  • A scratch directory you don’t mind filling with key material.

Work somewhere throwaway. A later step writes a private key, and you do not want that landing in a repository:

mkdir -p /tmp/sigillum-tutorial && cd /tmp/sigillum-tutorial

Install it

go install gitlab.com/phpboyscout/sigillum/cmd/sigillum@latest

The main package lives at cmd/sigillum/, so the install path ends in /cmd/sigillum. If you’d rather not build, there are pre-built binaries on the releases page.

sigillum --help

You should see sign, keys, certificate and decrypt in the command list, alongside framework built-ins like config, doctor, update and version. If the shell can’t find it, $(go env GOPATH)/bin is probably not on your PATH.

Generate a keypair

keys generate makes a fresh OpenPGP keypair in-process and writes both files. The algorithm is a required choice rather than a quiet default, so you always know which lane you’re in:

sigillum keys generate \
    --algorithm rsa --rsa-bits 4096 \
    --name "Test Signer" --email [email protected] \
    --output release.asc --private-output release.pem

RSA 4096 takes a few seconds. Two files, two log lines:

INFO Generated OpenPGP keypair algorithm=rsa public_output=release.asc private_output=release.pem creation_time=2026-10-12T14:35:41Z fingerprint=56275C8C11BEA0FE2C978D5303441C1E93D2658D
WARN Move the private-half file to offline storage now. private_output=release.pem

release.asc is the ASCII-armored public key. That’s the identity you publish and that verifiers trust. release.pem is the PKCS#1 PEM private key, written mode 0600, and it’s what the local backend signs with.

Write the fingerprint down. Each later step reports the same one, and that is how you catch having signed with the wrong key.

The RSA choice matters. RSA keys drive the OpenPGP path; Ed25519 keys drive the minisign path and cannot produce an OpenPGP signature at all.

Worth seeing what that looks like when you get it wrong. Generate an ed25519 key and try to sign with it and sigillum stops you:

ERROR unsupported key type for --format openpgp: ./ed.pem is an armored OpenPGP Ed25519 secret key, and OpenPGP signing takes an RSA PEM key only (keys generate --algorithm rsa); an Ed25519 key signs with --format minisign, from a private half written with --private-format pem

That tells you what you’ve got, why it won’t do, and which lane takes it. It didn’t always. At v0.4.1 the same mistake came back as no PEM block found in file, about a file sigillum had written itself thirty seconds earlier, so I raised it, and v0.5.3 carries the fix. If you’re on an older build and see the PEM error, you’ve picked the algorithm for the other lane rather than broken anything.

Sign a file

Real releases sign a checksum manifest rather than each artefact, so make something shaped like one:

echo "hello sigillum" > checksums.txt
sigillum sign \
    --backend local \
    --key-id ./release.pem \
    --public-key ./release.asc \
    checksums.txt
INFO Signed file backend=local key_id=./release.pem public_key=./release.asc input=checksums.txt output=checksums.txt.sig sig_creation_time=2026-10-12T14:35:41Z fingerprint=56275C8C11BEA0FE2C978D5303441C1E93D2658D

That wrote checksums.txt.sig, an ASCII-armored OpenPGP detached signature, 832 bytes of it. The default output is <input>.sig.

Notice the fingerprint on that line is the same one keys generate printed. It’s read back out of release.asc, so it’s the cheapest possible check that you signed with the identity you meant to, and it costs you nothing but looking.

Both key flags are there for one signature because they do different jobs. sigillum takes the signing identity from --public-key, and refuses to continue if the backend’s public half disagrees with it.

Verify with gpg

sigillum produces exactly what gpg --verify consumes. Import into a throwaway keyring so you aren’t touching your real one:

TMPGNUPG=$(mktemp -d)
gpg --homedir "$TMPGNUPG" --import release.asc
gpg --homedir "$TMPGNUPG" --verify checksums.txt.sig checksums.txt
gpg: key 03441C1E93D2658D: public key "Test Signer <[email protected]>" imported
gpg: Total number processed: 1
gpg:               imported: 1
gpg: Signature made Mon 12 Oct 2026 14:35:41 UTC
gpg:                using RSA key 56275C8C11BEA0FE2C978D5303441C1E93D2658D
gpg: Good signature from "Test Signer <[email protected]>" [unknown]
gpg: WARNING: This key is not certified with a trusted signature!
gpg:          There is no indication that the signature belongs to the owner.
Primary key fingerprint: 5627 5C8C 11BE A0FE 2C97  8D53 0344 1C1E 93D2 658D

That warning is expected and is not a failure. You imported a key without certifying it, so gpg has no basis for trusting the binding between the key and the name on it. The signature itself is good, and gpg is being precise about which of those two things it is vouching for.

Now prove the check is real. A verification you have never seen fail is a verification you are taking on faith:

echo "tampered" >> checksums.txt
gpg --homedir "$TMPGNUPG" --verify checksums.txt.sig checksums.txt
gpg: Signature made Mon 12 Oct 2026 14:35:41 UTC
gpg:                using RSA key 56275C8C11BEA0FE2C978D5303441C1E93D2658D
gpg: BAD signature from "Test Signer <[email protected]>" [unknown]

Clean up

The private key is the reason to tidy up:

rm -rf "$TMPGNUPG"
cd / && rm -rf /tmp/sigillum-tutorial

If you keep the directory instead, treat release.pem as a secret. sigillum writes private halves mode 0600, but it cannot stop you copying one into a git repository.

Where to go next

The docs carry the rest: signing a release artefact is the same command against an AWS KMS key with reproducible signatures and CI wiring, signing for Rust consumers is the minisign lane, and publishing a WKD tree serves your public key from your own domain so the trust anchor isn’t the same host as the code.

What sigillum does not do is the one I’d read next, and it includes why there is no sigillum verify, which comes down to verification belonging to an implementation that isn’t the signer.

If you want the long version of why any of this is worth the trouble, that’s the series.

Built with Hugo · Theme Stack designed by Jimmy