Sign with AWS KMS¶
Sign a release manifest with a key held in AWS KMS, where the private half
never leaves AWS: every signature is a remote kms:Sign call. The backend is
the standalone module
gitlab.com/phpboyscout/go/signing-aws-kms;
it implements signing.Backend and registers as aws-kms.
This mirrors Sign with the local backend. Only the backend module and the key setup differ. See Backends and the per-provider module pattern for why the AWS SDK lives in its own module.
Prerequisites¶
-
A KMS key of one of the two supported specs, matching the path you are signing for:
RSA_4096SIGN_VERIFY for the OpenPGP manifest path this guide covers. Use 4096 bits; the verify side rejects RSA below 3072.ECC_NIST_EDWARDS25519for the minisign artefact path, which the Rust consumers speak.
A key does one job or the other, never both, because the OpenPGP path is RSA-only and minisign is Ed25519-only. A project publishing both holds two KMS keys. Any other key spec returns
ErrUnsupportedKMSKeyType. - AWS credentials resolvable by the SDK default chain (environment,~/.aws/credentialsorAWS_PROFILE, an IAM role, or OIDC web identity). - The IAM principal needskms:GetPublicKeyandkms:Signon the key. - The key's ARN or alias.
Add the backend¶
import (
"gitlab.com/phpboyscout/go/signing"
_ "gitlab.com/phpboyscout/go/signing-aws-kms" // blank import registers "aws-kms"
)
The blank import runs the backend's init(), which registers it with the
signing registry under the name aws-kms.
Resolve a signer¶
backend, err := signing.Get("aws-kms")
if err != nil {
return err
}
signer, err := backend.NewSigner(ctx, "arn:aws:kms:eu-west-2:123456789012:key/abcd-…")
if err != nil {
return err
}
keyID is the KMS key ARN or alias.
The registered backend signs in eu-west-2 unless you name another region, and
it does not read AWS_REGION or the region in your shared AWS config. The
default reaches the AWS SDK as an explicit region, which outranks both. It
matches the terraform-aws-signing-kms module's default, so the house release
path works with no region configured. Credentials still come from the SDK's
default chain.
To sign somewhere else, name the region with the --kms-region CLI flag (see
below) or awskms.WithRegion, or build the backend with awskms.NewFromConfig
or awskms.NewFromSource so the region comes from a configuration you resolved
yourself. Get this wrong and the symptom points at the key rather than the
region, because KMS in eu-west-2 answers as though a key from another region
does not exist.
Sign a manifest and publish the key¶
This part is identical for every backend, because the signer is a stdlib
crypto.Signer:
import "gitlab.com/phpboyscout/go/signing/openpgpkey"
pub, err := openpgpkey.ArmoredPublicKey(signer, "Release", "release@example.com", time.Now())
// publish `pub`: embed it in consumers, or serve it via WKD
// (see "Publish keys via WKD")
sig, err := openpgpkey.DetachSign(signer, pub, bytes.NewReader(manifest), time.Now())
// ship `manifest`, `sig`, and `pub` alongside the release
Consumers verify with signing/verify exactly as in
Verify a release.
Programmatic configuration (region, logger)¶
Outside the registry, construct a configured signer or backend directly with
functional options. Importing the package has already registered the default
aws-kms backend, so a configured one is used directly rather than registered
again:
import (
"log/slog"
awskms "gitlab.com/phpboyscout/go/signing-aws-kms"
)
// region + logger via options
signer, err := awskms.NewSigner(ctx, "us-east-1", keyARN,
awskms.WithLogger(slog.Default()))
// or a configured backend, used without the registry
backend := awskms.New(awskms.WithRegion("us-east-1"))
signer, err = backend.NewSigner(ctx, keyARN)
Do not pass awskms.New(...) to signing.Register. Its name is aws-kms,
which the package's own init() registered on import, and Register panics on
a duplicate name. For a second, differently named AWS backend use
awskms.NewDefault(name, opts...), or signing.NewInstance("aws-kms", "prod")
when the eu-west-2 default will do. A minted instance starts from the default
region; a CLI such as signing-cli sets it from the selector
(--backend 'aws-kms:prod?kms-region=…'), not from a top-level --kms-region.
The optional *slog.Logger emits DEBUG-level diagnostics carrying only
non-secret identifiers (key id, region, key bit-length, algorithm). It never
logs credentials, the digest or signature bytes.
CLI flag¶
The backend implements an optional flag-registration interface that a CLI
front-end type-asserts for, and contributes --kms-region through it. The
canonical CLI is:
Credentials and IAM¶
No credential logic lives in the backend. It relies entirely on the AWS SDK
default chain. For CI, OIDC web-identity federation
(AWS_WEB_IDENTITY_TOKEN_FILE with AWS_ROLE_ARN) is the recommended keyless
path. Scope the role's trust policy to the specific pipeline (ref or tag) so a
leaked ARN can't be assumed elsewhere.
Limitations¶
- RSA, ECDSA P-256 and Ed25519 only. Any other KMS key spec returns
ErrUnsupportedKMSKeyType. P-256 is for container image signing. This recipe covers the RSA/OpenPGP path; for Ed25519 artefact signing see Sign artefacts with minisign. - RSASSA-PKCS1-v1_5 only. A caller requesting RSASSA-PSS gets
ErrPSSUnsupportedrather than a silent scheme downgrade. PKCS#1 v1.5 is what the OpenPGP RSA signing path here uses. - SHA-256, SHA-384 or SHA-512 for RSA. Any other hash returns
ErrUnsupportedHashFunc. - A 4096-byte signing message on the Ed25519 path. Exceeding it returns
ErrMessageTooLarge. Sign a digest, not an artefact;minisign.Signalready does. This is the constraint that makes the prehashed variant load-bearing, as Why there are two signature formats explains. - Asymmetric KMS keys do not auto-rotate. Rotation means minting a new key and dual-publishing through a transition window; there is no rotate button.
See also¶
- Sign with the local backend, the no-cloud path.
- Implement a custom backend to roll your own (GCP, Azure, Vault, HSM).
- Backends and the per-provider module pattern.
- API reference: pkg.go.dev/gitlab.com/phpboyscout/go/signing-aws-kms.