Aller au contenu
ecdsa.com
Node.js · WebCrypto

ethers.js

ethers.js v6, la bibliothèque JavaScript historique pour les portefeuilles et les contrats Ethereum : signatures r‖s‖v de 65 octets avec v = 27/28, hachage des messages EIP-191, nonces RFC 6979 et refus catégorique des signatures high-S.
Licence
MIT
Version vérifiée
6.17.0 ·
Code source
github.com/ethers-io/ethers.js
Documentation
docs.ethers.org/v6/

Compatibilité en un coup d’œil

ECDSAEthereum et Bitcoin

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

ethers couvre tout ce dont une application a besoin pour dialoguer avec Ethereum (fournisseurs, contrats, encodage ABI, ENS), et ses classes Wallet et SigningKey sont ce qu’utilise la plupart du code JavaScript pour signer. L’arithmétique de courbe vient de @noble/curves ; ethers 6.17.0 en fige la version 1.2.0.

wallet.signMessage() implémente personal_sign : elle préfixe le message par \x19Ethereum Signed Message:\n et sa longueur (EIP-191), hache avec keccak-256 et renvoie 65 octets r ‖ s ‖ v avec v = 27 ou 28. Les essais ont confirmé le condensat avec une implémentation indépendante de keccak et la signature avec la sortie RFC 6979 de @noble/curves : une même clé et un même message donnent donc toujours les mêmes octets. Pour signer un condensat déjà calculé (un condensat EIP-712, un hash de transaction), utilisez SigningKey.sign(), qui prend exactement 32 octets et n’ajoute aucun préfixe ; toute autre entrée échoue avec invalid digest length.

En entrée, ethers est tolérant sur le format, mais strict sur la forme. verifyMessage() a récupéré le signataire à partir de v = 27/28, de v = 0/1 et de la forme compacte EIP-2098 de 64 octets, mais a refusé la signature miroir high-S avec non-canonical s; use ._s, bien qu’elle soit mathématiquement valide. Les signatures produites par OpenSSL, un KMS cloud ou des modules matériels qui ne normalisent pas s doivent être converties en low-S (s → n − s, en inversant v) avant qu’ethers les accepte ; le vérificateur de signatures Ethereum indique sous quelle forme se trouve une signature.

La plupart des bugs de vérification dans les connexions par portefeuille ne tiennent pas du tout à la courbe : un message qui diffère par un saut de ligne ou une espace finale, un ID de chaîne différent dans un domaine EIP-712, ou un portefeuille de contrat intelligent dont les signatures exigent ERC-1271 au lieu d’ecrecover. Examinez les données typées avec l’inspecteur EIP-712 et les messages de connexion complets avec le vérificateur SIWE.

L’exemple signe un message de connexion, récupère l’adresse et affiche la forme compacte ; comparez les adresses récupérées, jamais les octets de signature, car une même signature a plusieurs encodages valides.

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/ethers/run.sh a exécuté ethers.js 6.17.0 sur Node.js 24.22.0; ethers depends on @noble/curves 1.2.0 dans l’image de conteneur node:24.22.0-bookworm-slim 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

    signMessage() renvoie 65 octets en hexadécimal : r, s et l’octet de récupération v.

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

    27-28

    v vaut 27 ou 28 dans les signatures que produit ethers.

    Relevé par la vérification
    fact recovery-v = 27-28
  • Préfixe de message

    eip-191

    hashMessage() est égal au keccak-256 de \x19Ethereum Signed Message:\n, de la longueur en octets écrite en décimal et du message, calculé de façon indépendante avec @noble/hashes.

    Relevé par la vérification
    fact message-prefix = eip-191
  • Fonction de hachage par défaut

    keccak256

    Les signatures de messages utilisent keccak-256, pas SHA-256.

    Relevé par la vérification
    fact ecdsa-default-hash = keccak256
  • Ce que l’appel de signature hache

    hashes-message-or-digest

    signMessage() hache le message avec le préfixe EIP-191 ; SigningKey.sign() signe tel quel un condensat de 32 octets et a produit une signature identique pour ce condensat.

    Relevé par la vérification
    fact ecdsa-prehash = hashes-message-or-digest
    evidence ecdsa-prehash: SigningKey.sign over the EIP-191 digest equals signMessage: true; a 2-byte input: invalid digest length
    evidence ecdsa-prehash: recoverAddress(digest, signature) returns the signer: true
  • Nonce ECDSA

    rfc6979

    RFC 6979 : la signature correspondait à la signature déterministe de @noble/curves sur le même condensat.

    Relevé par la vérification
    fact ecdsa-nonce = rfc6979
  • Low-S à la signature

    low-s

    Aucune valeur high-S parmi 64 signatures de messages.

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

    rejects-high-s

    La signature miroir (r, n − s) avec v inversé est refusée par verifyMessage() et Signature.from() avec non-canonical s; use ._s.

    Relevé par la vérification
    fact ecdsa-low-s-verify = rejects-high-s
    evidence ecdsa-low-s-verify: verifyMessage with (r, n - s, flipped v): non-canonical s; use ._s
    evidence ecdsa-low-s-verify: Signature.from on the high-S twin: non-canonical s; use ._s
  • Formes de signature acceptées

    v-27-28v-0-1eip-2098

    verifyMessage() a récupéré le signataire à partir de signatures de 65 octets avec v = 27/28 ou v = 0/1 et de l’encodage compact EIP-2098 de 64 octets.

    Relevé par la vérification
    fact signature-input-shapes = v-27-28, v-0-1, eip-2098
    evidence signature-input-shapes: 65-byte v 27/28: recovers the signer
    evidence signature-input-shapes: 65-byte v 0/1: recovers the signer
    evidence signature-input-shapes: 64-byte EIP-2098: recovers the signer
  • Courbes ECDSA

    secp256k1

    secp256k1 uniquement.

    Relevé par la vérification
    fact ecdsa-curves = secp256k1
  • EdDSA

    none

    Pas d’EdDSA.

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

    none

    Aucun algorithme post-quantique.

    Relevé par la vérification
    fact pqc = none
  • Formats de clé publique

    sec1-uncompressedsec1-compressedraw-xy

    SigningKey.computePublicKey() a converti des points SEC1 compressés en points non compressés et inversement, et a aussi accepté un x‖y de 64 octets sans le préfixe 0x04.

    Relevé par la vérification
    fact public-key-formats = sec1-uncompressed, sec1-compressed, raw-xy
    evidence public-key-formats: 64-byte x||y: true

Utilisation

Signer un message de connexion selon la sémantique de personal_sign, récupérer le signataire et afficher les formes de 65 octets et EIP-2098.
import { Signature, Wallet, verifyMessage } from "ethers";

const wallet = Wallet.createRandom();
const message = "Sign in to example.com\nNonce: 8f2c1a";

// personal_sign: EIP-191 prefix + keccak256, RFC 6979 nonce, low-S, v = 27 or 28.
const signature = await wallet.signMessage(message);
const { v, compactSerialized } = Signature.from(signature);

// Recover the address and compare it; never compare signature bytes.
const signer = verifyMessage(message, signature);

console.log("signature bytes:", (signature.length - 2) / 2, "v:", v);
console.log("signer matches:", signer === wallet.address);
console.log("EIP-2098 compact bytes:", (compactSerialized.length - 2) / 2);

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

Mise en place
npm init -y >/dev/null && npm install --no-audit --no-fund ethers@6.17.0
Commande
node usage.mjs
La sortie doit correspondre à cette expression régulière
signature bytes: 65 v: 2[78]\nsigner matches: true\nEIP-2098 compact bytes: 64

Bibliothèques