第四章:SM4密钥扩展算法详解
密钥扩展,说实话,是整个SM4算法里最容易被忽略,但又最关键的环节。我记得刚接触国密时,同事拿了一个FPGA实现,密钥扩展部分直接硬编码了轮密钥表——项目能跑,但一点灵活性都没有。后来我才明白,真正理解密钥扩展,才算摸到SM4的门道。
SM4的密钥只有128位,但我们需要32个32位的轮密钥。怎么从这128位里“变”出32个不一样的孩子?这就靠密钥扩展算法。说白了,它通过一组固定参数和可逆的迭代变换,把主密钥打散、混淆,生成每轮用的子密钥。
4.1 密钥扩展的“食材”:三个核心元素
做菜要先备料。密钥扩展需要三样东西:
- 主密钥 MK:128位,分成4个32位字:MK₀、MK₁、MK₂、MK₃。
- 系统参数 FK:也是4个32位固定值,用来初始化中间变量。
- 固定参数 CK:32个不同的32位常数,每轮用到一个,保证各轮不一样。
我建议把FK和CK当成算法内置的“调味包”——你不用记具体数值,但要知道它们的作用:FK让密钥初始状态和主密钥不同,CK让每轮扩展的过程差异化。
| 参数 | 长度 | 数值(十六进制示例) | 作用 |
|---|---|---|---|
| FK₀ | 32位 | A3B1BAC6 | 与主密钥异或得到初始中间变量 |
| FK₁ | 32位 | 56AA3350 | |
| FK₂ | 32位 | 677D9197 | |
| FK₃ | 32位 | B27022DC | |
| CKᵢ (i=0..31) | 32位 | 00070E15… | 每轮一个,与中间变量组合 |
小技巧:CK的生成规则其实有规律——每个字节由(4i+j)×7 mod 256计算得到,但你完全可以直接用预计算好的常量表。我在项目中写过自动生成CK的脚本,调试时发现和标准对照表一致才算放心。
4.2 扩展流程:从主密钥到32个轮密钥
整个流程可以分成两步走:
- 初始化:把MK和FK异或,得到4个中间字K₀、K₁、K₂、K₃。
- 迭代生成:对于 i = 0 到 31,用公式 Kᵢ₊₄ = Kᵢ ⊕ T'(Kᵢ₊₁ ⊕ Kᵢ₊₂ ⊕ Kᵢ₊₃ ⊕ CKᵢ),而这轮产生的 Kᵢ₊₄ 就是轮密钥 rkᵢ。
注意,T' 不是直接S盒,它先用4个S盒做非线性变换(τ),再做一个线性变换 L':循环左移13位再异或循环左移23位。我刚开始看公式时被绕晕了,后来干脆写成代码,跑一遍就知道怎么回事了。
核心公式:rkᵢ = Kᵢ₊₄ = Kᵢ ⊕ L'( τ( Kᵢ₊₁ ⊕ Kᵢ₊₂ ⊕ Kᵢ₊₃ ⊕ CKᵢ ) )
其中 τ 是把32位分成4字节,每个字节过同一个S盒,再拼回去。这个S盒和加密算法的S盒是一样的,256个元素,查表就行。线性变换 L' 的数学表达:
L'(B) = B ⊕ (B <<< 13) ⊕ (B <<< 23)
说白了就是自己做左移后和自己异或,实现扩散。
4.3 Python实现:撸一个密钥扩展函数
纸上得来终觉浅,我习惯直接写代码验证。下面是我常用的密钥扩展实现,用查表法加速:
# SM4 S盒常数(256字节)
SBOX = [
0xD6,0x90,0xE9,0xFE,0xCC,0xE1,0x3D,0xB7,0x16,0xB6,0x14,0xC2,0x28,0xFB,0x2C,0x05,
# ... 完整SBOX请参考国标文档,这里省略
]
def tau(in_val):
"""非线性变换 τ:拆4字节查S盒"""
return (SBOX[(in_val >> 24) & 0xFF] << 24) | \
(SBOX[(in_val >> 16) & 0xFF] << 16) | \
(SBOX[(in_val >> 8) & 0xFF] << 8) | \
SBOX[in_val & 0xFF]
def l_prime(in_val):
"""线性变换 L':B ⊕ (B<<<13) ⊕ (B<<<23)"""
return in_val ^ ((in_val << 13) | (in_val >> 19)) ^ ((in_val << 23) | (in_val >> 9))
def t_prime(in_val):
"""合成置换 T' = L'(τ(...))"""
return l_prime(tau(in_val))
def sm4_key_expansion(master_key):
"""主密钥128位 -> 32个32位轮密钥"""
FK = [0xA3B1BAC6, 0x56AA3350, 0x677D9197, 0xB27022DC]
CK = [...] # 32个常数,此处省略近全部
# 步骤1:初始化
MK = [(master_key >> (96 - 32*i)) & 0xFFFFFFFF for i in range(4)]
K = [MK[i] ^ FK[i] for i in range(4)]
# 步骤2:迭代生成32轮密钥
rk = []
for i in range(32):
temp = K[i+1] ^ K[i+2] ^ K[i+3] ^ CK[i]
K_next = K[i] ^ t_prime(temp)
K.append(K_next)
rk.append(K_next)
return rk[:32] # 只取32个
⚠ 我曾经犯过的错:在迭代里直接改K列表,导致后续计算用的不是正确的Kᵢ。注意K列表要不断append,而不是修改已有索引。另外,CK数组的索引要和轮数对齐,很多库默认CK从0开始,也OK。
4.4 一张图看懂密钥扩展
文字描述不如流程图画得清楚。我把整个密钥扩展的逻辑画成下面的结构图,建议大家长按保存:
4.5 为什么密钥扩展需要“T’”而不是普通S盒?
你可能会问,直接用S盒不行吗?嗯,这里有个坑。SM4的加密轮函数里用了T(含L线性变换),密钥扩展里的T'用的是L',两者线性部分不同。为什么?
我理解是为了保证可逆性和扩散性能。加密时L是左移2、10、18、24位,而L'是左移13、23位。如果混用,密钥扩展和加密的线性层会互相冲突,影响安全性。我记得某次做白盒分析时,尝试统一L和L',结果差分轨迹出现了规律性——这恰恰是设计者要避免的。
经验谈:如果你要自己实现SM4,千万别在密钥扩展里复用加密的T。老老实实按标准写T'。我把这个写进了团队的编码规范,从此少了一个bug来源。
4.6 避坑指南:密钥扩展的常见陷阱
- 轮密钥顺序:rk₀对应第一轮加密,不是最后一轮。解密时轮密钥要逆序使用。
- S盒大小写:SM4的S盒是16×16的表,但实际按一维256数组查就行。不要搞成二维,容易下标错。
- 循环左移的位数:Python里左移后或上右移,注意用
((val << shift) | (val >> (32 - shift))) & 0xFFFFFFFF保证32位。 - CK数组索引:有些参考文档CK索引从1开始,但标准里是0~31。对齐就好。
好了,密钥扩展这块核心内容就这些。你把它理解透了,后面的加密和解密其实就是直接用这些轮密钥,没什么神秘的了。