3、国密算法基础(上):SM2、SM3、SM4 原理
各位好,欢迎来到第三章。这一章我们不讲 TEE,专门把国密算法里最核心的三个——SM2、SM3、SM4——掰开揉碎讲清楚。
为什么要把它们放在前面?因为后面所有 TEE 里的密钥协商、数据加密、完整性校验,底层都是这三兄弟。你如果对它们的原理心里没底,后面看代码会非常痛苦。我自己刚接触国密时,就犯过把 SM2 当对称加密用的低级错误……嗯,今天咱们一次性把坑填上。
3.1 SM2 椭圆曲线公钥密码算法
SM2 本质上是一套基于椭圆曲线(ECC)的公钥密码体系。它包含三个子功能:数字签名、密钥交换、公钥加密。说白了,它既可以用作签名验签,也可以用来加密数据,还能帮双方安全地协商出一个对称密钥。
核心要点:SM2 的安全性依赖于椭圆曲线离散对数问题(ECDLP)。给定基点 G 和公钥 P = [d]G,想反推出私钥 d,目前没有多项式时间的解法。
3.1.1 曲线参数
SM2 使用一条特定的 256 位素域椭圆曲线。参数由国家密码管理局指定,所有实现必须使用同一套参数。我见过有人试图自己换曲线参数,结果导致互操作性全乱套——千万别这么干。
| 参数 | 含义 | 值(十六进制) |
|---|---|---|
| p | 素域模数 | FFFFFFFE FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF 00000000 FFFFFFFF FFFFFFFF |
| a | 曲线系数 | FFFFFFFE FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF 00000000 FFFFFFFF FFFFFFFC |
| b | 曲线系数 | 28E9FA9E 9D9F5E34 4D5A9E4B CF6509A7 F39789F5 15AB8F92 DDBCBD41 4D940E93 |
| Gx | 基点 x 坐标 | 32C4AE2C 1F198119 5F990446 6A39C994 8FE30BBF F2660BE1 715A4589 334C74C7 |
| Gy | 基点 y 坐标 | BC3736A2 F4F6779C 59BDCEE3 6B692153 D0A9877C C62A4740 02DF32E5 2139F0A0 |
| n | 基点阶 | FFFFFFFE FFFFFFFF FFFFFFFF FFFFFFFF 7203DF6B 21C6052B 53BBF409 39D54123 |
我的习惯:在代码里把这些参数写成常量,不要从外部文件加载。曾经有个项目因为参数文件被篡改,导致签名验证全部失效——教训深刻。
3.1.2 密钥对生成
生成 SM2 密钥对很简单:随机选一个私钥 d(1 < d < n-1),然后计算公钥 P = [d]G。这里 [d]G 表示椭圆曲线上的标量乘法,也就是把 G 点加 d 次。
// 伪代码示例:SM2 密钥对生成
function generateSM2KeyPair():
d = randomBigInteger(1, n-1) // 私钥
P = scalarMultiply(d, G) // 公钥
return (d, P)
实际项目中,随机数质量非常关键。我建议使用硬件真随机数发生器,或者至少用操作系统提供的 CSPRNG。用伪随机数生成私钥,等于把门锁交给小偷。
3.1.3 加密与解密
SM2 公钥加密采用混合加密模式:用临时密钥协商出一个对称密钥,再用对称算法加密实际数据。具体流程如下:
- 发送方生成随机数 k,计算临时公钥 C1 = [k]G
- 计算共享点 S = [k]P(P 是接收方公钥)
- 从 S 的 x、y 坐标派生对称密钥 t
- 用 t 加密明文 M,得到密文 C2
- 计算杂凑值 C3,用于完整性校验
- 最终密文为 C1 || C2 || C3
避坑指南:我曾经在实现 SM2 加密时,忘了检查共享点 S 是否为无穷远点。如果 k 选得不好,S 可能等于 O,此时派生出的密钥全是 0——攻击者可以直接解密。标准里明确要求检查 S != O,千万别漏。
3.2 SM3 密码杂凑算法
SM3 是国密标准的杂凑算法,输出 256 位(32 字节)的杂凑值。它的设计思路和 SHA-256 类似,但内部结构有差异。SM3 主要用于数字签名、消息认证码、随机数生成等场景。
3.2.1 算法流程
SM3 的处理过程分为三步:消息填充、迭代压缩、输出杂凑值。
- 消息填充:在消息末尾补一个 1,再补若干个 0,最后附加 64 位长度信息,使总长度是 512 的倍数。
- 迭代压缩:将填充后的消息分成 512 位(64 字节)的块,每块经过 64 轮压缩函数更新内部状态。
- 输出杂凑值:最终内部状态的 8 个 32 位字拼接成 256 位杂凑值。
压缩函数是 SM3 的核心。它使用布尔函数、置换函数和移位操作,把消息块和当前状态混合在一起。64 轮下来,输入的任何微小变化都会扩散到整个输出。
我个人经验:SM3 的软件实现性能大约在 200-400 MB/s(取决于 CPU 和优化程度)。如果需要在 TEE 里做大量哈希运算,建议用硬件加速指令。ARMv8.2 以上的芯片有 SM3 专用指令,速度能提升 5-10 倍。
3.2.2 与 SHA-256 的对比
| 特性 | SM3 | SHA-256 |
|---|---|---|
| 输出长度 | 256 位 | 256 位 |
| 轮数 | 64 | 64 |
| 消息块大小 | 512 位 | 512 位 |
| 布尔函数 | 16 个不同的布尔函数 | 4 个不同的布尔函数 |
| 安全强度 | 128 位(抗碰撞) | 128 位(抗碰撞) |
你可能会问:既然 SHA-256 已经够用,为什么还要用 SM3?原因很简单——合规。在等保三级、密评等场景下,必须使用国密算法。而且从安全角度,SM3 的布尔函数更复杂,理论上抗差分攻击的能力更强。
3.3 SM4 分组密码算法
SM4 是国密标准的分组密码算法,分组长度 128 位,密钥长度 128 位。它采用 Feistel 结构,共 32 轮迭代。SM4 主要用于数据加密,在 TEE 里常用于保护存储数据和通信信道。
3.3.1 加密流程
SM4 的加密过程可以概括为:将 128 位明文分成 4 个 32 位字,经过 32 轮迭代,最后输出 128 位密文。
// SM4 单轮加密伪代码
function sm4Round(X0, X1, X2, X3, rk):
// X0, X1, X2, X3 是 4 个 32 位字
// rk 是轮密钥
temp = X1 ^ X2 ^ X3 ^ rk
temp = SBox(temp) // 非线性变换
temp = L(temp) // 线性变换
X4 = X0 ^ temp
return (X1, X2, X3, X4)
这里的 SBox 是 8 位输入、8 位输出的非线性替换表,L 是 32 位输入的线性置换函数。32 轮之后,再对输出做一次反序变换,得到最终密文。
我的建议:SM4 的解密和加密结构完全一样,只是轮密钥的使用顺序相反。所以你可以用同一套代码实现加密和解密,只需要把轮密钥数组倒序即可。这在 TEE 里能节省不少代码空间。
3.3.2 工作模式
SM4 本身只定义了对 128 位数据块的加密。实际使用时,需要配合工作模式来加密任意长度的数据。常见模式包括:
- ECB(电子密码本):每个块独立加密,简单但不安全。相同明文块产生相同密文块,容易泄露模式信息。我建议永远不要用 ECB。
- CBC(密码分组链接):每个明文块先与前一个密文块异或,再加密。需要初始向量 IV,安全性较好。
- CTR(计数器模式):将计数器加密后与明文异或。可以并行计算,适合高性能场景。
- GCM(伽罗瓦/计数器模式):在 CTR 基础上增加认证标签,同时提供加密和完整性保护。TEE 里最推荐的模式。
避坑指南:我曾经在项目里用 SM4-CBC 加密大量数据,但忘了每次加密都生成新的 IV。结果攻击者通过观察密文模式,推断出了部分明文内容。记住:IV 必须随机且每次不同,绝不能重复使用。
3.4 三种算法的关系与协作
在实际的 TEE 系统中,SM2、SM3、SM4 很少单独使用。它们通常组合成一个完整的密码方案。举个例子:
- 用 SM2 密钥交换协议协商出一个对称密钥
- 用 SM3 对协商过程做完整性校验
- 用 SM4 加密后续的通信数据
这种组合方式既利用了 SM2 的非对称特性来安全传递密钥,又利用了 SM4 的高效性来加密大量数据,SM3 则确保整个过程没有被篡改。三者缺一不可。
这张图展示的就是 TEE 里最常见的密码方案组合。你想想看,如果只用 SM4 加密,密钥怎么安全地传给对方?如果只用 SM2 加密,性能又跟不上。所以三者配合,才是工程上的最优解。