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×。
FIBEMATE 采用 SM2-MLKEM-768 混合后量子握手,即每个 TLS 1.3 连接需要同时执行:
经典的 X25519 密钥交换仅需 ~0.1ms。如果不优化 SM2,它占 TLS 握手的 90% 以上时间,完全掩盖 ML-KEM 的性能优势。
目标:将 SM2 经典部分压缩至 1-5ms 级别,使总握手时间与 X25519-MLKEM 方案持平。
FIBEMATE 最初使用 jsbn(JavaScript Big Number)库对接 sm-crypto。jsbn 的内部表示是 28-bit limb 数组(而非 64-bit 原生整数),每个大数运算需要在 JS 层手动处理进位链。
| 操作 | jsbn (ms) | 瓶颈分析 |
|---|---|---|
| 域乘法(单次) | 0.005375 | 28-bit limbs 手动进位 |
| 密钥生成 | 11.654 | 256 次点加法 + 256 次倍点 |
| 签名 | 30.151 | 内层全为域乘法,倍点 + 加法 |
| 验签 | 28.359 | 两次通用标量乘(无预计算) |
| 加密 | 23.134 | KDF + 点乘 + C3 哈希 |
| 解密 | 11.698 | 一次固定基点乘 + KDF |
关键洞察: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 BigInt | 0.733 µs | 7.33× |
这个 7.3× 是所有后续优化的乘数因子——任何在上层减少的操作,都被这个因子放大。
难点:BigInt 右移 >> 是算术移位(保留符号位),需要 & 0xFFFFFFFFFFFFFFFFn 掩码处理逻辑移位场景。
问题:仿射坐标的点加法需要计算模逆(modInverse),而模逆是椭圆曲线运算中最昂贵的单步操作(O(n³) via 扩展欧几里得)。
方案:切换到 Jacobian 投影坐标 (X, Y, Z),其中仿射坐标 (x, y) = (X/Z², Y/Z³)。Jacobian 下的点加法和倍点完全避免模逆,只在最终输出时做一次逆变换(或直接在 Jacobian 域完成全部运算)。
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× 后效果显著。
问题:密钥生成和签名的核心是固定基点 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,920 | 1,664 | -57.5% |
| 密钥生成时间 | ~3.8ms | ~1.5ms | ~2.6× |
| 预计算开销 | — | 82ms / 16 KB | 一次性 |
问题:验签需要一般点(非固定基点)的标量乘 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;
}
实测效果:
| 操作 | binary | wNAF (w=4) | 提升 |
|---|---|---|---|
| 一般点标量乘 | 基准 | -16% 时间 | 1.16× |
边界效应:wNAF 理论加速 ~1.3×,实测仅 1.16×。原因:JS 引擎的函数调用开销、BigInt 临时对象分配(GC 压力)、V8 的 64-bit 原生路径已经极快,额外逻辑相对占比上升。
问题:jsbn 的 require("sm-crypto").sm3 将 hex 字符串按 UTF-8 文本处理,而 BigInt 实现直接传入 Uint8Array。这导致加密/解密的 C3 完整性哈希 计算不一致。
修复:
C3 = SM3(x₂ || M || y₂),其中 x₂/y₂ 必须是固定长度 32 字节的坐标值结论:BigInt SM2 实现与 sm-crypto 100% 互操作兼容。
| 操作 | jsbn (ms) | BigInt + Jacobian + Precomp + wNAF (ms) | 加速比 |
|---|---|---|---|
| 域乘法(单次) | 0.005375 | 0.000733 | 7.33× |
| 密钥生成 | 11.654 | 1.460 | 7.98× |
| 签名 | 30.151 | 4.868 | 6.19× |
| 验签 | 28.359 | 8.998 | 3.15× |
| 加密 | 23.134 | 2.757 | 8.39× |
| 解密 | 11.698 | 1.386 | 8.44× |
| 公钥派生 | 11.408 | 1.325 | 8.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%。
原 §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 状态 | 样本量 N | 说明 |
|---|---|---|---|
| 旧版 jsbn SM2 | ✅ 通过 | 2,000 | 基础时序测试,2026-05-30 |
| 新版 BigInt + Jacobian + wNAF | ✅ 通过 | 2,000 | 2026-06-16 TVLA v2,10/10 SM2 操作通过。wNAF 理论风险在 V8 环境下被噪声稀释,实测安全。详阅 侧信道分析页面 |
⚠️ 为什么新版需要重测?
📚 学术参考
🎉 状态更新(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 算法的使用须遵守《中华人民共和国密码法》及相关法规。本文仅展示性能优化技术,不构成安全审计或合规建议。
| 文件 | SHA256 | 时间戳 |
|---|---|---|
_bigint_sm2.js | 91cb89b7d86313c429aaa1c305ca0f0f54e43b0b2a37dfa3c9ea2964cee8ed6a | DigiCert 2026-06-16 |
_sm2_scalarmul.js | c1deb9901b4578a5596bed42675351d54428140fc981c40f239667c2efb8cd84 | DigiCert 2026-06-16 |
keccak.js | 517976e6b2ea32b06cec08d03e15bd31d24d18fa8e9956de3c2770f109a84a0b | DigiCert 2026-06-15 |
npm 包:@fibemate/keccak @fibemate/sm2-crypto(就绪,待发布)