Sign and verify a release¶
By the end of this you'll have signed a checksums manifest and watched a
TrustSet accept it — the exact flow a consumer like afmpeg uses to verify
ffmpeg-wasi's release assets. Allow about fifteen minutes.
You'll do both halves in one self-contained Go program so every moving part is visible. In a real deployment the signing half runs on the publisher's machine and the verifying half runs inside your tool; here they run side by side so the round trip is easy to follow.
What you need before you start¶
- Go 1.26 or newer (
go.moddeclaresgo 1.26.5). - A new module to experiment in:
mkdir verify-tutorial && cd verify-tutorial
go mod init example.test/verify-tutorial
go get gitlab.com/phpboyscout/go/signing
Nothing here touches the network at run time, and nothing is destructive — the program only writes to standard output.
Generate a signing key¶
In production the publisher holds the private key, often inside a KMS, and you only ever see the public half. For this walkthrough you generate an RSA key so you can play both roles.
The key must be at least 3072 bits. Anything weaker is refused when the trust
set is built, with verify.ErrWeakKey — see
the strength policy.
priv, _ := rsa.GenerateKey(rand.Reader, 3072)
now := time.Unix(0, 0) // a fixed time keeps this example reproducible
Generating a 3072-bit key takes a second or two, occasionally longer.
now is pinned deliberately. The creation time is baked into the key and its
self-signature, so pinning it makes every run produce byte-identical output.
Use a real timestamp for a real key, and never move it afterwards — a rotation
gets a new key, not a new timestamp on the old one.
Export the armoured public key¶
The publisher exports an ASCII-armoured OpenPGP public key. This is the thing you embed in your verifier at build time.
pub is a []byte holding a -----BEGIN PGP PUBLIC KEY BLOCK----- envelope.
ArmoredPublicKey accepts any crypto.Signer whose Public() returns an
*rsa.PublicKey — a local key here, a KMS handle in production. It does not
mint Ed25519 OpenPGP keys: an Ed25519 signer returns
openpgpkey.ErrUnsupportedKeyType.
Sign a checksums manifest¶
A release does not sign each asset individually. The publisher writes one checksums manifest listing a hash per asset, and signs that single file.
manifest := []byte("sha256 ffmpeg.wasm 0xc0ffee\n")
sig, _ := openpgpkey.DetachSign(priv, pub, bytes.NewReader(manifest), now)
DetachSign returns an ASCII-armoured detached signature and leaves the
manifest untouched. The publisher ships manifest, sig and pub alongside
the release assets.
DetachSign reads the whole reader into memory before signing, so bound the
input yourself if it comes from somewhere you do not control. A checksums
manifest is a few kilobytes, so it doesn't bite here.
Build a trust set and verify¶
Now switch hats: you're the consumer. Load the publisher's public key into a
TrustSet and verify the manifest against the signature.
trust, _ := verify.LoadTrustSet(pub)
err := trust.VerifyManifestSignature(manifest, sig)
if err != nil {
log.Fatalf("verification failed: %v", err)
}
fmt.Println("verified")
A nil error means two things: the manifest was signed by a key in the set,
and it hasn't changed since. It says nothing about the assets themselves — you
still hash each downloaded file and compare it to its manifest line.
The complete program¶
package main
import (
"bytes"
"crypto/rand"
"crypto/rsa"
"fmt"
"log"
"time"
"gitlab.com/phpboyscout/go/signing/openpgpkey"
"gitlab.com/phpboyscout/go/signing/verify"
)
func main() {
priv, err := rsa.GenerateKey(rand.Reader, 3072)
if err != nil {
log.Fatal(err)
}
now := time.Unix(0, 0)
pub, err := openpgpkey.ArmoredPublicKey(priv, "Release", "release@example.test", now)
if err != nil {
log.Fatal(err)
}
manifest := []byte("sha256 ffmpeg.wasm 0xc0ffee\n")
sig, err := openpgpkey.DetachSign(priv, pub, bytes.NewReader(manifest), now)
if err != nil {
log.Fatal(err)
}
trust, err := verify.LoadTrustSet(pub)
if err != nil {
log.Fatal(err)
}
if err := trust.VerifyManifestSignature(manifest, sig); err != nil {
log.Fatalf("verification failed: %v", err)
}
fmt.Println("verified")
}
Run it:
How to tell it worked¶
The program prints:
That's the success signal. To prove the check is real, change a byte in
manifest after signing — edit the checksum — and run it again. The program
now exits with a verification error instead.
Where to go next¶
- The recipe form, including reading the signer fingerprint and handling each failure mode: Verify a release.
- Sign your own manifests from a key on disk: Sign with the local backend.
- Sign binary artefacts for Rust consumers: Sign artefacts with minisign.
- Add a second, independent trust anchor over WKD: Configure trust.
- Why the composite cross-check matters: The trust model.