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

@noble/curves

Paul Miller による、監査済みで依存関係のない JavaScript の楕円曲線ライブラリです。NIST、secp256k1、Brainpool の各曲線での ECDSA、EdDSA、Schnorr などに対応し、バージョン 2 以降のデフォルトは決定的署名、low-S、メッセージのハッシュ化です。
ライセンス
MIT
確認したバージョン
2.4.0 ·
ソースコード
github.com/paulmillr/noble-curves
ドキュメント
github.com/paulmillr/noble-curves#readme

互換性の概要

ECDSAEdDSA

ECDSA の曲線
P-256P-384P-521secp256k1brainpoolP256r1brainpoolP384r1brainpoolP512r1
EdDSA
Ed25519Ed25519phEd25519ctxEd448Ed448ph
公開鍵の形式
sec1-uncompressedsec1-compressed

@noble/curves は、楕円曲線暗号を依存関係のない純粋な TypeScript で実装したライブラリで、これまでに複数回の監査を受けています。ethers v6、viem、多くの Bitcoin・Nostr ツールの内部で使われており、WebCrypto に必要な曲線がないときの定番の選択肢です。P-256、P-384、P-521、secp256k1、Brainpool の r1 曲線、事前ハッシュ版とコンテキスト版を含む Ed25519 と Ed448、BIP-340 の Schnorr 署名に対応しています。

デフォルトはどれも厳格です。署名は 64 バイトのコンパクトな r‖s で、nonce は RFC 6979 に従い(チェックでは RFC の P-256 のテストベクトルを再現できました)、s は常に下半分に正規化されます。検証も同様に厳格で、node:crypto が生成した high-S の署名はすべて拒否され、low-S の署名はすべて受け入れられました。OpenSSL、Java、Go、WebCrypto の署名は約半分の確率で high-S になるため、それらを検証するには lowS: false が必要です。また、DER の入力には format: "der" が必要で、これがないと 70 バイトの DER データは長さのエラーをスローします。

バージョン 2 では最初の引数の意味が変わりました。1.x の sign() と verify() は呼び出し側で計算したハッシュ値を受け取っていましたが、2.x では曲線に対応するハッシュ関数でメッセージを自らハッシュします。チェックでは、1.9.7 がメッセージの SHA-256 を渡したときにだけ RFC のベクトルを再現することを確認しました。そのため、コードを変更せずにアップグレードするとハッシュが二重にかかり、他のどのライブラリとの間でも失敗します。本当にダイジェストを持っている箇所では prehash: false を渡してください。また、バージョン 1 は P-256 の署名を正規化していなかったため、1.x で検証を通った署名が 2.x では失敗することがあります。

鍵は生のバイト列としてのみ扱えます。秘密鍵は 32 バイトの秘密スカラーで、公開鍵は SEC1 の点です(デフォルトは 33 バイトの圧縮形式で、指定すれば非圧縮形式)。SPKI、PKCS#8、JWK は読み込めないため、OpenSSL や JWKS から得た鍵は先に展開する必要があります。それには JWK 変換ツールと楕円曲線プレイグラウンドが役立ちます。このパッケージにポスト量子暗号のコードはなく、同じ作者が別のパッケージとして公開しています。

サンプルでは、noble を OpenSSL 方式の署名者と相互運用させる 2 つのオプション(format と lowS)と、noble 自身の署名による決定的な出力を示しています。エンコーディングの異なる署名を手作業で比較するには DER ⇄ 生形式の変換ツールを使ってください。ウォレットの署名には Ethereum 署名検証ツールを使えます。このツールも同じ曲線のコードで動作しています。

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

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

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

    raw

    sign() は 64 バイトのコンパクトな r‖s を返します。format: "der" を指定すると代わりに ASN.1 DER を返し、verify() でそれを読むにも同じオプションが必要です。

    チェックによる記録
    fact ecdsa-signature-encoding = raw
    evidence ecdsa-signature-encoding: default 64 bytes; format "der" gives 71 bytes that parse as DER: true
  • 署名時の low-S

    low-s

    7 つのワイエルシュトラス曲線のいずれでも、64 個の署名に high-S の値は現れませんでした。s は常に正規化されます。

    チェックによる記録
    fact ecdsa-low-s-sign = low-s
    evidence ecdsa-low-s-sign: no high-S in 64 signatures on any of the seven curves
  • 検証時の high-S

    rejects-high-s

    デフォルトのオプションでは、node:crypto が生成した high-S の署名はすべて拒否され、low-S の署名はすべて受け入れられました。lowS: false を指定すると、64 個すべてが検証を通りました。

    チェックによる記録
    fact ecdsa-low-s-verify = rejects-high-s
    evidence ecdsa-low-s-verify: 64 node:crypto signatures: every high-S one rejected: true; every low-S one accepted: true; all 64 accepted with lowS false: true
  • ECDSA の nonce

    rfc6979

    RFC 6979 に従います。付録 A.2.5 の P-256 のテストベクトルを、s を n − s に置き換えた形で正確に再現しました。extraEntropy: true を指定するとランダム性が加わります(ヘッジ付き署名)。

    チェックによる記録
    fact ecdsa-nonce = rfc6979
    evidence ecdsa-nonce: r matches RFC 6979 A.2.5: true; s equals n - s(RFC): true; extraEntropy true changes the bytes: true
  • 署名呼び出しがハッシュする対象

    hashes-message

    2.x は曲線に対応するハッシュ関数(P-256 では SHA-256)でメッセージをハッシュします。prehash: false を指定すると、自分で計算したダイジェストに対して署名と検証を行います。

    チェックによる記録
    fact ecdsa-prehash = hashes-message
  • バージョン 1 の署名入力

    digest

    1.9.7 の sign() は入力をダイジェストとして扱いました。RFC 6979 のベクトルを再現できたのは sign(sha256(m)) だけです。呼び出し箇所を変更せずに 1.x からアップグレードすると、ハッシュが二重にかかります。

    チェックによる記録
    fact v1-sign-input = digest
    evidence v1-sign-input: 1.x sign(sha256(m)) gives the RFC 6979 r: true; 1.x sign(m) does: false
  • バージョン 1 の P-256 low-S

    either

    1.9.7 は P-256 で RFC の high-S の値をそのまま返しました。すべての曲線での正規化は 2.x からの新しい動作です。

    チェックによる記録
    fact v1-p256-low-s-sign = either
    evidence v1-p256-low-s-sign: 1.x P-256 returned the RFC's high s unchanged: true
  • 不正な形式の ECDSA 署名

    throws

    長さの誤った署名(format: "der" を指定しない DER のバイト列や 3 バイトのデータ)は expected Uint8Array of length 64 をスローします。形式は正しくても誤っている署名では false が返ります。

    チェックによる記録
    fact ecdsa-malformed-signature = throws
    evidence ecdsa-malformed-signature: DER without format: throws expected Uint8Array of length 64, got length=71
    evidence ecdsa-malformed-signature: 3 bytes: throws expected Uint8Array of length 64, got length=3
    evidence ecdsa-malformed-signature: 64 zero bytes: false
  • ECDSA の曲線

    P-256P-384P-521secp256k1brainpoolP256r1brainpoolP384r1brainpoolP512r1

    P-256、P-384、P-521 は nist.js、secp256k1 は secp256k1.js、Brainpool の r1 曲線は misc.js にあります。

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

    Ed25519Ed25519phEd25519ctxEd448Ed448ph

    ph 版と ctx 版を含む Ed25519、Ed448、Ed448ph のすべてで署名と検証ができました。

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

    none

    このパッケージにポスト量子アルゴリズムはありません。

    チェックによる記録
    fact pqc = none
    evidence pqc: exports of the package mention ML-DSA or ML-KEM: false
  • 公開鍵の形式

    sec1-uncompressedsec1-compressed

    公開鍵は SEC1 の点(圧縮形式または非圧縮形式)です。64 バイトの x‖y と SPKI DER は拒否されました。

    チェックによる記録
    fact public-key-formats = sec1-uncompressed, sec1-compressed
  • 公開鍵のデフォルト形式

    sec1-compressed

    getPublicKey() は、非圧縮形式を得るために false を渡さない限り、33 バイトの圧縮形式の点を返します。

    チェックによる記録
    fact default-public-key = sec1-compressed
  • 秘密鍵の形式

    raw-scalar

    秘密鍵は 32 バイトの生のスカラーです。PKCS#8 DER は拒否されました。

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

    BIP-340

    secp256k1.js の schnorr は BIP-340 を実装しています。x 座標のみの公開鍵を使い、署名は 64 バイトです。

    チェックによる記録
    fact schnorr = BIP-340

使い方

node:crypto による DER 署名を、noble がそれを受け入れられる程度に寛容になるオプションで検証し、次に noble 自身で決定的に署名します。
import { generateKeyPairSync, sign } from "node:crypto";
import { p256 } from "@noble/curves/nist.js";

// A signature made elsewhere: node:crypto (OpenSSL) emits DER, with s in either half.
const { privateKey, publicKey } = generateKeyPairSync("ec", { namedCurve: "P-256" });
const message = new TextEncoder().encode("order #1042 approved");
const der = sign("sha256", message, privateKey);
const jwk = publicKey.export({ format: "jwk" });
const point = Uint8Array.from([4, ...Buffer.from(jwk.x, "base64url"), ...Buffer.from(jwk.y, "base64url")]);

// noble 2.x hashes the message itself and rejects high-S unless told otherwise.
console.log("verified:", p256.verify(der, message, point, { format: "der", lowS: false }));

// Its own signatures are compact r||s, RFC 6979 and low-S: the same bytes every time.
const secretKey = p256.utils.randomSecretKey();
const first = p256.sign(message, secretKey);
const second = p256.sign(message, secretKey);
console.log("deterministic:", Buffer.from(first).equals(Buffer.from(second)), first.length);

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

セットアップ
npm init -y >/dev/null && npm install --no-audit --no-fund @noble/curves@2.4.0
コマンド
node usage.mjs
一致すべき出力(正規表現)
verified: true\ndeterministic: true 64

ライブラリ