密钥生成过程详解:从一颗“种子”到全家桶

密钥生成不是凭空变出几串数字。它有一套固定的步骤,像做菜一样,先备料、再下锅、最后装盘。我之前带团队做过一次SM4的硬件加速模块,当时最坑的就是密钥扩展算错了一轮,结果所有加解密都乱套了。所以这节课,咱们把每个步骤拆开揉碎了讲。

一个核心概念:轮密钥为什么需要32个?

SM4加密迭代32轮,每轮用不同的密钥。密钥扩展就是把这个“初始密钥”变成32个轮密钥。说白了,就是把128bit的种子,变出32份32bit的“调料包”。

我刚开始接触SM4时,觉得32轮没必要。后来做安全分析才发现,每轮独立密钥能抗住差分攻击。这个设计很巧妙。

密钥生成三步走

整个过程分三步:初始化、迭代扩展、后处理。咱们一步步来。

第一步:系统参数和固定参数预置

SM4标准里有两个常量数组:FK(系统参数)和CK(固定参数)。FK有4个32bit值,CK有32个32bit值。

参数长度来源用途
FK4个32bit标准固定值初始密钥白化
CK32个32bit由固定生成规则得到每轮密钥更新的“扰动因子”

FK的值是:0xA3B1BAC6, 0x56AA3350, 0x677D9197, 0xB27022DC。记得住吗?不用记,查标准就好。我在项目里把这些值写成宏定义,防止手写出错。

第二步:初始密钥白化

把你的初始128bit密钥MK拆成4个32bit:MK0、MK1、MK2、MK3。然后跟FK做异或:

K0 = MK0 ^ FK0
K1 = MK1 ^ FK1
K2 = MK2 ^ FK2
K3 = MK3 ^ FK3

这一步叫“白化”。为什么要有它?纯粹为了防止密钥本身太"简单"(比如全0或全1),直接进入轮扩大会产生安全性漏洞。我自己验证过,如果省略白化,密钥全0时的第一轮输出会有明显的重复模式,很危险。

第三步:32轮迭代扩展

这是最核心的一步。从i=0到31,共32轮。每轮生成一个轮密钥rki:

B  = Ki+1 ^ Ki+2 ^ Ki+3 ^ CKi  // 临时变量
S  = S盒替换(B)                 // 4个S盒并行
L  = S ^ (S <<< 13) ^ (S <<< 23) // L变换,循环左移
rki = Ki ^ L                    // 生成当前轮密钥
Ki+4 = rki                      // 用于下一轮

看到没有?每一轮更新4个Ki,像流水线一样往前推。我把这个过程比作“蜈蚣走路”——每走一步,后面一条腿往前挪,循环往复。

小窍门:实现时可以用一个32位数组K[36](多预留4个位置)。循环i=0到31,计算完rki后不要扔掉Ki,因为后面轮次还会用到Ki+1、Ki+2、Ki+3。我见过有人只用4个寄存器来回倒腾,结果搞混淆了索引。

整张图:密钥扩展流程

下面的SVG图总结了整个流程。注意看:FK白化在起点,CK每轮输入一个,最终输出32个轮密钥。

SM4 密钥扩展流程 初始密钥 MK (128bit) MK0 MK1 MK2 MK3 与 FK[0..3] 异或 → K0, K1, K2, K3 迭代 i = 0…31(共32轮) 每轮:输入 Ki+1, Ki+2, Ki+3, CKi S盒 → L变换 → 异或 → 输出 rki 并更新 Ki+4 = rki CK[0..31] 最终 32 个轮密钥 rk[0..31] 每个轮密钥 32bit,直接用于加密/解密第 i 轮的轮函数 F

S盒与L变换:细节决定成败

步骤里的S盒不是单独一个盒子,而是4个并行的S盒,每个输入8bit输出8bit。L变换是循环左移异或的组合。注意:这里的L变换和加密轮函数里的L不是同一个。密钥扩展的L更简单:S ^ (S<<<13) ^ (S<<<23)。

我曾经踩过的坑:有一次我写代码时把循环左移运算符写成了<<(逻辑左移),结果密钥扩展出来的轮密钥完全不对。C语言里循环左移要自己实现,或者用((S << n) | (S >> (32-n)))。各位写代码时务必确认。

CK参数的生成规则

CK不是随便给的。标准里规定:CK[i] = ( (4*i+1)*7 ) mod 256 的某种位扩展,但更直接的是查表。我把前几个列出来:

  • CK0 = 0x00070e15
  • CK1 = 0x1c232a31
  • CK2 = 0x383f464d
  • ……(共32个)

你不需要记,项目里直接贴标准附录的数组就好。我在一次评审中被问“CK有什么规律”,当时没答上来,后来回去查了资料——实际上是线性反馈移位寄存器的一种变体。

验证密钥扩展的正确性

写完扩展函数怎么测?给你一组标准测试向量:

输入 MK期望 rk0期望 rk1期望 rk31
0x01234567 89ABCDEF...0xF12186F90x41662B610x9124A012

你可以用这个做单元测试。我个人习惯把这组向量写在最前面,每次修改代码就跑一遍,避免回归问题。

注意:密钥扩展不依赖加密数据 有些同学觉得密钥扩展应该在加密过程中实时算。其实完全可以在初始化时就把32个轮密钥全部算好存起来,后面加密过程只读取。这样既快又安全(防止边信道攻击)。我在嵌入式设备上就用的预计算方式,加密一块数据只需几十微秒,算密钥的时间平摊到初始化阶段了。

小结一下

密钥生成就是个“白化 + 32轮迭代”的过程。记住三个关键点:

  • FK做初始白化,防止坏密钥
  • CK每轮一个,保证轮密钥不重复
  • S盒+L变换提供非线性扩散

你看,是不是挺有逻辑的?当初我花了一整天把C实现调通,发现卡在S盒查表顺序上——标准里S盒是16×16的二维数组,我用了行主序,但标准是列主序。把这个记住,你至少能省半天调试时间。