ecdsa.com

Verify a signature · Go

How to verify an ECDSA signature in Go

Go verifies ECDSA signatures with the standard library alone, but it splits the work across packages: encoding/pem and crypto/x509 load the key, and crypto/sha256 hashes the message — because ecdsa.VerifyASN1 takes a digest, not the message.

Tested with Go 1.26 (standard library only).

verify.go — verify a DER signature
package main
 
import (
"crypto/ecdsa"
"crypto/sha256"
"crypto/x509"
"encoding/pem"
"fmt"
"os"
)
 
func main() {
pemBytes, _ := os.ReadFile("pub.pem")
block, _ := pem.Decode(pemBytes) // "PUBLIC KEY" — SPKI
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
panic(err)
}
ecPub := pub.(*ecdsa.PublicKey)
 
message, _ := os.ReadFile("message.txt")
signature, _ := os.ReadFile("sig.der") // ASN.1 DER
 
digest := sha256.Sum256(message) // Go verifies a digest, not the message
if ecdsa.VerifyASN1(ecPub, digest[:], signature) {
fmt.Println("valid")
} else {
fmt.Println("INVALID")
}
}
raw r‖s signatures — split the halves and use ecdsa.Verify
r := new(big.Int).SetBytes(rawSig[:32]) // needs "math/big"
s := new(big.Int).SetBytes(rawSig[32:])
ok := ecdsa.Verify(ecPub, digest[:], r, s)

How it works

  1. pem.Decode extracts the DER block from BEGIN PUBLIC KEY; x509.ParsePKIXPublicKey parses the SPKI structure and returns any — type-assert to *ecdsa.PublicKey.
  2. Unlike Node.js and Python, Go does not hash for you: compute sha256.Sum256(message) and pass the digest slice.
  3. ecdsa.VerifyASN1 consumes ASN.1 DER (OpenSSL's output). For raw 64-byte signatures, load each half into a big.Int and call ecdsa.Verify.

Gotchas

  • Passing the message where the digest belongs is the classic Go ECDSA bug — verification simply returns false. The digest argument should be exactly the hash size (32 bytes for SHA-256).
  • If your PEM says BEGIN CERTIFICATE, parse it with x509.ParseCertificate and use cert.PublicKeyParsePKIXPublicKey does not accept certificates.
  • VerifyASN1 uses a strict DER parser: trailing bytes or non-minimal integers are rejected. A signature that passes elsewhere but fails in Go may be sloppily encoded — inspect it in the converter.

Related recipes