SM4 算法原理:Feistel 结构、密钥扩展、轮函数
SM4 是国密标准里的分组密码算法,做加解密巡检绕不开它。说实话,刚接触 SM4 那会儿,我第一反应是「跟 AES 比起来好像没那么复杂?」—— 后来在调性能瓶颈时才发现,它的 Feistel 结构和密钥扩展有不少坑,搞清楚了才能写出靠谱的巡检脚本。
Feistel 结构:对称的拆解与重组
SM4 使用的是平衡 Feistel 结构,说白了就是把一块 128 位的数据平分成左右两半,每轮只用轮函数更新右半,左半直接复制过去。我习惯这么记:左不动,右变化,然后交叉换位。
具体流程:
- 输入 128 位明文
X = (X0, X1, X2, X3),每个字 32 位。 - 第 i 轮(i=0,1,...,31):
Xi+1 = Xi ⊕ T(Xi+1 ⊕ Xi+2 ⊕ Xi+3 ⊕ rki),其中 T 是轮函数。 - 最后 4 个字反序输出得到密文。
举个简化的例子:假设初始状态是 (A, B, C, D),第一轮后变成 (A, B, C, A⊕T(B⊕C⊕D⊕rk0))。你看,只有第四个块变了。
为什么用 Feistel 结构?它有个好处:加解密过程完全一样,只是轮密钥顺序反过来。我在做巡检脚本时,就是把密钥扩展生成的 rk 数组顺序填进去,解密时从后往前读,代码几乎一样,少写很多 if 分支。
密钥扩展:从 128 位到 32 轮密钥
密钥扩展就是把 128 位主密钥(4 个字)扩充出 32 个 32 位轮密钥 rk。我最初以为跟 AES 一样复杂,实则不然。SM4 的密钥扩展使用相同的 Feistel 轮结构,但固定常数 FK 和 CK 来避免滑动攻击。
步骤很简单:
- 初始变换: 主密钥
MK = (MK0, MK1, MK2, MK3)与系统参数 FK 异或得到K0~K3:
K0 = MK0 ⊕ FK0,
K1 = MK1 ⊕ FK1,
K2 = MK2 ⊕ FK2,
K3 = MK3 ⊕ FK3。 - 迭代: 对 i=0..31:
rki = Ki+4 = Ki ⊕ T'(Ki+1 ⊕ Ki+2 ⊕ Ki+3 ⊕ CKi)
其中 T' 跟轮函数 T 几乎一样,但线性变换 L 只有 13/21/23 位循环左移,没有 2/10/18/24 位。
| 参数 | 长度 | 作用 |
|---|---|---|
| FK0~FK3 | 各 32 位 | 系统参数,固定值,消除主密钥模式 |
| CK0~CK31 | 各 32 位 | 固定常数,每轮不同,由公式计算 |
| rk0~rk31 | 各 32 位 | 轮密钥,供轮函数使用 |
轮函数 T:合成置换的秘密
轮函数是 SM4 的核心战斗力。输入 32 位,输出 32 位。分为两步:非线性变换 τ 和 线性变换 L。
- τ 变换: 把 32 位拆成 4 个字节,分别过 S 盒(320 字节表),再拼回 32 位。S 盒是 8×8 的置换表,你可以在国标里查到。说白了就是个查表操作。
- L 变换: 对 τ 的输出做循环左移异或:
B ⊕ (B <<< 2) ⊕ (B <<< 10) ⊕ (B <<< 18) ⊕ (B <<< 24)
整个轮函数 T = L(τ(·))。注意密钥扩展的 T' 只用 13/21/23 位循环左移,两者不一样。
// 伪代码:轮函数 T
uint32_t T(uint32_t x) {
uint8_t a0, a1, a2, a3;
a0 = Sbox[(x >> 24) & 0xFF];
a1 = Sbox[(x >> 16) & 0xFF];
a2 = Sbox[(x >> 8) & 0xFF];
a3 = Sbox[(x) & 0xFF];
uint32_t B = (a0 << 24) | (a1 << 16) | (a2 << 8) | a3;
return B ^ rotl(B, 2) ^ rotl(B, 10) ^ rotl(B, 18) ^ rotl(B, 24);
}
三者的关系图谱
下面这张图把 Feistel 结构、密钥扩展、轮函数串在了一起。你看,密钥扩展产出的 rk 送入 Feistel 结构的每一轮,Feistel 结构里每轮调用一次轮函数 T。三者缺一不可。
为什么巡检要关注这三块?
我在实际巡检中发现,很多设备的 SM4 实现问题就出在密钥扩展或轮函数的 L 变换上。有个典型案例:某硬件 IP 的密钥扩展模块把 T' 里的循环左移位数写错了(用了轮函数的版本),导致所有加解密都错。巡检脚本如果只比对最后结果,很可能漏掉这种中间错误 —— 所以我建议在脚本里加一个「轮密钥正确性校验」的步骤。
另外,Feistel 结构决定了 SM4 的加解密共用同一套代码,这对性能巡检很友好 —— 你只需要测加密性能(或解密性能),另一个方向可以直接推导出来,不需要分别压测。