ecdsa.com

Sign a message · Go

How to sign a message with ECDSA in Go

Go signs with ECDSA using only the standard library, and it makes two things explicit that other languages hide: you hash the message yourself, and you choose between DER output (ecdsa.SignASN1) and the bare r and s integers (ecdsa.Sign).

Tested with Go 1.26 (standard library only).

sign.go — SHA-256 digest, DER output
package main
 
import (
"crypto/ecdsa"
"crypto/rand"
"crypto/sha256"
"crypto/x509"
"encoding/pem"
"os"
)
 
func main() {
pemBytes, _ := os.ReadFile("key.pem")
block, _ := pem.Decode(pemBytes)
key, err := x509.ParsePKCS8PrivateKey(block.Bytes) // "BEGIN PRIVATE KEY"
if err != nil {
panic(err)
}
priv := key.(*ecdsa.PrivateKey)
 
message, _ := os.ReadFile("message.txt")
digest := sha256.Sum256(message) // sign the digest, not the message
 
signature, err := ecdsa.SignASN1(rand.Reader, priv, digest[:]) // ASN.1 DER
if err != nil {
panic(err)
}
os.WriteFile("sig.der", signature, 0o644)
}
raw r‖s output — FillBytes keeps the fixed width
r, s, err := ecdsa.Sign(rand.Reader, priv, digest[:])
if err != nil {
panic(err)
}
raw := make([]byte, 64)
r.FillBytes(raw[:32]) // left-pads with zeros — critical for short values
s.FillBytes(raw[32:])

How it works

  1. x509.ParsePKCS8PrivateKey reads BEGIN PRIVATE KEY; for legacy BEGIN EC PRIVATE KEY files use x509.ParseECPrivateKey instead.
  2. Hash first (sha256.Sum256), then sign the digest — Go's signer never touches the raw message.
  3. SignASN1 emits DER for OpenSSL-compatible consumers; Sign returns r and s as *big.Int when you need raw or custom encodings.
  4. Always pass rand.Reader — the nonce must be cryptographically random, and there is no valid reason to substitute anything else.

Gotchas

  • Building raw output with r.Bytes() instead of FillBytes drops leading zero bytes, producing a short signature roughly 1 time in 256 — an intermittent bug that vanishes when you retry.
  • The digest length should match the curve's field size (32 bytes for P-256). Go will sign a digest of any length by truncating per the standard, so a mistake here still "works" — until another stack disagrees.
  • Go emits signatures without low-S normalization; if a Bitcoin-adjacent consumer rejects roughly half your signatures, normalize s to n − s when s > n/2.

Related recipes