Pular para o conteúdo
ecdsa.com
Go

go-ethereum crypto

O pacote crypto do go-ethereum (geth): assinatura secp256k1 sobre um hash de 32 bytes sem prefixo de mensagem, assinaturas de 65 bytes com v = 0/1 e um verificador que rejeita high-S, enquanto a recuperação o aceita.
Licença
LGPL-3.0-or-later
Versão verificada
1.17.8 ·
Código-fonte
github.com/ethereum/go-ethereum
Documentação
pkg.go.dev/github.com/ethereum/go-ethereum/crypto

Visão geral da compatibilidade

ECDSAEthereum e Bitcoin

Curvas ECDSA
secp256k1
EdDSA
none
Formatos de chave pública
sec1-uncompressedsec1-compressed

Serviços em Go que lidam com assinaturas Ethereum costumam importar github.com/ethereum/go-ethereum/crypto e accounts. Com o cgo habilitado, o pacote usa a cópia da libsecp256k1 embutida no go-ethereum; com CGO_ENABLED=0, ele recorre à implementação em Go puro do decred — o teste rodou os dois builds, e todos os fatos saíram idênticos.

crypto.Sign(hash, key) recebe exatamente 32 bytes (caso contrário, hash is required to be exactly 32 bytes) e não calcula hash nem adiciona prefixo. Uma mensagem de login precisa passar antes por accounts.TextHash, que adiciona o prefixo EIP-191 e calcula o hash com keccak-256 — o teste conferiu isso com um keccak independente. Assinar em vez disso o keccak puro da mensagem produz uma assinatura que nunca vai coincidir com a do ethers, do viem ou das carteiras.

A assinatura tem 65 bytes, com o ID de recuperação como v = 0 ou 1, enquanto carteiras, ethers, viem e web3.py colocam 27 ou 28 nessa posição. crypto.Ecrecover rejeitou v = 27/28 com invalid signature recovery id, então subtraia 27 na entrada e some 27 na saída, como faz o exemplo. Os nonces seguem a RFC 6979 — a assinatura coincidiu com uma implementação de referência independente —, e o s é sempre baixo (low-S).

Os dois caminhos de verificação divergem quanto a high-S. crypto.VerifySignature, que recebe 64 bytes sem o v, rejeitou a assinatura espelhada (r, n − s), mas crypto.Ecrecover e crypto.SigToPub recuperaram dela a chave pública correta, como faz o contrato pré-compilado ecrecover da EVM. Por isso, contratos e serviços que recuperam endereços precisam rejeitar high-S por conta própria se tratam uma assinatura como identificador único.

As chaves públicas circulam como pontos SEC1 — na forma não comprimida, de 65 bytes, ou na forma comprimida, de 33 bytes —, e as chaves privadas, como escalares de 32 bytes. Para ver qual convenção uma assinatura segue, cole-a no verificador de assinaturas Ethereum; nonces repetidos entre transações de um mesmo endereço aparecem no verificador de reutilização de nonce no Ethereum.

Os fatos e as verificações por trás deles

Nada abaixo foi copiado da documentação. A verificação content/libraries/verify/go-ethereum-crypto/run.sh executou go-ethereum crypto 1.17.8 na imagem de contêiner golang:1.27.3-trixie em 20 de outubro de 2026; a página precisa corresponder ao que a verificação imprimiu, e cada fato lista essas linhas.

  • Codificação da assinatura ECDSA

    rsv-65

    crypto.Sign retorna 65 bytes: r, s e o ID de recuperação.

    Registrado pela verificação
    fact ecdsa-signature-encoding = rsv-65
  • ID de recuperação v

    0-1

    O último byte é 0 ou 1; crypto.Ecrecover rejeitou 27/28 com invalid signature recovery id.

    Registrado pela verificação
    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
  • Prefixo da mensagem

    eip-191

    O hash EIP-191 é fornecido por accounts.TextHash, que coincidiu com um cálculo independente do keccak-256 da mensagem com prefixo; o próprio crypto.Sign não adiciona prefixo.

    Registrado pela verificação
    fact message-prefix = eip-191
    evidence message-prefix: accounts.TextHash equals keccak256 of the EIP-191 prefixed message; crypto.Sign itself adds no prefix
  • Entrada do hash na chamada de assinatura

    takes-digest

    crypto.Sign recebe um hash de 32 bytes e falha com hash is required to be exactly 32 bytes para qualquer outra entrada.

    Registrado pela verificação
    fact ecdsa-prehash = takes-digest
    evidence ecdsa-prehash: crypto.Sign on a 31-byte input: hash is required to be exactly 32 bytes (31)
  • Hash padrão

    none

    Nenhum hash é calculado dentro de crypto.Sign.

    Registrado pela verificação
    fact ecdsa-default-hash = none
  • Nonce do ECDSA

    rfc6979

    RFC 6979: a assinatura foi igual à de uma implementação independente da RFC 6979 (conferida com o vetor de teste da RFC), com o s normalizado.

    Registrado pela verificação
    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 ao assinar

    low-s

    Nenhum valor high-S em 64 assinaturas.

    Registrado pela verificação
    fact ecdsa-low-s-sign = low-s
  • High-S ao verificar

    rejects-high-s

    crypto.VerifySignature retornou false para a assinatura com s substituído por n − s.

    Registrado pela verificação
    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>)
  • Recuperação a partir de assinaturas high-S

    accepted

    crypto.Ecrecover recuperou a chave pública do signatário a partir da assinatura high-S espelhada com o v invertido.

    Registrado pela verificação
    fact ecrecover-high-s = accepted
  • Curvas ECDSA

    secp256k1

    Apenas secp256k1.

    Registrado pela verificação
    fact ecdsa-curves = secp256k1
  • Formatos de chave pública

    sec1-uncompressedsec1-compressed

    crypto.UnmarshalPubkey lê o ponto não comprimido de 65 bytes, e crypto.DecompressPubkey, o ponto comprimido de 33 bytes.

    Registrado pela verificação
    fact public-key-formats = sec1-uncompressed, sec1-compressed
    evidence public-key-formats: UnmarshalPubkey(65 bytes): <nil>; DecompressPubkey(33 bytes): <nil>
  • Formatos de chave privada

    raw-scalar

    As chaves privadas são escalares de 32 bytes (crypto.ToECDSA, crypto.HexToECDSA).

    Registrado pela verificação
    fact private-key-formats = raw-scalar
  • EdDSA

    none

    Sem EdDSA neste pacote.

    Registrado pela verificação
    fact eddsa = none
  • Algoritmos pós-quânticos

    none

    Sem algoritmos pós-quânticos.

    Registrado pela verificação
    fact pqc = none

Uso

Assine uma mensagem EIP-191, converta o v entre o 0/1 do go-ethereum e o 27/28 do formato de transmissão e recupere o signatário.
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))
}

O executor de trechos de código roda cada exemplo literalmente na imagem de contêiner golang:1.27.3-trixie, e a execução falha se a saída não corresponder.

Comando
GOFLAGS=-mod=mod go run .
A saída deve corresponder a (expressão regular)
v on the wire: 2[78] signer matches: true

Bibliotecas