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 verifier
  • KeyResolver chain: EmbeddedResolver, WKDResolver, CompositeResolver (fingerprint cross-check between embedded and WKD)
  • SignatureProvider optional interface on pkg/vcs/release (Direct + Bitbucket implementations)
  • SelfUpdater verify-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.sh now signs via go run ./cmd/gtb sign --backend aws-kms (replaces the previous gpg --detach-sign shell-out). The aws-kms backend uses the AWS SDK Go v2 default credential chain — no aws-cli or external assume-role-with-web-identity call required.
  • .gitlab-ci.yml overlays the goreleaser job from the phpboyscout/cicd/goreleaser@v0.10.1 component 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_FILE
    • before_script that 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/y84sdmnksfqswe7fxf5mzjg53tbdz8f5gpg --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 check against the updated .goreleaser.yaml
  • glab ci lint against the updated .gitlab-ci.yml
  • scripts/sign-release.sh exercised locally against the real AWS KMS key — produced a gpg --verify-accepted signature with fingerprint 6E20…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

  1. First signed release — merge the releaser-pleaser MR after this lands; the resulting tag triggers the goreleaser job which now signs checksums.txt and attaches checksums.txt.sig to the Release. (Task #172.)
  2. Flip DefaultRequireSignature = true in internal/cmd/root/root.go once 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:

  1. Fetch checksums.txt and checksums.txt.sig from the Release
  2. Resolve the embedded key via internal/trustkeys/keys/signing-key-v1.asc
  3. Fetch the WKD copy from openpgpkey.phpboyscout.uk
  4. Refuse to proceed if fingerprints disagree (CompositeResolver)
  5. Verify the signature against the agreed key (TrustSet.VerifyManifestSignature)
  6. Verify checksums against the freshly-downloaded binaries
  7. 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.

Edited by Matt Cockayne

Merge request reports

Loading