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)}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 valuess.FillBytes(raw[32:])How it works
x509.ParsePKCS8PrivateKeyreadsBEGIN PRIVATE KEY; for legacyBEGIN EC PRIVATE KEYfiles usex509.ParseECPrivateKeyinstead.- Hash first (
sha256.Sum256), then sign the digest — Go's signer never touches the raw message. SignASN1emits DER for OpenSSL-compatible consumers;Signreturnsrandsas*big.Intwhen you need raw or custom encodings.- 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 ofFillBytesdrops 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.