Sign release artefacts with gtb sign¶
gtb sign produces an armored OpenPGP detached signature over a
single file using the same backend abstraction gtb keys mint
uses. In CI, that's typically AWS KMS via OIDC β no operator ever
holds the signing-key secret, no gpg install needed on the
runner.
This how-to walks the operator side (local signing for testing,
plus the CI integration with OIDC) and the verification path
(gpg --verify and the in-tool verifier).
Prerequisites¶
gtbβ₯ the version that ships Spec 2026-06-09-sign-command.- The public-key file (
release.asc) corresponding to the signing key, already published β embedded ininternal/trustkeys/keys/and served via WKD per How-to: publish via WKD.gtb signreads identity (creation time, UID) from this file. - For the
aws-kmsbackend: AWS credentials in the standard SDK chain. In local terminal that meansaws login+aws configure export-credentials --format env; in CI it means an OIDC assume-role step (see below).
One file, one sig β local invocation¶
gtb sign \
--backend aws-kms \
--kms-region eu-west-2 \
--key-id alias/gtb-release-signing-v1 \
--public-key ./release.asc \
--output checksums.txt.sig \
checksums.txt
The logged INFO line includes the public-key fingerprint so you can spot-check it matches the trust anchor:
INFO Signed file backend=aws-kms key_id=alias/gtb-release-signing-v1
public_key=./release.asc input=checksums.txt output=checksums.txt.sig
sig_creation_time=2026-06-09T15:11:32Z
fingerprint=6E2072BBF83DFAAF006300C495DDAC333C37AA35
--output defaults to <input>.sig if omitted. The command refuses
to write a .sig over its own input. The guard compares
filepath.Clean'd paths and falls back to os.SameFile, so a
different spelling of the same file (./x vs x, an absolute vs
relative form, or a symlink) is still refused rather than silently
clobbering the input.
Reproducible signatures¶
Add --created <rfc3339> to pin the signature's creation timestamp.
Two re-runs of gtb sign over the same content with the same key
and the same --created produce byte-identical .sig files β
useful when you want to reproduce-from-scratch a previous release's
artefacts in a SLSA-style chain.
gtb sign \
--backend aws-kms \
--kms-region eu-west-2 \
--key-id alias/gtb-release-signing-v1 \
--public-key ./release.asc \
--created 2026-06-09T15:11:32Z \
checksums.txt
Local signing with the local backend¶
For tutorials, development, or air-gapped builds:
gtb keys generate --algorithm rsa --rsa-bits 4096 \
--name "Test Signer" --email [email protected] \
--output release.asc --private-output release.pem
gtb sign \
--backend local \
--key-id ./release.pem \
--public-key ./release.asc \
checksums.txt
local accepts PKCS#1 and PKCS#8 PEM private keys (unencrypted β
v0.1 doesn't decrypt encrypted PEMs; use filesystem-level
encryption like LUKS or age until that lands).
Verify¶
gtb sign produces what gpg --verify consumes β and what the
in-tool verifier (TrustSet.VerifyManifestSignature in
pkg/setup/signing.go) consumes during a self-update.
# Import the published public key into a clean keyring + verify.
TMPGNUPG=$(mktemp -d)
gpg --homedir "$TMPGNUPG" --import release.asc
gpg --homedir "$TMPGNUPG" --verify checksums.txt.sig checksums.txt
# Expect: "Good signature from <UID>" with the published fingerprint.
rm -rf "$TMPGNUPG"
CI integration: GitLab + AWS OIDC¶
The setup is split into two layers:
- Infra (one-time, Terraform). The
terraform-aws-signing-kmsmodule already provisions: - The OIDC identity provider for
https://gitlab.comin your AWS account. - An IAM signer role with
kms:Sign+kms:GetPublicKeyon the release key only. - A trust policy pinning the role to
project_path:phpboyscout/go-tool-base:ref_type:tag:ref:v*β only tag pipelines on the specific project may assume it. MR/branch pipelines and non-v*tags fail the OIDC subject filter.
The module's signing_kms_signer_role_arn output is the role ARN
you pass to CI as AWS_ROLE_ARN.
- Pipeline (in this repo's
.gitlab-ci.yml). In the goreleaser job, declareid_tokensand do the assume-role inbefore_script:
goreleaser:
id_tokens:
AWS_WEB_IDENTITY_TOKEN:
aud: https://gitlab.com
variables:
AWS_ROLE_ARN: arn:aws:iam::<account>:role/gtb-release-signing-v1-signer
before_script:
- |
creds=$(aws sts assume-role-with-web-identity \
--role-arn "$AWS_ROLE_ARN" \
--role-session-name "gtb-release-${CI_COMMIT_TAG}" \
--web-identity-token "$AWS_WEB_IDENTITY_TOKEN" \
--query 'Credentials.[AccessKeyId,SecretAccessKey,SessionToken]' \
--output text)
export AWS_ACCESS_KEY_ID=$(echo "$creds" | awk '{print $1}')
export AWS_SECRET_ACCESS_KEY=$(echo "$creds" | awk '{print $2}')
export AWS_SESSION_TOKEN=$(echo "$creds" | awk '{print $3}')
Goreleaser's signs block then invokes
scripts/sign-release.sh checksums.txt checksums.txt.sig
which is a thin wrapper around gtb sign --backend aws-kms. The
AWS env vars set by the before_script are picked up by the
AWS SDK's default credential chain β gtb sign itself knows
nothing about OIDC, which keeps it portable across CI platforms.
Cross-tool compatibility¶
gtb sign output verifies under:
gpg --verify(any modern OpenPGP implementation accepting v4 RSA-PKCS1v15 signatures)pkg/setupin-tool verifier duringgtb updateopenpgp.CheckArmoredDetachedSignaturefromProtonMail/go-crypto(the library used by both)
The unit test suite in pkg/openpgpkey/sign_test.go exercises the
go-crypto path; the integration test gated by INT_TEST_SIGN=1
additionally invokes gpg --verify to catch any drift between our
framing and the reference implementation.
What if my signer doesn't match the public key¶
gtb sign reads the public half from --public-key and compares
the RSA modulus + exponent to what the backend returns. If they
disagree, the command errors out with:
Before producing a signature. This is the safety check that
catches "I copied the wrong KMS alias into the script" before it
becomes an unverifiable .sig file in production.
If you intend to rotate the signing key, do it in this order:
- Mint a new key via
gtb keys mint --backend aws-kms ..., producingrelease-v2.asc. - Sign the rotation manifest with the old key (using the old
release.ascas--public-key). - Publish
release-v2.ascvia WKD alongside the old key for a transition period. - Embed both keys in
internal/trustkeys/keys/. - Cut the next release; it now signs with v2.
See the Phase 2 prep doc for the full rotation runbook.
Related¶
- Spec: gtb sign β design decisions, RFC details, threat model.
pkg/openpgpkeyβ theDetachSignlibrary function the CLI wraps.- How-to: publish via WKD β the matching trust-anchor publication step.
- How-to: add a signing backend β for consumers who want GCP KMS, Vault, YubiKey, etc.