密钥生成过程详解:从一颗“种子”到全家桶
密钥生成不是凭空变出几串数字。它有一套固定的步骤,像做菜一样,先备料、再下锅、最后装盘。我之前带团队做过一次SM4的硬件加速模块,当时最坑的就是密钥扩展算错了一轮,结果所有加解密都乱套了。所以这节课,咱们把每个步骤拆开揉碎了讲。
一个核心概念:轮密钥为什么需要32个?
SM4加密迭代32轮,每轮用不同的密钥。密钥扩展就是把这个“初始密钥”变成32个轮密钥。说白了,就是把128bit的种子,变出32份32bit的“调料包”。
我刚开始接触SM4时,觉得32轮没必要。后来做安全分析才发现,每轮独立密钥能抗住差分攻击。这个设计很巧妙。
密钥生成三步走
整个过程分三步:初始化、迭代扩展、后处理。咱们一步步来。
第一步:系统参数和固定参数预置
SM4标准里有两个常量数组:FK(系统参数)和CK(固定参数)。FK有4个32bit值,CK有32个32bit值。
| 参数 | 长度 | 来源 | 用途 |
|---|---|---|---|
| FK | 4个32bit | 标准固定值 | 初始密钥白化 |
| CK | 32个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,像流水线一样往前推。我把这个过程比作“蜈蚣走路”——每走一步,后面一条腿往前挪,循环往复。
整张图:密钥扩展流程
下面的SVG图总结了整个流程。注意看:FK白化在起点,CK每轮输入一个,最终输出32个轮密钥。
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... | 0xF12186F9 | 0x41662B61 | 0x9124A012 |
你可以用这个做单元测试。我个人习惯把这组向量写在最前面,每次修改代码就跑一遍,避免回归问题。
小结一下
密钥生成就是个“白化 + 32轮迭代”的过程。记住三个关键点:
- FK做初始白化,防止坏密钥
- CK每轮一个,保证轮密钥不重复
- S盒+L变换提供非线性扩散
你看,是不是挺有逻辑的?当初我花了一整天把C实现调通,发现卡在S盒查表顺序上——标准里S盒是16×16的二维数组,我用了行主序,但标准是列主序。把这个记住,你至少能省半天调试时间。