3. SM4 密钥长度与密钥扩展算法归纳总结

说实话,我见过不少刚接触 SM4 的同学,上来就问“密钥是不是越长越好?”嗯,这个问题其实挺有意思。SM4 的密钥长度是固定的 128 位,不像 AES 还能选 192、256。但长度固定不代表扩展过程简单,里面有不少坑。今天我把这块彻底拆开给你讲明白。

3.1 密钥长度:128 位,不多不少

SM4 的密钥长度写在国标里:128 bits,也就是 16 字节。为什么是 128?

我在项目里用过 SM4,也对比过其他国密算法。128 位在安全性和性能之间做了个不错的平衡。你想想,现在芯片级实现 SM4,128 位密钥的硬件面积和功耗都是很成熟的。

关键点:SM4 不支持其他密钥长度。如果你看到有人传进来 192 位密钥,那肯定是错误的。我排查过几次这样的 case,都是上层把 ASCII 字符串直接当密钥用,没做哈希截断。

属性SM4AES-128
密钥长度128 bits (固定)128 bits
分组长度128 bits128 bits
轮数32 轮10 轮
密钥扩展方式32 位字轮迭代字扩展+轮常数

你看,密钥长度保持一致,但轮数多了不少。这跟扩展算法和 S 盒的设计有关。

3.2 密钥扩展算法:从 4 个字到 32 个字

SM4 的密钥扩展,说白了就是把初始的 128 位密钥变成 32 个轮密钥,每轮用 1 个 32 位字。这个过程的逻辑很强,但实现起来容易踩坑。

第一步:原始密钥与系统参数异或

初始密钥分成 4 个 32 位字:MK₀, MK₁, MK₂, MK₃。然后跟系统参数 FK 异或,得到中间变量 K₀~K₃:

K₀ = MK₀ ⊕ FK₀
K₁ = MK₁ ⊕ FK₁
K₂ = MK₂ ⊕ FK₂
K₃ = MK₃ ⊕ FK₃

这里的 FK 是固定的系统参数,国标里给了四个 32 位常量。我刚开始做的时候觉得这不就是个 XOR 吗,能有什么问题?后来发现有人在实现里把 FK 写反了顺序,导致整个轮密钥全错。

警告:FK₀ ~ FK₃ 的值必须严格按国标 BM/GM/T 0002-2012 附录。我曾经遇到一个第三方库,把所有 FK 写成了 0xFFFFFFFF,结果加解密错乱。查了两天才定位到。

第二步:32 轮迭代生成轮密钥

从 K₀~K₃ 开始,第 i 轮(i=0~31)生成轮密钥 rkᵢ:

rkᵢ = Kᵢ₊₄ = Kᵢ ⊕ T'(Kᵢ₊₁ ⊕ Kᵢ₊₂ ⊕ Kᵢ₊₃ ⊕ CKᵢ)

其中 T' 是合成置换,包含非线性变换和线性变换。非线性变换就是 4 个并行的 S 盒,每个 S 盒把 8 位输入映射成 8 位输出。线性变换是个 32 位循环左移再异或的操作:

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

这里的 CKᵢ 是轮常数,也是国标写好的 32 个固定值。我建议你直接查表,不用自己算,容易算错。

3.3 轮密钥生成全流程

为了方便你理解,我画了张流程图。记住,这个流程跟加密流程很像,但线性变换 L' 跟加密时的 L 不一样——加密用的是 L(B) = B ⊕ (B<<<2) ⊕ (B<<<10) ⊕ (B<<<18) ⊕ (B<<<24)。千万别搞混。

SM4 密钥扩展流程 初始密钥 128 bit → MK₀ MK₁ MK₂ MK₃ K₀~K₃ = MKᵢ ⊕ FKᵢ 循环 i = 0 → 31(32轮) A = Kᵢ₊₁ ⊕ Kᵢ₊₂ ⊕ Kᵢ₊₃ ⊕ CKᵢ B = T'(A) (S盒 + 移位L') rkᵢ = Kᵢ₊₄ = Kᵢ ⊕ B 输出 32 个轮密钥 rk₀, rk₁, …, rk₃₁ 每个 32 bit,用于 32 轮加密/解密

技巧:密钥扩展可以预先计算好并存起来,尤其是做 ECB/CTR 等非 CBC 模式时,每次加密只算一次扩展就够了。我在嵌入式项目里这样优化过,速度提升了 2 倍。

3.4 避坑指南:密钥扩展里常见的错误

这些年我 review 过不少 SM4 的实现,总结几个高频问题:

  1. FK 或 CK 用错:有人用加密轮数减了 1 对应的 CK,或者把 FK 和 CK 搞反。查表一定要逐字节对。
  2. S 盒查表实现未对齐:SM4 S 盒是 8→8 的映射,有些人在实现时把输入字节当成索引,但没做大小端转换。如果你的平台是小端,直接按数组下标查就行。
  3. 线性变换 L' 与 L 搞混:加密时用的是 L,密钥扩展用的是 L'。差之毫厘,谬以千里。我亲眼见过一个测试向量全是反例,最后发现是 L 用错了。
  4. 轮密钥生成顺序颠倒:加密时用 rk₀, rk₁, … rk₃₁;解密时用 rk₃₁, rk₃₀, … rk₀。不能用同一顺序。

曾经踩过的坑:有一次我在芯片里硬件实现密钥扩展,以为用加法器替代 XOR 会更高效,结果仿真全错。SM4 的 XOR 就是 XOR,不能偷懒换成加法,因为进位会破坏线性性质。查了两天波形才明白——嗯,从此老老实实用 XOR。

3.5 轮密钥的存储与实例

32 个轮密钥,每个 4 字节,一共 128 字节。常见的做法是开一个 uint32_t rk[32] 数组。如果你在 ARM Cortex-M 上做,可以放在 SRAM 里,或者在初始化时先扩展好。举例:

// SM4 密钥扩展示例(伪C代码)
void sm4_key_expand(uint8_t key[16], uint32_t rk[32]) {
    uint32_t K[36];
    // 1. 初始4个字
    for (int i = 0; i < 4; i++) {
        K[i] = (key[4*i] << 24) | (key[4*i+1] << 16) |
               (key[4*i+2] << 8) | key[4*i+3];
        K[i] ^= FK[i];          // 与系统参数异或
    }
    // 2. 32轮迭代
    for (int i = 0; i < 32; i++) {
        uint32_t B = K[i+1] ^ K[i+2] ^ K[i+3] ^ CK[i];
        B = tau(B);              // 非线性变换:4个S盒
        B = l_prime(B);          // 线性变换 L'
        K[i+4] = K[i] ^ B;
        rk[i] = K[i+4];
    }
}

注意 tau 函数内部就是 4 个 S 盒查表,l_prime 就是循环移位。你细看这段代码,会发现它其实跟加密流程里的轮函数很像,只是少了那个常数。

3.6 密钥扩展的安全性考量

我一直强调:密钥扩展本身不是随机数生成器。它只是把原始密钥的信息均匀扩散到 32 个轮密钥中。所以你原始密钥的熵有多高,最终的安全性就有多高。不要用“1234567890abcdef”这种弱密钥,SM4 一样扛不住暴力猜解。

另外,密钥扩展算法是可逆的。如果你拿到了任何一个轮密钥,理论上可以逆向推出原始密钥(虽然计算量不小)。所以在实际系统中,轮密钥也要当成敏感数据保护——别直接暴露在 JTAG 调试接口上。

个人建议:做产品时,密钥扩展和加密函数尽量放在同一个安全的内存区域,完成后马上擦除临时缓冲区。我在 IoT 固件中见过有人把扩展后的 32 个轮密钥明文写进日志,那简直是给攻击者送弹药。

3.7 小结:记住这几个数字

要素值
密钥长度128 bit (16 byte)
轮密钥总数32 个(每个 32 bit)
系统参数 FK4 个固定 32 位常量
轮常数 CK32 个固定 32 位常量
S 盒1 个 8×8 查表,重复 4 次
扩展中线性变换 L'B ⊕ (B<<<13) ⊕ (B<<<23)

好了,密钥扩展这块核心内容就到这。你如果能把上面这个流程手写一遍,基本就掌握了 SM4 密钥扩展的精髓。下回我们再聊加密轮函数,那又是另一层门道。