SM3 哈希计算流程
要理解校验为什么会失败,得先搞清楚 SM3 到底怎么算的,对吧?
SM3 的计算流程,说白了就是三步走:消息填充 → 迭代压缩 → 输出。听起来简单,但每一步都有坑。我个人习惯把这三个阶段比作「备料、烹饪、装盘」,任何一个环节出问题,最终出来的「菜」——也就是哈希值——就不对。
我记得有一次排查一个校验失败的问题,折腾了大半天,最后发现是消息填充时少补了一个比特。这种错误,你光看输出根本看不出来,得把流程拆开一步一步对。
下面我们逐一拆解,看看每个环节都做了什么,容易在哪里翻车。
核心要点:SM3 的输入是任意长度的消息(比特串),输出固定 256 比特(32 字节)的哈希值。中间过程全部在 32 位字(word)上操作。
2.1 消息填充(Padding)
SM3 要求输入的消息长度必须是 512 比特(64 字节)的整数倍。如果不满足,就要填充。
填充规则很简单,就三步:
- 补一个 1:在消息末尾追加一个比特 '1'。
- 补若干 0:接着补 k 个 '0',k 满足
(原始比特长度 + 1 + k) ≡ 448 mod 512。 - 补 64 比特长度:最后把原始消息的长度(用 64 位无符号整数表示)附加上去。
为什么要这么设计?你想想看,这样做的好处是:
- 第一步的 '1' 是个哨兵,告诉算法「这里开始是填充数据了」。
- 它保证了填充后的消息正好是 512 比特的整数倍。
- 长度信息的补入,让算法知道原始消息有多长,从而消除不同长度产生相同填充结果的可能。
举个具体的例子。假设原始消息只有 1 比特:
原始消息: 1 (二进制:'1')
原始长度: 1 bit
填充步骤:
1. 补 '1': 1|1 = '11'
2. 补 k 个 '0': k = 448 - (1 + 1) mod 512 = 448 - 2 = 446
所以这里补 446 个 0
-> '11' + 446个'0'
3. 补 64 比特长度: 原始长度 1 = 0x0000 0000 0000 0001
-> 最终填充后的消息是 512 比特整
2.2 迭代压缩(Iterative Compression)
填充完之后,消息就被分成一个个 512 比特的分组。每个分组都会经过一个叫「压缩函数」的东西处理。处理的时候,会带一个 256 比特的「中间状态」进去,输出一个新的 256 比特状态。这个状态一路传下去,最后一个分组处理完,就得到了最终的哈希值。
我建议把迭代压缩理解成一个「流水线」:
- 初始化状态(IV):SM3 规定了一个初始值,共 8 个 32 位字。这个值是固定的,谁也改不了。如果初始化不对,后面全错。
- 消息扩展(Message Expansion):这也是很容易出错的一步。每个 512 比特分组(16 个 32 位字)被扩展成 132 个 32 位字(W0~W67 和 W'0~W'63)。扩展算法本质上是个线性反馈移位寄存器,用到的循环移位和异或操作,在软件里容易写岔。
- 64 轮压缩(64 Rounds):用扩展出来的 132 个字,加上当前的状态,进行 64 轮迭代。每一轮都用到了布尔函数(异或、与、或、非)和循环移位。这一步的计算量最大,也是校验失败的高发区——轮函数的一个位移写错,结果就全歪了。
每轮的压缩过程可以用这个伪代码表示:
// 假设有 8 个寄存器 A, B, C, D, E, F, G, H
// 每轮输入:扩展字 Wj 和 W'j
for j = 0 to 63:
SS1 = ((A leftrotate 12) + E + (Tj leftrotate j)) leftrotate 7
SS2 = SS1 XOR (A leftrotate 12)
TT1 = FFj(A, B, C) + D + SS2 + W'j
TT2 = GGj(E, F, G) + H + SS1 + Wj
D = C
C = B leftrotate 9
B = A
A = TT1
H = G
G = F leftrotate 19
F = E
E = P0(TT2)
// 其中 Tj 是固定常数,FFj 和 GGj 是布尔函数,P0 是置换函数
为什么要做这么多轮?说白了,轮数越多,消息之间的「雪崩效应」越强——输入稍微变一个比特,输出就天翻地覆。这也是哈希函数安全性的基石。
2.3 输出(Output)
最后一个分组处理完之后,当前的状态(A, B, C, D, E, F, G, H 这 8 个 32 位字)就是最终的哈希值。把它们按顺序拼接起来,得到 256 比特的输出。
这一步本身逻辑简单,但容易出幺蛾子的地方是:
- 字节序问题:输出时每个字是按大端还是小端拼接?SM3 规定是大端。如果你的系统默认是小端,输出前需要反转每个字内的字节顺序。
- 截断问题:SM3 标准输出就是 256 比特。如果你只取前 128 比特或其他长度,那就不是标准的 SM3 了。
- 十六进制表示:我们通常看到的哈希值是 64 个十六进制字符。如果大小写、多余的空格或换行符混入,会导致字符串比较失败。每次输出前,我习惯先统一转成小写,去掉所有空白字符,再去做校验。
整体流程框架图
为了让你更直观地看到这三个阶段之间的关系,我画了一张流程图。别小看这张图,它是我排查 SM3 问题时最常用的「地图」。上面标出了每个阶段的输入、输出和关键操作,你在对照调试时一眼就能看到自己卡在哪一步。
看到这张图,你应该对整个流程有个整体印象了。从消息填充开始,到最后的 256 比特哈希值输出,中间每一步都有严格的规则和固定的参数。任何一个环节出现偏差,最终的哈希值就会完全不一样。
这就是为什么排查 SM3 校验失败时,需要「拆开来看」。别只看最后的结果对不对,要从消息填充、消息扩展、64 轮压缩、以及最终的输出格式上,逐个环节去排查。很多看似玄学的校验失败,查到最后,往往就是填充时少补了一个零,或者字节序没转过来。