What two methods does Go's crypto.Signer interface declare, and what problem does it solve?
answer
- two methods, one of them is not signing
- the interface hides where the key lives
- opts is itself a one-method interface
- crypto.SHA256 can be passed as opts
- no context parameter for a remote call
basics
~20 scrypto.Signer declares Public() and Sign(rand, digest, opts). It lets code produce signatures without ever holding the private key bytes, so a key locked in a hardware token or a remote key service satisfies the same interface as an in-memory key.
solid answer
~40 sIt is two methods: `Public() crypto.PublicKey` and `Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error)`. `crypto.SignerOpts` is itself a single method, `HashFunc() crypto.Hash`, and `crypto.Hash` satisfies it, so you can pass `crypto.SHA256` straight through as `opts`. The point is that the interface exposes only the operation, never the key material: `ed25519.PrivateKey`, `*ecdsa.PrivateKey` and `*rsa.PrivateKey` all implement it, and so can a thin wrapper that forwards to a hardware token or a remote key service where the private key cannot be exported at all. So take `crypto.Signer` in any function of yours that needs a signature, and use `Public()` to learn the algorithm and to publish the verification key. Note what the interface does not have: no `context.Context`, and `rand` is ignored by implementations that supply their own randomness.
code
go · 15 linestype remoteSigner struct {
pub crypto.PublicKey
keyID string
}
var _ crypto.Signer = (*remoteSigner)(nil)
func (s *remoteSigner) Public() crypto.PublicKey { return s.pub }
func (s *remoteSigner) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) {
if opts.HashFunc() != crypto.SHA256 {
return nil, fmt.Errorf("remote signer: unsupported hash %v", opts.HashFunc())
}
return s.callKeyService(s.keyID, digest) // key bytes stay on the far side
}go deeper
Recall that crypto.Signer is an interface with exactly two methods and that Public() is one of them. Know that the standard library's private key types already satisfy it, so you rarely implement it yourself.
Be able to write the full Sign signature, explain that crypto.SignerOpts is only HashFunc(), and show why crypto.SHA256 can be handed straight in as the opts value.
Show how you would put a hardware or remote key behind it: validate opts, cache the Public() value, and add the timeouts, retries and metrics the interface itself gives you nowhere to put.
Argue for accepting crypto.Signer at package boundaries so a change of key custody is a wiring change rather than a rewrite, and be honest about the cost: no context, per-call latency, and errors that are now network errors.
## The declaration ```go type Signer interface { Public() PublicKey Sign(rand io.Reader, digest []byte, opts SignerOpts) (signature []byte, err error) } ``` That lives in the `crypto` package — the small root package that holds cross-algorithm types and nothing else. Three supporting declarations matter: - `crypto.PublicKey` and `crypto.PrivateKey` are empty interfaces (`any`). They carry no methods; they exist to name intent in a signature. - `crypto.SignerOpts` is `interface { HashFunc() Hash }` — one method, reporting which hash the caller used on the message. - `crypto.Hash` is an integer enum (`crypto.SHA256`, `crypto.SHA512`, …) that has its own `HashFunc()` method returning itself. That is why you can pass `crypto.SHA256` directly as the `opts` argument without wrapping it. Algorithms with extra knobs define richer options — `*rsa.PSSOptions` for RSA-PSS, `*ed25519.Options` for the pre-hashed Ed25519 variant — and those types implement `HashFunc()` too. ## Why the interface exists The private key is the one thing in a signing system you would rather not have. `crypto.Signer` is Go's answer: it names the *capability* to sign rather than the *material* that signs. Once your code takes a `crypto.Signer`: - an in-memory `ed25519.PrivateKey` works, because it has both methods; - a `*ecdsa.PrivateKey` or `*rsa.PrivateKey` works for the same reason; - a hardware token, a smartcard, or a remote key service works if you write a small struct whose `Sign` forwards the digest over the wire and whose `Public()` returns a cached public key. In the last case the private key never exists in your process's address space, never lands in a heap dump or a core file, and cannot be exfiltrated by a memory-disclosure bug. That is the whole payoff, and it costs one parameter type at the boundary. `crypto.Decrypter` is the mirror interface for the decryption direction, with `Public()` and `Decrypt(rand, msg, opts)`. ## Reading the parameters honestly **`rand io.Reader`.** Some schemes need randomness per signature — RSA-PSS salting, classic ECDSA nonces — so the interface offers a source. Others do not: Ed25519 is deterministic and ignores the argument entirely, and a hardware implementation generates its own randomness inside the device. Pass `crypto/rand.Reader` and be done; a fixed reader belongs only in a test that must be reproducible. **`digest []byte`.** The name is a convention, not a contract enforced by the type system, and this is the sharp edge. For ECDSA and RSA it really is a digest, and its length must match the hash named by `opts`. For Ed25519 it is the *message*, because Ed25519 hashes internally — `ed25519.PrivateKey.Sign` requires `opts.HashFunc()` to report `crypto.Hash(0)` and returns an error otherwise. A helper that always hashes and always passes `crypto.SHA256` therefore works with two of the three standard key types and fails with the third. **`opts crypto.SignerOpts`.** Never `nil`: implementations call `opts.HashFunc()` immediately, so a nil value is a nil-interface panic rather than a polite default. ## What it does not give you The interface predates the ubiquity of `context.Context` and does not carry one. When the implementation is a network call to a key service, that hurts: per-call deadlines, cancellation, and request-scoped tracing have nowhere to live inside the interface. The usual answer is to put them on the wrapper struct — a signer built with a timeout and retry policy baked in — or to construct a fresh, context-bound signer per request. Along with the missing context, remember that signing has become a fallible round trip: the `error` return that was decorative for an in-memory key is now the common case under a partial outage. `Public()` returns `crypto.PublicKey`, so callers type-assert to `ed25519.PublicKey`, `*ecdsa.PublicKey` or `*rsa.PublicKey` before they can marshal or verify with it. Assert once, at construction, and fail fast if the algorithm is not one your verifiers understand — that check is much cheaper there than in the first request after a key change.
- What does Public() return, and why is that type so vague?It returns crypto.PublicKey, which is an empty interface, so you type-assert it to ed25519.PublicKey, *ecdsa.PublicKey or *rsa.PublicKey before marshalling or verifying with it. It is vague because crypto.Signer must span every algorithm. The payoff is that a caller can publish the verification key without knowing which kind it holds.
- Why does crypto.Signer take a rand io.Reader if a hardware key ignores it?Some schemes need per-signature randomness — RSA-PSS salting, classic ECDSA nonces — so the interface has to offer a source. Deterministic schemes like Ed25519 and devices that generate randomness internally ignore the argument. Pass crypto/rand.Reader everywhere except a test that must reproduce a fixed signature.
- What makes crypto.Signer awkward when the key is held by a remote service?There is no context.Context on Sign, so deadlines, cancellation and tracing have to be carried on the wrapper struct or by constructing a per-request signer. And every signature is now a network round trip: the error return stops being decorative, latency is milliseconds not microseconds, and Public() should be cached rather than fetched per call.
- What breaks if a caller passes nil as the opts argument?A nil-interface panic. Implementations call opts.HashFunc() as the first thing they do, and there is no default to fall back on. Pass a crypto.Hash value — crypto.SHA256 for a SHA-256 digest, crypto.Hash(0) when the scheme signs the unhashed message.
saying these in an interview costs you the question
- Thinks crypto.Signer hands out the private key bytes
- Says only standard library key types can implement it
- Passes nil as the opts argument
- Assumes Public() returns a concrete key type without assertion
- Expects a context.Context parameter on Sign
- Believes the digest parameter means every scheme pre-hashes