3. SM3 常量与函数:骨架里的数字基因
聊完了 SM3 的宏观流程,咱们得钻到算法的“细胞”里看看了。说白了,哈希算法就是用一堆精心设计的常量和函数,把输入数据揉碎、打乱、再压缩。今天要讲的初始 IV、布尔函数和置换函数,就是 SM3 最底层的“起手式”和“手法”。
我个人习惯把这三样东西看作算法的“预设灵魂”、“逻辑剪刀”和“搅拌器”。它们具体干了什么?咱们一个一个来拆。
3.1 初始 IV:算法的“出厂设置”
你想想看,SM3 在处理第一个消息分组的时候,它总得有个初始状态吧?不然拿什么跟第一块数据做运算?这个初始状态,就是 初始值(IV)。它就像一台精密仪器的零点校准,所有后续的迭代都从这里起步。
SM3 的初始 IV 是 8 个 32 比特的字(共 256 比特)。这些数字不是随机选的,它们来源于自然常数的十六进制表示。我当年第一次接触密码学标准时,以为这些数是拍脑袋定的——后来才明白,用自然常数就是为了消除“设计者可能埋后门”的嫌疑。
| 变量 | 值 | 说明 |
|---|---|---|
| V₀(0) | 0x7380166F | 第 1 个寄存器初值 |
| V₁(1) | 0x4914B2B9 | 第 2 个寄存器初值 |
| V₂(2) | 0x172442D7 | 第 3 个寄存器初值 |
| V₃(3) | 0xDA8A0600 | 第 4 个寄存器初值 |
| V₄(4) | 0xA96F30BC | 第 5 个寄存器初值 |
| V₅(5) | 0x163138AA | 第 6 个寄存器初值 |
| V₆(6) | 0xE38DEE4D | 第 7 个寄存器初值 |
| V₇(7) | 0xB0FB0E4E | 第 8 个寄存器初值 |
你可能会问:“这些数看起来好乱,有什么规律吗?” 嗯,它们的来历是这样的:取自然常数 e 的平方根,再取小数部分的十六进制表示。比如 0x7380166F,就是从 e 的平方根里“抠”出来的。这个设计哲学,让 IV 脱离了人为控制的嫌疑。
3.2 布尔函数:SM3 里的“逻辑剪刀手”
算法在迭代压缩时,要根据当前处在哪个轮次,选择不同的运算规则。SM3 总共有 64 轮压缩,前 16 轮和后 48 轮分别使用两组不同的布尔函数。
说白了,布尔函数就是根据输入比特,决定输出 0 还是 1 的一套规则。 SM3 用了两个核心布尔函数:FF 和 GG。每个函数又根据轮次拆成两个版本。
3.2.1 函数 FF (用于变量 A 的更新)
- 当 0 ≤ j ≤ 15(前 16 轮):
FFj(X, Y, Z) = X ⊕ Y ⊕ Z - 当 16 ≤ j ≤ 63(后 48 轮):
FFj(X, Y, Z) = (X ∧ Y) ∨ (X ∧ Z) ∨ (Y ∧ Z)
前 16 轮的异或操作很简单,就是三路取反。后 48 轮的“多数投票函数”更有意思——如果 X、Y、Z 里 1 多,结果就输出 1,否则输出 0。这在硬件实现上非常高效。
FF 的场景,导致哈希校验失败了一整天。排查时发现,前 16 轮我用成了多数投票函数。记住:前 16 轮只管异或,别想复杂了。
3.2.2 函数 GG (用于变量 E 的更新)
- 当 0 ≤ j ≤ 15:
GGj(X, Y, Z) = X ⊕ Y ⊕ Z - 当 16 ≤ j ≤ 63:
GGj(X, Y, Z) = (X ∧ Y) ∨ (¬X ∧ Z)
看到了吗?前 16 轮 FF 和 GG 居然一模一样,都是异或。后 48 轮就不一样了:FF 是多数投票,GG 是条件选择(如果 X 为真选 Y,否则选 Z)。
这里要注意: ¬X 是对 X 按位取反。SM3 标准里没有直接用符号“~”,而是写成了“1 ⊕ X”。效果是一样的,但对新手容易造成困惑。我建议你在实现时直接写 ~X & 0xFFFFFFFF,只取低 32 位,免得 Python 无符号整数溢出。
3.3 置换函数:消息的“搅拌器”
布尔函数负责逻辑处理,但光有逻辑不够,还需要让位与位之间充分“交叉感染”。这个任务就交给了 置换函数。SM3 里有两个置换函数,一个用在消息扩展,一个用在压缩函数内部。
3.3.1 函数 P₀ (压缩函数中使用)
P₀ 负责把压缩函数的中间结果打散。它的定义是:
P₀(X) = X ⊕ (X <<< 9) ⊕ (X <<< 17)
其中 <<< 表示循环左移。这个操作很妙——它把原本在比特 0 的信息,通过两次不同距离的移位,扩散到多个位置上。我在项目中遇到过,一个输入的微小变化(比如改了一比特),经过 P₀ 后,输出中大约一半的比特都会翻转。这正是哈希算法想要的“雪崩效应”。
3.3.2 函数 P₁ (消息扩展中使用)
P₁ 用在消息扩展阶段,负责从已有的 16 个字生成第 17 到第 68 个字。它的定义是:
P₁(X) = X ⊕ (X <<< 15) ⊕ (X <<< 23)
对比一下 P₀ 用的 9 和 17 位移,P₁ 用的是 15 和 23 位。为什么选这几个数字?这是经过严格数学证明的——它们能让消息扩展的线性复杂度达到最优,从而抵抗差分攻击。
<<。原因是 Python 整数有无限精度,左移后直接变成大整数。正确的做法是:def left_rotate(x, n): return ((x << n) | (x >> (32 - n))) & 0xFFFFFFFF
3.4 本章知识框架图
下面这张图梳理了 III、布尔函数 FF/GG、置换函数 P₀/P₁ 在 SM3 算法中的位置关系。你看,IV 是起点,布尔函数和置换函数分别在消息扩展和压缩函数里扮演不同角色——它们共同编织了一张安全的密码学网络。
- 初始 IV 是 8 个取自自然常数的定值,每个轮次压缩前“注入”寄存器。
- 布尔函数 FF 和 GG 前 16 轮都用异或,后 48 轮分别切换为“多数投票”和“条件选择”。
- 置换函数 P₀(压缩用)和 P₁(扩展用)通过不同距离的循环左移实现比特扩散。
- 这些常量和函数共同保证了 SM3 的“雪崩效应”和抗碰撞能力。
嗯,常量和函数是枯燥的,但它们构成了整座大厦最坚实的砖石。下回你看到 SM3 的代码,可以试着在脑海中对号入座:这一步是 FF 在剪切,那一步是 P₀ 在搅拌。理解了这个层面,你才真正开始“看到”算法的运作,而不是被它吓住。