SM3算法原理

说起SM3算法,很多人第一反应是“国密标准,跟SHA-256长得像”。确实,它们都属于Merkle-Damgård结构,但SM3在细节上做了不少优化。我在做国密改造项目时,一开始也以为直接照搬SHA-256的逻辑就行,结果一跑测试向量,输出完全不对。嗯,最后老老实实翻了遍标准文档。

SM3的核心逻辑可以拆成四个部分:消息填充、初始值IV、迭代压缩函数、64轮循环。你把这四个东西搞清楚了,算法就算通了一半。

消息填充

SM3处理消息时,每次吃进512位(64字节)。但如果消息长度不是512的倍数,就得填充。我见过不少新手自己实现时,填充规则写错,导致摘要怎么都对不上。

填充规则其实很简单:

  • 在消息末尾先补一个“1”比特
  • 然后补“0”比特,直到长度满足 (原长度 + 填充长度 + 64) 是 512 的倍数
  • 最后附加64位,表示原始消息的长度(单位是比特)

换句话说,哪怕消息正好是512的倍数,也要填充一个完整的512位分组。为什么要这样?因为你不加那个“1”,接收方没法区分“自然结尾”和“填充结尾”。

一个小提示:我曾经遇到过填充分组里长度域写错的情况。注意,长度是大端序(Big-Endian),64位里高32位存高比特。如果你用的CPU是小端序,记得做字节序转换。

初始值IV

SM3的初始值IV是8个32位字。你可以认为是算法的“种子”。标准文档里给了定死的值:

变量 值(十六进制)
V0 7380166f
V1 4914b2b9
V2 172442d7
V3 da8a0600
V4 a96f30bc
V5 163138aa
V6 e38dee4d
V7 b0fb0e4e

这些值是怎么来的?其实就是前8个素数的平方根的小数部分,取前32位。说白了,跟SHA-256的初始值一个套路。

迭代压缩函数

这部分是SM3算法的核心引擎。每次处理一个512位的消息分组,把当前状态(Vi)压缩成新的状态(Vi+1)。压缩函数里又包含消息扩展和64轮压缩两个阶段。

你想想看,64轮压缩究竟在做什么?其实就是在反复混合消息和状态,让任何一位输入的变化都能扩散到整个输出。

我习惯把压缩函数拆成三块来看:

  1. 消息扩展:将16个原始消息字,扩展成64个轮常数。扩展算法用了异或和移位,比SHA-256稍微复杂一点。
  2. 64轮迭代:每轮用两个非线性函数(FF和GG)、两个置换(P0和P1),以及一个轮常数,更新8个状态字。
  3. 状态更新:每轮结束后,8个字循环移位,准备下一轮。

64轮循环

64轮循环里,每轮的常量Tj是分段定义的。前16轮用0x79cc4519,后48轮用0x7a879d8a。这两个常量的来历?同IV一样,也是从素数来的。

我曾经踩过一个坑——在实现循环时,把轮常数Tj算错了。因为标准文档里Tj的公式是分段给的,我一开始没注意边界条件,结果从第17轮起就开始偏差。调试花了半天,最后发现是个右移位数写成了常量。算是交了一笔学费。

注意:64轮循环里用的消息字Wj,是按顺序从扩展后的64个字里取的。但每轮更新状态时,顺序是:A先更新,然后B,C,D,E,F,G,H。不要搞乱了,否则输出会完全不对。

SM3选用的非线性函数FF和GG,其实跟SHA-256的Σ函数很接近,但细节上做了调整。FF用了三输入异或,GG用了选择函数。你对比下两个标准就会发现,SM3的轮函数混合程度比SHA-256更深,安全性理论上更优。

我用一个SVG图来展示SM3的整体流程,方便你对照理解:

SM3 算法整体流程框架 原始消息 消息填充:补1+补0+长度 (对齐到512位分组) 512位分组(Bi) 初始值IV 迭代压缩:消息扩展 + 64轮循环 FF/GG函数 · P0/P1置换 · 轮常数Tj 256位摘要 关键要点 • 每次只处理一个512位分组,状态逐级传递 • 64轮里每轮的消息字Wj和常数Tj都不同 • 所有分组处理结束后,当前状态V就是最终摘要

一句话总结

SM3算法的整个流程可以概括为:消息经过填充对齐到512位分组,用8个32位字初始化状态,然后每个分组通过消息扩展和64轮迭代压缩,不断更新状态。所有分组处理完毕,当前状态就是256位的摘要输出。

我在项目里调试时,最喜欢用已知测试向量来验证。比如标准文档附录里给的“abc”的摘要,或者空串的摘要。你只要把IV写对,填充做对,64轮循环的每一步算一遍,结果就能对上。我曾经因为一个移位运算符优先级写错,浪费了一个下午。嗯,从那以后我开始用单元测试逐轮验证中间值。