Skip to content
ecdsa.com
Go

Go crypto/ecdsa (standard library)

Go's standard-library signature packages — crypto/ecdsa with crypto/x509, ed25519, mlkem and mldsa: DER signatures over a digest you supply, randomized nonces that ignore your Reader, NIST curves only.
License
BSD-3-Clause
Checked version
1.27.3 ·
Source
github.com/golang/go
Documentation
pkg.go.dev/crypto/ecdsa

Compatibility at a glance

ECDSAEdDSAPost-quantum

ECDSA curves
P-224P-256P-384P-521
EdDSA
Ed25519Ed25519phEd25519ctx
Public key formats
spki-derx509sec1-uncompressed
Private key formats
pkcs8-dersec1-derraw-scalar

Go ships its cryptography in the standard library, and since Go 1.24 the core primitives live in the Go Cryptographic Module built for FIPS 140-3. crypto/ecdsa signs and verifies, crypto/x509 reads keys and certificates, and Go 1.27.3 adds crypto/mlkem (ML-KEM-768/1024) and crypto/mldsa (ML-DSA-44/65/87) next to crypto/ed25519. There is no secp256k1: Ethereum and Bitcoin code uses go-ethereum's or decred's packages.

Everything in crypto/ecdsa operates on a digest you computed — the package never hashes. Worse than a wrong signature, a long input is silently truncated to the curve size: a signature over a 46-byte input verified for a different input with the same first 32 bytes. Always hash first, as the sample does. SignASN1, VerifyASN1 and the crypto.Signer interface speak ASN.1 DER; WebCrypto, JWS and other raw r‖s peers need Sign/Verify with the integers padded by FillBytes — Bytes() drops leading zero bytes, so every r or s that happens to start with a zero byte comes out one byte short.

Signatures are randomized and, since Go 1.26, the io.Reader you pass is ignored unless GODEBUG=cryptocustomrand=1 is set: two signatures made with an all-zero Reader differed. Tests that relied on a fixed Reader to get reproducible signatures stopped working; the documented replacement is testing/cryptotest.SetGlobalRandom. s is not normalized and VerifyASN1 accepts both halves, so signatures for strict low-S verifiers need s replaced by n − s.

Key parsing is DER-first. x509.ParsePKIXPublicKey given PEM text fails with asn1: structure error: tags don't match — call pem.Decode first. ecdsa.ParseUncompressedPublicKey and ecdsa.ParseRawPrivateKey read the bare SEC1 point and scalar; compressed points are refused, and there is no reader for passphrase-encrypted PKCS#8. A malformed signature makes VerifyASN1 return false, never an error.

To compare a Go signature with another stack, convert it in the DER ⇄ raw converter and check it in the signature verifier; certificate parsing problems are easiest to see in the certificate decoder.

Facts and the checks behind them

Nothing below is copied from documentation. The check content/libraries/verify/go-crypto-ecdsa/run.sh ran Go crypto/ecdsa (standard library) 1.27.3 in the container image golang:1.27.3-trixie on October 20, 2026; the page must match what it printed, and each fact lists those lines.

  • ECDSA signature encoding

    der

    SignASN1, VerifyASN1 and PrivateKey.Sign (the crypto.Signer path) use ASN.1 DER; Sign and Verify take r and s as *big.Int.

    Recorded by the check
    fact ecdsa-signature-encoding = der
  • ECDSA nonce

    random

    Randomized: two signatures of the same digest differ, as the package documentation states (entropy is mixed with the key and the message).

    Recorded by the check
    fact ecdsa-nonce = random
    evidence ecdsa-nonce: two signatures with an all-zero Reader are identical: false
  • Random source argument

    ignored

    Since Go 1.26 the io.Reader argument is ignored: two signatures made with an all-zero Reader were different. GODEBUG=cryptocustomrand=1 restores the old behaviour for now.

    Recorded by the check
    fact go-rand-reader-argument = ignored
  • Low-S when signing

    either

    s is not normalized: high-S values appeared among 64 signatures.

    Recorded by the check
    fact ecdsa-low-s-sign = either
  • High-S when verifying

    accepts-high-s

    VerifyASN1 accepted the signature with s replaced by n − s.

    Recorded by the check
    fact ecdsa-low-s-verify = accepts-high-s
  • What the signing call hashes

    takes-digest

    All functions take a digest; nothing in crypto/ecdsa hashes the message.

    Recorded by the check
    fact ecdsa-prehash = takes-digest
  • Digest longer than the curve order

    truncated

    Input longer than the curve order is truncated: a signature over a 46-byte input verified for a different input sharing its first 32 bytes.

    Recorded by the check
    fact ecdsa-long-input = truncated
    evidence ecdsa-long-input: a signature over a 46-byte input verified for a different input with the same first 32 bytes: true
  • Malformed ECDSA signature

    returns-false

    VerifyASN1 returned false for a raw r‖s signature and for a 3-byte blob.

    Recorded by the check
    fact ecdsa-malformed-signature = returns-false
  • ECDSA curves

    P-224P-256P-384P-521

    P-224, P-256, P-384 and P-521; crypto/elliptic offers no secp256k1 or Brainpool curves.

    Recorded by the check
    fact ecdsa-curves = P-224, P-256, P-384, P-521
    evidence ecdsa-curves: crypto/elliptic exposes P224, P256, P384 and P521 only
  • EdDSA

    Ed25519Ed25519phEd25519ctx

    Ed25519 plus the RFC 8032 Ed25519ph and Ed25519ctx variants through ed25519.Options.

    Recorded by the check
    fact eddsa = Ed25519, Ed25519ph, Ed25519ctx
  • Ed25519 private key layout

    expanded-64

    ed25519.PrivateKey is 64 bytes, the seed followed by the public key; NewKeyFromSeed builds it from the 32-byte seed.

    Recorded by the check
    fact ed25519-private-key = expanded-64
  • Post-quantum algorithms

    ML-KEMML-DSA

    ML-KEM-768 and ML-KEM-1024 encapsulated and decapsulated, and ML-DSA-44/65/87 signed and verified, in Go 1.27.3.

    Recorded by the check
    fact pqc = ML-KEM, ML-DSA
    evidence pqc: crypto/mlkem offers ML-KEM-768 and ML-KEM-1024; crypto/mldsa offers ML-DSA-44, ML-DSA-65 and ML-DSA-87
  • Public key formats

    spki-derx509sec1-uncompressed

    SPKI DER via x509.ParsePKIXPublicKey, certificates, and uncompressed SEC1 points via ecdsa.ParseUncompressedPublicKey; PEM needs pem.Decode first and compressed points are refused.

    Recorded by the check
    fact public-key-formats = spki-der, x509, sec1-uncompressed
    evidence public-key-formats: ParsePKIXPublicKey on PEM text: asn1: structure error: tags don't match (16 vs {class:0 tag:13 length:45 isCompound:true}) {optional:false explicit:false application:false private:false defaultValue:<nil> tag:<nil> stringType:0 timeType:0 set:false omitEmpty:false} publicKeyInfo @2
    evidence public-key-formats: ParseUncompressedPublicKey on a compressed point: ecdsa: invalid uncompressed public key
    evidence public-key-formats: ecdh.P256().NewPublicKey on a compressed point: crypto/ecdh: invalid public key
  • Private key formats

    pkcs8-dersec1-derraw-scalar

    PKCS#8 DER, SEC1 DER and the raw scalar via ecdsa.ParseRawPrivateKey; there is no parser for passphrase-encrypted PKCS#8.

    Recorded by the check
    fact private-key-formats = pkcs8-der, sec1-der, raw-scalar
    evidence private-key-formats: no parser for passphrase-encrypted PKCS#8 in the standard library

Usage

Hash first, sign as DER for OpenSSL-style peers, and build a correctly padded raw r‖s for WebCrypto and JOSE.
package main

import (
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
	"crypto/sha256"
	"fmt"
	"math/big"
)

func main() {
	key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	// crypto/ecdsa never hashes: pass a digest, never the message itself.
	digest := sha256.Sum256([]byte("order #1042 approved"))

	der, _ := ecdsa.SignASN1(rand.Reader, key, digest[:]) // ASN.1 DER for OpenSSL, Java, .NET peers
	fmt.Println("DER verified:", ecdsa.VerifyASN1(&key.PublicKey, digest[:], der))

	// Raw r||s for WebCrypto and JOSE: FillBytes pads to 32 bytes; Bytes() would not.
	r, s, _ := ecdsa.Sign(rand.Reader, key, digest[:])
	raw := make([]byte, 64)
	r.FillBytes(raw[:32])
	s.FillBytes(raw[32:])
	ok := ecdsa.Verify(&key.PublicKey, digest[:], new(big.Int).SetBytes(raw[:32]), new(big.Int).SetBytes(raw[32:]))
	fmt.Println("raw verified:", ok, len(raw))
}

The snippet harness runs every sample verbatim in the container image golang:1.27.3-trixie, and the run fails unless the output matches.

Command
go run .
Output must match (regular expression)
DER verified: true\nraw verified: true 64

Libraries