Skip to content
ecdsa.com
Go

go-ethereum crypto

The crypto package of go-ethereum (geth): secp256k1 signing over a 32-byte hash with no message prefix, 65-byte signatures with v = 0/1, and a verifier that rejects high-S while recovery accepts it.
License
LGPL-3.0-or-later
Checked version
1.17.8 ·
Source
github.com/ethereum/go-ethereum
Documentation
pkg.go.dev/github.com/ethereum/go-ethereum/crypto

Compatibility at a glance

ECDSAEthereum and Bitcoin

ECDSA curves
secp256k1
EdDSA
none
Public key formats
sec1-uncompressedsec1-compressed

Go services that handle Ethereum signatures usually import github.com/ethereum/go-ethereum/crypto and accounts. With cgo enabled the package uses the libsecp256k1 copy vendored in go-ethereum; with CGO_ENABLED=0 it falls back to the pure-Go decred implementation — the check ran both builds and every fact came out identical.

crypto.Sign(hash, key) takes exactly 32 bytes (hash is required to be exactly 32 bytes otherwise) and does no hashing and no prefixing. A login message must go through accounts.TextHash first, which adds the EIP-191 prefix and hashes with keccak-256 — the check confirmed it against an independent keccak. Signing the bare keccak of the message instead produces a signature that ethers, viem and wallets will never match.

The signature is 65 bytes with the recovery id as v = 0 or 1, while wallets, ethers, viem and web3.py put 27 or 28 there. crypto.Ecrecover rejected v = 27/28 with invalid signature recovery id, so subtract 27 on the way in and add it on the way out, as the sample does. Nonces follow RFC 6979 — the signature matched an independent reference implementation — and s is always low.

The two verification paths disagree about high-S. crypto.VerifySignature, which takes 64 bytes without v, rejected the mirrored (r, n − s) signature, but crypto.Ecrecover and crypto.SigToPub recovered the right public key from it, as the EVM's ecrecover precompile does. Contracts and services that recover addresses must therefore reject high-S themselves if they treat a signature as a unique identifier.

Public keys move as 65-byte uncompressed or 33-byte compressed SEC1 points and private keys as 32-byte scalars. To see which convention a signature follows, paste it into the Ethereum signature verifier; repeated nonces across transactions of one address show up in the Ethereum nonce reuse checker.

Facts and the checks behind them

Nothing below is copied from documentation. The check content/libraries/verify/go-ethereum-crypto/run.sh ran go-ethereum crypto 1.17.8 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

    rsv-65

    crypto.Sign returns 65 bytes: r, s and the recovery id.

    Recorded by the check
    fact ecdsa-signature-encoding = rsv-65
  • Recovery id v

    0-1

    The last byte is 0 or 1; crypto.Ecrecover rejected 27/28 with invalid signature recovery id.

    Recorded by the check
    fact recovery-v = 0-1
    evidence recovery-v: Ecrecover with v = 27/28: invalid signature recovery id
    evidence recovery-v: signer address recovered from the 0/1 signature matches: true
  • Message prefix

    eip-191

    EIP-191 hashing is provided by accounts.TextHash, which matched an independent keccak-256 computation of the prefixed message; crypto.Sign itself adds no prefix.

    Recorded by the check
    fact message-prefix = eip-191
    evidence message-prefix: accounts.TextHash equals keccak256 of the EIP-191 prefixed message; crypto.Sign itself adds no prefix
  • What the signing call hashes

    takes-digest

    crypto.Sign takes a 32-byte hash and fails with hash is required to be exactly 32 bytes for anything else.

    Recorded by the check
    fact ecdsa-prehash = takes-digest
    evidence ecdsa-prehash: crypto.Sign on a 31-byte input: hash is required to be exactly 32 bytes (31)
  • Default hash

    none

    No hashing is done inside crypto.Sign.

    Recorded by the check
    fact ecdsa-default-hash = none
  • ECDSA nonce

    rfc6979

    RFC 6979: the signature equalled an independent RFC 6979 implementation's (checked against the RFC's test vector) with s normalized.

    Recorded by the check
    fact ecdsa-nonce = rfc6979
    evidence ecdsa-nonce: two signatures of the same digest identical: true; equal to the reference RFC 6979 signature with s normalized: true
  • Low-S when signing

    low-s

    No high-S value among 64 signatures.

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

    rejects-high-s

    crypto.VerifySignature returned false for the signature with s replaced by n − s.

    Recorded by the check
    fact ecdsa-low-s-verify = rejects-high-s
    evidence ecdsa-low-s-verify: VerifySignature on (r, n - s): false; Ecrecover on (r, n - s, flipped v): recovers the signer true (err <nil>)
  • Recovery from high-S signatures

    accepted

    crypto.Ecrecover recovered the signer's public key from the mirrored high-S signature with v flipped.

    Recorded by the check
    fact ecrecover-high-s = accepted
  • ECDSA curves

    secp256k1

    secp256k1 only.

    Recorded by the check
    fact ecdsa-curves = secp256k1
  • Public key formats

    sec1-uncompressedsec1-compressed

    crypto.UnmarshalPubkey reads the 65-byte uncompressed point and crypto.DecompressPubkey the 33-byte compressed one.

    Recorded by the check
    fact public-key-formats = sec1-uncompressed, sec1-compressed
    evidence public-key-formats: UnmarshalPubkey(65 bytes): <nil>; DecompressPubkey(33 bytes): <nil>
  • Private key formats

    raw-scalar

    Private keys are 32-byte scalars (crypto.ToECDSA, crypto.HexToECDSA).

    Recorded by the check
    fact private-key-formats = raw-scalar
  • EdDSA

    none

    No EdDSA in this package.

    Recorded by the check
    fact eddsa = none
  • Post-quantum algorithms

    none

    No post-quantum algorithms.

    Recorded by the check
    fact pqc = none

Usage

Sign an EIP-191 message, convert v between go-ethereum's 0/1 and the wire's 27/28, and recover the signer.
package main

import (
	"fmt"

	"github.com/ethereum/go-ethereum/accounts"
	"github.com/ethereum/go-ethereum/crypto"
)

func main() {
	key, _ := crypto.GenerateKey()
	message := []byte("Sign in to example.com\nNonce: 8f2c1a")

	// crypto.Sign takes a 32-byte hash and adds no prefix: hash with TextHash (EIP-191).
	sig, _ := crypto.Sign(accounts.TextHash(message), key)

	// Wallets and ethers/viem send v = 27/28; go-ethereum wants 0/1.
	wire := append([]byte{}, sig...)
	wire[64] += 27

	// Coming back from the wire: undo the offset before recovering.
	in := append([]byte{}, wire...)
	if in[64] >= 27 {
		in[64] -= 27
	}
	pub, err := crypto.SigToPub(accounts.TextHash(message), in)
	fmt.Println("v on the wire:", wire[64], "signer matches:", err == nil && crypto.PubkeyToAddress(*pub) == crypto.PubkeyToAddress(key.PublicKey))
}

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
GOFLAGS=-mod=mod go run .
Output must match (regular expression)
v on the wire: 2[78] signer matches: true

Libraries