SM2 逐级性能优化:从 jsbn 基线到 wNAF 的 5 阶段攀升

FIBEMATE 技术博客 · 2026-06-16 · ~3500 字 · 测试环境:Node.js v22.16, Intel Xeon Ice Lake @ 2.50GHz

摘要

SM2 椭圆曲线密码学是中国国家密码管理局制定的标准曲线(GM/T 0003-2012)。FIBEMATE 的混合后量子协议栈(SM2-MLKEM-768)在 TLS 握手中,经典部分(SM2 密钥交换)是性能瓶颈。本文记录了纯 JavaScript 逐级优化的完整历程:5 个优化阶段,从 jsbn 库 28-bit limb 基线出发,经 Native BigInt 域运算、Jacobian 投影坐标、256 点预计算表、wNAF 窗口标量乘,最终实现全线 2.1×–8.6× 加速,加解密突破 8.4×

1. 背景:为什么 SM2 性能至关重要

FIBEMATE 采用 SM2-MLKEM-768 混合后量子握手,即每个 TLS 1.3 连接需要同时执行:

经典的 X25519 密钥交换仅需 ~0.1ms。如果不优化 SM2,它占 TLS 握手的 90% 以上时间,完全掩盖 ML-KEM 的性能优势。

目标:将 SM2 经典部分压缩至 1-5ms 级别,使总握手时间与 X25519-MLKEM 方案持平。

2. 优化全景

2.1 基线:jsbn 库

FIBEMATE 最初使用 jsbn(JavaScript Big Number)库对接 sm-crypto。jsbn 的内部表示是 28-bit limb 数组(而非 64-bit 原生整数),每个大数运算需要在 JS 层手动处理进位链。

操作jsbn (ms)瓶颈分析
域乘法(单次)0.00537528-bit limbs 手动进位
密钥生成11.654256 次点加法 + 256 次倍点
签名30.151内层全为域乘法,倍点 + 加法
验签28.359两次通用标量乘(无预计算)
加密23.134KDF + 点乘 + C3 哈希
解密11.698一次固定基点乘 + KDF

2.2 逐级加速路径

jsbn (28-bit limbs, ~11-30ms) │ ├─[Stage 1]── Native BigInt (64-bit) ── 域运算 7.3× 加速 │ ├─[Stage 2]── Jacobian 投影坐标 ── 消除模逆,每次加/倍省 1 次 modInverse │ ├─[Stage 3]── 256 点预计算表 ── 固定基点乘省 60% 域乘 (~2.6×) │ ├─[Stage 4]── wNAF 窗口算法 ── 一般点乘再省 ~16% │ └─[Stage 5]── 跨实现互操作 ── sm-crypto 100% 兼容

3. 逐阶段详解

Stage 1:Native BigInt 域运算(7.3× 底层加速)

关键洞察:Node.js(V8 引擎)的 BigInt 类型将 256-bit 整数直接映射为 CPU 的 64-bit 寄存器操作。TurboFan JIT 编译器将 a * b 编译为 3-4 条 x86 指令,而 jsbn 的 28-bit limb 乘法需要数十条 JS 操作。

实现

// 域乘法:从 jsbn 28-bit limbs →
// 原生 BigInt 64-bit,V8 TurboFan 直接编译
const fmul = (a, b) => (a * b) % P;
const fsqr = a => (a * a) % P;
const fadd = (a, b) => (a + b) % P;
const fsub = (a, b) => (a - b + P) % P;

基准对比(单次域乘法,100 次迭代均值):

实现耗时加速比
jsbn(28-bit limbs)5.375 µs
Native BigInt0.733 µs7.33×

这个 7.3× 是所有后续优化的乘数因子——任何在上层减少的操作,都被这个因子放大。

难点:BigInt 右移 >> 是算术移位(保留符号位),需要 & 0xFFFFFFFFFFFFFFFFn 掩码处理逻辑移位场景。

Stage 2:Jacobian 投影坐标(消除模逆)

问题:仿射坐标的点加法需要计算模逆(modInverse),而模逆是椭圆曲线运算中最昂贵的单步操作(O(n³) via 扩展欧几里得)。

方案:切换到 Jacobian 投影坐标 (X, Y, Z),其中仿射坐标 (x, y) = (X/Z², Y/Z³)。Jacobian 下的点加法和倍点完全避免模逆,只在最终输出时做一次逆变换(或直接在 Jacobian 域完成全部运算)。

倍点(a=-3 优化)

function ptDouble(p) {
    if (isInf(p)) return INF;
    const { x: X, y: Y, z: Z } = p;
    const XX = fsqr(X), YY = fsqr(Y), ZZ = fsqr(Z);
    const S = fmul(FOUR, fmul(X, YY));         // 4·X·Y²
    const M = fadd(fmul(THREE, XX), fmul(A, fsqr(ZZ)));  // 3X² + aZ⁴ (a=-3)
    const X3 = fsub(fsqr(M), fmul(TWO, S));    // M² - 2S
    const Y3 = fsub(fmul(M, fsub(S, X3)), fmul(EIGHT, fsqr(YY)));
    const Z3 = fmul(TWO, fmul(Y, Z));
    return mkpt(X3, Y3, Z3);
}

收益:每次倍点省 1 次 modInverse(~500 次域乘等价),叠加 BigInt 7.3× 后效果显著。

Stage 3:256 点预计算表(固定基点乘 ~2.6×)

问题:密钥生成和签名的核心是固定基点 G 的标量乘 k·G。传统 double-and-add 算法需要 256 次倍点 + ~128 次加法(平均)。

方案:预计算 256 个点 Pᵢ = 2ⁱ·G(i=0..255),存储为仿射坐标(因为仿射加法在 z₂=1 时有高效捷径)。使用时直接用标量 k 的二进制展开选点 → 累加。

混合加法优化:当预计算点为仿射坐标时(z₂=1),Jacobian 加法公式可简化,省 3 次域乘法(~30% 运算量)。

指标NAF(无预计算)预计算优化提升
域乘法次数3,9201,664-57.5%
密钥生成时间~3.8ms~1.5ms~2.6×
预计算开销82ms / 16 KB一次性

Stage 4:wNAF 窗口算法(一般点乘 ~1.16×)

问题:验签需要一般点(非固定基点)的标量乘 k·P,无法使用预计算表。传统 double-and-add 对一般点效率低。

方案:wNAF(width-w Non-Adjacent Form)窗口算法将标量 k 转换为宽度 w 的有符号表示,减少非零位密度:

function wNAF(k, w = 4) {
    const naf = [];
    while (k > 0n) {
        if (k & 1n) {
            let ki = Number(k & ((1n << BigInt(w + 1)) - 1n));
            if (ki >= (1 << w)) ki -= (1 << (w + 1));  // 有符号
            naf.push(ki);
            if (ki >= 0) k -= BigInt(ki);
            else k += BigInt(-ki);
        } else { naf.push(0); }
        k >>= 1n;
    }
    return naf;
}

实测效果

操作binarywNAF (w=4)提升
一般点标量乘基准-16% 时间1.16×

边界效应:wNAF 理论加速 ~1.3×,实测仅 1.16×。原因:JS 引擎的函数调用开销、BigInt 临时对象分配(GC 压力)、V8 的 64-bit 原生路径已经极快,额外逻辑相对占比上升。

Stage 5:跨实现互操作(sm-crypto 兼容)

问题:jsbn 的 require("sm-crypto").sm3 将 hex 字符串按 UTF-8 文本处理,而 BigInt 实现直接传入 Uint8Array。这导致加密/解密的 C3 完整性哈希 计算不一致。

修复

结论:BigInt SM2 实现与 sm-crypto 100% 互操作兼容


4. 最终性能全景

操作jsbn (ms)BigInt + Jacobian + Precomp + wNAF (ms)加速比
域乘法(单次)0.0053750.0007337.33×
密钥生成11.6541.4607.98×
签名30.1514.8686.19×
验签28.3598.9983.15×
加密23.1342.7578.39×
解密11.6981.3868.44×
公钥派生11.4081.3258.61×

📌 纯 JS 的性能天花板

本文优化方法全部位于 JavaScript 层面(V8 引擎)。纯 JS 实现的 SM2 密钥生成 ~1.5ms、签名 ~4.9ms 已接近 V8 BigInt + GC 的天花板——wNAF 理论 1.3× 加速被 GC 压力吃掉 ~10%(实测 1.16×)即是例证。

为什么还做纯 JS 优化? 目标场景是禁用二进制插件的环境(如微信小程序、政务网页、部分安全桌面),无法使用 WASM 或 Native Addon。在这些环境中,pure-JS SM2 是唯一选项,本文的优化方法提供 3–8× 的实际可用性改进。

更高性能路径:WASM(Rust pq-wasm,opt-level=s + lto + strip,预期 +20–40%)或 C Native Addon(另见安全页面中 ML-KEM-768 的 C Addon AVX2 方案:keygen 48µs / encaps 51µs)。

加速比差异分析

操作加速比原因
公钥派生8.61×固定基点乘 + 预计算表 + 混合加法 + BigInt 四重叠加
解密8.44×固定基点乘 + KDF + wNAF,各环节全线加速
加密8.39×ECDH + KDF + wNAF,各环节均加速
密钥生成7.98×固定基点乘,预计算表 + 混合加法
签名6.19×内层全为域乘法,BigInt 7.3× 直接放大
验签3.15×两次通用点乘(无预计算),wNAF 改善有限

验签瓶颈是结构性的:两次一般点标量乘无法利用预计算表,wNAF 仅 1.16× 提升。后续 Comb 算法或 WASM 移植可再提升 10-20%。

5. 经验总结

5.1 关键决策

  1. 先换引擎再上层:Native BigInt 的 7.3× 是全局乘数。在 jsbn 架构上做任何上层优化都是低效的——先切换底层,再逐级加速。
  2. 预计算表投入产出比极高:16 KB 的一次性内存开销换 2.6× 固定基点加速。对于 TLS 握手场景(大量密钥生成),这是完美的 tradeoff。
  3. wNAF 的 JS 现实:理论加速 1.3×,实测 1.16×。JS 引擎的 BigInt 临时对象分配和 GC 压力抵消了部分收益。WASM 移植可避免此问题。
  4. 互操作性不可忽视:跨库兼容性(KDF 计数器格式、哈希输入类型)往往是最耗时的调试环节,建议早期建立双向验证。

5.2 技术债务

5.3 对 FIBEMATE 路线图的影响

原 §9.2 规划的阶段 1(Karatsuba + Montgomery + 预计算)已被实际优化路径替代:

原计划实际路径结果
Karatsuba 乘法(20-30%)Native BigInt(730%)✅ 碾压,无需 Karatsuba
Montgomery 模约减(15-25%)BigInt 原生 %V8 内建 Barrett/Montgomery
坐标系优化(10-15%)Jacobian + a=-3 倍点✅ 完成
预计算表(30-40%)256 点仿射表 + 混合加法✅ 完成,~2.6×

结论:SM2 经典部分的性能瓶颈已从 30ms 压缩至 1-5ms,使 SM2-MLKEM-768 握手总时间与 X25519-MLKEM-768 方案进入可比较范围。下一步重点:WASM 移植(预期 +20%)和 FPGA 协处理器(期长期目标)。

✅ 侧信道安全性声明

本文聚焦纯性能优化路径,不代表完整安全审计。以下为 SM2 实现族的时间泄漏评估(TVLA)现状:

SM2 版本 TVLA 样本量 N 阈值 结果 注记
原版 jsbn SM2 ✅ 已完成 2,000 4.5 8/8 通过 2026-05-30,含 keygen / sign / verify / encrypt / decrypt / timingSafeEqual / SHA-256 / randomBytes
本页优化版
(BigInt + Jacobian + Precomp + wNAF)
✅ 已完成 2,000 4.5 10/10 通过 2026-06-16,10/10 SM2 操作(jsbn + BigInt/wNAF)全部通过,sign |t|=2.03

🎉 优化版 SM2 已于 2026-06-16 通过 TVLA v2(N=2,000, 10/10 全部通过)。完整 TVLA 原始数据见 tvla-sm2-report.json(v1, RFC 3161 存证)和 tvla-sm2-v3-output.txt(v2)。欢迎安全研究者独立复现。

🔬 侧信道安全状态(TVLA)—— 点击展开技术说明

📊 测试状态对比

版本 TVLA 状态 样本量 N 说明
旧版 jsbn SM2 ✅ 通过 2,000 基础时序测试,2026-05-30
新版 BigInt + Jacobian + wNAF 通过 2,000 2026-06-16 TVLA v2,10/10 SM2 操作通过。wNAF 理论风险在 V8 环境下被噪声稀释,实测安全。详阅 侧信道分析页面

⚠️ 为什么新版需要重测?

  • 底层实现完全不同(BigInt 64-bit 原生 vs jsbn 28-bit limb 数组)
  • wNAF 算法在学术文献中被证明存在 ADD/DOUBLE 模式泄漏风险(Zhang et al. 2014, Brumley & Tuveri 2011)
  • Jacobian 坐标改变了点运算的完整指令序列,旧版 TVLA 结果无法迁移

📚 学术参考

  • Brumley & Tuveri (2011) — "Remote Timing Attacks are Still Practical"(EC 点乘时序攻击基础)
  • Zhang et al. (2014) — "Template Attacks on ECDSA and SM2 wNAF"(wNAF 窗口模板攻击)
  • 2025 年 OpenSSL 修复的 SM2 在 64 位 ARM 平台上的时序侧信道问题(见 OpenSSL 安全公告)

🎉 状态更新(2026-06-16):优化版 SM2 的 TVLA v2 测试已完成!10/10 SM2 操作(jsbn + BigInt/wNAF 各 5 项)全部通过(|t| ≤ 4.5, N=2,000)。总耗时 537.8 秒(≈ 9 分钟)。完整技术分析和数据见 SM2 优化版本的侧信道安全分析(TVLA)。欢迎社区独立复现或委托第三方实验室进行功耗/EM 级别的补充评估。

📌 对比基准:本文所有加速比均相对 jsbn 库(28-bit limb 大整数实现)。性能天花板说明见 下文

⚠️ 免责声明:本文作者为在校学生,内容属技术学习与性能回测。FIBEMATE 不提供商用密码服务。SM2 算法的使用须遵守《中华人民共和国密码法》及相关法规。本文仅展示性能优化技术,不构成安全审计或合规建议。

6. 存证

文件SHA256时间戳
_bigint_sm2.js91cb89b7d86313c429aaa1c305ca0f0f54e43b0b2a37dfa3c9ea2964cee8ed6aDigiCert 2026-06-16
_sm2_scalarmul.jsc1deb9901b4578a5596bed42675351d54428140fc981c40f239667c2efb8cd84DigiCert 2026-06-16
keccak.js517976e6b2ea32b06cec08d03e15bd31d24d18fa8e9956de3c2770f109a84a0bDigiCert 2026-06-15

npm 包:@fibemate/keccak @fibemate/sm2-crypto(就绪,待发布)