Skip to content

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.
  • NewSigner returns a crypto.Signer wrapping the remote key. Your backend decides how to interpret keyID (an ARN or alias for AWS, a resource name for GCP, a key handle for an HSM). The signer's Public() must return a type the caller can consume: openpgpkey requires *rsa.PublicKey, and minisign requires ed25519.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