feat(setup): Phase 2 self-update signature verification
Summary
Phase 2 of remote-update-checksum-verification: KMS-backed OpenPGP signature verification of checksums.txt on every gtb update, refusing the update unless the signature chains to a trust anchor embedded in the binary AND cross-checked against an externally-served WKD copy.
After landing this MR the producer chain is fully wired end-to-end. Verification will only enforce signatures (DefaultRequireSignature = true) after the first signed release exists in the wild — that flip is a separate follow-up so any breakage shows up on a dedicated commit rather than blended with the infra wiring.
What's in the branch
Verifier (in-binary, pkg/setup)
TrustSet+ minimum-strength policy + detached-signature verifierKeyResolverchain:EmbeddedResolver,WKDResolver,CompositeResolver(fingerprint cross-check between embedded and WKD)SignatureProvideroptional interface onpkg/vcs/release(Direct + Bitbucket implementations)SelfUpdaterverify-before-parse gate- Config keys:
update.require_signature,update.key_source,update.external_key_email,update.require_external_crosscheck
Trust anchor (internal/trustkeys/keys/)
Two real keys, minted via gtb keys generate + gtb keys mint --backend aws-kms:
| Key | Algorithm | Fingerprint |
|---|---|---|
| Rotation authority | ed25519 (v4 EdDSA, offline storage) | 2B26 6584 0904 7ED0 8B56 CEBD CF5B 8DBB 5D9F 19C2 |
| Signing key v1 | rsa4096 (AWS KMS alias/gtb-release-signing-v1) |
6E20 72BB F83D FAAF 0063 00C4 95DD AC33 3C37 AA35 |
internal/trustkeys/trustkeys_test.go parses both via setup.NewEmbeddedResolver at test time and pins the fingerprint set, so any accidental key change fails CI.
Build side: scripts/sign-release.sh + .gitlab-ci.yml overlay
scripts/sign-release.shnow signs viago run ./cmd/gtb sign --backend aws-kms(replaces the previousgpg --detach-signshell-out). The aws-kms backend uses the AWS SDK Go v2 default credential chain — no aws-cli or externalassume-role-with-web-identitycall required..gitlab-ci.ymloverlays the goreleaser job from thephpboyscout/cicd/goreleaser@v0.10.1component with:id_tokens.AWS_WEB_IDENTITY_TOKEN(aud:https://gitlab.com)variables:AWS_ROLE_ARN,AWS_REGION,AWS_ROLE_SESSION_NAME,AWS_WEB_IDENTITY_TOKEN_FILEbefore_scriptthat writes the OIDC token to disk where the SDK expects it
The signer IAM role (gtb-release-signing-v1-signer) is provisioned by terraform-aws-signing-kms@0.1.0; its trust policy pins assume-role to project_path:phpboyscout/go-tool-base:ref_type:tag:ref:v*. Only this project's release-tag pipelines can sign.
WKD endpoint (separate operational step, already live)
Public keys are also served via WKD at openpgpkey.phpboyscout.uk/.well-known/openpgpkey/phpboyscout.uk/hu/y84sdmnksfqswe7fxf5mzjg53tbdz8f5 — gpg --auto-key-locate clear,wkd --locate-keys release@phpboyscout.uk resolves both keys with matching fingerprints. The verifier's CompositeResolver cross-checks the embedded copy against this URL on every update.
Validation
-
go build ./... -
golangci-lint run(0 issues) -
go test -race ./... -
goreleaser checkagainst the updated.goreleaser.yaml -
glab ci lintagainst the updated.gitlab-ci.yml -
scripts/sign-release.shexercised locally against the real AWS KMS key — produced agpg --verify-accepted signature with fingerprint6E20…AA35 - Full MR pipeline green (pipeline 2588424298): lint, go-test, go-test-e2e, govulncheck, trivy, gitleaks, osv-scanner, semgrep, pages
What's left after merging
- First signed release — merge the releaser-pleaser MR after this lands; the resulting tag triggers the goreleaser job which now signs
checksums.txtand attacheschecksums.txt.sigto the Release. (Task #172.) - Flip
DefaultRequireSignature = trueininternal/cmd/root/root.goonce the signed release exists in the wild. (Task #173, separate small commit.)
Producer/verifier round-trip
Once steps 1 + 2 are done, a gtb update --debug against the next release will:
- Fetch
checksums.txtandchecksums.txt.sigfrom the Release - Resolve the embedded key via
internal/trustkeys/keys/signing-key-v1.asc - Fetch the WKD copy from
openpgpkey.phpboyscout.uk - Refuse to proceed if fingerprints disagree (
CompositeResolver) - Verify the signature against the agreed key (
TrustSet.VerifyManifestSignature) - Verify checksums against the freshly-downloaded binaries
- Replace the binary
A compromise of GitLab alone, or AWS alone, or Cloudflare alone, cannot push a fake update. Two of the three trust anchors need to fall, simultaneously.