3. SM2 算法概述:国密标准体系、曲线参数与密钥对生成
聊到 TEE 里的密钥派生,SM2 是绕不开的一环。我个人习惯把 SM2 看作「国密体系里的 ECDSA + ECDH 合体版」——它既做签名验签,也做密钥交换,还支持加密解密。说白了,一条曲线包办了非对称密码的三大核心场景。
嗯,咱们先捋清楚它的来龙去脉,再动手看密钥对到底怎么算出来的。
3.1 国密标准体系:SM2 在哪儿站着
国密标准不是只有 SM2 一个孤零零的算法。它是一整套密码工具链。我整理了一张表,方便你对照着看:
| 标准编号 | 算法名称 | 核心用途 | 对标国际算法 |
|---|---|---|---|
| GB/T 32918 | SM2 | 签名、密钥交换、加密 | ECDSA / ECDH / ECIES |
| GB/T 32905 | SM3 | 密码杂凑(哈希) | SHA-256 |
| GB/T 32907 | SM4 | 对称分组加密 | AES-128 |
| GB/T 36968 | SM9 | 基于标识的密码 | IBC / BF-IBE |
你在 TEE 里做密钥派生,最常打交道的组合就是 SM2 + SM3。SM2 负责非对称层的密钥对生成和签名,SM3 负责派生时的哈希压缩。我在项目里见过有人只用 SM2 不做 SM3 摘要,结果签名长度失控——嗯,这个坑后面会细讲。
3.2 SM2 曲线参数:一条精心设计的椭圆曲线
SM2 使用的椭圆曲线定义在素域 Fp 上,参数是国密局公开的。你想想看,如果曲线参数有后门,整个密码体系就塌了。所以这些参数都是公开可验证的,没有任何「魔法数字」。
核心参数如下:
| 参数 | 值(十六进制) | 说明 |
|---|---|---|
| p | FFFFFFFE FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF 00000000 FFFFFFFF FFFFFFFF | 素域模数,256 位 |
| a | FFFFFFFE FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF 00000000 FFFFFFFF FFFFFFFC | 曲线方程 y² = x³ + ax + b 中的 a |
| b | 28E9FA9E 9D9F5E34 4D5A9E4B CF6509A7 F39789F5 15AB8F92 DDBCBD41 4D940E93 | 曲线方程中的 b |
| Gx | 32C4AE2C 1F198119 5F990446 6A39C994 8FE30BBF F2660BE1 715A4589 334C74C7 | 基点 G 的 x 坐标 |
| Gy | BC3736A2 F4F6779C 59BDCEE3 6B692153 D0A9877C C62A4740 02DF32E5 2139F0A0 | 基点 G 的 y 坐标 |
| n | FFFFFFFE FFFFFFFF FFFFFFFF FFFFFFFF 7203DF6B 21C6052B 53BBF409 39D54123 | 基点 G 的阶(素数) |
关键点:SM2 的 p 和 n 都是 256 位,但 n 略小于 p。这意味着私钥空间是 2²⁵⁶ 量级,暴力破解在可预见的未来完全不现实。
我记得第一次手动验证这些参数时,用 OpenSSL 的 EC 命令逐项比对,发现 G 点坐标完全吻合——嗯,心里踏实了。曲线方程是 y² = x³ + ax + b,a 的值是 p - 3,这是为了加速点运算做的优化。
3.3 SM2 密钥对生成原理:从随机数到公钥
密钥对生成其实就三步。我习惯用「掷骰子 → 乘法 → 检查」来记:
- 生成私钥 d:在 [1, n-2] 范围内取一个安全的随机数。
- 计算公钥 P:P = d · G,即私钥乘以基点 G 的标量乘法。
- 验证:检查 P 是否在曲线上,且不是无穷远点。
你可能会问:为什么私钥范围是 [1, n-2] 而不是 [1, n-1]?因为 d = n-1 时,d · G = -G,虽然合法但容易在实现中引入侧信道风险。国密标准直接把这个值排除掉了。
下面这张图把流程画清楚了:
实战建议:在 TEE 里生成 SM2 私钥时,千万别用软件伪随机数。我见过一个案例,某厂商用 C 标准库的 rand() 生成私钥,结果两个设备产出了相同的密钥对——嗯,后果很严重。TEE 内部通常提供 TEE_GenerateRandom(),直接用那个。
3.4 标量乘法的实现要点
P = d · G 是整个密钥对生成的计算瓶颈。d 是 256 位的随机数,G 是曲线上的固定点。这个运算不能直接算 d 次加法——2²⁵⁶ 次加法,宇宙毁灭都算不完。
实际用的是 双倍-加(Double-and-Add) 算法。我简单说一下思路:
- 把 d 写成二进制,比如 d = (d₂₅₅ d₂₅₄ ... d₀)₂
- 从最高位开始,每步先做一次点倍(2 × Q)
- 如果当前位是 1,再做一次点加(Q + G)
代码实现大概长这样:
// SM2 标量乘法:P = d · G
// 输入:私钥 d(256 位大整数),基点 G
// 输出:公钥 P(曲线上的点)
sm2_point_t scalar_mult(sm2_bn_t d, sm2_point_t G) {
sm2_point_t Q = INFINITY; // 初始化为无穷远点
sm2_point_t R = G;
// 从最高位到最低位遍历
for (int i = 255; i >= 0; i--) {
Q = point_double(Q); // 点倍:Q = 2 * Q
if (test_bit(d, i)) { // 如果当前位为 1
Q = point_add(Q, R); // 点加:Q = Q + G
}
}
return Q;
}
注意:上面的代码是教学简化版。生产环境必须使用 恒定时间(constant-time) 实现,否则通过功耗或电磁分析就能把私钥 d 的每一位猜出来。我在 TEE 里做审计时,发现过开源库的标量乘法没有做恒定时间处理——那个漏洞可以直接用简单功耗分析(SPA)攻破。
3.5 公钥的编码格式
SM2 公钥是一个曲线上的点,包含 x 和 y 两个 256 位坐标。国密标准定义了两种编码方式:
| 格式 | 长度 | 说明 |
|---|---|---|
| 未压缩格式 | 65 字节 | 0x04 || x || y,最常用 |
| 压缩格式 | 33 字节 | 0x02/0x03 || x,通过 x 恢复 y |
在 TEE 内部存储时,我建议用未压缩格式。虽然多 32 字节,但省去了恢复 y 坐标的计算开销。TEE 的内存通常够用,计算资源反而更宝贵。
嗯,到这里 SM2 的曲线参数和密钥对生成原理就讲清楚了。你想想看,整个流程其实就依赖一个安全的随机数和一个高效的标量乘法。后面讲 TEE 内密钥派生时,我们会把私钥 d 换成派生因子,但数学内核完全一样。