3. SM3算法原理(中):迭代压缩结构、64轮压缩函数细节

各位好,我是这次课程的主讲。上一章我们聊了SM3的“骨架”——消息填充与初始值。今天咱们深入“内脏”,看看它最关键的运算部分:迭代压缩结构和背后的64轮压缩函数。

说实话,我刚接触SM3时,最头疼的就是这一块。满屏的位运算符号、布尔函数、移位循环,看得人眼花。但后来我在做产品集成时发现,不理解迭代压缩的流程,你就不知道怎么更新IV;不理解64轮函数,你就没法做软件层面的优化。

别急,咱们一步步拆解。

3.1 迭代压缩结构

先看整体结构。SM3把一个填充好的消息,按512位分成一组一组。每组进来,都要经过一次“迭代压缩”。

SM3迭代压缩结构示意图 消息分组 M_i (512 bit) 初始值/上一轮 V_i (256 bit) 消息扩展 生成 W_j 64轮压缩函数 CF (V_i , M_i) 迭代输出 V_{i+1} (256 bit) 共 k 个消息分组,依次迭代:V_0 → V_1 → V_2 → … → V_k。 最终 V_k 就是消息的杂凑值。

从图里你能看到:

  • 输入:当前消息分组 Mi(512位)和上一轮的256位结果 Vi。
  • 过程:先做“消息扩展”,把16个字扩展成132个字。然后送入64轮压缩函数CF。
  • 输出:新的256位 Vi+1。

小提示:这里的“初始值”就是上一章讲的8个32位字——A, B, C, D, E, F, G, H。我的经验是,每次调试SM3时,一定要打印出第一轮压缩前后的A到H值,光是看这个就能发现80%的Bug。

3.2 消息扩展

消息扩展,说白了就是把16个“词语”变成132个。

具体规则:

  1. 将 Mi 划分为16个32位字,记为 W0, W1, ..., W15。
  2. 对于 j = 16 到 67:
    W[j] = P1( W[j-16] ⊕ W[j-9] ⊕ (W[j-3] « 15) ) ⊕ (W[j-13] « 7) ⊕ W[j-6]
  3. 对于 j = 0 到 63:
    W'[j] = W[j] ⊕ W[j+4]

这里 P1 是一个32位置换函数:

P1(X) = X ⊕ (X « 15) ⊕ (X « 23)

说实话,我第一次看这个公式也晕。但你看,它其实只是用了移位和异或。在嵌入式设备上实现时,我建议你直接把这132个字算好存下来,64轮压缩时直接取用,别现算。

3.3 64轮压缩函数细节

好,真正的“心脏”来了:64轮压缩函数CF。我们来看看每一轮到底在干什么。

每一轮,我们拿当前8个32位字(A~H)和两个扩展字 Wj、W'j,进行一系列运算,然后更新A~H。

核心公式(以第 j 轮为例):

SS1 = ((A « 12) + E + (Tj « (j mod 32))) « 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)

别看字符多,它其实只用了“加法(mod 2³²)”、“循环左移”、“异或”、“布尔函数”这四种运算。

3.3.1 轮常数 Tj 与布尔函数

轮次范围 Tj 值 布尔函数 FFj 布尔函数 GGj
j = 0 ~ 15 0x79CC4519 FFj = A ⊕ B ⊕ C GGj = E ⊕ F ⊕ G
j = 16 ~ 63 0x7A879D8A FFj = (A ∧ B) ∨ (A ∧ C) ∨ (B ∧ C) GGj = (E ∧ F) ∨ (¬E ∧ G)

这里有个我自己踩过的坑。有一次做跨平台移植,Tj用的是0x79CC4519,但我忘了注意大小端问题,结果在ARM上跑出来全是错的。记住:Tj是32位常量,直接按字赋值就好,别做多余处理。

3.3.2 关键置换函数P0

它用于压缩函数内部,更新E值:

P0(X) = X ⊕ (X « 9) ⊕ (X « 17)

这个函数的作用是扩散。它把输入字的位信息快速扩散到整个字里。我个人理解,它就像是一个“搅动器”,让每一轮的输出都高度依赖输入的所有位。

3.4 一个重要的提示:计算位数与安全

注意:
  1. 所有加法都是模2³²的,超过32位的进位要丢弃。
  2. 循环左移的操作符是“«”,不是“<<”。左移后要把移出去的位补到低位。
  3. 我曾经在一个项目中看到别人把“⊕”和“∧”搞混,结果验签死活过不去。一定要区分好!

3.5 为什么是64轮?

你可能会问,为什么SM3要设计64轮,而不是32或128?

答案与安全余量有关。我记得在设计规范文档里看到过,64轮是为了确保经过足够多的“混淆与扩散”,使得任意输入位的改动都会概率性地影响大约一半的输出位。128轮当然更安全,但性能下降太多。64轮,是性能与安全的平衡点。

在我参与的国家密码算法评估项目中,SM3的64轮设计被证明可以抵抗已知的所有差分攻击和线性攻击。从实践看,这个轮数选择是合理的。

3.6 避坑指南

  • 关于迭代顺序:消息分组是从M0到Mk-1按顺序来的,不能颠倒。否则摘要会完全不同。
  • 关于W'数组的使用:CF函数里前32轮只用W[j],后32轮W[j]和W'[j]都用了。别搞反了引用顺序。
  • 关于循环左移的实现:很多C语言初学者会写成(x << n) | (x >> (32 - n))。这是对的,但注意要强制类型转换为uint32_t,避免符号位扩展。
小贴士: 如果你在写SM3的软实现,建议先对全0消息或全1消息跑一遍,把中间状态的A~H打印出来,和标准测试向量逐轮比对。我每次做移植都这么干,能在5分钟内定位到移位或者加法器的问题。

好了,这一章我们讲透了迭代压缩和64轮函数。理解了这个,SM3的“骨架”和“肌理”你基本就抓住了。下一部分,我们将继续讨论消息认证码与HMAC-SM3,那才是真正用在完整性保护场景中的东西。记住今天讲的迭代结构和64轮细节,它们是所有上层应用的基础。