Skip to content

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.
  • NewSigner returns a crypto.Signer wrapping the remote key. Your backend decides how to interpret keyID (an ARN/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 currently 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