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
嗯,听着跟吃大碗面一样顺溜。但细节里藏着不少坑。
系统参数 FK 与常量 CK
SM4 在密钥扩展里预定义了 4 个 FK 和 32 个 CK。这些值都是国家标准里写死的,你不能改,也不需要记,但实现时一定要跟标准对。
| 参数类别 | 个数 | 作用 | 生成方式 |
|---|---|---|---|
| FK | 4 | 与 MK 异或,初始化 K 数组 | 固定常量 |
| CK | 32 | 每轮参与非线性变换与移位 | 通过固定初值 + 线性移位 |
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]
迭代生成 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。结果加解密双双出错——教训惨痛。
子密钥 rk 的生成顺序必须从 i=0 到 i=31,不能颠倒。解密时直接把数组 rk 倒序使用即可。但你要是把 rk 生成的顺序反过来,那就全盘皆输。
一个实用的巡检清单
我把平时巡检密钥扩展时的检查点整理成一个列表,你可以在代码 review 或集成测试时逐条核对。
- FK0~FK3 是否与标准一致?尤其注意数字 0xA3B1BAC6 有没有写错十六进制位。
- CK 数组是否完整 32 个?使用公式生成时,循环左移次数别算错。
- K 数组初始化时的异或顺序:
MK[0]^FK[0],不是反过来。 - Sbox 盒是否与加密轮共用?密钥扩展和加密轮用的是同一个 Sbox,别另写一套。
- 线性变换 L' 的左移位数:13 和 23,注意方向是循环左移(C 语义中的左移后补右边低位),有的语言用
_rotl。 - K 数组更新时,新旧顺序别搞错:旧 K[0] 被丢弃,新 K[3] = 当前的 rk。
- 解密子密钥顺序:实现解密时要把 rk 数组逆序传给解密轮函数。
你要是把这些点都盯住了,密钥扩展基本不会出问题。
密钥扩展流程一目了然
下面我画了一张流程图,把从主密钥输入到 32 轮子密钥输出的完整路径展示出来。你对照着看,每一步都很清晰。
这张图把密钥扩展的四个阶段画清楚了:
- 输入阶段:128位 MK 进入。
- 初始化阶段:与 FK 异或产生 K0~K3。
- 迭代阶段:每轮用 CK[i] 做异或,S盒非线性变换,再套 L' 线性变换,最后与 K0 异或得到 rk[i]。
- 输出阶段:32 轮结束后得到完整的子密钥数组。
#ifdef SM4_DEBUG 打印每个 rk[i] 的值,跟官方测试向量对比。我在第一次实现时就靠这个 debug 开关快速定位到了 L' 左移位数的 bug。
好了,这一块内容你掌握了吗?说实话,密钥扩展本身不复杂,但容易在实现细节上摔跟头。巡检时盯着那 7 个检查点,加上 Sbox 和 L' 两个“坑”,基本就能把隐患扼杀在开发阶段。下次你遇到 SM4 加解密结果不一致的问题,第一个去查密钥扩展——八成是这的问题。