4. SM4 密钥扩展算法:密钥调度与轮密钥生成
SM4 的密钥扩展,说白了就是把你的 128 位初始密钥,转换成 32 个 32 位的轮密钥。这 32 个轮密钥,每一轮加解密都会用到。我自己刚开始接触 SM4 时,总觉得密钥扩展就是简单复制几轮,结果写出来直接跑飞。嗯,这里面的细节,咱们还是得仔细梳理一遍。
4.1 密钥调度:从 128 位到 32 个 32 位字的拆分
你先拿到的是一把 128 位的原始密钥,记为 MK = (MK₀, MK₁, MK₂, MK₃),每个 MK 是 32 位。SM4 在正式进入 32 轮迭代之前,会先把这些 32 位字做一些“预处理”操作。我见过有人直接把 MK₀~MK₃ 扔进迭代,结果生成的轮密钥完全不对。注意,这里有一个固定异或参数——系统参数 FK。
FK 有 4 个常数,分别是:
FK₀ = A3B1BAC6
FK₁ = 56AA3350
FK₂ = 677D9197
FK₃ = B27022DC
讲一下预处理公式:
(K₀, K₁, K₂, K₃) = (MK₀ ⊕ FK₀, MK₁ ⊕ FK₁, MK₂ ⊕ FK₂, MK₃ ⊕ FK₃)
这个操作相当于把 MK 和 FK 做了一次“白化”。我曾经在某个国产加密卡项目中,因为没注意 FK 的字节序(大小端问题),导致加解密结果跟标准测试向量对不上,排查了整整一下午。所以建议你在实现时,先把 FK 和 MK 按字节序统一,再做异或。
4.2 轮密钥生成:32 轮迭代的核心逻辑
得到 K₀~K₃ 后,接下来就要跑 32 轮,生成 32 个轮密钥 rk₀ ~ rk₃₁。每一轮的公式如下:
rk_i = K_{i+4} = K_i ⊕ T'(K_{i+1} ⊕ K_{i+2} ⊕ K_{i+3} ⊕ CK_i)
其中,T'() 是一个“非线性变换 + 线性变换”的复合函数,跟加解密里的 T 变换类似,但线性部分做了一个简单的循环左移 13 位(不是加解密中的 2、10、18 位)。你想想看,SM4 的设计者为什么要这么改?其实是为了让轮密钥的扩散性更好,避免密钥泄露后反推初始密钥。
再说说 CK_i。它是一个 32 位的固定参数,每一轮不同。CK 的生成规则也很机械:
- 将 4 个字节分别记为
ck_i,j,其中 j = 0,1,2,3 - ck_i,j = (4×i + j) × 7 mod 256
说白了,就是从 0 开始,每次加 7,取模 256,按顺序填满 4 个字节,凑成 32 位。我个人习惯在代码中预计算一个 32 个元素的 ck_data 数组,运行时直接查表,速度更快。
4.3 核心变换 T' 拆解
T' 先做非线性变换(S 盒替换),再做线性变换。SM4 的 S 盒跟 AES 不一样,是一个 8×8 的置换表。我手头常用的 Python 实现会直接硬编码 S 盒为一个列表。
线性变换部分:
L'(B) = B ⊕ (B <<< 13) ⊕ (B <<< 23)
注意这里的循环左移位数。加解密里的 L 是 (B ⊕ (B<<<2) ⊕ (B<<<10) ⊕ (B<<<18) ⊕ (B<<<24)),密钥扩展的 L' 要简单一些。为什么?因为轮密钥不需要像数据那样强的扩散,省一点开销。
4.4 完整流程一览
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 拆分 MK 为 4 个 32 位字 | MK₀, MK₁, MK₂, MK₃ |
| 2 | 与 FK 异或 | K₀~K₃ = MK ⊕ FK |
| 3 | 对 i = 0..31 迭代 | 计算 rk_i = K_{i+4} |
| 3.1 | 合并输入 | X = K_{i+1} ⊕ K_{i+2} ⊕ K_{i+3} ⊕ CK_i |
| 3.2 | 非线性变换 | Y = S盒替换(X 的每个字节) |
| 3.3 | 线性变换 L' | Z = Y ⊕ (Y<<<13) ⊕ (Y<<<23) |
| 3.4 | 生成新 K | K_{i+4} = K_i ⊕ Z |
4.5 Python 代码示例(核心片段)
def sm4_key_expand(mk):
# FK 常量
FK = (0xA3B1BAC6, 0x56AA3350, 0x677D9197, 0xB27022DC)
# CK 预计算
CK = []
for i in range(32):
ck = 0
for j in range(4):
ck = (ck << 8) | ((4*i + j) * 7 % 256)
CK.append(ck)
K = [0] * 36
for i in range(4):
K[i] = mk[i] ^ FK[i]
rk = [0] * 32
for i in range(32):
x = K[i+1] ^ K[i+2] ^ K[i+3] ^ CK[i]
# 非线性变换:S盒替换
a = sm4_s_box(x)
# 线性变换 L'
a = a ^ rotate_left(a, 13) ^ rotate_left(a, 23)
K[i+4] = K[i] ^ a
rk[i] = K[i+4]
return rk
这个函数里 sm4_s_box 是查表替换,rotate_left 是循环左移。我通常会把这些辅助函数单独封装,方便测试和复用。
4.6 一张图看懂密钥扩展
下面我把整个流程画成了结构图。节点和箭头清晰展示了 MK 如何一步步变成 32 个轮密钥。
4.7 关于性能与实现的几点经验
讲个实际案例。我在一个嵌入式设备上移植 SM4,CPU 只有 72MHz,RAM 很紧张。当时我直接把 CK 在运行时计算,结果每次启动都要算一遍,慢得不能忍。后来改成预计算静态数组,一劳永逸。你如果做嵌入式开发,我建议把 FK、CK 和 S 盒都写在只读数据段里,省空间又提速。
另外,注意字节序。SM4 标准文档里用的是大端序,但很多参考代码(尤其是 Python 实现的)习惯用小端模拟。我曾经因为这个问题跟同事吵了一架,最后发现是双方对 MK 的输入解析方式不同。解决办法:统一在算法入口处做一次字节序转换,内部全部按标准走。
好,密钥扩展的逻辑就梳理到这儿。接下来你就能拿着这些轮密钥,去进行加解密运算了。记住,密钥扩展可以预计算,也可以在线实时算,看你的场景需求。我个人倾向预计算,一劳永逸。