跳到主要内容
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
私钥格式
raw-scalar

@noble/curves 用纯 TypeScript 实现椭圆曲线密码学,没有任何依赖,并经过多次审计。它是 ethers v6、viem 以及许多比特币和 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 风格的签名方互通的两个选项——format 和 lowS——以及 noble 自身签名时的确定性输出。要手动比较同一签名的不同编码,可以使用 DER ⇄ 原始格式转换器;对于钱包签名,以太坊签名验证工具运行的就是同一套曲线代码。

事实及其依据的检查

以下内容均非抄自文档。检查脚本 content/libraries/verify/noble-curves/run.sh 于2026年10月20日在容器镜像 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

    在全部七条 Weierstrass 曲线上,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() 把输入当作摘要:只有 sign(sha256(m)) 才能复现 RFC 6979 的向量。从 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

    对于 P-256,1.9.7 原样返回了该 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

    nist.js 提供 P-256、P-384 和 P-521,secp256k1.js 提供 secp256k1,misc.js 提供 Brainpool r1 曲线。

    检查记录
    fact ecdsa-curves = P-256, P-384, P-521, secp256k1, brainpoolP256r1, brainpoolP384r1, brainpoolP512r1
  • EdDSA

    Ed25519Ed25519phEd25519ctxEd448Ed448ph

    Ed25519 及其 ph 和 ctx 变体、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

    除非传入 false 以获取未压缩形式,否则 getPublicKey() 返回 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 坐标的公钥(x-only)的 64 字节签名。

    检查记录
    fact schnorr = BIP-340

用法

用选项把 noble 的检查放宽到能接受 node:crypto 的 DER 签名,验证该签名,再用 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

库