Saltar al contenido
ecdsa.com
Go

go-ethereum crypto

El paquete crypto de go-ethereum (geth): firma secp256k1 sobre un hash de 32 bytes sin prefijo de mensaje, firmas de 65 bytes con v = 0/1 y un verificador que rechaza las firmas high-S, aunque la recuperación las acepta.
Licencia
LGPL-3.0-or-later
Versión comprobada
1.17.8 ·
Código fuente
github.com/ethereum/go-ethereum
Documentación
pkg.go.dev/github.com/ethereum/go-ethereum/crypto

Compatibilidad de un vistazo

ECDSAEthereum y Bitcoin

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

Los servicios Go que manejan firmas de Ethereum suelen importar github.com/ethereum/go-ethereum/crypto y accounts. Con cgo habilitado, el paquete usa la copia de libsecp256k1 integrada en go-ethereum; con CGO_ENABLED=0 recurre a la implementación de decred escrita en Go puro. La comprobación ejecutó ambas compilaciones y todos los datos resultaron idénticos.

crypto.Sign(hash, key) recibe exactamente 32 bytes (si no, falla con hash is required to be exactly 32 bytes) y no calcula ningún hash ni añade ningún prefijo. Un mensaje de inicio de sesión debe pasar antes por accounts.TextHash, que añade el prefijo EIP-191 y calcula el hash con keccak-256; la comprobación lo contrastó con una implementación independiente de keccak. Si en su lugar firmas el keccak del mensaje sin más, obtienes una firma que nunca coincidirá con la de ethers, viem o las billeteras.

La firma ocupa 65 bytes, con el ID de recuperación como v = 0 o 1, mientras que las billeteras, ethers, viem y web3.py ponen ahí 27 o 28. crypto.Ecrecover rechazó v = 27/28 con invalid signature recovery id, así que resta 27 al recibir la firma y súmalo al enviarla, como hace el ejemplo. Los nonces siguen el RFC 6979 —la firma coincidió con una implementación de referencia independiente— y s siempre es bajo (low-S).

Las dos vías de verificación discrepan sobre high-S. crypto.VerifySignature, que recibe 64 bytes sin v, rechazó la firma reflejada (r, n − s), pero crypto.Ecrecover y crypto.SigToPub recuperaron de ella la clave pública correcta, igual que el contrato precompilado ecrecover de la EVM. Por eso, los contratos y servicios que recuperan direcciones deben rechazar high-S por su cuenta si tratan una firma como identificador único.

Las claves públicas se intercambian como puntos SEC1, ya sean puntos sin comprimir de 65 bytes o puntos comprimidos de 33 bytes, y las claves privadas, como escalares de 32 bytes. Para ver qué convención sigue una firma, pégala en el verificador de firmas de Ethereum; los nonces repetidos entre transacciones de una misma dirección aparecen en el comprobador de reutilización de nonces en Ethereum.

Datos y las comprobaciones que los respaldan

Nada de lo que sigue está copiado de la documentación. La comprobación content/libraries/verify/go-ethereum-crypto/run.sh ejecutó go-ethereum crypto 1.17.8 en la imagen de contenedor golang:1.27.3-trixie el 20 de octubre de 2026; la página debe coincidir con lo que imprimió, y cada dato cita esas líneas.

  • Codificación de la firma ECDSA

    rsv-65

    crypto.Sign devuelve 65 bytes: r, s y el ID de recuperación.

    Registrado por la comprobación
    fact ecdsa-signature-encoding = rsv-65
  • ID de recuperación v

    0-1

    El último byte es 0 o 1; crypto.Ecrecover rechazó 27/28 con invalid signature recovery id.

    Registrado por la comprobación
    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
  • Prefijo del mensaje

    eip-191

    El hash EIP-191 lo proporciona accounts.TextHash, cuyo resultado coincidió con un cálculo independiente de keccak-256 sobre el mensaje con prefijo; crypto.Sign no añade ningún prefijo por sí mismo.

    Registrado por la comprobación
    fact message-prefix = eip-191
    evidence message-prefix: accounts.TextHash equals keccak256 of the EIP-191 prefixed message; crypto.Sign itself adds no prefix
  • De qué calcula el hash la llamada de firma

    takes-digest

    crypto.Sign recibe un hash de 32 bytes y, con cualquier otra cosa, falla con hash is required to be exactly 32 bytes.

    Registrado por la comprobación
    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 predeterminado

    none

    crypto.Sign no calcula ningún hash internamente.

    Registrado por la comprobación
    fact ecdsa-default-hash = none
  • Nonce de ECDSA

    rfc6979

    RFC 6979: la firma fue igual a la de una implementación independiente de RFC 6979 (contrastada con el vector de prueba del RFC) con s normalizado.

    Registrado por la comprobación
    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 al firmar

    low-s

    Ningún valor high-S entre 64 firmas.

    Registrado por la comprobación
    fact ecdsa-low-s-sign = low-s
  • High-S al verificar

    rejects-high-s

    crypto.VerifySignature devolvió false para la firma con s sustituido por n − s.

    Registrado por la comprobación
    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>)
  • Recuperación a partir de firmas high-S

    accepted

    crypto.Ecrecover recuperó la clave pública del firmante a partir de la firma high-S reflejada con v invertido.

    Registrado por la comprobación
    fact ecrecover-high-s = accepted
  • Curvas ECDSA

    secp256k1

    Solo secp256k1.

    Registrado por la comprobación
    fact ecdsa-curves = secp256k1
  • Formatos de clave pública

    sec1-uncompressedsec1-compressed

    crypto.UnmarshalPubkey lee el punto sin comprimir de 65 bytes y crypto.DecompressPubkey, el comprimido de 33 bytes.

    Registrado por la comprobación
    fact public-key-formats = sec1-uncompressed, sec1-compressed
    evidence public-key-formats: UnmarshalPubkey(65 bytes): <nil>; DecompressPubkey(33 bytes): <nil>
  • Formatos de clave privada

    raw-scalar

    Las claves privadas son escalares de 32 bytes (crypto.ToECDSA, crypto.HexToECDSA).

    Registrado por la comprobación
    fact private-key-formats = raw-scalar
  • EdDSA

    none

    Este paquete no incluye EdDSA.

    Registrado por la comprobación
    fact eddsa = none
  • Algoritmos poscuánticos

    none

    Sin algoritmos poscuánticos.

    Registrado por la comprobación
    fact pqc = none

Uso

Firma un mensaje EIP-191, convierte v entre el 0/1 de go-ethereum y el 27/28 que se usa al transmitir la firma, y recupera al firmante.
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))
}

El arnés de pruebas de fragmentos de código ejecuta cada ejemplo tal cual en la imagen de contenedor golang:1.27.3-trixie, y la ejecución falla si la salida no coincide.

Comando
GOFLAGS=-mod=mod go run .
La salida debe coincidir (expresión regular)
v on the wire: 2[78] signer matches: true

Bibliotecas