SM4 密钥扩展——32 轮子密钥生成算法详解

说实话,很多刚接触 SM4 的朋友觉得密钥扩展就是算几个常数,没啥好研究的。但我在做性能巡检的时候发现,密钥扩展这块反而是 bug 高发区。

一次内部安全审计,同事的 SM4 实现加密结果总跟标准对不上。查了两天才发现——他把 FK 和 CK 的值搞反了。这种问题在单元测试阶段极难暴露,因为密文看起来 “像那么回事”,只有跟标准向量一比对才露馅。

所以,想做好巡检,你得把密钥扩展的逻辑刻在脑子里。下面我带你逐个嚼碎。

密钥扩展到底在做什么?

SM4 的密钥长度是 128 位,但加密一轮需要 32 位子密钥。你想想看,一轮用 32 位,32 轮就用 1024 位。那原来的 128 位怎么变出 1024 位?靠的就是密钥扩展算法。

核心流程很简单:

  • 把 128 位主密钥 MK 拆成 4 个 32 位字
  • 跟系统参数 FK 做异或,得到初始中间字 K0~K3
  • 然后循环迭代 32 次,每次生成一个子密钥 rk_i

嗯,听着跟吃大碗面一样顺溜。但细节里藏着不少坑。

重要概念:密钥扩展不是加密过程的“可选项”,它是加密和解密共用的。解密时只需要把 rk 的顺序反过来就行。所以巡检时如果发现加解密结果不一致,千万别忘了检查子密钥顺序。

系统参数 FK 与常量 CK

SM4 在密钥扩展里预定义了 4 个 FK 和 32 个 CK。这些值都是国家标准里写死的,你不能改,也不需要记,但实现时一定要跟标准对。

参数类别个数作用生成方式
FK4与 MK 异或,初始化 K 数组固定常量
CK32每轮参与非线性变换与移位通过固定初值 + 线性移位

FK 的 4 个值我记得很清楚,因为它们有规律:

FK0 = 0xA3B1BAC6
FK1 = 0x56AA3350
FK2 = 0x677D9197
FK3 = 0xB27022DC

CK 的生成反而最容易被忽略。我做过一个项目,直接手写 32 个常量时漏掉了 CK24,导致最后一轮子密钥全是错的。所以个人习惯:要么用公式生成,要么用标准代码库拷过来,别手输。

CK 生成公式是这样的:

CK_initial = [0x00070E15, 0x1C232A31, 0x383F464D, 0x545B6269,
             0x70777E85, 0x8C939AA1, 0xA8AFB6BD, 0xC4CBD2D9,
             0xE0E7EEF5, 0xFC030A11, 0x181F262D, 0x343B4249,
             0x50575E65, 0x6C737A81, 0x888F969D, 0xA4ABB2B9,
             0xC0C7CED5, 0xDCE3EAF1, 0xF8FF060D, 0x141B2229,
             0x30373E45, 0x4C535A61, 0x686F767D, 0x848B9299,
             0xA0A7AEB5, 0xBCC3CAD1, 0xD8DFE6ED, 0xF4FB0209,
             0x10171E25, 0x2C333A41, 0x484F565D, 0x646B7279]
我的小技巧:实现时将 CK 数组和 FK 数组统一放在一个只读段里,编译时就能检查越界。巡检时直接比对标准向量,比人眼快得多。

迭代生成 32 轮子密钥

有了 K0~K3 和 CK 数组,剩下的就是循环 32 次。我把公式写在一起,方便你对照代码看。

// 伪代码
uint32_t K[4] = {MK[0]^FK[0], MK[1]^FK[1], MK[2]^FK[2], MK[3]^FK[3]};
for (i=0; i < 32; i++) {
    uint32_t temp = K[1] ^ K[2] ^ K[3] ^ CK[i];
    // 非线性变换 τ
    uint8_t b[4];
    b[0] = Sbox[(temp >> 24) & 0xFF];
    b[1] = Sbox[(temp >> 16) & 0xFF];
    b[2] = Sbox[(temp >> 8)  & 0xFF];
    b[3] = Sbox[(temp)       & 0xFF];
    // 线性变换 L'
    uint32_t B = (b[0]<<24) | (b[1]<<16) | (b[2]<<8) | b[3];
    B = B ^ (B << 13) ^ (B << 23);
    // 生成子密钥
    rk[i] = K[0] ^ B;
    // 更新 K 数组,为下一轮做准备
    K[0] = K[1]; K[1] = K[2]; K[2] = K[3]; K[3] = rk[i];
}

看到没?它的结构和 SM4 加密轮函数几乎一模一样,只是线性变换 L' 用的是循环左移 13 和 23 位,不是加密轮里的 2、10、18、24。我刚开始学的时候总把这两个 L 搞混,分别写成了加密轮里的 L。结果加解密双双出错——教训惨痛。

避坑指南:我曾经在一个产品中因为密钥扩展的 L' 算错了,导致同一明文在不同设备上的加密结果不同。排查时发现两台设备用的 SM4 实现版本不一样,一台正确,一台把左移位数写成了 (13, 23) 但顺序反了。这种 bug 在性能测试中很难发现,因为它会产生正常的密文,只是跟标准不兼容。

子密钥 rk 的生成顺序必须从 i=0 到 i=31,不能颠倒。解密时直接把数组 rk 倒序使用即可。但你要是把 rk 生成的顺序反过来,那就全盘皆输。

一个实用的巡检清单

我把平时巡检密钥扩展时的检查点整理成一个列表,你可以在代码 review 或集成测试时逐条核对。

  1. FK0~FK3 是否与标准一致?尤其注意数字 0xA3B1BAC6 有没有写错十六进制位。
  2. CK 数组是否完整 32 个?使用公式生成时,循环左移次数别算错。
  3. K 数组初始化时的异或顺序:MK[0]^FK[0],不是反过来。
  4. Sbox 盒是否与加密轮共用?密钥扩展和加密轮用的是同一个 Sbox,别另写一套。
  5. 线性变换 L' 的左移位数:13 和 23,注意方向是循环左移(C 语义中的左移后补右边低位),有的语言用 _rotl。
  6. K 数组更新时,新旧顺序别搞错:旧 K[0] 被丢弃,新 K[3] = 当前的 rk。
  7. 解密子密钥顺序:实现解密时要把 rk 数组逆序传给解密轮函数。

你要是把这些点都盯住了,密钥扩展基本不会出问题。

密钥扩展流程一目了然

下面我画了一张流程图,把从主密钥输入到 32 轮子密钥输出的完整路径展示出来。你对照着看,每一步都很清晰。

SM4 密钥扩展流程 128位主密钥 MK FK0~FK3(系统参数) CK0~CK31(常量) K0~K3 = MK[i] XOR FK[i] 循环 i = 0 → 31 temp = K1^K2^K3^CK[i] S盒变换 + L' (循环左移13,23) rk[i] = K0 ^ B 输出 rk[0]~rk[31]

这张图把密钥扩展的四个阶段画清楚了:

  • 输入阶段:128位 MK 进入。
  • 初始化阶段:与 FK 异或产生 K0~K3。
  • 迭代阶段:每轮用 CK[i] 做异或,S盒非线性变换,再套 L' 线性变换,最后与 K0 异或得到 rk[i]。
  • 输出阶段:32 轮结束后得到完整的子密钥数组。
我的建议:写代码时把 CK 和 FK 的声明放在密钥扩展函数的最顶部,并且用 #ifdef SM4_DEBUG 打印每个 rk[i] 的值,跟官方测试向量对比。我在第一次实现时就靠这个 debug 开关快速定位到了 L' 左移位数的 bug。

好了,这一块内容你掌握了吗?说实话,密钥扩展本身不复杂,但容易在实现细节上摔跟头。巡检时盯着那 7 个检查点,加上 Sbox 和 L' 两个“坑”,基本就能把隐患扼杀在开发阶段。下次你遇到 SM4 加解密结果不一致的问题,第一个去查密钥扩展——八成是这的问题。