Implement a custom backend¶
Add a signing backend for a KMS or HSM (GCP KMS, Azure Key Vault, a PKCS#11
HSM) without the signing library ever depending on that provider's SDK. The
provider SDK lives in your module; signing only sees the crypto.Signer
you hand back.
This is how the AWS KMS backend
(signing-aws-kms) is
built: a separate module that the library core has no knowledge of. See
Dependency inversion for why the
contract is shaped this way.
The contract¶
A backend implements one small, CLI-agnostic interface:
type Backend interface {
Name() string
NewSigner(ctx context.Context, keyID string) (crypto.Signer, error)
}
Name()returns a stable identifier, unique in the process. The default you register is lowercase kebab-case (gcp-kms); a named instance carries<type>:<instance>(gcp-kms:prod). Duplicate registrations panic at init.NewSignerreturns acrypto.Signerwrapping the remote key. Your backend decides how to interpretkeyID(an ARN or alias for AWS, a resource name for GCP, a key handle for an HSM). The signer'sPublic()must return a type the caller can consume:openpgpkeyrequires*rsa.PublicKey, andminisignrequiresed25519.PublicKey.
Backends do not resolve credentials themselves; the caller configures the SDK, agent, or environment before the signer is invoked.
Implement and register¶
The private-key operation stays on the remote service. Your crypto.Signer
forwards Sign calls to the KMS and exposes the public key via Public().
Register the backend from init() so a blank import activates it.
package gcpkms
import (
"context"
"crypto"
"gitlab.com/phpboyscout/go/signing"
// your provider SDK imports live here, in YOUR module
)
type backend struct{}
func (backend) Name() string { return "gcp-kms" }
func (backend) NewSigner(ctx context.Context, keyID string) (crypto.Signer, error) {
// keyID is, e.g., a Cloud KMS key version resource name.
// Construct and return a crypto.Signer that forwards Sign() to KMS
// and whose Public() returns the key's *rsa.PublicKey.
return newKMSSigner(ctx, keyID)
}
func init() {
signing.Register(backend{})
}
Activate by blank import¶
Downstream binaries opt in by blank-importing the backend package, the same
activate-by-side-effect pattern as net/http/pprof and the image/* decoders:
import (
"context"
"gitlab.com/phpboyscout/go/signing"
_ "example.com/yourtool/gcpkms" // registers "gcp-kms"
)
func sign(ctx context.Context) error {
backend, err := signing.Get("gcp-kms")
if err != nil {
return err // signing.ErrUnknownBackend if the import is missing
}
signer, err := backend.NewSigner(ctx, "projects/.../cryptoKeyVersions/1")
if err != nil {
return err
}
_ = signer // hand to openpgpkey.ArmoredPublicKey / DetachSign
return nil
}
Because the provider SDK is imported only by example.com/yourtool/gcpkms, a
binary that never blank-imports it never links it. signing.Names() reports
exactly the backends a given binary compiled in.
Optional: named instances¶
If one binary may need several differently configured instances of your
backend (one per project or region, say), give the backend a name field and
register a factory alongside the default:
type backend struct{ name string }
func (b backend) Name() string { return b.name }
func init() {
signing.Register(backend{name: "gcp-kms"})
signing.RegisterFactory("gcp-kms", func(canonicalName string) (signing.Backend, error) {
return backend{name: canonicalName}, nil // canonicalName is "gcp-kms:<instance>"
})
}
signing.NewInstance("gcp-kms", "prod") then calls your factory with
gcp-kms:prod, and refuses the result unless its Name() returns exactly that.
The type name must be lowercase kebab-case with no colon, or RegisterFactory
panics. NewInstance does not register the backend it returns, and how an
instance gets its settings is up to your backend and the CLI that selects it.
The rules are in
the backend registry.
Optional: backend-specific CLI flags¶
The core interface is intentionally flag-free so the module pulls in no flag library. If your CLI front-end needs per-backend flags (a region, a profile, a key-rotation window), define an optional interface in the front-end and type-assert for it. The library core neither defines nor depends on it:
// Defined by your CLI, not by the signing library.
type FlagRegistrar interface {
RegisterFlags(fs *pflag.FlagSet)
}
backend, _ := signing.Get(name)
if fr, ok := backend.(FlagRegistrar); ok {
fr.RegisterFlags(cmd.Flags())
}
Backends that need no flags simply do not implement the interface, and the core stays dependency-light.
See also¶
- Sign with the local backend: the reference backend, useful as an implementation template.
gitlab.com/phpboyscout/go/signing-aws-kms, a real separate-module backend you can model yours on. See also Backends and the per-provider module pattern.- Dependency inversion: the module-graph reasoning behind injected backends.
- The backend registry: panic conditions,
lookup semantics, named instances, and what
keyIDmeans per backend. - The registry API (
signing.Register,signing.Get,signing.Names,signing.RegisterFactory,signing.NewInstance,signing.ErrUnknownBackend) on pkg.go.dev/gitlab.com/phpboyscout/go/signing.