The problem it exists for
Most projects publish a checksum file next to their binaries and call it verification. It catches a download that got corrupted on the way, and that’s about all it catches, because whoever can swap the binary can swap the checksum file sitting beside it. The spec that started all of this puts it in one line: “If an attacker can replace the binary, they can also replace checksums.txt.”
So the question isn’t how to publish a checksum. It’s how to prove a release came from you, when the place you publish it is exactly the place an attacker would go.
How it works
Sign with a key nobody can copy
sigillum signs a release in one of two formats: an OpenPGP signature over the checksum file, which gpg checks, or a minisign signature beside each file, which minisign checks. The key lives in a key service and never leaves it: sigillum sends a digest of what it’s signing and gets a signature back, so there’s no key file to leak, copy or forget on a laptop. (For minisign it hashes the file locally and sends only the 64-byte hash, which is how a large binary gets signed by a key that can’t leave the service.) Here that service is AWS KMS, because that’s what the estate runs on, and a local key file works too for trying it out.
It’s a single binary that knows nothing about what language your project is written in, which is why Rust and other non-Go pipelines can sign with it without building anything of mine first.
Bring your own key store
AWS KMS is what I use, not what it’s tied to. The whole signing family was designed from the start so the key store is a plug-in. go/signing defines one small contract (take a key reference, hand back a standard Go signer) and knows nothing about any cloud: each key service is a module of its own, and the core can’t even compile in a vendor SDK, because a test fails the build if one sneaks in. AWS KMS is one such module and the local key file is another, and GCP KMS, Azure Key Vault, HashiCorp Vault or a PKCS#11 HSM would each be a third, written the same way. There’s a how-to for writing your own, and go/encryption follows the same pattern for decryption, down to a smartcard if that’s where your key lives.
Publish the key somewhere else
A signature is only as good as your confidence in the public key that checks it. The key is built into the tools that verify with it, and it’s also published over WKD (a well-known address on a domain I control, where gpg knows to look for it). The two copies have to agree, so forging a release means breaking into the forge and the key host at the same time. Publish your key where they can’t touch it goes through why that host was picked.
Verify before anything installs
go/signing is the library that checks those signatures. It’s what go-tool-base’s self-update uses: an update is verified against the built-in key and the WKD copy before it replaces anything, and a tool generated with signing switched on refuses an unsigned update outright. If the WKD copy can’t be reached at all, self-update carries on with the built-in key alone, so a tool behind a broken proxy can still patch itself, but two copies that disagree always stop it. krites is stricter about the models it downloads: there, an unreachable key host means try again later.
Encrypted reports, with no key to hold
go/encryption runs the same idea the other way round, for reports somebody sends you encrypted. The private half stays in KMS, and the only thing KMS is asked to do is the one secret step of the key exchange; everything after that, including the decryption itself, happens on your machine. So a person can read a report without the key ever existing anywhere it could be stolen from. Two encrypted emails in twenty years is where it came from.
The keys, as infrastructure
Two OpenTofu modules create the keys and the permissions around them. The signing module makes one KMS signing key and a role that only named CI jobs can use, through short-lived OIDC credentials. No person and no stored credential is granted signing, and a configuration that would let any job sign is refused before anything is created. The encryption module makes the decryption keys with no pipeline granted access to them, so reading a report is left to a person, and it refuses the key type that would break that, at plan time.
Decisions and what they cost
- GPG over cosign. Signatures are checked with GPG and a key carried in the binary, chosen over cosign so verification still works offline and behind a firewall (go-tool-base spec 0056). WKD was picked to publish the key over Keybase, key servers and DNS. What it cost: there’s no transparency log, so if the forge and the key host were both broken into at once, nothing would notice.
- A standalone signer. Signing moved out of go-tool-base into sigillum and a small module behind it (gtb spec 0176, sigillum spec 0001), because two projects were compiling the whole framework just to sign a file (cicd spec 0097). What it cost: signing now has two front doors, sigillum and
gtb sign, over the same library. - Two keys where one would do. OpenPGP signatures use RSA and minisign uses Ed25519, because the OpenPGP library can’t sign Ed25519 with a key it can’t hold in memory, which is exactly the kind of key KMS gives you. What it cost: a project publishing both formats keeps two keys. One key or two is how that got settled.
- Key stores as plug-ins. The libraries depend on a contract, never on a provider, and every key service lives in its own module (go/signing’s design notes), so adding one never touches the core and nobody compiles an SDK they don’t use. What it cost: sigillum picks its backends when it’s built, so a new key store means building sigillum with that module in it, and so far only AWS KMS and the local key file exist.
- ECDH for decryption, not RSA. KMS’s RSA decryption only supports the padding OpenPGP doesn’t use, so reports are encrypted to a key-agreement key instead (sigillum spec 0004). What it cost: each certificate carries two keys, one to certify and one to decrypt.
Proof in use
- Releases across the estate are signed with it: sigillum itself, colophon, krites and the artefact channel sign their checksum files with sigillum, rust-tool-base signs its binaries with minisign through KMS, and go-tool-base signs through its own wrapper over the same library.
- Checked by hand on 10 October 2026: sigillum v0.5.2’s checksum signature comes back as a good signature in
gpg, using the key fetched over WKD. - go/signing is a direct dependency of go-tool-base, keryx, krites, colophon and afmpeg, and reaches three more tools through go-tool-base. The key-service modules’ release merge requests run cicd’s go-core-currency check against the core’s latest release.
- Eight tools build and publish their release binaries through cicd’s goreleaser component. colophon, krites and sigillum sign theirs with sigillum during that release, and go-tool-base signs through its own wrapper.
- The security contact publishes its key the same way, over WKD: an RSA key to certify with and a P-256 key to decrypt to, the shape the encryption module builds.
Use it when, and when not to
Use it if you want people to be able to check that a release really came from you, using gpg or minisign they already have, and you’d rather the signing key lived somewhere it can’t be copied, whichever key store that is. It doesn’t care what your project is written in.
Know the limits first. Of the key stores, only AWS KMS and a local key file are built today. The others the design was made for are still outstanding, and until they land, anything else is a module you’d write yourself against the same contract. There’s no transparency log, timestamping or notarisation yet, and no key rotation policy, and a minisign key can’t be revoked. The encryption module runs on OpenTofu only.
Where it’s going
Key rotation, Linux package signing and Windows Authenticode are all drafted on go-tool-base’s wiki, alongside the Google Cloud and Azure KMS backends. sigillum’s own next step is exposing signing to AI agents as MCP tools (spec 0003).
What it covers
- encryption The other direction: assemble an OpenPGP certificate whose private halves live in a KMS, and decrypt a message addressed to it from a raw ECDH shared secret. Built because KMS can sign an OpenPGP key but cannot decrypt one, so the unwrap has to happen outside it.
- encryption-aws-kms The AWS KMS backend for the encryption module, deriving the shared secret and certifying inside KMS so no private key material leaves it.
- signing A small, standalone module for creating and verifying signatures on files.
- signing-aws-kms The AWS KMS backend for the signing module, keeping private release-signing keys inside KMS while the public API stays framework-free.
- signing-cli The shareable sign and keys command builders, and nothing else. Splitting the commands off means go-tool-base and the standalone sigillum can offer the same ones without a dependency cycle between them.
- terraform-aws-encryption-kms A KMS-held OpenPGP identity for receiving encrypted mail: a certification primary, an ECDH subkey, and reader and certifier roles kept deliberately apart so neither can do the other's job.
- terraform-aws-signing-kms The KMS-backed release signing module: asymmetric keys, CI signer role and the policy shape needed to sign without ever exporting the key.
The story in posts
- Publish your key where the platform can't touch it
- One key, or two?
- The framework doesn't know where your signing key lives
- The gpg command that hung (so I built signing into the tool)
- Release trust without the framework
- Sign your own binaries with go-tool-base, part 7: rotation and break-glass
- Bought, not stolen
- Sign your own binaries with go-tool-base, part 6: sign every release with GoReleaser
- Sign your own binaries with go-tool-base, part 5: embed the key and require verification
- Sign your own binaries with go-tool-base, part 4: mint and publish your public key
Last reviewed .