Your exported signing function takes ed25519.PrivateKey. Would you change it to crypto.Signer before other teams depend on it?
answer
- a parameter type is a promise
- where will this key live in two years
- the standard library already defines it
- what the interface leaves you nowhere to put
- the concrete key already satisfies it
basics
~20 sUsually yes, and before consumers exist. crypto.Signer lets a hardware token or a remote key service be dropped in without touching a caller, while ed25519.PrivateKey hard-codes the algorithm and the assumption that the key loads into your process.
solid answer
~40 sI would change it, and change it before consumers exist, because a parameter type on an exported function is the thing you cannot take back. `crypto.Signer` costs nothing at the call site — `ed25519.PrivateKey` already implements it — and buys key-custody flexibility: a token or a remote key service satisfies the interface, and the private key never has to exist in this process's memory. It is also the standard library's interface, not one I invented, so there is no abstraction of mine to maintain. The cost I would state plainly: `Public()` returns `crypto.PublicKey`, so callers type-assert; `Sign` carries no `context.Context`, so deadlines and retries live in a wrapper; and signing becomes a fallible round trip rather than local CPU. I would also settle message-versus-digest in the doc comment, since Ed25519 and ECDSA disagree.
code
go · 12 lines// Before: the key must be loadable into this process, and it must be Ed25519.
func SignRelease(key ed25519.PrivateKey, manifest []byte) ([]byte, error) {
return ed25519.Sign(key, manifest), nil
}
// After: in-memory keys, hardware tokens and remote key services all fit.
func SignRelease(signer crypto.Signer, manifest []byte) ([]byte, error) {
if _, ok := signer.Public().(ed25519.PublicKey); !ok {
return nil, errors.New("release signing requires an Ed25519 key")
}
return signer.Sign(rand.Reader, manifest, crypto.Hash(0))
}go deeper
Know that ed25519.PrivateKey already satisfies crypto.Signer, so switching the parameter to the interface still accepts an ordinary in-memory key and breaks no existing caller.
Be able to explain what the interface hides — where the key physically lives — and what changes for a caller, chiefly type-asserting the value that Public() returns.
Show that you would validate the signer's algorithm once at construction and wrap it with the timeout, retry and metrics that the interface itself leaves no room for.
Own the tradeoff out loud: an exported parameter type is a promise you cannot withdraw, and key custody may not be your decision to make. Name who can overrule you and what a migration costs once consumers have shipped.
## What is actually being decided The parameter type of an exported function is a promise. Once other teams import the package and build against `SignRelease(key ed25519.PrivateKey, ...)`, changing it means a coordinated migration across every consumer. The interesting question is therefore not "which is nicer" but **which commitment can you live with for the lifetime of the API**, and who is entitled to overrule you. ## The case for crypto.Signer **It decouples the operation from the custody of the key.** `ed25519.PrivateKey` is `[]byte`: to call the function, the caller must have the raw secret in this process's address space. That forecloses hardware tokens, smartcards and cloud key services — exactly the direction key custody moves as a system grows or enters an audit. `crypto.Signer` names only the ability to sign, and a twenty-line wrapper makes a remote key satisfy it. **It costs the caller nothing today.** `ed25519.PrivateKey` already implements `crypto.Signer`, so an existing caller passes the same value it always did. **It is not your abstraction.** A common objection to interfaces at boundaries is that you are inventing a vocabulary consumers must learn. `crypto.Signer` is defined by the standard library, understood everywhere, and already what other crypto APIs accept. That removes most of the usual coupling argument. **Testing improves.** A fake signer that records the digests it was given is trivial; faking a real key is not much harder, but asserting on calls is. ## What you give up, stated honestly 1. **Compile-time algorithm knowledge.** With `crypto.Signer` anyone can pass an RSA key into a system whose verifiers only understand Ed25519. The mitigation is a type assertion on `Public()` at construction — fail at startup with a clear message rather than in the first request. 2. **`Public()` is `crypto.PublicKey`.** Callers assert to a concrete type before marshalling or verifying. Cache the asserted value; do not call `Public()` per request. 3. **No `context.Context` on `Sign`.** For a remote key this is the real wound: per-call deadlines, cancellation and request-scoped tracing have to be carried on the wrapper struct or by constructing a per-request signer. Decide which, once, and document it. 4. **New failure modes.** Signing was infallible in practice; now it can time out, be throttled, or fail on credentials. Every caller's error path gets exercised for the first time. 5. **Latency and rate limits.** Microseconds become milliseconds, and a key service has quotas. If the function signs per item in a hot loop, an interface that permits a remote key invites a design that cannot use one — which is itself worth knowing early. ## The message-versus-digest question rides along Because Ed25519 signs the message and ECDSA and RSA sign a digest, an algorithm-agnostic parameter type forces you to say which one your function takes. Deciding it in the doc comment — and, if you take the message, enforcing a size limit — is part of the same call. Taking a digest keeps payloads tiny but rules out plain Ed25519 through the interface; taking the message keeps every algorithm available but pushes whole artifacts through the service. ## Who can overrule you This is why the decision is not purely the API owner's. Key custody is usually the security or platform team's remit. If policy already says production signing keys live in a hardware module, then a signature that requires raw key bytes is non-compliant by construction, and the only question is whether it is fixed cheaply now or expensively after consumers ship. Compliance regimes that mandate an approved cryptographic module make the same call for you. Conversely, if the organisation has no key service, no plan for one, and this is one internal binary, `ed25519.PrivateKey` is defensible — the honest version of that argument is "we accept the migration cost if custody changes", not "we will never need it". ## If consumers already exist Don't edit the signature in place. Add the interface-taking entry point alongside it — a second function, or a constructor that takes a `crypto.Signer` — and reduce the original to a one-line adapter, since the concrete key already satisfies the interface. Document a removal window. The cost of the transition is the argument for making the call before anyone depends on it. ## How I would summarise the call Take `crypto.Signer`; assert the algorithm once at construction and fail fast; wrap the signer with your own timeout, retry and metrics because the interface leaves no room for them; state message-or-digest in the doc comment; and name explicitly, in the review, that this is a key-custody decision the security owner may take out of your hands.
- Who can overrule the API owner on this, and on what grounds?The team that owns key custody — security or platform. If policy says production signing keys must live in a hardware module or a managed key service, a function taking raw ed25519.PrivateKey is non-compliant by construction and has to change: cheaply now, expensively once consumers have shipped against it. Regimes that mandate an approved cryptographic module make the same call for you.
- What do you actually lose by accepting crypto.Signer, and how do you mitigate each loss?Compile-time knowledge of the algorithm, a place to put a context, and cheap local calls. Mitigate by asserting the dynamic type of Public() once at construction and failing at startup, wrapping the signer with your own timeout, retry and metrics, and caching the public key instead of calling Public() per request.
- The library is already published with the concrete type. How do you make the change?Add rather than edit: a second entry point or a constructor taking crypto.Signer, with the original reduced to a one-line adapter, since ed25519.PrivateKey already satisfies the interface. Document a removal window and move consumers over. The size of that dance is the argument for deciding the parameter type before anyone depends on it.
- When is keeping the concrete ed25519.PrivateKey the right call?When the key genuinely cannot move — a single internal binary with a key on disk, no key service in the organisation and none planned — and when the function is on a hot path where a remote signer would be unusable anyway. State it as an accepted migration cost rather than as a claim that custody will never change.
saying these in an interview costs you the question
- Says the concrete type is fine because it avoids an interface call
- Treats an exported parameter type as easy to change later
- Invents a home-grown Signer interface instead of the standard one
- Ignores that crypto.Signer gives no place for a context or a timeout
- Claims crypto.Signer still forces the key into process memory
- Never mentions who owns key custody in the organisation