第四章: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个轮密钥

整个流程可以分成两步走:

  1. 初始化:把MK和FK异或,得到4个中间字K₀、K₁、K₂、K₃。
  2. 迭代生成:对于 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 一张图看懂密钥扩展

文字描述不如流程图画得清楚。我把整个密钥扩展的逻辑画成下面的结构图,建议大家长按保存:

SM4密钥扩展流程 主密钥 MK 系统参数 FK ⊕ K₀,K₁,K₂,K₃ 迭代 i = 0..31 X = Kᵢ₊₁⊕Kᵢ₊₂⊕Kᵢ₊₃⊕CKᵢ Y = T'(X) = L'(τ(X)) Kᵢ₊₄ = Kᵢ ⊕ Y CKᵢ 常数 32个轮密钥 rk₀..rk₃₁ 💡 每次迭代产出1个轮密钥 Kᵢ₊₄ 就是本轮轮密钥,同时参与后续迭代

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。对齐就好。

好了,密钥扩展这块核心内容就这些。你把它理解透了,后面的加密和解密其实就是直接用这些轮密钥,没什么神秘的了。