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

elliptic

Fedor Indutny による elliptic は、10 年にわたってブラウザー向けバンドルやウォレットを支えてきた、純粋な JavaScript による ECDSA・EdDSA ライブラリです。デフォルトでは決定的ですが low-S ではなく、入力はダイジェストのみで、P-521 で誤った nonce が生じるという未解決のアドバイザリがあります。
ライセンス
MIT
確認したバージョン
6.6.1 ·
ソースコード
github.com/indutny/elliptic
ドキュメント
github.com/indutny/elliptic#readme

互換性の概要

ECDSAEdDSA

ECDSA の曲線
P-224P-256P-384P-521secp256k1
EdDSA
Ed25519
公開鍵の形式
sec1-uncompressedsec1-compressed

elliptic は、NIST 曲線と secp256k1 による ECDSA、そして Ed25519 を、bn.js を基盤とする素の JavaScript で実装しています。crypto-browserify、古い Ethereum や Bitcoin のライブラリ、Webpack 4 がブラウザー向けバンドルに注入していた Node.js のポリフィルを通じて広まったため、今でもロックファイルに現れます。ethers v6 と viem は代わりに @noble/curves を基盤にしています。こちらは同じ曲線に、より厳格なデフォルトで対応しています。

API は多くのライブラリと 3 つの点で異なり、それが相互運用のバグの原因になります。まず、sign() はメッセージではなくダイジェストを受け取り、渡されたバイト列をそのままダイジェストとして扱います。チェックでは、生のメッセージに署名すると、その SHA-256 に署名した場合とは異なる署名になりました。次に、戻り値は r、s、recoveryParam を持つ Signature オブジェクトなので、送受信する形式はシリアライズの方法次第です。OpenSSL 方式の相手には toDER() を、生の r‖s には両方の値を 32 バイトにパディングしたものを使います。そして、canonical: true を渡さない限り s は正規化されず、検証は low-S と high-S のどちらも受け入れます。

nonce は RFC 6979 に従っており、P-256 のテストベクトルも正確に再現できました。しかし、6.6.1 でも未解決のアドバイザリ GHSA-848j-6mx2-7j84(CVE-2025-14505)は、途中の値の先頭にゼロのバイトがあるとその値が切り詰められると報告しています。チェックでもこれを再現できました。SHA-512 を使う P-521 では、2,048 個の固定入力のうち 9 個で RFC 6979 とは異なる nonce が生成されました。一方、SHA-256 を使う secp256k1 と SHA-512 を使う P-256 では、それぞれ 4,096 個の入力で差異はありませんでした。こうした署名はそれでも検証を通りますが、同じ鍵で同じメッセージに別の RFC 6979 実装で署名すると、関連した nonce が生じます。アドバイザリによれば、これにより鍵が漏洩するおそれがあります。P-521 には elliptic を使わないでください。関連した nonce がなぜ問題になるかは、nonce 再利用チェッカーを参照してください。

それ以前のアドバイザリは 6.5.7〜6.6.1 で修正されています。その中には、BER エンコードの署名を受け入れてしまう問題や、ハッシュの代わりに文字列に署名したときに鍵を抽出できてしまうバグが含まれます。チェックでは、6.6.1 は先頭に冗長なゼロを含む DER 署名を拒否しました。鍵は 16 進数の文字列またはバイト配列(圧縮形式または非圧縮形式の SEC1 の点と、生のスカラー)で、PEM、SPKI、JWK には対応していません。

サンプルでは、先にハッシュを計算し、正規化された low-S 署名を指定して、両方のシリアライズ形式を示しています。elliptic で作った署名が他の環境で失敗する場合は、DER ⇄ 生形式の変換ツールで 2 つのエンコーディングを比較してください。

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

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

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

    structured

    sign() は r、s、recoveryParam を持つオブジェクトを返します。toDER() は ASN.1 DER を生成し、生の r‖s は 2 つの値を曲線のサイズにパディングして組み立てる必要があります。

    チェックによる記録
    fact ecdsa-signature-encoding = structured
    evidence ecdsa-signature-encoding: sign() returns an object with r, s and recoveryParam 0; toDER() gives 72 bytes that parse as DER: true
  • 署名呼び出しがハッシュする対象

    takes-digest

    sign() と verify() への入力はダイジェストとして扱われます。生のメッセージに署名すると、その SHA-256 に署名した場合とは異なる署名になりました。

    チェックによる記録
    fact ecdsa-prehash = takes-digest
  • ECDSA の nonce

    rfc6979

    HMAC-DRBG による RFC 6979 です。付録 A.2.5 の P-256 のテストベクトルを、s が high-S である点も含めて正確に再現しました。

    チェックによる記録
    fact ecdsa-nonce = rfc6979
    evidence ecdsa-nonce: r and s equal RFC 6979 A.2.5 (high s kept): true
  • RFC 6979 と異なる nonce

    P-521/SHA-512

    SHA-512 を使う P-521 では、2,048 個の固定入力のうち 9 個で RFC 6979 とは異なる nonce が使われました(参照実装は @noble/curves)。secp256k1/SHA-256 と P-256/SHA-512 では、それぞれ 4,096 個の入力で差異はありませんでした。これは、6.6.1 までのすべてのバージョンで未解決のアドバイザリ GHSA-848j-6mx2-7j84 と一致します。

    チェックによる記録
    fact rfc6979-divergence = P-521/SHA-512
    evidence rfc6979-divergence: secp256k1 with SHA-256, 4096 fixed inputs: 0 nonces differ from RFC 6979, 0 of those signatures still verify
    evidence rfc6979-divergence: P-256 with SHA-512, 4096 fixed inputs: 0 nonces differ from RFC 6979, 0 of those signatures still verify
    evidence rfc6979-divergence: P-521 with SHA-512, 2048 fixed inputs: 9 nonces differ from RFC 6979, 9 of those signatures still verify
  • 署名時の low-S

    either

    デフォルトでは正規化されません。RFC のテストベクトルは high-S の s のまま返されました。canonical: true を指定すると、代わりに n − s が返ります。

    チェックによる記録
    fact ecdsa-low-s-sign = either
    evidence ecdsa-low-s-sign: canonical: true returns n - s: true
  • 検証時の high-S

    accepts-high-s

    同じ署名の high-S 形式と low-S 形式のどちらも検証を通りました。

    チェックによる記録
    fact ecdsa-low-s-verify = accepts-high-s
    evidence ecdsa-low-s-verify: the RFC signature (high s) and its low-s twin both verify: true
  • DER の厳密さ

    rejects-non-minimal

    r の先頭に冗長なゼロを含む DER 署名は Signature without r or s で拒否されましたが、toDER() による最小のエンコーディングは検証を通りました。

    チェックによる記録
    fact der-strictness = rejects-non-minimal
    evidence der-strictness: the minimal DER from toDER() verifies when passed back as bytes: true
    evidence der-strictness: non-minimal INTEGER encoding: throws Signature without r or s
  • 不正な形式の ECDSA 署名

    throws

    verify() に 64 バイトの生の r‖s データや 3 バイトの配列を渡すと、false を返さずに Signature without r or s がスローされます。

    チェックによる記録
    fact ecdsa-malformed-signature = throws
    evidence ecdsa-malformed-signature: 64-byte raw r||s: throws Signature without r or s
    evidence ecdsa-malformed-signature: 3 bytes: throws Signature without r or s
  • ECDSA の曲線

    P-224P-256P-384P-521secp256k1

    P-224、P-256、P-384、P-521、secp256k1 で署名と検証ができました。Brainpool 曲線はありません。

    チェックによる記録
    fact ecdsa-curves = P-224, P-256, P-384, P-521, secp256k1
  • EdDSA

    Ed25519

    new elliptic.eddsa("ed25519") による Ed25519 です。

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

    none

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

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

    sec1-uncompressedsec1-compressed

    keyFromPublic() は、16 進数の SEC1 の点(圧縮形式・非圧縮形式)と、16 進数の x 座標と y 座標を持つオブジェクトを読み込めました。0x04 のプレフィックスがない 64 バイトの x‖y は拒否されました。

    チェックによる記録
    fact public-key-formats = sec1-uncompressed, sec1-compressed
    evidence public-key-formats: an object of hex x and y coordinates also works: true; there is no PEM, SPKI, PKCS#8 or JWK parser
  • 秘密鍵の形式

    raw-scalar

    秘密鍵は 16 進数またはバイト列の生のスカラーです。PEM、PKCS#8、JWK のパーサーはありません。

    チェックによる記録
    fact private-key-formats = raw-scalar

使い方

メッセージをハッシュし、secp256k1 で正規化された low-S 署名として署名して、DER と生の r‖s にシリアライズします。
import { createHash } from "node:crypto";
import elliptic from "elliptic";

const ec = new elliptic.ec("secp256k1");
const key = ec.genKeyPair();

// elliptic signs exactly the bytes it is given: hash the message yourself,
// and pass bytes — never a string.
const digest = createHash("sha256").update("order #1042 approved").digest();

// canonical: true keeps s in the lower half, which Bitcoin and noble verifiers require.
const signature = key.sign(digest, { canonical: true });

const der = Buffer.from(signature.toDER());
const raw = Buffer.concat([signature.r.toArrayLike(Buffer, "be", 32), signature.s.toArrayLike(Buffer, "be", 32)]);
console.log("low-S:", signature.s.cmp(ec.nh) <= 0);
console.log("verified:", key.verify(digest, der), "raw bytes:", raw.length);

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

セットアップ
npm init -y >/dev/null && npm install --no-audit --no-fund elliptic@6.6.1
コマンド
node usage.mjs
一致すべき出力(正規表現)
low-S: true\nverified: true raw bytes: 64

ライブラリ