本文へスキップ
ecdsa.com
Node.js · WebCrypto

ethers.js

ethers.js v6 は、Ethereum のウォレットとコントラクトを扱う定番の JavaScript ライブラリです。v = 27/28 の 65 バイトの r‖s‖v 署名、EIP-191 によるメッセージのハッシュ化、RFC 6979 の nonce を使い、high-S 署名は一律に拒否します。
ライセンス
MIT
確認したバージョン
6.17.0 ·
ソースコード
github.com/ethers-io/ethers.js
ドキュメント
docs.ethers.org/v6/

互換性の概要

ECDSAEthereum と Bitcoin

EdDSA
none
公開鍵の形式
sec1-uncompressedsec1-compressedraw-xy

ethers は、アプリケーションが Ethereum とやり取りするために必要なもの(プロバイダー、コントラクト、ABI のエンコードとデコード、ENS)をすべて備えています。JavaScript のコードの多くは、その Wallet クラスと SigningKey クラスで署名しています。曲線の演算には @noble/curves を使っており、ethers 6.17.0 はそのバージョン 1.2.0 に固定しています。

wallet.signMessage() は personal_sign を実装しています。メッセージの前に \x19Ethereum Signed Message:\n とメッセージの長さを付け(EIP-191)、keccak-256 でハッシュし、v = 27 または 28 の r ‖ s ‖ v からなる 65 バイトを返します。チェックでは、ダイジェストを独立した別の keccak 実装と、署名を @noble/curves の RFC 6979 の出力と照合して確認しました。そのため、同じ鍵とメッセージからは常に同じバイト列が得られます。EIP-712 のダイジェストやトランザクションハッシュなど、計算済みのハッシュに署名するには SigningKey.sign() を使ってください。この関数はちょうど 32 バイトを受け取り、プレフィックスを付けません。それ以外の長さでは invalid digest length で失敗します。

ethers は入力の形状には寛容ですが、正規形であるかどうかには厳格です。verifyMessage() は v = 27/28、v = 0/1、64 バイトの EIP-2098 コンパクト形式のいずれからも署名者を復元しましたが、s を n − s に置き換えた high-S 署名は、数学的には有効であるにもかかわらず non-canonical s; use ._s で拒否しました。s を正規化しない OpenSSL、クラウドの KMS、ハードウェアモジュールが生成した署名を ethers で受け入れられるようにするには、先に low-S に変換(s を n − s に置き換え、v を反転)しなければなりません。署名がどの形式かは、Ethereum 署名検証ツールで確認できます。

ウォレットによるログインで起きる検証のバグの多くは、曲線とはまったく関係がありません。改行や末尾の空白だけが異なるメッセージ、EIP-712 ドメインで異なるチェーン ID、ecrecover ではなく ERC-1271 で署名を検証する必要があるスマートコントラクトウォレットなどが原因です。型付きデータは EIP-712 インスペクターで、ログインメッセージ全体は SIWE 検証ツールで確認してください。

サンプルでは、ログインメッセージに署名し、アドレスを復元して、コンパクト形式を示しています。同じ署名には有効なエンコーディングが複数あるため、比較するのは復元したアドレスにしてください。署名のバイト列を比較してはいけません。

事実とその根拠となるチェック

以下の内容はドキュメントからの転記ではありません。2026年10月20日に、チェック content/libraries/verify/ethers/run.sh がコンテナイメージ node:24.22.0-bookworm-slim 内で ethers.js 6.17.0 を Node.js 24.22.0; ethers depends on @noble/curves 1.2.0 で実行しました。このページの内容はその出力と一致していなければならず、各事実には該当する出力行を示しています。

  • ECDSA 署名のエンコーディング

    rsv-65

    signMessage() は r、s、リカバリ用のバイト v からなる 65 バイトを 16 進数の文字列で返します。

    チェックによる記録
    fact ecdsa-signature-encoding = rsv-65
  • リカバリ ID v

    27-28

    ethers が生成する署名では、v は 27 または 28 です。

    チェックによる記録
    fact recovery-v = 27-28
  • メッセージのプレフィックス

    eip-191

    hashMessage() は、\x19Ethereum Signed Message:\n、10 進数で表したバイト長、メッセージを連結して @noble/hashes で独立に計算した keccak-256 と一致します。

    チェックによる記録
    fact message-prefix = eip-191
  • デフォルトのハッシュ

    keccak256

    メッセージ署名には SHA-256 ではなく keccak-256 を使います。

    チェックによる記録
    fact ecdsa-default-hash = keccak256
  • 署名呼び出しがハッシュする対象

    hashes-message-or-digest

    signMessage() は EIP-191 のプレフィックスを付けてメッセージをハッシュします。SigningKey.sign() は 32 バイトのダイジェストにそのまま署名し、そのダイジェストに対して同一の署名を生成しました。

    チェックによる記録
    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 です。署名は、同じダイジェストに対する @noble/curves の決定的署名と一致しました。

    チェックによる記録
    fact ecdsa-nonce = rfc6979
  • 署名時の low-S

    low-s

    64 個のメッセージ署名の中に high-S の値はありませんでした。

    チェックによる記録
    fact ecdsa-low-s-sign = low-s
  • 検証時の high-S

    rejects-high-s

    v を反転させた鏡像の署名 (r, n − s) は、verifyMessage() でも Signature.from() でも non-canonical s; use ._s で拒否されます。

    チェックによる記録
    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
  • 受け付ける署名の形式

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

    verifyMessage() は、v = 27/28 または v = 0/1 の 65 バイトの署名と、64 バイトの EIP-2098 コンパクトエンコーディングのいずれからも署名者を復元しました。

    チェックによる記録
    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 の曲線

    secp256k1

    secp256k1 のみです。

    チェックによる記録
    fact ecdsa-curves = secp256k1
  • EdDSA

    none

    EdDSA には対応していません。

    チェックによる記録
    fact eddsa = none
  • ポスト量子アルゴリズム

    none

    ポスト量子アルゴリズムには対応していません。

    チェックによる記録
    fact pqc = none
  • 公開鍵の形式

    sec1-uncompressedsec1-compressedraw-xy

    SigningKey.computePublicKey() は SEC1 の圧縮形式と非圧縮形式の点を相互に変換でき、0x04 のプレフィックスがない 64 バイトの x‖y も受け入れました。

    チェックによる記録
    fact public-key-formats = sec1-uncompressed, sec1-compressed, raw-xy
    evidence public-key-formats: 64-byte x||y: true

使い方

personal_sign の方式でログインメッセージに署名し、署名者を復元して、65 バイトの形式と 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);

スニペットのテストハーネスは、すべてのサンプルをコンテナイメージ node:24.22.0-bookworm-slim 内でそのまま実行し、出力が一致しなければ実行は失敗します。

セットアップ
npm init -y >/dev/null && npm install --no-audit --no-fund ethers@6.17.0
コマンド
node usage.mjs
一致すべき出力(正規表現)
signature bytes: 65 v: 2[78]\nsigner matches: true\nEIP-2098 compact bytes: 64

ライブラリ