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:
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", ...)).
Related¶
- Implement a custom backend: the task version of this page.
- Backends and the per-provider module pattern: why each provider is its own module.
- Errors:
ErrUnknownBackend,ErrUnknownBackendTypeand the rest.