Dependency inversion¶
This page explains why signing defines backends as an interface and lets
consumers inject them, rather than bundling cloud SDKs. The practical recipe is
Implement a custom backend.
The problem with bundling backends¶
Release signing in the real world happens against AWS KMS, GCP KMS, Azure Key Vault, or a PKCS#11 HSM. The obvious design would be to ship integrations for all of them in the library. That choice is poisonous for a light library:
- The AWS SDK alone is enormous, and GCP, Azure, and HSM stacks each add more.
- A consumer that only ever verifies releases (the common case — think
afmpegcheckingffmpeg-wasi's assets) would inherit every cloud SDK transitively, despite never signing anything. - Security review and supply-chain surface scale with the dependency graph. A verifier should not have to audit three cloud SDKs it never calls.
Inverting the dependency¶
So the library inverts the relationship. It defines a minimal contract:
type Backend interface {
Name() string
NewSigner(ctx context.Context, keyID string) (crypto.Signer, error)
}
and a global registry (Register, Get, Names). The consumer implements
the backend in its own module, imports the heavy SDK there, and registers the
backend from init(). The library depends on crypto.Signer — a standard
library interface — and never on any provider SDK. Control is inverted: the
high-level policy (how to sign and verify) lives in the library; the low-level
detail (which KMS, which SDK) lives in the consumer and is plugged in.
A light local (PEM) backend ships as the default reference implementation, so
the common signing path works out of the box without any cloud dependency.
Why this keeps consumer graphs tiny¶
This is not just tidiness — it is a property of Go's module graph. A dependency that is never imported by any compiled package is pruned: it is not linked into the binary, and with module-graph pruning it need not even be downloaded to build. Because the AWS/GCP/Azure SDKs are imported only by the consumer's backend package, they enter a binary's graph only when that binary blank-imports that backend.
The consequences:
- A verify-only tool depends on go-crypto and cockroachdb/errors, full stop.
- A tool signing via the
localbackend adds no cloud SDK. - A tool signing via AWS KMS pulls in the AWS SDK — but only because it chose to import its own KMS backend, not because the library forced it.
signing.Names() reflects exactly this: it reports the backends a given binary
actually compiled in, which is the set of backend packages that binary chose to
import.
Activate by side effect¶
Backends are turned on by blank import — the same pattern as net/http/pprof,
the image/* decoders, and database drivers. Importing the package for its
init() side effect registers the backend; resolving it by name (signing.Get)
dispatches to it. This keeps activation explicit and local to main, and keeps
the core free of any flag or plugin machinery — a backend that needs CLI flags
declares an optional interface that the front-end type-asserts, never the
library.
The stdlib-seam corollary¶
The same instinct shapes the rest of the API. Where a heavier design would
invent its own abstractions, signing accepts standard-library types at its
seams: crypto.Signer for keys, *http.Client for WKD fetches (nil → a
stdlib 30s-timeout client), and *slog.Logger for logging (nil → disabled).
Each seam is a place a consumer can inject a hardened or instrumented
implementation without the library taking on a dependency to support it.
See also¶
- Implement a custom backend — do it.
- The trust model — what the verifier guarantees.
- Threat model — including how the library's own distribution integrity is established.