GCP KMS & Azure Key Vault signing backends (item C1)¶
- Authors
- Matt Cockayne, Claude Opus 4.8 (AI drafting assistant)
- Date
- 21 June 2026
- Status
- DRAFT
Summary¶
GTB ships two production-grade signing backends today β
pkg/signing/kms (aws-kms,
RSA SIGN_VERIFY keys held in AWS KMS) and
pkg/signing/local (local,
an on-disk PEM key for the tutorial / no-cloud path) β plus the
signing.Backend registry in pkg/signing
that both gtb keys mint and gtb sign dispatch against by name.
This spec adds two more cloud-HSM backends as sibling subpackages
under pkg/signing/:
| Name | Package | SDK | Held key |
|---|---|---|---|
gcp-kms |
pkg/signing/gcpkms |
cloud.google.com/go/kms/apiv1 |
RSA RSA_SIGN_PKCS1_* CryptoKeyVersion |
azure-kv |
pkg/signing/azurekv |
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys |
RSA key, RS256/RS384/RS512 |
Each backend is blank-import-activated exactly like aws-kms:
import _ "gitlab.com/phpboyscout/go-tool-base/pkg/signing/gcpkms"
import _ "gitlab.com/phpboyscout/go-tool-base/pkg/signing/azurekv"
Once imported, they appear in signing.Names() and are usable as
--backend gcp-kms / --backend azure-kv for both gtb keys mint
and gtb sign with zero changes to those commands β that is the
whole point of the registry.
No new public types are introduced on pkg/signing. The
Backend interface (Name, RegisterFlags, NewSigner) is
unchanged; this is purely additive, following the "the only known
evolution path is additive" note in the package doc.
Motivation¶
- Multi-cloud release signing. The
aws-kmsbackend rooted GTB's own Phase 2 self-update signing chain (live since v0.12.2). Tool authors building on GTB whose release engineering lives on GCP or Azure currently have to write and maintain their own backend (the add-a-signing-backend how-to shows the recipe, and already uses a hypotheticalgcp-kmsas its worked example). Promoting the two most-requested cloud HSMs to first-class, tested, semver-versioned subpackages removes that per-consumer cost and gives the how-to two real references instead of one. - Same recipe, three clouds. AWS, GCP, and Azure all expose an
opaque asymmetric RSA key with a "fetch public half" + "sign a
digest remotely" pair. The minting recipe (
GetPublicKeyβ oneSignβ OpenPGP framing) is identical; only the SDK call shapes differ. Implementing both alongsideaws-kmskeeps the three in lockstep and exercises the registry's multi-backend wiring with more than one cloud SDK present. - Validates the extensibility claim. Shipping a second and third backend through the registry is the strongest possible evidence that the registry is the right extension point β and confirms the blank-import dead-code-elimination story holds with heavyweight cloud SDKs, not just the AWS one.
Non-goals¶
- No secrets-provider registry. See "Relationship to the rejected
secrets-provider registry" below. This spec touches only the
signing
Backendregistry, which is already accepted. - No Ed25519 / ECDSA minting. The OpenPGP minting path
(
pkg/openpgpkey) requiressigner.Public()to be an*rsa.PublicKey(seepkg/openpgpkey/openpgpkey.goline ~55 andsign.goline ~50). Both new backends are therefore RSA-only, mirroringaws-kms. ECDSA/Ed25519 support is a separate, cross-cutting change topkg/openpgpkeyand is explicitly out of scope (recorded as a follow-up in Open questions). - No credential-mode plumbing. Like
aws-kms, both backends defer entirely to their SDK's ambient credential chain (Application Default Credentials for GCP;DefaultAzureCredentialfor Azure). They do not read GTB'spkg/credentialsconfig. Credential resolution is a caller/CI concern, consistent with theBackendcontract note: "Backends do not own credential resolution." - No change to
gtb keys mint/gtb signsource. Both commands already enumeratesigning.Names()and callRegisterFlagson every registered backend. Adding backends to the binary is a one-line blank-import incmd/gtb/signing.go.
Relationship to the rejected secrets-provider registry¶
The feature-decisions log
records a rejected proposal: a bundled SecretsProvider plugin
registry with vendor implementations (OS keychain, HashiCorp Vault,
AWS SSM, 1Password). The rejection rationale: secrets management is
deployment-specific, and shipping one vendor adapter "opens a rabbit
hole of vendor-specific adapters" that belong in the tool, not the
framework. The log's own summary: "Backend is the extension
point; SecretsProvider as originally proposed (a plugin registry
with vendor implementations) is still rejected."
This work does not conflict with that rejection, for three reasons:
- Different registry, different purpose. The rejected item was
about reading arbitrary secrets at runtime for config composition.
This is about the
signing.Backendregistry β a release-engineering surface for producing OpenPGP signatures from an HSM-held key. The signing registry was accepted and shipped (thegtb keys mintspec and thegtb signspec), with GCP KMS / Azure / Vault / YubiKey called out from day one as the intended extension targets. - No private-key material crosses the boundary. A
SecretsProviderhands raw secret bytes to the caller. Asigning.Backendnever exposes private-key material β it returns an opaquecrypto.SignerwhoseSignis a remote round-trip. That is the explicit "don't expose your private-key material" pitfall in the how-to, and it is the structural reason adding cloud backends here is not the rejected pattern. - The framework already endorses adding exactly these. The
add-a-signing-backend how-to uses
gcp-kmsas its worked example and names GCP KMS / Azure Key Vault as first-class candidates. Promoting two of them from "you could write this" to "we ship and test this" is consistent with the accepted design, not a reversal of the rejected one.
There is no conflict. The distinction to preserve in review: new
signing backends via the accepted signing.Backend registry are
sanctioned; a vendor-implementation secrets registry is not.
Background: the pattern to mirror (pkg/signing/kms)¶
The AWS backend is the template. Each new backend reproduces its exact structure:
<name>.goβ package doc explaining the blank-import activation, the backend-specific CLI flag(s), and credential resolution; thebackendstruct implementingName/RegisterFlags/NewSigner; the exportedNewSigner(ctx, β¦)constructor that builds the real cloud client and delegates to the unexportednewSigner; and aninit()that callssigning.Register(&backend{β¦}).signer.goβ a small<vendor>Clientinterface capturing only the SDK methods used (so tests supply a fake without the SDK's own mock framework), the<vendor>Signeradapting the remote key tocrypto.Signer, andnewSigner(ctx, client, keyID)which fetches the public half, parses it to*rsa.PublicKey(rejecting non-RSA), and returns the signer.Signmapsopts.HashFunc()to the vendor's RSASSA-PKCS1-v1_5 algorithm, rejects PSS (*rsa.PSSOptions) rather than silently downgrading, and rejects unsupported hashes.<name>_test.goβ table-driven,t.Parallel(), fake client; the same eight scenarioskms_test.gocovers (registration, flag parse, RSA happy path, non-RSA rejection, GetPublicKey error, malformed DER, nil opts, PSS rejection, unsupported-hash, hashβalgorithm mapping, vendor-Sign error wrapping).
The crucial split β newSigner (testable via a fake to 100%) vs
NewSigner (the thin cloud-config wrapper that needs real cloud creds)
β is what lets the inner logic hit the coverage bar while the outer
wrapper is excluded. See
Test coverage & .coverage-policy.yaml.
Sentinel-error parity¶
Each backend exports errors.Is-able sentinels matching the AWS set,
named per-vendor:
ErrUnsupported<Vendor>KeyTypeβ public half is not RSA.ErrUnsupportedHashFuncβ caller requested a hash the vendor's RSA Sign does not map to.ErrPSSUnsupportedβ caller requested RSASSA-PSS; refused, not downgraded.
SDK confirmation: signer surfaces & algorithm support¶
Confirmed against the current Go SDKs and provider docs (June 2026).
GCP Cloud KMS β cloud.google.com/go/kms/apiv1¶
- Client:
*kms.KeyManagementClient(fromkms "cloud.google.com/go/kms/apiv1"), protos inkmspb "cloud.google.com/go/kms/apiv1/kmspb". - Public half:
GetPublicKey(ctx, *kmspb.GetPublicKeyRequest{Name: <version-resource-name>})returns a*kmspb.PublicKeywhosePemfield is a PEM-encoded SubjectPublicKeyInfo. Decode the PEM block, thenx509.ParsePKIXPublicKeyβ assert*rsa.PublicKey. (Note: GCP returns PEM, where AWS returns raw DER β the only meaningful divergence from the AWS adapter.) - Sign:
AsymmetricSign(ctx, *kmspb.AsymmetricSignRequest{Name, Digest}). The request carries aDigestoneof withSha256/Sha384/Sha512fields; the digest's hash must match the key version's configured algorithm. Response*kmspb.AsymmetricSignResponsehas aSignature []byteplus aSignatureCrc32Cintegrity checksum the backend SHOULD verify. - Algorithms for OpenPGP detached signing (RSA, this backend):
RSA_SIGN_PKCS1_2048_SHA256,RSA_SIGN_PKCS1_3072_SHA256,RSA_SIGN_PKCS1_4096_SHA256,β¦_4096_SHA512. GCP keys are algorithm-pinned: a CryptoKeyVersion is created for one specificRSA_SIGN_PKCS1_<size>_<hash>andAsymmetricSignrejects a digest whose hash differs. The backend therefore readsopts.HashFunc(), populates the matchingDigestoneof field (SHA-256/384/512), and lets GCP enforce the match β surfacing a wrapped error if the operator's--created/hash choice and the key's pinned algorithm disagree. GCP also offersRSA_SIGN_PSS_*and EC (EC_SIGN_P256_SHA256, β¦) algorithms, but PSS and EC are out of scope (PSS rejected; EC needs the non-RSA minting path). crypto.Signershape: no first-partycrypto.Signeris provided by the SDK; the adapter is the standard ~15-line wrapper (a well-known community pattern, e.g.salrashid123/kms_golang_signer), identical in shape to the AWSkmsSigner.
Azure Key Vault β azure-sdk-for-go/sdk/security/keyvault/azkeys¶
- Client:
*azkeys.Clientconstructed with a vault URL and anazcore.TokenCredential(typicallyDefaultAzureCredentialfromazidentity). - Public half:
GetKey(ctx, name, version, nil)βGetKeyResponse{KeyBundle{Key *JSONWebKey}}. The JWK carries the RSA modulus/exponent (N,E) forKeyTypeRSA/RSA-HSM; the backend reconstructs an*rsa.PublicKeyfrom those big-endian byte fields (no DER/PEM step β JWK is the native form here). - Sign:
Sign(ctx, name, version, azkeys.SignParameters{Algorithm: <alg>, Value: <digest>}, nil)βSignResponse{KeyOperationResult{Result []byte}}.Valueis the pre-hashed digest;Resultis the raw signature. - Algorithms for OpenPGP detached signing (RSA, this backend):
azkeys.SignatureAlgorithmRS256/RS384/RS512(RSASSA-PKCS1-v1_5, the OpenPGP-compatible scheme). The backend mapsopts.HashFunc()(SHA-256/384/512) to these. Unlike GCP, an Azure RSA key is not algorithm-pinned β the same key signs with RS256/384/512 β so the hashβalgorithm mapping is purely caller-driven, exactly like the AWS adapter. Azure also supportsPS256/384/512(PSS β rejected here) andES256/384/512/ES256K(EC β out of scope, non-RSA path). - ECDSA raw-signature caveat (documented, not hit here): Azure
returns EC signatures as raw
RβS, not ASN.1/DER β relevant only if EC support is added later via the non-RSA minting path. RSA (RS256/384/512) returns the standard PKCS#1 v1.5 signature with no re-encoding needed, so this RSA-only backend is unaffected. Recorded here so the future EC follow-up does not rediscover it.
Cross-backend summary¶
| Concern | AWS (aws-kms) |
GCP (gcp-kms) |
Azure (azure-kv) |
|---|---|---|---|
| Public-half format | DER (SPKI) | PEM (SPKI) | JWK (N/E) |
| Sign input | digest | digest (in Digest oneof) |
digest (Value) |
| Sign output | raw PKCS#1 v1.5 sig | raw sig (+CRC32C) | raw sig |
| RSA PKCS#1 v1.5 | β (this backend) | β RSA_SIGN_PKCS1_* |
β RS256/384/512 |
| Key algorithm-pinned? | no | yes (one hash per version) | no |
| PSS | rejected | rejected | rejected |
| EC / Ed25519 | n/a (KMS no Ed25519) | out of scope | out of scope |
All three converge on the same crypto.Signer contract
pkg/openpgpkey consumes: Public() β *rsa.PublicKey, Sign(digest,
PKCS1v15).
Design decisions¶
D1 β Package layout & naming¶
New subpackages pkg/signing/gcpkms (name gcp-kms) and
pkg/signing/azurekv (name azure-kv). Package directory names avoid
hyphens (Go convention); the registry name is kebab-case to match
the existing aws-kms. CLI flags are vendor-namespaced to avoid
collisions when multiple backends are compiled in (cobra binds every
registered backend's flags onto one flag set, so names must be globally
unique across backends):
gcp-kms:--gcp-key-versionis not added separately β the GCP full resource name is the--key-id(e.g.projects/p/locations/l/keyRings/r/cryptoKeys/c/cryptoKeyVersions/1). Optional flag--gcp-credentials-file(path to a service-account JSON; default empty β Application Default Credentials).azure-kv:--key-idis the full key identifier URL (https://<vault>.vault.azure.net/keys/<name>/<version>), from which the vault URL, key name, and version are parsed. No extra required flag; optional--azure-tenant-idreserved for future explicit tenant pinning (default empty βDefaultAzureCredential).
Open question OQ1 β confirm the
--key-idsemantics above (full GCP resource name; full Azure key URL) versus splitting vault host into its own flag. Leaning to "key-id is the whole identifier" for symmetry withaws-kmsaccepting a full ARN.
D2 β RSA-only, enforced at newSigner¶
Both backends reject a non-RSA public half with
ErrUnsupported<Vendor>KeyType, exactly as aws-kms does. This keeps
the backends honest against the pkg/openpgpkey RSA precondition and
fails early with a clear message rather than deep in OpenPGP framing.
D3 β PSS refusal, hash mapping (parity with aws-kms)¶
Sign refuses *rsa.PSSOptions with ErrPSSUnsupported (no silent
scheme downgrade β a contract violation in an exported crypto.Signer),
and maps only SHA-256/384/512 to the vendor's PKCS#1 v1.5 algorithm,
returning ErrUnsupportedHashFunc otherwise. For gcp-kms the mapping
additionally selects the Digest oneof field; GCP enforces the
key-algorithm match server-side and the backend wraps any mismatch.
D4 β Credential resolution deferred to the SDK chain¶
No pkg/credentials involvement. GCP uses Application Default
Credentials (GOOGLE_APPLICATION_CREDENTIALS / metadata server /
gcloud ADC); Azure uses DefaultAzureCredential (env β
workload-identity β managed-identity β Azure CLI). Each package doc
documents the chain and the CI/OIDC story (GCP Workload Identity
Federation; Azure OIDC federated credentials), mirroring how the
aws-kms doc covers IAM Roles Anywhere / web-identity. This keeps the
backends free of GTB-specific credential config and consistent with the
Backend contract.
D5 β Dead-code elimination preserved (the load-bearing constraint)¶
The cloud SDKs are heavy (the GCP KMS client pulls gRPC + a large
genproto closure; the Azure SDK pulls azcore/azidentity). A
regulated or size-sensitive downstream must be able to link none of
a SDK it does not use. The blank-import activation pattern already
guarantees this: a backend's init() only runs β and its SDK is only in
the link closure β if some package blank-imports it. The structural
rules that keep this true:
- No
internal/cmd/*package may import a cloud-SDK backend directly.keysandsignreference onlypkg/signing(registry), neverpkg/signing/gcpkmsetc. (verified: currentkeysandsignimport onlypkg/signing). - The only on/off switch is
cmd/gtb/signing.go, mirroring the keychain switch incmd/gtb/keychain.go. The shippedgtbbinary blank-imports all four backends there; a downstream tool'smainimports only what it wants. - Compile-time + link-time smoke fixtures prove the omission actually elides the SDK β see Smoke fixtures.
This is the same dead-code-elimination story as the keychain
blank-import (cmd/gtb/keychain.go: "linker dead-code elimination then
keeps go-keyring, godbus, and wincred out of the artefact") and the
existing AWS slice (cmd/gtb-no-aws-smoke).
Smoke fixtures¶
The repo already ships cmd/gtb-no-aws-smoke β a main that
blank-imports only pkg/signing/local, with a unit test asserting
signing.Names() returns exactly {"local"} (proving no other backend
leaks in transitively). We extend the proof to the new SDKs:
- Reuse, don't multiply. Rather than one
no-<vendor>binary per cloud, generalise to a singlecmd/gtb-min-signing-smokefixture (local-only) whose unit test assertssigning.Names() == {"local"}, i.e. none ofaws-kms/gcp-kms/azure-kvleak in. This subsumes the existinggtb-no-aws-smokeintent and covers all three cloud SDKs at once. (OQ2: keep the existinggtb-no-aws-smokename/binary and add the broader assertion, or rename? Leaning to keeping the existing binary and broadening its assertion to avoid churn in any CI job that references it.) - Link-time arm. The
gtb-no-aws-smokepackage doc describes a CIgo list -deps/ symbol check asserting nogithub.com/aws/*lands in the closure, but no such CI job currently exists in.gitlab-ci.ymlorscripts/(confirmed β onlyapidiff.sh,coverage-policy.sh,sign-release.share present). This spec adds ascripts/signing-deps-smoke.shthat runsgo list -deps ./cmd/gtb-min-signing-smokeand greps the closure forgithub.com/aws/,cloud.google.com/go/kms, andgithub.com/Azure/azure-sdk-for-go, failing if any appear. Wired as a non-blocking advisory CI job initially (consistent withapidiff/coverage-policybeing advisory), promotable to blocking later.
Open question OQ2/OQ3 β (a) reuse vs rename the smoke binary; (b) advisory vs blocking for the new deps-smoke job from day one.
D6 β go.mod weight & the standard binary¶
The standard gtb binary will link all three cloud SDKs (it
blank-imports them in cmd/gtb/signing.go), growing the default binary
and go.sum. This is acceptable for the reference binary β it is the
"batteries included" artefact β and is precisely why the elision story
(D5) matters for downstreams. (OQ4: do we want a documented
cmd/gtb-aws-only / minimal reference build, or is the how-to guidance
"omit the import in your own main" sufficient? Leaning to sufficient β
downstreams build their own main; we should not multiply reference
binaries.)
Public API¶
No change to pkg/signing. Two new packages, each exporting:
// pkg/signing/gcpkms
func NewSigner(ctx context.Context, keyID, credentialsFile string) (crypto.Signer, error)
var ErrUnsupportedGCPKeyType, ErrUnsupportedHashFunc, ErrPSSUnsupported error
// pkg/signing/azurekv
func NewSigner(ctx context.Context, keyID string) (crypto.Signer, error)
var ErrUnsupportedAzureKeyType, ErrUnsupportedHashFunc, ErrPSSUnsupported error
Both register a backend in init(). The exported NewSigner
mirrors kms.NewSigner β for integration tests and tool authors wiring
signing into their own command structure rather than gtb keys mint.
Test coverage & .coverage-policy.yaml¶
pkg/signing/kms is in the excluded list of
.coverage-policy.yaml with the
rationale:
"AWS KMS adapter; inner
newSigneris 100% via a fake, theNewSignerAWS-config wrapper is untestable without real AWS."
The new packages get the same treatment β the inner
newSigner/Sign/algorithm-mapping logic is covered to 100% via a
fake <vendor>Client (the signer.go interface is the seam), and only
the thin NewSigner real-client constructor is uncovered. Two new
excluded entries:
- { pkg: pkg/signing/gcpkms, reason: "GCP Cloud KMS adapter; inner newSigner is 100% via a fake, the NewSigner ADC-config wrapper is untestable without real GCP" }
- { pkg: pkg/signing/azurekv, reason: "Azure Key Vault adapter; inner newSigner is 100% via a fake, the NewSigner DefaultAzureCredential wrapper is untestable without real Azure" }
The coverage policy requires a rationale per exclusion and that this file stay in sync with the coverage-gap spec's Bucket A table β both will be updated.
Unit tests (hermetic, fake client)¶
Mirror kms_test.go per backend: registration; RegisterFlags
parse; RSA happy path (assert modulus matches the fetched public key,
assert the correct vendor algorithm flows through on SHA-256/384/512);
non-RSA rejection; GetPublicKey/GetKey error; malformed public-half
(bad PEM for GCP, malformed JWK for Azure); nil opts; PSS rejection
(assert the remote Sign is not called); unsupported-hash; vendor
Sign-error wrapping. GCP adds one case: digest-hash vs key-algorithm
mismatch surfaces a wrapped error.
Integration tests (gated, desktop/CI-cred)¶
Per the project's env-var gating
(internal/testutil),
add *_integration_test.go gated by INT_TEST_GCP=1 / INT_TEST_AZURE=1
(and INT_TEST=1 for all). These require real cloud credentials and a
provisioned RSA signing key, so they are deferred like the existing
VCS/chat-live integration tests β registered in the integration-test
inventory and in the deferred-integration follow-up, not run on the
homelab.
E2E / BDD¶
Extend features/cli/sign.feature and features/cli/keys.feature
only with signing.Names()-style assertions that the new backends
register and surface in --help (no live cloud calls in BDD). The
existing internal/cmd/sign / internal/cmd/keys packages are
E2E-covered exclusions in the coverage policy and need no source change.
Documentation impact¶
docs/components/signing.mdβ addgcp-kmsandazure-kvto the built-in backend table and to the "Compile-time backend opt-out" section (the elision story now covers three SDKs).docs/how-to/add-signing-backend.mdβ the worked example already uses a hypotheticalgcp-kms; update it to reference the now-realpkg/signing/gcpkmsandpkg/signing/azurekvas production examples alongsidekms/local, and correct the GCPSignsketch to populate theDigestoneof.docs/concepts/release-binary-signing.mdβ note multi-cloud rooting options.- Package docs in each new package (the doc comment is the primary
reference for the CLI flag + credential chain), mirroring the
kms.gopackage doc.
Migration notes¶
Additive β no breaking change. Per the pre-1.0 policy this ships as a
minor bump (feat(signing):). No docs/migration/ entry needed (no
public-API break). The standard binary gains the two SDKs in its link
closure; downstreams already on a custom main are unaffected unless
they opt in.
Implementation plan (TDD)¶
pkg/signing/azurekvfirst (simplest: JWK public half, no algorithm-pinning, mapping identical to AWS).signer.go+ fake β tests green βazurekv.go(backend +init+ flags) β registration test.pkg/signing/gcpkms(adds PEM decode +Digestoneof + the key-algorithm-match case). Same test-first order.- Blank-import both in
cmd/gtb/signing.go. - Generalise
cmd/gtb-no-aws-smoke(or addcmd/gtb-min-signing-smoke) scripts/signing-deps-smoke.sh; wire the advisory CI job.- Two
.coverage-policy.yamlexclusions with rationale. - Docs (components/signing, add-signing-backend how-to, concepts).
/gtb-verify(tests, race, lint, mocks);go list -depssmoke;just snapshotsanity on binary size delta.
Open questions¶
- OQ1 β
--key-idsemantics. Confirm--key-idis the full identifier for both (GCP CryptoKeyVersion resource name; Azure key URL), with vault/project derived from it, vs splitting into separate flags. (Drafted as: full identifier, for ARN-symmetry withaws-kms.) - OQ2 β Smoke fixture. Keep
cmd/gtb-no-aws-smokeand broaden its assertion to "no aws/gcp/azure", or introduce a freshcmd/gtb-min-signing-smoke? (Drafted as: keep + broaden.) - OQ3 β Deps-smoke CI job. Advisory (
allow_failure: true, likeapidiff) or blocking from day one? (Drafted as: advisory, since there is no link-time check today at all.) - OQ4 β Reference binary weight. Accept the standard
gtblinking all three SDKs, or document a minimal reference build? (Drafted as: accept; downstreams build their ownmain.) - OQ5 β Algorithm validation depth (GCP). Should
gcp-kmspre-fetch the key version's algorithm and validate the hash before calling Sign (clearer error), or let GCP reject server-side and wrap? (Drafted as: let GCP reject + wrap, to keep one round-trip per mint like the AWS path.) - OQ6 β EC/Ed25519 follow-up. Track a separate spec to extend
pkg/openpgpkeyto accept ECDSA/EdDSAcrypto.Signers, unlockingEC_SIGN_*(GCP) andES*(Azure) β note the Azure raw-RβSβ ASN.1 re-encoding requirement captured above.
Resolutions (open questions confirmed with user 2026-06-21)¶
- OQ1 β
--key-idsemantics β RESOLVED: full identifier, single flag for both backends (GCP CryptoKeyVersion resource name; Azure key URL), vault/project derived from it β symmetric with theaws-kmsARN model. - OQ2 β Smoke fixture β RESOLVED: keep and broaden
cmd/gtb-no-aws-smoketo assert "no aws/gcp/azure symbols". One fixture across all cloud SDKs. (Now framed as guarding the downstream elision property β see OQ4 β notgtb's own distribution.) - OQ3 β Deps-smoke CI job β RESOLVED: advisory (
allow_failure: true, likeapidiff/coverage-policy); there is no link-time check today, so start advisory and tighten later. - OQ4 β Reference-binary weight β RESOLVED: the
gtbbinary links all three cloud backends; accept the extra baggage. Users must never switch binaries to switch signing backend β onegtbserves aws/gcp/azure. This differs from the keychain precedent (which gates a local capability). Rejected: splitting backends into separate submodules (B β fragments the binary/module story) and a download-at-init plugin (C β re-opens a rejected decision, breaks the CGO-off/FIPS static build, and puts downloaded code on the highest-trust signing path). The blank-import registry mechanism still allows a regulated downstream to build a leanermain, but GTB pursues no minimal reference build and does not treat elision as a goal forgtbitself. - OQ5 β GCP algorithm validation β RESOLVED: let GCP reject server-side and wrap the error; no pre-fetch of the key algorithm β keeps one round-trip per mint, matching the AWS path.
- OQ6 β EC/Ed25519 β RESOLVED: separate follow-up spec. This spec stays
RSA-only (current
pkg/openpgpkeyconstraint). Track extendingopenpgpkeyto accept ECDSA/EdDSAcrypto.Signers β unlocking GCPEC_SIGN_*and AzureES*(incl. the Azure raw-RβSβ ASN.1 re-encoding) β as its own spec.
References¶
pkg/signing/backend.go,registry.goβ the registry.pkg/signing/kms/kms.go,signer.go,kms_test.goβ the pattern.pkg/signing/local/local.go.internal/cmd/keys/mint.go,internal/cmd/sign/sign.goβ the consumers (unchanged).cmd/gtb/signing.go,cmd/gtb/keychain.go,cmd/gtb-no-aws-smoke/β the on/off switch and the elision smoke..coverage-policy.yaml.- Specs:
2026-06-08-keys-mint-command.md,2026-06-09-sign-command.md. - How-to:
add-signing-backend.md. - Decisions:
feature-decisions.md(rejected secrets-provider registry). - SDKs:
cloud.google.com/go/kms/apiv1;github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys.