跳到主要内容
ecdsa.com
Node.js

elliptic

Fedor Indutny 的 elliptic,十年来众多浏览器 bundle 和钱包背后的纯 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
私钥格式
raw-scalar

elliptic 基于 bn.js,用纯 JavaScript 实现了 NIST 曲线和 secp256k1 上的 ECDSA,以及 Ed25519。它通过 crypto-browserify、较早的以太坊和比特币库,以及 Webpack 4 注入浏览器 bundle 的 Node.js polyfill 广泛传播,所以至今仍会出现在锁文件中。ethers v6 和 viem 则改为基于 @noble/curves 构建,它覆盖相同的曲线,默认设置更严格。

它的 API 有三处与大多数库不同,容易引发互操作问题。sign() 接受的是摘要而不是消息,并把收到的任何字节都当作摘要——本次检查对原始消息签名,得到的签名与对其 SHA-256 签名的结果不同。它返回带有 r、s 和 recoveryParam 的 Signature 对象,因此传输格式完全取决于你如何序列化:对 OpenSSL 风格的对端用 toDER(),要得到原始 r‖s 则把两个值都填充到 32 字节。此外,除非传入 canonical: true,否则 s 不会被规范化,而验证时 high-S 和 low-S 都接受。

nonce 遵循 RFC 6979——精确复现了 P-256 的测试向量——但安全公告 GHSA-848j-6mx2-7j84(CVE-2025-14505)指出,某个中间值带有前导零字节时会被截断,该公告在 6.6.1 中仍未关闭。本次检查复现了这个问题:在 P-521 搭配 SHA-512 时,2,048 个固定输入中有 9 个生成的 nonce 与 RFC 6979 不同;而 secp256k1 搭配 SHA-256、P-256 搭配 SHA-512 时,各自的 4,096 个输入中都没有出现差异。这些签名仍能验证通过,但如果用另一个 RFC 6979 实现以同一密钥对同一消息签名,得到的 nonce 之间存在关联,公告指出这可能导致密钥泄露。在 P-521 上应避免使用 elliptic;关联 nonce 为何危险,参见 nonce 重用检测工具。

更早的安全公告已在 6.5.7 到 6.6.1 中修复,其中包括接受 BER 编码签名的问题,以及对字符串而不是哈希签名时可提取密钥的缺陷;在本次检查中,6.6.1 拒绝了带有多余前导零的 DER 签名。密钥是十六进制字符串或字节数组——SEC1 点(压缩或未压缩)和原始标量——不支持 PEM、SPKI 或 JWK。

示例先计算哈希,再请求规范的 low-S 签名,并展示两种序列化形式。如果 elliptic 生成的签名在别处验证失败,可以在 DER ⇄ 原始格式转换器中比较它的两种编码。

事实及其依据的检查

以下内容均非抄自文档。检查脚本 content/libraries/verify/elliptic/run.sh 于2026年10月20日在容器镜像 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 则需要把两个值填充到曲线长度后自行拼接。

    检查记录
    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

    RFC 6979,采用 HMAC-DRBG:精确复现了附录 A.2.5 中的 P-256 测试向量,包括其中的 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

    在 P-521 搭配 SHA-512 时,2,048 个固定输入中有 9 个使用的 nonce 与 RFC 6979 不同(参考实现为 @noble/curves);secp256k1/SHA-256 和 P-256/SHA-512 各 4,096 个输入中均未出现差异。这与安全公告 GHSA-848j-6mx2-7j84 相符,该公告涉及 6.6.1 及以前的所有版本,至今仍未关闭。

    检查记录
    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 值。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

    把 64 字节的原始 r‖s 数据块或 3 字节的数组传给 verify() 时,会抛出 Signature without r or s,而不是返回 false。

    检查记录
    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() 能读取十六进制的 SEC1 点(压缩或未压缩),以及由十六进制 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

    私钥是以十六进制或字节形式表示的原始标量;没有 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

库