4. SM4密钥扩展算法实现

SM4 的加密轮函数本身并不复杂,真正决定实现质量与安全性的,往往是密钥扩展(Key Expansion)这一环。它把 128 位主密钥扩展成 32 个 32 位轮密钥 rk[0]…rk[31],供 32 轮迭代使用。密钥扩展与加密轮函数共享同一套非线性结构,但输入来源、参数和输出组织方式不同。理解 FK、CK 的取值规则、密钥生成流程以及轮密钥的存储与验证方法,是写出正确 SM4 实现的前提。

一、系统参数 FK 与固定参数 CK

SM4 标准定义了两个常量集合。系统参数 FK 是 4 个 32 位字,用于对主密钥做初始“加盐”式混淆,取值固定为:

FK[0] = 0xA3B1BAC6
FK[1] = 0x56AA3350
FK[2] = 0x677D9197
FK[3] = 0xB27022DC

固定参数 CK 共 32 个 32 位字,对应 32 个轮密钥。它的生成规则是:CK[i] 的 4 个字节依次为 ck[i][j] = (4i + j) × 7 mod 256,其中 i = 0…31,j = 0…3。也就是说,CK 不是随意选取的随机数,而是由一条确定的模运算公式生成,任何实现都必须得到完全相同的 128 字节序列。前几个 CK 值为:

CK[0] = 0x00070E15
CK[1] = 0x1C232A31
CK[2] = 0x383F464D
CK[3] = 0x545B6269
...

FK 与 CK 的作用不同:FK 只在密钥扩展开始时与主密钥异或一次,用于打破主密钥的规律性;CK 则在每一轮与中间状态异或,使 32 个轮密钥彼此不同。二者都是公开常量,不构成密钥材料,但取值错误会直接导致轮密钥全错、加解密结果与标准测试向量不符。

SM4 密钥扩展数据流:MK → K[0..3] → 32 轮 → rk[0..31] 主密钥 MK 128 bit = MK[0..3] 系统参数 FK 4 × 32 bit 常量 ⊕ 初始状态 K[0..3] K[i] = MK[i] ⊕ FK[i] 轮密钥生成(i = 0..31) K[i+4] = K[i] ⊕ T'(K[i+1]⊕K[i+2]⊕K[i+3]⊕CK[i]) rk[i] = K[i+4] 固定参数 CK[0..31] ck[i][j] = (4i+j)×7 mod 256 轮密钥数组 rk[0..31](32 × 32 bit = 1024 bit) rk[0] rk[1] rk[2] … rk[30] rk[31] → 依次供 32 轮加密/解密使用 K[i+1..i+3] 参与

二、密钥生成流程与 T′ 变换

密钥扩展的完整流程可以概括为“先异或 FK,再迭代 32 轮”。设主密钥为 MK[0..3],首先计算初始状态 K[0] = MK[0] ⊕ FK[0],K[1] = MK[1] ⊕ FK[1],K[2] = MK[2] ⊕ FK[2],K[3] = MK[3] ⊕ FK[3]。随后对 i = 0 到 31 依次执行:

K[i+4] = K[i] ⊕ T'(K[i+1] ⊕ K[i+2] ⊕ K[i+3] ⊕ CK[i])
rk[i]  = K[i+4]

其中 T′ 是密钥扩展专用的合成变换,由线性变换 L′ 与 S 盒替换复合而成。与加密轮函数中的 T 相比,T′ 的线性变换 L′ 更简单,只包含循环左移和异或:

L'(B) = B ⊕ (B <<< 13) ⊕ (B <<< 23)

而 T 的线性变换 L 为 B ⊕ (B<<<2) ⊕ (B<<<10) ⊕ (B<<<18) ⊕ (B<<<24)。两者共用同一个 S 盒,但移位参数不同,这是实现时最容易混淆的地方。T′ 的输入是 32 位字,先按字节拆成 4 个字节,分别查 S 盒,再拼回 32 位字,最后做 L′ 变换。S 盒替换是逐字节的,因此实现中通常写成查表或位运算两种形式。

以教学示例为例,假设主密钥 MK = 0x01234567 89ABCDEF FEDCBA98 76543210(仅为演示用,非标准测试向量)。先与 FK 异或得到 K[0..3],再按上述公式迭代。第一轮中,K[1]⊕K[2]⊕K[3]⊕CK[0] 得到一个 32 位中间值,经 S 盒和 L′ 后与 K[0] 异或,得到 K[4],即 rk[0]。后续每轮重复该过程,直到产生 rk[31]。整个过程是确定性的,相同主密钥必然得到相同轮密钥序列。

三、轮密钥的存储与组织

轮密钥的存储方式直接影响加解密代码的复用性。常见做法是定义一个长度为 32 的 uint32_t 数组 rk[32],按轮次顺序存放。加密时第 i 轮使用 rk[i];解密时轮密钥顺序反转,第 i 轮使用 rk[31-i]。这样加密和解密可以共用同一套轮函数,只需传入不同的轮密钥指针或索引方向。

在内存布局上,需要注意字节序问题。SM4 标准以 32 位字为单位定义运算,但实际数据在内存中可能是大端或小端。如果实现中直接按字节数组处理,必须在加载主密钥和输出轮密钥时统一字节序约定。推荐做法是:内部运算全部使用 uint32_t,输入输出时显式做字节序转换,避免因平台差异导致轮密钥不一致。对于嵌入式场景,若 RAM 紧张,也可以不存储全部 32 个轮密钥,而是在每轮加密前实时计算当前轮密钥,但这样会增加计算量,通常只在极端资源受限时采用。

四、轮密钥验证方法

验证轮密钥是否正确,最直接的方法是使用标准测试向量。SM4 标准给出了主密钥与明文均为 0x0123456789ABCDEFFEDCBA9876543210 时的加密结果,其密文为 0x681EDF34D206965E86B3E94F536E4246。实现完成后,先扩展密钥,再执行加密,若输出与标准密文一致,说明密钥扩展和轮函数都正确。若不一致,可进一步单独检查轮密钥:用已知主密钥计算 rk[0],与参考实现或标准附录中的中间值比对。

另一种验证思路是“加解密互逆”。用同一主密钥分别执行加密和解密,若解密结果等于原始明文,说明轮密钥顺序反转逻辑正确。还可以构造边界用例:主密钥全 0、全 F、单比特翻转等,观察轮密钥是否随之变化且无异常。对于工程实现,建议把轮密钥打印成十六进制序列,与可信实现逐轮比对,定位是 S 盒、L′ 移位还是 CK 取值出错。

调试建议:先固定一个主密钥,打印 K[0..3] 和 rk[0..3],与手工计算或参考代码比对。若 K[0..3] 正确而 rk 错误,问题多半在 T′ 的 S 盒或 L′ 移位;若 K[0..3] 就错误,则检查 FK 异或和字节序。
常见错误:把 T′ 的 L′ 写成加密用的 L(移位 2/10/18/24),或把 CK 的生成公式记成 (4i+j)×7 之外的系数。这两类错误不会导致程序崩溃,但会使所有轮密钥偏离标准,最终密文完全错误,且难以通过肉眼发现。

综上,SM4 密钥扩展的核心是:用 FK 初始化、用 CK 逐轮混淆、用 T′ 做非线性与线性混合,最终得到 32 个轮密钥。实现时抓住“常量正确、移位正确、字节序统一”三条主线,再配合标准向量和加解密互逆验证,就能稳定地完成密钥扩展模块。