Zum Inhalt springen
ecdsa.com
Node.js · WebCrypto

ethers.js

ethers.js v6, die altbewährte JavaScript-Bibliothek für Ethereum-Wallets und Smart Contracts: 65-Byte-Signaturen r‖s‖v mit v = 27/28, Hashing von Nachrichten nach EIP-191, Nonces nach RFC 6979 und eine strikte Ablehnung von High-S-Signaturen.
Lizenz
MIT
Geprüfte Version
6.17.0 ·
Quellcode
github.com/ethers-io/ethers.js
Dokumentation
docs.ethers.org/v6/

Kompatibilität auf einen Blick

ECDSAEthereum und Bitcoin

ECDSA-Kurven
secp256k1
EdDSA
none
Formate öffentlicher Schlüssel
sec1-uncompressedsec1-compressedraw-xy

ethers deckt alles ab, was eine Anwendung für die Kommunikation mit Ethereum braucht – Provider, Contracts, ABI-Kodierung, ENS –, und mit seinen Klassen Wallet und SigningKey signiert der meiste JavaScript-Code. Die Kurvenarithmetik stammt aus @noble/curves; ethers 6.17.0 pinnt davon die Version 1.2.0.

wallet.signMessage() implementiert personal_sign: Die Methode stellt der Nachricht \x19Ethereum Signed Message:\n und ihre Länge voran (EIP-191), hasht mit keccak-256 und gibt 65 Byte r ‖ s ‖ v mit v = 27 oder 28 zurück. Die Prüfung bestätigte den Digest mit einer unabhängigen keccak-Implementierung und die Signatur mit der Ausgabe von @noble/curves nach RFC 6979 – derselbe Schlüssel und dieselbe Nachricht ergeben also immer dieselben Bytes. Um einen Hash zu signieren, den du bereits berechnet hast – einen EIP-712-Digest, einen Transaktions-Hash –, verwende SigningKey.sign(): Die Methode nimmt genau 32 Byte und stellt kein Präfix voran; alles andere scheitert mit invalid digest length.

Bei Eingaben akzeptiert ethers verschiedene Formate, besteht aber auf der kanonischen Form. verifyMessage() stellte den Signierer aus v = 27/28, aus v = 0/1 und aus der kompakten 64-Byte-Form nach EIP-2098 wieder her, lehnte aber die gespiegelte High-S-Signatur mit non-canonical s; use ._s ab – obwohl sie mathematisch gültig ist. Signaturen aus OpenSSL, Cloud-KMS oder Hardwaremodulen, die s nicht normalisieren, müssen in Low-S umgewandelt werden (s → n − s, v umschalten), bevor ethers sie akzeptiert; der Ethereum-Signatur-Verifizierer zeigt, in welcher Form eine Signatur vorliegt.

Die meisten Fehler bei der Signaturprüfung von Wallet-Anmeldungen haben mit der Kurve gar nichts zu tun: eine Nachricht, die sich um einen Zeilenumbruch oder ein abschließendes Leerzeichen unterscheidet, eine andere Chain-ID in der EIP-712-Domain oder eine Smart-Contract-Wallet, deren Signaturen ERC-1271 statt ecrecover brauchen. Untersuche Typed Data mit dem EIP-712-Inspektor und vollständige Anmeldenachrichten mit dem SIWE-Verifizierer.

Das Beispiel signiert eine Anmeldenachricht, stellt die Adresse wieder her und zeigt die kompakte Form; vergleiche wiederhergestellte Adressen, nie die Bytes der Signatur, denn dieselbe Signatur hat mehrere gültige Kodierungen.

Fakten und die Prüfungen dahinter

Nichts im Folgenden ist aus einer Dokumentation abgeschrieben. Das Prüfskript content/libraries/verify/ethers/run.sh hat ethers.js 6.17.0 unter Node.js 24.22.0; ethers depends on @noble/curves 1.2.0 am 20. Oktober 2026 im Container-Image node:24.22.0-bookworm-slim ausgeführt; die Seite muss mit seiner Ausgabe übereinstimmen, und jeder Fakt führt die zugehörigen Zeilen auf.

  • Kodierung von ECDSA-Signaturen

    rsv-65

    signMessage() gibt 65 Byte als Hex zurück: r, s und das Recovery-Byte v.

    Von der Prüfung aufgezeichnet
    fact ecdsa-signature-encoding = rsv-65
  • Recovery-ID v

    27-28

    In den Signaturen, die ethers erzeugt, ist v 27 oder 28.

    Von der Prüfung aufgezeichnet
    fact recovery-v = 27-28
  • Nachrichtenpräfix

    eip-191

    hashMessage() entspricht keccak-256 über \x19Ethereum Signed Message:\n, die dezimale Bytelänge und die Nachricht, unabhängig mit @noble/hashes berechnet.

    Von der Prüfung aufgezeichnet
    fact message-prefix = eip-191
  • Standard-Hash

    keccak256

    Nachrichtensignaturen verwenden keccak-256, nicht SHA-256.

    Von der Prüfung aufgezeichnet
    fact ecdsa-default-hash = keccak256
  • Was der Signaturaufruf hasht

    hashes-message-or-digest

    signMessage() hasht die Nachricht mit dem Präfix nach EIP-191; SigningKey.sign() signiert einen 32-Byte-Digest unverändert und erzeugte für diesen Digest die identische Signatur.

    Von der Prüfung aufgezeichnet
    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
  • ECDSA-Nonce

    rfc6979

    RFC 6979: Die Signatur stimmte mit der deterministischen Signatur von @noble/curves über denselben Digest überein.

    Von der Prüfung aufgezeichnet
    fact ecdsa-nonce = rfc6979
  • Low-S beim Signieren

    low-s

    Kein High-S-Wert unter 64 Nachrichtensignaturen.

    Von der Prüfung aufgezeichnet
    fact ecdsa-low-s-sign = low-s
  • High-S bei der Prüfung

    rejects-high-s

    Die gespiegelte Signatur (r, n − s) mit umgeschaltetem v lehnen verifyMessage() und Signature.from() mit non-canonical s; use ._s ab.

    Von der Prüfung aufgezeichnet
    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
  • Akzeptierte Signaturformen

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

    verifyMessage() stellte den Signierer aus 65-Byte-Signaturen mit v = 27/28 oder v = 0/1 und aus der kompakten 64-Byte-Kodierung nach EIP-2098 wieder her.

    Von der Prüfung aufgezeichnet
    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
  • ECDSA-Kurven

    secp256k1

    Nur secp256k1.

    Von der Prüfung aufgezeichnet
    fact ecdsa-curves = secp256k1
  • EdDSA

    none

    Kein EdDSA.

    Von der Prüfung aufgezeichnet
    fact eddsa = none
  • Post-Quanten-Algorithmen

    none

    Keine Post-Quanten-Algorithmen.

    Von der Prüfung aufgezeichnet
    fact pqc = none
  • Formate öffentlicher Schlüssel

    sec1-uncompressedsec1-compressedraw-xy

    SigningKey.computePublicKey() konvertierte zwischen komprimierten und unkomprimierten Punkten im SEC1-Format und akzeptierte auch ein 64 Byte langes x‖y ohne das Präfix 0x04.

    Von der Prüfung aufgezeichnet
    fact public-key-formats = sec1-uncompressed, sec1-compressed, raw-xy
    evidence public-key-formats: 64-byte x||y: true

Verwendung

Signiere eine Anmeldenachricht mit der Semantik von personal_sign, stelle den Signierer wieder her und zeige die 65-Byte-Form und die Form nach 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);

Die Snippet-Testumgebung führt jedes Beispiel unverändert im Container-Image node:24.22.0-bookworm-slim aus, und der Lauf schlägt fehl, wenn die Ausgabe nicht übereinstimmt.

Einrichtung
npm init -y >/dev/null && npm install --no-audit --no-fund ethers@6.17.0
Befehl
node usage.mjs
Ausgabe muss passen (regulärer Ausdruck)
signature bytes: 65 v: 2[78]\nsigner matches: true\nEIP-2098 compact bytes: 64

Bibliotheken