skip to content

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

level: middleimportance: should knowfreq 40%

answer

  1. the message is not what goes in
  2. you compute the hash yourself
  3. the output is not a fixed length
  4. two integers wrapped in a SEQUENCE
  5. the fixed-width layout needs conversion

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.

solid answer

~40 s

The signature is `ecdsa.SignASN1(rand io.Reader, priv *ecdsa.PrivateKey, hash []byte) ([]byte, error)`. The `hash` argument is a digest you computed yourself — typically `sum := sha256.Sum256(msg)` and then `sum[:]` — because ECDSA signs a digest rather than a message; if the digest is longer than the curve order, the leftmost bits are used. Pass `crypto/rand.Reader` as `rand`. The returned bytes are an ASN.1 DER structure wrapping the two integers r and s, so the length varies from signature to signature — usually 70 to 72 bytes on P-256, sometimes fewer when an integer has leading zero bytes. Any code that expects a fixed 64-byte r‖s pair has to convert. `ecdsa.VerifyASN1(pub, hash, sig)` returns a bool, and `*ecdsa.PrivateKey`'s `crypto.Signer` method produces the same DER form.

code

go · 11 lines
go
sum := sha256.Sum256(manifest)

sig, err := ecdsa.SignASN1(rand.Reader, priv, sum[:])
if err != nil {
	return err
}
// len(sig) varies between signatures: DER wraps the integers r and s.

if !ecdsa.VerifyASN1(&priv.PublicKey, sum[:], sig) {
	return errors.New("signature did not verify")
}

go deeper

for a junior

Remember that ECDSA in Go signs a digest you compute first, usually with sha256.Sum256 followed by sum[:], and that crypto/rand.Reader is the reader you pass.

for a middle

Be able to say what DER wraps — the two integers r and s — and why that makes the output length differ between two signatures over the very same digest.

for a senior

Expect a wire-format question: converting between DER and fixed-width r-then-s, and diagnosing an interop failure where a counterparty rejects every signature you send.

for a principal

Decide once, at the boundary, which signature encoding your systems speak, and write it into the contract. Leaving it per-consumer means every verifier you will ever have carries conversion code.

## The two functions ```go func SignASN1(rand io.Reader, priv *PrivateKey, hash []byte) ([]byte, error) func VerifyASN1(pub *PublicKey, hash, sig []byte) bool ``` They are the pair you should reach for in `crypto/ecdsa`. Everything interesting about them is in the two byte slices. ## The input is a digest, and you compute it ECDSA is defined over a number derived from a hash of the message; the Go API does not hide that. `hash` is the digest, and choosing it is your job: ```go sum := sha256.Sum256(manifest) sig, err := ecdsa.SignASN1(rand.Reader, priv, sum[:]) ``` `sha256.Sum256` returns a `[32]byte` array, so `sum[:]` slices it. Two failure modes follow directly. Passing the whole message where a digest is expected "works" — the function accepts any byte slice — and produces a signature that nothing else will ever verify, because every other implementation hashes first. And hashing twice, once in your helper and once again in a wrapper, produces the same silent interop failure. If the digest is longer than the curve's order, ECDSA uses its leftmost bits, so a SHA-512 digest with P-256 is legal but only the first bits participate; matching the hash size to the curve (SHA-256 with P-256, SHA-384 with P-384) is the convention. Contrast this with Ed25519, whose `ed25519.Sign` takes the message because the scheme hashes internally. The two APIs look similar and want opposite inputs. ## The randomness argument is not decorative ECDSA needs a fresh secret value per signature. If that value repeats across two signatures made with the same key, the private key can be recovered from the pair; if it is merely biased, enough signatures still leak it. That is why `SignASN1` takes an `io.Reader` at all, and why the only correct production argument is `crypto/rand.Reader`. A fixed or seeded reader belongs in nothing but a test that must reproduce an exact byte string. Ed25519 has no such parameter because it derives its per-signature value deterministically from the key and the message. ## The output is DER, so the length varies An ECDSA signature is mathematically a pair of integers, r and s. There are two common ways to put that pair on a wire: 1. **ASN.1 DER** — a SEQUENCE of two INTEGERs, each minimally encoded and each carrying a leading zero byte when its top bit is set. This is what `SignASN1` emits. On P-256 the result is usually 70–72 bytes and occasionally shorter, because r or s can happen to have leading zero bytes. 2. **Fixed-width r‖s** — each integer left-padded with zeros to the curve's byte length and concatenated, giving exactly 64 bytes on P-256. This is what JOSE/JWS ES256 and the browser WebCrypto API use. Go's `crypto/ecdsa` speaks only the first. When a counterparty rejects every signature you produce and the digests match, this encoding mismatch is the usual cause. Converting is mechanical: decode the DER with `encoding/asn1` into a struct of two `*big.Int` fields, then write each one as a fixed-width big-endian slice. What you must never do is chop bytes off the front of the DER blob — the tags, lengths and any leading zero bytes are not padding. ## What about ecdsa.Sign? `ecdsa.Sign(rand, priv, hash) (r, s *big.Int, err error)` is the older, lower-level form: it hands you the raw integers and leaves the encoding entirely to you. That is how mismatched wire formats get invented in the first place, so prefer `SignASN1` unless you specifically need a non-DER layout, in which case the `big.Int` form saves you a decode step. The matching `ecdsa.Verify` takes r and s as `*big.Int` values, not bytes. ## Through the interface `*ecdsa.PrivateKey` also has a `Sign(rand, digest, opts)` method, so it satisfies `crypto.Signer`, and that method returns the same ASN.1 DER encoding. Its `opts` must report a real hash function — `crypto.SHA256` for a SHA-256 digest — and the digest length is expected to match. `priv.PublicKey` is an embedded field, so `&priv.PublicKey` is what you hand to `VerifyASN1`.

  • Why must ecdsa.SignASN1 be given a real random reader when ed25519.Sign takes none at all?
    ECDSA needs a fresh secret value for every signature; repeat it across two signatures with the same key and the private key falls out of the pair, and even a biased source leaks it over many signatures. Ed25519 derives that value deterministically from the key and the message, so it needs no randomness — which is why ed25519.Sign has no reader parameter and ed25519.PrivateKey.Sign ignores the one it is handed.
  • A counterparty expects a fixed 64-byte signature but ecdsa.SignASN1 gives variable-length bytes. What is going on?
    They want the raw r-then-s layout, each integer left-padded to the curve's byte length — the form JWS ES256 and WebCrypto use. Go emits ASN.1 DER instead. Decode with encoding/asn1 into a struct of two *big.Int values and re-emit fixed-width big-endian halves with FillBytes. Never strip bytes off the front of the DER blob; the tags and lengths are not padding.
  • What is ecdsa.Sign, and why prefer SignASN1?
    ecdsa.Sign returns the raw pair (r, s *big.Int) and leaves the encoding to you, which is exactly how incompatible wire formats get invented. SignASN1 emits the standard DER encoding other implementations expect and pairs with VerifyASN1. Use the big.Int form only when you must produce a different layout anyway.

saying these in an interview costs you the question

  • Passes the whole message to ecdsa.SignASN1 instead of a digest
  • Assumes an ECDSA signature is a fixed 64 bytes
  • Uses a fixed or seeded reader as the rand argument
  • Thinks DER and the raw r-then-s layout are byte-identical
  • Strips leading bytes off DER to reach 64 bytes
  • Hashes the message twice, once outside and once in a wrapper