skip to content

Ed25519 and crypto.Signer

ed25519.Sign is two calls, but a production key sits behind crypto.Signer so the private bytes never enter your process, and Sign takes the message where ecdsa takes a digest.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

questions

5

What does ed25519.GenerateKey return in Go, and how do you sign and verify a message with it?

level: juniorimportance: must knowfreq 50%

answer

  1. three return values, one is error
  2. the whole message goes in
  3. no digest, no hash argument
  4. the output length never varies
  5. verification hands back a bool

basics

~10 s

ed25519.GenerateKey returns a public key, a private key, and an error. ed25519.Sign(priv, message) returns a 64-byte signature over the whole message, and ed25519.Verify(pub, message, sig) returns a bool saying whether that signature is valid.

solid answer

~40 s

`ed25519.GenerateKey(rand)` returns `(PublicKey, PrivateKey, error)`; passing `nil` as the reader makes it use `crypto/rand.Reader`, which is what you want in production. Signing is a package function, not a method on a hash: `sig := ed25519.Sign(priv, message)` takes the entire message — Ed25519 hashes it internally, in two passes — and always returns exactly 64 bytes (`ed25519.SignatureSize`). Verification is `ed25519.Verify(pub, message, sig)`, which returns a plain `bool`, so treat `false` as a hard rejection rather than an error to log and continue past. Both key types are `[]byte` underneath: the public key is 32 bytes, the private key 64. `ed25519.Sign` panics if the private key is not `ed25519.PrivateKeySize` long, so never build one by casting arbitrary bytes — rebuild it from a stored 32-byte seed with `ed25519.NewKeyFromSeed`.

code

go · 11 lines
go
pub, priv, err := ed25519.GenerateKey(nil) // nil reader means crypto/rand.Reader
if err != nil {
	return err
}

manifest := []byte("release-1.4.2 sha256:9f86d0...")
sig := ed25519.Sign(priv, manifest) // always 64 bytes

if !ed25519.Verify(pub, manifest, sig) {
	return errors.New("manifest signature invalid")
}

go deeper

for a junior

Be ready to write the three calls from memory: GenerateKey, Sign, Verify. Know that the whole message goes into ed25519.Sign, that the signature is 64 bytes, and that Verify hands back a bool.

for a middle

Explain why the private key is 64 bytes while the seed is 32, what priv.Public() gives you and why it needs a type assertion, and why passing nil to ed25519.GenerateKey is safe.

for a senior

Expect to say where the private key lives, how it is loaded, and why casting stored bytes into ed25519.PrivateKey turns a storage bug into a panic inside a request handler.

for a principal

Own what the service actually signs — the artifact bytes, or a manifest naming them by digest. That choice, not the algorithm, is what consumers will be verifying years from now.

## The three calls Go puts Ed25519 in `crypto/ed25519`, and the whole everyday surface is three functions plus two named byte-slice types. **Generating.** `func GenerateKey(rand io.Reader) (PublicKey, PrivateKey, error)` returns the public key first, the private key second, and an error third. If you pass `nil` as the reader, the package uses `crypto/rand.Reader` — the operating system's cryptographically secure source — which is the right choice in production. Passing your own reader is for tests and for deterministic key-derivation schemes; a `math/rand` source here would make every key you generate predictable. **Signing.** `func Sign(privateKey PrivateKey, message []byte) []byte` takes the *message*, not a hash of it, and returns a signature that is always 64 bytes (`ed25519.SignatureSize`). There is no error return: for a well-formed key the operation cannot fail. This surprises people who come from RSA or ECDSA, where you always hash first and hand over a digest. Ed25519 hashes internally as part of the scheme — it makes two passes over the message — so the API takes the message and does the hashing itself. **Verifying.** `func Verify(publicKey PublicKey, message, sig []byte) bool` returns a bare boolean. There is no error type and no reason code, deliberately: there is exactly one interesting outcome and no diagnosis a caller should branch on. Write it as a guard — `if !ed25519.Verify(pub, msg, sig) { reject }` — and make the rejection total. ## The types are just byte slices `ed25519.PublicKey` and `ed25519.PrivateKey` are both defined as `[]byte`. That makes them cheap to store and to pass around, and it makes one class of bug very easy to write, because any `[]byte` of any length converts to them without complaint: - `ed25519.PublicKeySize` is 32 - `ed25519.PrivateKeySize` is 64 - `ed25519.SeedSize` is 32 - `ed25519.SignatureSize` is 64 The private key is 64 bytes because it stores the 32-byte seed followed by the 32-byte public key. That is why `priv.Public()` is free — it just returns the tail — and why `priv.Seed()` gives you back the 32 bytes that are the actual secret. If your storage layer keeps 32 bytes, you must rebuild the key with `ed25519.NewKeyFromSeed(seed)`. Converting those 32 bytes directly with `ed25519.PrivateKey(seed)` compiles fine and then **panics** inside `ed25519.Sign`, because `Sign` checks the length and panics when it is not `PrivateKeySize`. `ed25519.Verify` panics the same way on a public key that is not 32 bytes. In a service, that shows up as a panic in a request handler with a stack frame in `crypto/ed25519` rather than in your own code — which is the tell that the key material, not your logic, is the wrong shape. ## Getting the public key back `priv.Public()` returns `crypto.PublicKey`, which is an empty interface, so you type-assert: ```go pub, ok := priv.Public().(ed25519.PublicKey) ``` The vague return type exists because `Public()` is one half of the `crypto.Signer` interface, which has to cover every algorithm. `ed25519.PrivateKey` also has a `Sign` method, so it satisfies `crypto.Signer` as-is. ## Practical notes - **Sign what you mean.** Signing a manifest that names artifacts by digest is usually better than signing each artifact's bytes: the signature stays small, and what a consumer verifies is the full statement, not one file in isolation. - **The signature length never varies.** Anything that treats an Ed25519 signature as variable-length is confusing it with ECDSA's DER encoding. - **There is no key size to choose.** Ed25519 has one parameter set. Compared to RSA or ECDSA there are no curve, key-size, or padding decisions to get wrong, which is a large part of its appeal. - **Encoding for the wire** is your problem: the raw 32/64-byte slices are not self-describing, so most systems base64 them or wrap the public key in a standard structure before publishing it.

  • What happens if you convert a stored 32-byte seed straight to ed25519.PrivateKey and call ed25519.Sign with it?
    It compiles, because ed25519.PrivateKey is just []byte, and then panics at run time: ed25519.Sign panics when the key is not ed25519.PrivateKeySize, which is 64. Rebuild the key with ed25519.NewKeyFromSeed instead of casting. ed25519.Verify panics the same way on a public key that is not 32 bytes.
  • You loaded only the private key. How do you get the matching public key?
    Call priv.Public() and type-assert the result to ed25519.PublicKey, since the method returns crypto.PublicKey, an empty interface. It is cheap because an ed25519.PrivateKey is the 32-byte seed followed by the 32-byte public key — but call Public() rather than slicing the tail yourself.
  • Why does ed25519.Verify return a bool rather than an error?
    There is exactly one outcome worth acting on: the signature is valid or it is not. A reason code would invite callers to branch on why a verification failed, and telling an attacker why is never useful. Treat false as a total rejection — do not retry, do not fall back, do not log a partial reason as if it were recoverable.

The private key is a signet ring and the message is the whole letter: you press the ring on the letter itself rather than on a summary of it, and the mark it leaves is always the same size.

saying these in an interview costs you the question

  • Hashes the message first and signs the digest with ed25519.Sign
  • Says the signature length depends on the message size
  • Treats a false from ed25519.Verify as a soft warning
  • Casts arbitrary stored bytes to ed25519.PrivateKey
  • Passes a math/rand source to ed25519.GenerateKey
  • Thinks there is a key size or curve to choose for Ed25519
open as a page

What two methods does Go's crypto.Signer interface declare, and what problem does it solve?

level: middleimportance: should knowfreq 45%

basics

~20 s

crypto.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.

open as a page

What does ecdsa.SignASN1 take as its input, and how is the signature it returns encoded?

level: middleimportance: should knowfreq 40%

basics

~10 s

ecdsa.SignASN1(rand, priv, hash) takes an already-computed digest, not the message, and returns the signature ASN.1 DER-encoded as the integer pair r and s. That encoding is variable-length. ecdsa.VerifyASN1 checks it and returns a bool.

open as a page

Your exported signing function takes ed25519.PrivateKey. Would you change it to crypto.Signer before other teams depend on it?

level: principalimportance: should knowfreq 35%

basics

~20 s

Usually 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.

open as a page

Signing through crypto.Signer with a SHA-256 digest works for ECDSA keys but fails for Ed25519. Why?

level: seniorimportance: nice to knowfreq 30%

basics

~20 s

Ed25519 signs the whole message and hashes it internally, so ed25519.PrivateKey.Sign requires opts.HashFunc() to be crypto.Hash(0) and rejects a SHA-256 digest. ECDSA and RSA keys expect the opposite: a named hash and a digest of matching length.

open as a page