Implement a custom backend¶
Add a signing backend for a KMS or HSM — AWS KMS, 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 exactly how AWS, GCP, and Azure backends are added: each is an external package 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, lowercase, kebab-case identifier, unique in the process. Duplicate registrations panic at init.NewSignerreturns acrypto.Signerwrapping the remote key. Your backend decides how to interpretkeyID(an ARN/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 —openpgpkeycurrently requires*rsa.PublicKey.
Backends do not resolve credentials themselves; the caller configures the SDK, agent, or environment before the signer is invoked.
Implement and register¶
The key insight is that the heavy crypto 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 awskms
import (
"context"
"crypto"
"gitlab.com/phpboyscout/signing"
// your provider SDK imports live here, in YOUR module
)
type backend struct{}
func (backend) Name() string { return "aws-kms" }
func (backend) NewSigner(ctx context.Context, keyID string) (crypto.Signer, error) {
// keyID is, e.g., an AWS KMS key ARN or alias.
// 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 image/* decoders:
import (
"context"
"gitlab.com/phpboyscout/signing"
_ "example.com/yourtool/awskms" // registers "aws-kms"
)
func sign(ctx context.Context) error {
backend, err := signing.Get("aws-kms")
if err != nil {
return err // signing.ErrUnknownBackend if the import is missing
}
signer, err := backend.NewSigner(ctx, "arn:aws:kms:...:key/...")
if err != nil {
return err
}
_ = signer // hand to openpgpkey.ArmoredPublicKey / DetachSign
return nil
}
Because the AWS SDK is imported only by example.com/yourtool/awskms, a binary
that never blank-imports it never links it. signing.Names() reports exactly
the backends a given binary compiled in.
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/signing-aws-kms— a real, separate-module backend (AWS KMS) you can model yours on; see also Backends and the per-provider module pattern.- Dependency inversion — the module-graph reasoning behind injected backends.
- The registry API:
signing.Register,signing.Get,signing.Names,signing.ErrUnknownBackendon pkg.go.dev/gitlab.com/phpboyscout/signing.