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

viem

wevm の viem は、wagmi が使っている Ethereum 用の TypeScript インターフェースです。v = 27/28 の 65 バイトの r‖s‖v 署名、EIP-191 によるハッシュ化、RFC 6979 を使います。ただし ethers と異なり、high-S 署名からも署名者を復元し、EIP-2098 のコンパクト署名は読み込めません。
ライセンス
MIT
確認したバージョン
2.57.7 ·
ソースコード
github.com/wevm/viem
ドキュメント
viem.sh/

互換性の概要

ECDSAEthereum と Bitcoin

EdDSA
none
公開鍵の形式
sec1-uncompressed

viem は、Ethereum のクライアント、アカウント、コントラクト、ABI のエンコードとデコードを扱う、型付きでツリーシェイキング可能なライブラリで、wagmi の基盤でもあります。privateKeyToAccount() で作るローカルアカウントは @noble/curves で署名します(viem 2.57.7 はそのバージョン 1.9.1 に依存しています)。そのため署名は RFC 6979 による決定的な low-S 署名で、同じ鍵とメッセージなら ethers が生成するものと同一です。

account.signMessage() は EIP-191 のプレフィックスを付けて keccak-256 でハッシュし、末尾が v = 27 または 28 の 65 バイトを返します。hash パラメーターを指定した sign() は 32 バイトのダイジェストにそのまま署名します。EIP-712 のダイジェストやトランザクションハッシュはこの方法で署名します。チェックでは、どちらも独立した別の実装と照合して確認しました。

検証は ethers より寛容です。recoverMessageAddress() とユーティリティ関数の verifyMessage は、s を n − s に置き換えた high-S 署名を受け入れました。数学的には有効ですが、ethers は non-canonical s で拒否する形式です。署名の重複排除や保存を行うシステムでは、自分で s を正規化してください。そうしないと、同じメッセージに対して見た目の異なる 2 つ目の署名が受け入れられてしまいます。逆に、64 バイトの EIP-2098 コンパクト形式は、どちらの関数でも invalid signature length で失敗しました。先に parseCompactSignature と compactSignatureToSignature で展開してください。

単体のユーティリティ関数 verifyMessage が確認できるのは、外部所有アカウントだけです。スマートコントラクトウォレット(Safe、アカウント抽象化ウォレット、パスキーウォレット)は ERC-1271 または ERC-6492 で署名し、コントラクトに問い合わせるのはパブリッククライアントの verifyMessage アクションだけです。型付きデータは EIP-712 インスペクターで、ログインメッセージは SIWE 検証ツールで、生の署名は Ethereum 署名検証ツールで確認してください。

サンプルはログインメッセージに署名し、アドレスに対する検証と署名者の復元という 2 つの方法で確認します。

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

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

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

    rsv-65

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

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

    27-28

    v は 27 または 28 です。parseSignature() はこれを yParity の 0 または 1 としても返します。

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

    eip-191

    hashMessage() は、EIP-191 のプレフィックス、10 進数で表した長さ、メッセージを連結して独立に計算した keccak-256 と一致します。

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

    keccak256

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

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

    hashes-message-or-digest

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

    チェックによる記録
    fact ecdsa-prehash = hashes-message-or-digest
    evidence ecdsa-prehash: sign() over the EIP-191 digest equals signMessage(): 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

    accepts-high-s

    v を反転させた (r, n − s) の署名からも、recoverMessageAddress() で署名者を復元でき、ユーティリティ関数の verifyMessage でも検証を通りました。

    チェックによる記録
    fact ecdsa-low-s-verify = accepts-high-s
    evidence ecdsa-low-s-verify: recoverMessageAddress with (r, n - s, flipped v): recovers the signer
  • 受け付ける署名の形式

    v-27-28v-0-1

    v = 27/28 または v = 0/1 の 65 バイトの署名からは署名者を復元できました。64 バイトの EIP-2098 形式は invalid signature length で失敗しました。

    チェックによる記録
    fact signature-input-shapes = v-27-28, v-0-1
    evidence signature-input-shapes: v-27-28: recovers the signer
    evidence signature-input-shapes: v-0-1: recovers the signer
    evidence signature-input-shapes: eip-2098: invalid signature length
    evidence signature-input-shapes: verifyMessage utility, high-S twin: true
    evidence signature-input-shapes: verifyMessage utility, eip-2098: invalid signature length
    evidence signature-input-shapes: eip-2098 expanded with parseCompactSignature and compactSignatureToSignature: recovers the signer
    evidence signature-input-shapes: parseSignature returns v 28 and yParity 1
  • ECDSA の曲線

    secp256k1

    secp256k1 のみです。

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

    none

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

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

    none

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

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

    sec1-uncompressed

    account.publicKey は 65 バイトの SEC1 非圧縮形式の点です。

    チェックによる記録
    fact public-key-formats = sec1-uncompressed
    evidence public-key-formats: account.publicKey is 65 bytes starting with 0x04: true

使い方

ローカルアカウントでログインメッセージに署名し、アドレスに対して検証してから署名者を復元します。
import { recoverMessageAddress, verifyMessage } from "viem";
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";

const account = privateKeyToAccount(generatePrivateKey());
const message = "Sign in to example.com\nNonce: 8f2c1a";

// EIP-191 personal_sign, RFC 6979, low-S, v = 27 or 28.
const signature = await account.signMessage({ message });

// For EOAs this is ecrecover plus an address comparison; for smart-contract
// wallets use the publicClient.verifyMessage action, which also checks ERC-1271.
const valid = await verifyMessage({ address: account.address, message, signature });
const signer = await recoverMessageAddress({ message, signature });

console.log("signature bytes:", (signature.length - 2) / 2);
console.log("valid:", valid, "signer matches:", signer === account.address);

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

セットアップ
npm init -y >/dev/null && npm install --no-audit --no-fund viem@2.57.7
コマンド
node usage.mts
一致すべき出力(正規表現)
signature bytes: 65\nvalid: true signer matches: true

ライブラリ