Aller au contenu
ecdsa.com
Go

go-ethereum crypto

Le paquet crypto de go-ethereum (geth) : signature secp256k1 d’un condensat de 32 octets sans préfixe de message, signatures de 65 octets avec v = 0/1, et un vérificateur qui rejette les signatures high-S alors que la récupération les accepte.
Licence
LGPL-3.0-or-later
Version vérifiée
1.17.8 ·
Code source
github.com/ethereum/go-ethereum
Documentation
pkg.go.dev/github.com/ethereum/go-ethereum/crypto

Compatibilité en un coup d’œil

ECDSAEthereum et Bitcoin

Courbes ECDSA
secp256k1
EdDSA
none
Formats de clé publique
sec1-uncompressedsec1-compressed

Les services Go qui traitent des signatures Ethereum importent généralement github.com/ethereum/go-ethereum/crypto et accounts. Avec cgo activé, le paquet utilise la copie de libsecp256k1 embarquée dans go-ethereum ; avec CGO_ENABLED=0, il se rabat sur l’implémentation decred en pur Go. Les essais ont porté sur les deux builds, et chaque constat était identique de l’un à l’autre.

crypto.Sign(hash, key) prend exactement 32 octets (sinon, il échoue avec hash is required to be exactly 32 bytes) et n’effectue ni hachage ni ajout de préfixe. Un message de connexion doit d’abord passer par accounts.TextHash, qui ajoute le préfixe EIP-191 et hache avec keccak-256 ; les essais l’ont confirmé avec un keccak indépendant. Signer à la place le simple keccak du message produit une signature qui ne correspondra jamais à celles d’ethers, de viem et des portefeuilles.

La signature fait 65 octets, avec l’identifiant de récupération sous la forme v = 0 ou 1, alors que les portefeuilles, ethers, viem et web3.py y placent 27 ou 28. crypto.Ecrecover a rejeté v = 27/28 avec invalid signature recovery id : soustrayez donc 27 à l’entrée et rajoutez-le à la sortie, comme le fait l’exemple. Les nonces suivent la RFC 6979 (la signature correspondait à celle d’une implémentation de référence indépendante) et s est toujours dans la moitié inférieure.

Les deux chemins de vérification divergent sur high-S. crypto.VerifySignature, qui prend 64 octets sans v, a rejeté la signature miroir (r, n − s), mais crypto.Ecrecover et crypto.SigToPub en ont récupéré la bonne clé publique, comme le fait le contrat précompilé ecrecover de l’EVM. Les contrats et les services qui récupèrent des adresses doivent donc rejeter eux-mêmes les signatures high-S s’ils traitent une signature comme un identifiant unique.

Les clés publiques circulent sous forme de point SEC1 non compressé de 65 octets ou de point compressé de 33 octets, et les clés privées sous forme de scalaires de 32 octets. Pour savoir quelle convention suit une signature, collez-la dans le vérificateur de signatures Ethereum ; les nonces répétés entre les transactions d’une même adresse apparaissent dans le vérificateur de réutilisation de nonce Ethereum.

Les faits et les vérifications qui les fondent

Rien de ce qui suit n’est copié d’une documentation. La vérification content/libraries/verify/go-ethereum-crypto/run.sh a exécuté go-ethereum crypto 1.17.8 dans l’image de conteneur golang:1.27.3-trixie le 20 octobre 2026 ; la page doit correspondre à ce que la vérification a affiché, et chaque fait cite les lignes en question.

  • Encodage des signatures ECDSA

    rsv-65

    crypto.Sign renvoie 65 octets : r, s et l’identifiant de récupération.

    Relevé par la vérification
    fact ecdsa-signature-encoding = rsv-65
  • Identifiant de récupération v

    0-1

    Le dernier octet vaut 0 ou 1 ; crypto.Ecrecover a rejeté 27/28 avec invalid signature recovery id.

    Relevé par la vérification
    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
  • Préfixe de message

    eip-191

    Le hachage EIP-191 est fourni par accounts.TextHash, dont le résultat correspondait à un calcul keccak-256 indépendant du message préfixé ; crypto.Sign lui-même n’ajoute aucun préfixe.

    Relevé par la vérification
    fact message-prefix = eip-191
    evidence message-prefix: accounts.TextHash equals keccak256 of the EIP-191 prefixed message; crypto.Sign itself adds no prefix
  • Ce que l’appel de signature hache

    takes-digest

    crypto.Sign prend un condensat de 32 octets et échoue avec hash is required to be exactly 32 bytes pour toute autre entrée.

    Relevé par la vérification
    fact ecdsa-prehash = takes-digest
    evidence ecdsa-prehash: crypto.Sign on a 31-byte input: hash is required to be exactly 32 bytes (31)
  • Fonction de hachage par défaut

    none

    Aucun hachage n’est effectué dans crypto.Sign.

    Relevé par la vérification
    fact ecdsa-default-hash = none
  • Nonce ECDSA

    rfc6979

    RFC 6979 : la signature était identique à celle d’une implémentation indépendante de la RFC 6979 (contrôlée avec le vecteur de test de la RFC), avec s normalisé.

    Relevé par la vérification
    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 à la signature

    low-s

    Aucune valeur high-S parmi 64 signatures.

    Relevé par la vérification
    fact ecdsa-low-s-sign = low-s
  • High-S à la vérification

    rejects-high-s

    crypto.VerifySignature a renvoyé false pour la signature dont s a été remplacé par n − s.

    Relevé par la vérification
    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>)
  • Récupération à partir de signatures high-S

    accepted

    crypto.Ecrecover a récupéré la clé publique du signataire à partir de la signature miroir high-S, avec v inversé.

    Relevé par la vérification
    fact ecrecover-high-s = accepted
  • Courbes ECDSA

    secp256k1

    secp256k1 uniquement.

    Relevé par la vérification
    fact ecdsa-curves = secp256k1
  • Formats de clé publique

    sec1-uncompressedsec1-compressed

    crypto.UnmarshalPubkey lit le point non compressé de 65 octets et crypto.DecompressPubkey le point compressé de 33 octets.

    Relevé par la vérification
    fact public-key-formats = sec1-uncompressed, sec1-compressed
    evidence public-key-formats: UnmarshalPubkey(65 bytes): <nil>; DecompressPubkey(33 bytes): <nil>
  • Formats de clé privée

    raw-scalar

    Les clés privées sont des scalaires de 32 octets (crypto.ToECDSA, crypto.HexToECDSA).

    Relevé par la vérification
    fact private-key-formats = raw-scalar
  • EdDSA

    none

    Pas d’EdDSA dans ce paquet.

    Relevé par la vérification
    fact eddsa = none
  • Algorithmes post-quantiques

    none

    Aucun algorithme post-quantique.

    Relevé par la vérification
    fact pqc = none

Utilisation

Signer un message EIP-191, convertir v entre le 0/1 de go-ethereum et le 27/28 transmis sur le réseau, puis récupérer le signataire.
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))
}

Le banc de test des extraits de code exécute chaque exemple tel quel dans l’image de conteneur golang:1.27.3-trixie, et l’exécution échoue si la sortie ne correspond pas.

Commande
GOFLAGS=-mod=mod go run .
La sortie doit correspondre à cette expression régulière
v on the wire: 2[78] signer matches: true

Bibliothèques