SM3算法核心原理详解
SM3的原理,说白了就是一个“不可逆的压缩吃豆人”。你把任意长度的消息扔进去,它经过四步操作,最后吐出一个固定256位的“指纹”。为什么这四步重要?因为后面我们在做密钥派生时,所有边界保护的“硬度”,都依赖这个流程的稳定性。
我习惯把这四步分成“预处理”和“迭代处理”两个阶段。下面这张图,就是整个SM3的生命周期:
这张图把四个步骤串在了一起。下面我们一步步拆开看。
1. 消息填充
你输入的原始消息,长度五花八门。SM3要求处理的块大小是512位(64字节)。不够怎么办?那就补。我见过不少新手直接往后面加0,那肯定出错。
- 先加一个"1"比特(即0x80字节)。
- 接着补"0",直到消息长度模512等于448位。
- 最后附加64位原始消息长度(大端序)。
举个例子:原始消息长度假设是440位。先加1位,变成441位。然后补7个0,凑到448位。最后粘上64位的长度值。这样总长度就是512的整数倍。
2. 消息扩展
这一步把填充后的消息块(16个字,每个字32位),扩展成132个字。你想想看,为什么要扩展?目的是为了增加“扩散性”,让输入的任何微小变化都能迅速扩散到整个压缩过程。
扩展规则分为两部分:
- W[0] ~ W[15]:直接取原始消息的16个字。
- W[16] ~ W[67]:通过异或、循环移位等操作生成,公式如下:
// W_j 生成(j从16到67)
W[j] = P1(W[j-16] ⊕ W[j-9] ⊕ (W[j-3] <<< 15))
⊕ (W[j-13] <<< 7) ⊕ W[j-6]
// 其中 P1(X) = X ⊕ (X <<< 15) ⊕ (X <<< 23)
- W'[j]:用于第二轮计算的扩展值,j从0到63。
W'[j] = W[j] ⊕ W[j+4]
嗯,这里要注意。W和W'是两套数组。W有68个字,W'有64个字。它们配合起来,保证了每轮迭代都有足够的“新鲜材料”。
3. 迭代压缩
这是SM3的核心。整个压缩过程要跑64轮。每轮用两个常数(T)和两个函数(FF、GG)来更新8个状态字。
| 轮次 | 所用常数 T | FF函数 | GG函数 |
|---|---|---|---|
| 第0~15轮 | 0x79cc4519 | FF = X ⊕ Y ⊕ Z | GG = X ⊕ Y ⊕ Z |
| 第16~63轮 | 0x7a879d8a | FF = (X & Y) | (X & Z) | (Y & Z) | GG = (X & Y) | (~X & Z) |
为什么不全程用一个函数?因为前16轮需要强非线性,后48轮需要强布尔运算。这种设计,我个人认为,很好地平衡了安全性和实现效率。
每轮压缩的伪代码大致如下:
for j = 0 to 63:
SS1 = ((A <<< 12) + E + (Tj <<< j)) <<< 7
SS2 = SS1 ⊕ (A <<< 12)
TT1 = FFj(A, B, C) + D + SS2 + W'[j]
TT2 = GGj(E, F, G) + H + SS1 + W[j]
D = C
C = B <<< 9
B = A
A = TT1
H = G
G = F <<< 19
F = E
E = P0(TT2)
// 注意:P0函数为 P0(X) = X ⊕ (X <<< 9) ⊕ (X <<< 17)
这里最关键的是变量之间的置换关系。你每次更新A和E后,其他字要跟着旋转。这个旋转量为什么是9和19?当年国标制定时,一定经过了充分的差分分析验证。
4. 输出生成
64轮迭代结束后,把当前8个状态字与上一轮的初始值(即IV或上一块的结果)做异或,得到这一块的最终状态。如果这是最后一个消息块,那这8个字拼接起来,就是最终的256位杂凑值。
举个例子,假设上一块的初始值为V0,当前块处理后的状态为V1:
V_下一块初始 = V0 ⊕ V1
如此反复,直到所有块都处理完。最后,把8个32位字按顺序输出——就是我们需要的SM3值。
好了,现在你知道了SM3的四个步骤。消息填充保证输入对齐,消息扩展提供扩散性,迭代压缩是核心安全保障,输出生成固定结果。这四个环节环环相扣,少一个都不行。等你后面看到它在密钥派生中的应用,就会明白为什么每一轮迭代都这么“斤斤计较”了。