3. SM3 常量与函数:骨架里的数字基因

聊完了 SM3 的宏观流程,咱们得钻到算法的“细胞”里看看了。说白了,哈希算法就是用一堆精心设计的常量和函数,把输入数据揉碎、打乱、再压缩。今天要讲的初始 IV、布尔函数和置换函数,就是 SM3 最底层的“起手式”和“手法”。

我个人习惯把这三样东西看作算法的“预设灵魂”、“逻辑剪刀”和“搅拌器”。它们具体干了什么?咱们一个一个来拆。

3.1 初始 IV:算法的“出厂设置”

你想想看,SM3 在处理第一个消息分组的时候,它总得有个初始状态吧?不然拿什么跟第一块数据做运算?这个初始状态,就是 初始值(IV)。它就像一台精密仪器的零点校准,所有后续的迭代都从这里起步。

SM3 的初始 IV 是 8 个 32 比特的字(共 256 比特)。这些数字不是随机选的,它们来源于自然常数的十六进制表示。我当年第一次接触密码学标准时,以为这些数是拍脑袋定的——后来才明白,用自然常数就是为了消除“设计者可能埋后门”的嫌疑。

表 3-1:SM3 初始值 IV(十六进制)
变量 值 说明
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 个寄存器初值
个人习惯: 当我用 Python 实现 SM3 时,会把 IV 定义为一个元组。这样既防止意外修改,又在函数间传递时异常清晰。

你可能会问:“这些数看起来好乱,有什么规律吗?” 嗯,它们的来历是这样的:取自然常数 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 里实现循环左移,千万别直接用 <<。原因是 Python 整数有无限精度,左移后直接变成大整数。正确的做法是:
def left_rotate(x, n): return ((x << n) | (x >> (32 - n))) & 0xFFFFFFFF

3.4 本章知识框架图

下面这张图梳理了 III、布尔函数 FF/GG、置换函数 P₀/P₁ 在 SM3 算法中的位置关系。你看,IV 是起点,布尔函数和置换函数分别在消息扩展和压缩函数里扮演不同角色——它们共同编织了一张安全的密码学网络。

SM3 常量与函数框架 SM3算法核心构件 初始值 IV (8×32比特) 布尔函数(逻辑运算) FF₀-₆₃ GG₀-₆₃ 置换函数(位扩散) P₀(压缩) P₁(扩展) 共同作用于:消息扩展 + 压缩函数迭代 注:IV 提供初始状态,逻辑函数和置换函数在每轮迭代中协同工作
要点回顾:
  1. 初始 IV 是 8 个取自自然常数的定值,每个轮次压缩前“注入”寄存器。
  2. 布尔函数 FF 和 GG 前 16 轮都用异或,后 48 轮分别切换为“多数投票”和“条件选择”。
  3. 置换函数 P₀(压缩用)和 P₁(扩展用)通过不同距离的循环左移实现比特扩散。
  4. 这些常量和函数共同保证了 SM3 的“雪崩效应”和抗碰撞能力。

嗯,常量和函数是枯燥的,但它们构成了整座大厦最坚实的砖石。下回你看到 SM3 的代码,可以试着在脑海中对号入座:这一步是 FF 在剪切,那一步是 P₀ 在搅拌。理解了这个层面,你才真正开始“看到”算法的运作,而不是被它吓住。