SM4 算法原理:Feistel 结构、密钥扩展、轮函数

SM4 是国密标准里的分组密码算法,做加解密巡检绕不开它。说实话,刚接触 SM4 那会儿,我第一反应是「跟 AES 比起来好像没那么复杂?」—— 后来在调性能瓶颈时才发现,它的 Feistel 结构和密钥扩展有不少坑,搞清楚了才能写出靠谱的巡检脚本。

Feistel 结构:对称的拆解与重组

SM4 使用的是平衡 Feistel 结构,说白了就是把一块 128 位的数据平分成左右两半,每轮只用轮函数更新右半,左半直接复制过去。我习惯这么记:左不动,右变化,然后交叉换位。

个人经验: 我在巡检日志分析时发现,很多实现把 Feistel 的「左-右互换」步骤写成了单独循环,其实可以用地址偏移直接实现,减少一次内存复制。

具体流程:

  1. 输入 128 位明文 X = (X0, X1, X2, X3),每个字 32 位。
  2. 第 i 轮(i=0,1,...,31):Xi+1 = Xi ⊕ T(Xi+1 ⊕ Xi+2 ⊕ Xi+3 ⊕ rki),其中 T 是轮函数。
  3. 最后 4 个字反序输出得到密文。

举个简化的例子:假设初始状态是 (A, B, C, D),第一轮后变成 (A, B, C, A⊕T(B⊕C⊕D⊕rk0))。你看,只有第四个块变了。

为什么用 Feistel 结构?它有个好处:加解密过程完全一样,只是轮密钥顺序反过来。我在做巡检脚本时,就是把密钥扩展生成的 rk 数组顺序填进去,解密时从后往前读,代码几乎一样,少写很多 if 分支。

注意: SM4 是 32 轮,但轮函数里那个 T 合成置换用的是 S 盒 + 循环移位,不是简单的 XOR。我曾经调一个误把 rk 位置放错的日志,折腾了两天才发现 —— 所以巡检时一定要验证中间轮的 rk 正确性。

密钥扩展:从 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 位轮密钥,供轮函数使用
避坑指南: 我曾经在巡检脚本里直接用了 rk 数组,发现解密不对。后来发现密钥扩展的 T' 跟轮函数的 T 不一样 —— 它少了一层循环左移。很多文档写「相同」其实是简化说法,实际实现要分两个函数。

轮函数 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 算法核心关系图 密钥扩展 输入: MK (128 bit) rk0…rk31 Feistel 结构 (32轮) 左半 L 右半 R rk 送入每轮 轮函数 T τ + L 合成 输入 输出 ⊕ 左半 说明: 密钥扩展生成32个轮密钥rk,每个rk参与一轮Feistel的轮函数T。轮函数由S盒(τ)和线性移位(L)组成。加解密流程相同,仅rk顺序相反。

为什么巡检要关注这三块?

我在实际巡检中发现,很多设备的 SM4 实现问题就出在密钥扩展或轮函数的 L 变换上。有个典型案例:某硬件 IP 的密钥扩展模块把 T' 里的循环左移位数写错了(用了轮函数的版本),导致所有加解密都错。巡检脚本如果只比对最后结果,很可能漏掉这种中间错误 —— 所以我建议在脚本里加一个「轮密钥正确性校验」的步骤。

经验之谈: 写巡检用例时,用标准向量分别验证密钥扩展和轮函数中间值,不要只测加解密结果。我曾经靠这个发现过一个第三方库的 S 盒实现偏移了 1 位的问题。

另外,Feistel 结构决定了 SM4 的加解密共用同一套代码,这对性能巡检很友好 —— 你只需要测加密性能(或解密性能),另一个方向可以直接推导出来,不需要分别压测。