SM3算法的数学基础:布尔函数、置换与模运算

聊SM3之前,我建议大家先别急着看代码。先把数学底子打扎实了,后面看算法流程就像看地图一样清楚。说白了,SM3的核心就是三种数学工具的组合拳:布尔函数、置换操作和模运算。你想想看,如果没有这些基础,直接去看那64轮压缩函数,很容易懵。

我在项目中遇到过一个兄弟,直接拿SM3的参考实现改吧改吧就用。结果测向量的时候死活不对。查了两天,最后发现是布尔函数里一个位运算的优先级搞错了。所以,嗯,我们先从最底层的砖块开始垒。

布尔函数:SM3的非线性核心

SM3用了两个布尔函数——FF_j和GG_j。它们负责把三个32位的字揉在一起,输出一个新的32位字。

这两个函数分阶段表现不同。我把它们的真值表逻辑整理了一下:

轮次范围 函数 逻辑表达式 别名
0 ≤ j ≤ 15 FF_j(X,Y,Z) X ⊕ Y ⊕ Z 异或函数
16 ≤ j ≤ 63 FF_j(X,Y,Z) (X ∧ Y) ∨ (X ∧ Z) ∨ (Y ∧ Z) 多数函数
0 ≤ j ≤ 15 GG_j(X,Y,Z) X ⊕ Y ⊕ Z 异或函数
16 ≤ j ≤ 63 GG_j(X,Y,Z) (X ∧ Y) ∨ (¬X ∧ Z) 选择函数

注意看,在前16轮里,FF和GG其实是一样的,都是简单的异或。但到了后48轮,它们就开始分岔了。我个人的理解是——前16轮做快速扩散,后48轮做非线性混淆。这个设计思路在SHA-2里也能看到影子,但SM3的布尔函数选得更讲究。

核心要点:多数函数 (X∧Y)∨(X∧Z)∨(Y∧Z) 的输出始终与多数输入一致,具备良好的雪崩效应。选择函数 (X∧Y)∨(¬X∧Z) 则是根据X的位值,在Y和Z之间做选择。这两个函数都是非线性的,这是SM3抗密码分析的关键。

置换函数:消息扩散的关键

光有布尔函数还不够。SM3还设计了两个置换函数——P_0和P_1。它们的作用就是把输入字的位顺序打乱重排。说白了,就是让1个比特的变化能影响到更多的比特。

P_0用在消息扩展部分:

P_0(X) = X ⊕ (X <<< 9) ⊕ (X <<< 17)

P_1用在压缩函数部分:

P_1(X) = X ⊕ (X <<< 15) ⊕ (X <<< 23)

这两个函数都用了3次异或和两次循环移位。你可能会问,为什么选9和17、15和23这些数字?我个人觉得,因为它们在32位字长内互为补数——9+23=32,17+15=32。这样设计可以保证每个比特经过扩散后,在输出中覆盖尽可能多的位置。

我的一点经验:在硬件实现里,循环移位是免费的——只是重新连线,不需要任何逻辑门。所以SM3选这两个置换函数,对硬件实现非常友好。如果你在做FPGA或ASIC实现,这一点会很爽。

模运算:32位字中的加法

SM3里的加法都是模232的。什么意思?就是32位无符号整数加法,溢出直接丢掉。这在密码算法里太常见了——SHA-256也用同样的套路。

我拿个实际场景举例。假设两个32位字相加:

A   = 0xFFFFFFFF
B   = 0x00000001
A + B = 0x00000000  (丢掉进位)

这个溢出特性在SM3的压缩函数里被用得淋漓尽致。每一轮迭代都有两次模加操作:

  • SS1 = ((A <<< 12) + E + (T_j <<< j)) <<< 7
  • SS2 = SS1 ⊕ (A <<< 12)

这里的+就是模232加法。T_j是常量,前16轮用0x79CC4519,后48轮用0x7A879D8A。我个人习惯是记下这两个数字的十六进制形式,调试向量的时候经常要用到。

避坑指南:我曾经因为模加法的溢出处理翻过车。在C语言里,unsigned int的溢出是定义良好的——高位截断。但在Python或者大整数环境下,你必须手动做& 0xFFFFFFFF模操作。忘了这一步,结果全错。

三者如何协同工作

理解了这三个基础工具,SM3的完整迭代图景就清晰了。我画了一张结构图,展示消息如何通过这三层加工变成杂凑值:

SM3数学基础与迭代结构 SM3 数学基础与迭代结构图 消息块 (512 bit) 消息扩展 W₀~W₆₇, W'₀~W'₆₃ 使用 P₁(X) = X ⊕ (X<<<15) ⊕ (X<<<23) 64轮压缩函数 布尔函数: FFⱼ, GGⱼ 置换函数: P₀(X) 模 2³² 加法 (+) 杂凑值 (256 bit) 每轮迭代8个寄存器 A~H 共64轮,分两阶段

从这张图能看得很清楚:消息进来先走消息扩展——这里用到P_1置换;然后进入64轮迭代——每轮里FF/GG做布尔运算,P_0做置换,模加做累加;最后出256位杂凑值。

总结一下这三者的关系

我在项目里喜欢用一个比喻:

  • 布尔函数是「厨师」——决定食材怎么搭配
  • 置换函数是「搅拌器」——把味道均匀扩散
  • 模运算是「高压锅」——强制在32位空间里反应

缺了哪一个,SM3的安全性都会大打折扣。我建议你把这三个基础吃透了再去啃后续的压缩函数和消息扩展——它们本质上就是这三个工具的不同组合调用。

一个实操小技巧:写代码的时候,把布尔函数、置换函数、模加操作分别封装成独立的函数或宏。这样不仅好调试,将来做优化(比如查表法、位切片法)也方便。我在做SM3性能优化时就靠这个重构省了大量时间。