Skip to content

The backend registry

The root signing package holds one process-global registry mapping a backend name to a Backend, and a second map from a backend type to a factory for named instances of it. One sync.RWMutex guards both, so concurrent Get, Names and NewInstance calls are safe.

The Backend contract

type Backend interface {
    Name() string
    NewSigner(ctx context.Context, keyID string) (crypto.Signer, error)
}

Name returns a stable identifier, unique across the process. A registered default is lowercase kebab-case (local, aws-kms); an instance minted by NewInstance carries <type>:<instance> (aws-kms:prod). NewSigner returns a crypto.Signer wrapping the held key; the backend decides what keyID means.

The interface is deliberately CLI-agnostic (it mentions no flag library), so the core module can stay dependency-light. A backend needing its own flags implements an optional interface defined by the CLI front end, which type-asserts for it. The core neither defines nor depends on such an interface.

Backends do not resolve credentials. The caller configures the SDK, agent or environment before the signer is used.

Register(b Backend)

Adds b to the registry. Called from a backend package's init(), so that a blank import activates it: the same activate-by-side-effect pattern as net/http/pprof and the image/* decoders.

It panics, rather than returning an error, in three cases:

Condition Panic message
b is nil signing: Register called with nil Backend
b.Name() returns "" signing: Backend.Name() returned empty string
the name is already registered signing: duplicate Backend registration: <name>

All three are programming errors at process init. Failing fast beats a silently overridden backend, which would mean signing with an unexpected key.

There is no Unregister. Once a backend package is imported, its backend is present for the life of the process.

Get(name string) (Backend, error)

Returns the registered backend, or ErrUnknownBackend wrapped with a listing so the message says what this binary actually supports:

  • with backends registered: "gcp-kms" (available: aws-kms, local)
  • with an empty registry: "local" (no backends are registered — this binary was built without any signing backends compiled in)

The second message almost always means a missing blank import.

Lookup is exact: no case folding, no aliasing, no fuzzy matching. Get sees only what Register added, so a <type>:<instance> name is not found unless someone registered that instance explicitly.

Names() []string

Returns every registered name, sorted alphabetically. It reports the backends this binary compiled in, which is exactly the set of backend packages the binary chose to import. That makes it useful for rendering the accepted values of a --backend flag. Factories are not listed; a type that offers instances appears through the default it registers under the same name.

Named instances

A binary sometimes needs two configurations of one backend type at once, such as two AWS accounts. A backend package offers that by registering a factory next to its default.

RegisterFactory(typeName string, construct func(canonicalName string) (Backend, error))

Records how to construct a named instance of typeName. Call it from init() beside Register, and register a same-named default too, so that a bare selection of the type still resolves and the type appears in Names().

It panics in three cases:

Condition Panic message
typeName is not lowercase kebab-case with no colon (^[a-z0-9][a-z0-9-]*$) signing: RegisterFactory called with invalid type name: <type>
construct is nil signing: RegisterFactory called with nil construct for type: <type>
a factory for typeName already exists signing: duplicate factory registration: <type>

The colon ban is what keeps a <type>:<instance> selector unambiguous to split.

NewInstance(typeName, instance string) (Backend, error)

Calls the factory for typeName with the canonical name <typeName>:<instance> and returns what it builds. The result is not registered: the caller owns it, and Get will not find it.

The factory runs outside the registry lock, so it may itself call Register or Get without deadlocking. NewInstance returns an error when:

Condition Error
instance is empty signing: empty instance name for backend type "<type>"
no factory is registered for typeName ErrUnknownBackendType, wrapped with the type name
the factory returns an error that error, wrapped as constructing signing instance "<type>:<instance>"
the factory returns nil, or a typed nil signing: factory for type "<type>" returned a nil Backend
the result's Name() is not the canonical name signing: factory for type "<type>" returned Backend named "<got>", want "<type>:<instance>"

Those are the only checks. Which characters an instance name may contain, and how an instance receives its settings, belong to the CLI front end that parses the selector, not to this module.

Both backends in the table below register a factory. The local factory's instances differ from the default only in their name, because local has no per-instance settings.

ResetForTesting()

Clears the registry and every registered factory. The ForTesting suffix is the Go-standard signal that this is a test helper; production code must not call it. It is exported rather than hidden behind a test-only build so that tests in other packages can drive registry-mutating scenarios against fake backends.

What keyID means per backend

keyID is a single string whose format each backend defines.

Backend name Module keyID is Key types
local gitlab.com/phpboyscout/go/signing/local (ships in this module) A filesystem path to an unencrypted PEM file RSA (PKCS#1 or PKCS#8) and Ed25519 (PKCS#8)
aws-kms gitlab.com/phpboyscout/go/signing-aws-kms A KMS key ARN or alias RSA_4096 SIGN_VERIFY for the OpenPGP path, ECC_NIST_EDWARDS25519 for the minisign path

The local backend is stateless and declares no flags; the PEM path arrives per call as keyID. It reads whatever path it is given, following symlinks, with no canonicalisation. That is appropriate for an operator-supplied path, and worth knowing if the path could come from somewhere less trusted.

Registering a configured backend

A backend that needs configuration can be constructed directly and used without the registry. That is how the AWS backend takes a region or a logger:

backend := awskms.New(awskms.WithRegion("us-east-1"))
signer, err := backend.NewSigner(ctx, keyARN)

Do not pass that backend to Register. Importing a backend package at all, blank or not, runs its init(), which has already registered the default name, so a second registration under aws-kms panics. To put a configured backend in the registry, give it a name nothing else uses (awskms.NewDefault("aws-kms-us", ...)).