Invalid Curve Attack & CRT Private Key Recovery
A deep dive into how JavaScript ECC libraries missing public-key curve validation leak private-key remainders through small-order subgroups, then reconstruct the full key via the Chinese Remainder Theorem (CRT).
一句话总结:某些 JS 椭圆曲线库在校验「对方公钥是否真的落在正确曲线上」这一步存在缺陷,攻击者就能利用小阶子群泄漏私钥的若干余数,再用**中国余数定理(CRT)**把这些余数合并,从「公钥 + ECDH 交互」中还原出完整私钥。
1. 背景
在 ECDH(Elliptic Curve Diffie–Hellman)密钥交换里,双方各自持有私钥,通过交换公钥协商出共享密钥:
其中 是私钥(一个标量), 是对方发来的公钥(曲线上的一个点), 表示椭圆曲线标量乘法。
安全前提: 必须真的落在双方约定的那条曲线上。如果库没有校验这一点,攻击者就可以发送一个「看似合法、实则不在正确曲线上」的点,从而引发一系列灾难性后果——这就是无效曲线攻击(invalid curve attack),又称扭曲线攻击(twist attack)。
JavaScript 生态里受影响最典型的库:
| 库 | 底层大数库 | 说明 |
|---|---|---|
elliptic | bn.js | 最流行的纯 JS 椭圆曲线库 |
secp256k1 | elliptic / native | cryptocoinjs/secp256k1-node |
node-jose | elliptic / jsbn | JOSE/JWT 实现 |
它们本质上都是「用 JS 大数数学库实现椭圆曲线运算」,所以常被笼统地称作「JS 数学库」。
2. 椭圆曲线基础(铺垫)
2.1 曲线方程
一条椭圆曲线(Weierstrass 形式,模素数 )定义为:
曲线上的点 加上一个「无穷远点」,构成一个加法群。secp256k1 的曲线就是:
(即 。)
2.2 标量乘法与点的阶
标量乘法就是把点重复相加 次:
一个点 的阶是满足下面条件的最小正整数 :
阶为 意味着这个点生成的循环子群只有 个元素:。
3. 攻击原理:小阶子群泄漏余数
3.1 关键等式
如果攻击者能让受害者用私钥 对一个阶为小整数 的点 做标量乘法:
由于 ,我们有:
也就是说,共享密钥 只编码了私钥 关于 的余数 ,其余的高位信息完全丢失。
3.2 如何拿到「阶为 l 的恶意点」
攻击者不直接使用正确曲线,而是使用:
- 扭曲曲线(twist):把曲线常数 换成另一个 (例如 secp256k1 的 对应 ),得到另一条曲线,其群阶往往含有小素因子;
- 在这些小因子对应的小阶子群里取一个点 (阶为 )。
因为受害者只做了「算 」这一件事、没校验点是否在正确曲线上,攻击者的恶意点就会被照单全收。
具体到
secp256k1的 CVE-2024-48930:loadCompressedPublicKey用Y = sqrt(X^3 + 7)从压缩的 恢复 ,当给定的 无解时,它实际上解了另一条曲线 上的点,得到一个低阶点。
3.3 恢复单个余数
有了 且 的阶为 ,攻击者只需在 内暴力枚举:
找到的那个 就满足 。因为 很小,这是可行的。
4. 中国余数定理合并
对不同的互素小阶 重复上面过程,攻击者得到一组同余式:
用中国余数定理合并。记:
则唯一解(模 )为:
且 。
完整还原的条件:当 (对 256 位私钥即 )时,,私钥被完整还原;否则只能还原 的低 位。
5. 攻击流程
flowchart TD
A["攻击者选定小阶 l"] --> B["构造阶为 l 的恶意点 P<br/>(不在正确曲线上)"]
B --> C["把 P 作为『公钥』发给受害者"]
C --> D["受害者计算 ECDH<br/>S = [d]·P"]
D --> E["因 [l]P = O<br/>S 只泄漏 r = d mod l"]
E --> F["攻击者暴力枚举 k ∈ [0,l)<br/>恢复 r = d mod l"]
F --> G{"还有其他小阶?"}
G -->|是| A
G -->|否| H["用中国余数定理 CRT<br/>合并所有余数"]
H --> I["还原完整私钥 d"]
6. 攻击时序(攻击者 ↔ 受害者)
sequenceDiagram
autonumber
participant A as 攻击者
participant V as 受害者
A->>V: 恶意点 P1(阶 l1)
V->>V: 计算 S1 = [d]·P1
V-->>A: 返回 S1(或基于 S1 的密文)
A->>A: 暴力枚举 → r1 = d mod l1
A->>V: 恶意点 P2(阶 l2)
V-->>A: S2 = [d]·P2
A->>A: r2 = d mod l2
Note over A,V: …… 重复 n 次,凑够 M = ∏ li > d ……
A->>A: CRT 合并 r1,r2,…,rn → 完整私钥 d
7. CRT 合并示意
flowchart LR
subgraph S["泄漏的余数"]
R1["r1 = d mod l1"]
R2["r2 = d mod l2"]
R3["r3 = d mod l3"]
RN["… rn = d mod ln"]
end
R1 --> C
R2 --> C
R3 --> C
RN --> C
C["中国余数定理 CRT<br/>x = Σ ri·Mi·yi mod M"] --> D["私钥 d<br/>(当 M > d)"]
8. 动画演示(SVG · SMIL 自动播放)
以下为自包含的 SVG 动画(SMIL,无脚本)。注意:SVG 动画在 <img> 里会被浏览器禁用,需用 <iframe>/<object> 嵌入(或直接打开文件)才能播放。
CRT 私钥恢复动画(余数逐步合并、进度条推进):
迷你椭圆曲线无效曲线攻击动画(攻击步骤依次展开):
9. 可运行 PoC(全部纯标准库,无依赖)
- 真正的迷你椭圆曲线无效曲线攻击(Node.js):
assets/invalid_curve_attack.js—— 在模 97 的曲线 上,从扭曲曲线 真实地构造小阶点、做 ECDH、恢复 ,再 CRT 还原私钥。 - CRT 数学 + 小阶子群抽象演示(Node.js):
assets/crt_private_key_recovery.js
运行:node assets/invalid_curve_attack.js
核心 CRT 实现(JS / BigInt):
function crt(residues, moduli) {
let M = 1n;
for (const m of moduli) M *= m;
let x = 0n;
for (let i = 0; i < residues.length; i++) {
const Mi = M / moduli[i];
const [, inv] = egcd(Mi, moduli[i]); // Mi * inv ≡ 1 (mod moduli[i])
x += residues[i] * Mi * (inv % moduli[i]);
}
x %= M;
if (x < 0n) x += M;
return x;
}
10. 相关 CVE 速览
| CVE | 库 | 年份 | 关键点 |
|---|---|---|---|
| CVE-2024-48930 | secp256k1(npm) | 2024 | 压缩公钥未校验曲线,CRT 还原私钥 |
| CVE-2020-28498 | elliptic(npm) | 2020 | derive() 未校验点是否在曲线上 |
| CVE-2017-16007 | node-jose(npm) | 2017 | ECDH-ES 无效曲线攻击 |
| CVE-2020-0601 | Windows CryptoAPI | 2020 | CurveBall,同类原理(非 JS) |
| CVE-2026-23966 | sm-crypto(npm) | 2026 | SM2 解密逻辑可恢复私钥 |
| CVE-2025-14505 | elliptic(npm) | 2025 | RFC 6979 的 k 截断致私钥泄露(另一类) |
各 CVE 的详细分析见同目录下对应文章: