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”,接收方没法区分“自然结尾”和“填充结尾”。
初始值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轮压缩究竟在做什么?其实就是在反复混合消息和状态,让任何一位输入的变化都能扩散到整个输出。
我习惯把压缩函数拆成三块来看:
- 消息扩展:将16个原始消息字,扩展成64个轮常数。扩展算法用了异或和移位,比SHA-256稍微复杂一点。
- 64轮迭代:每轮用两个非线性函数(FF和GG)、两个置换(P0和P1),以及一个轮常数,更新8个状态字。
- 状态更新:每轮结束后,8个字循环移位,准备下一轮。
64轮循环
64轮循环里,每轮的常量Tj是分段定义的。前16轮用0x79cc4519,后48轮用0x7a879d8a。这两个常量的来历?同IV一样,也是从素数来的。
我曾经踩过一个坑——在实现循环时,把轮常数Tj算错了。因为标准文档里Tj的公式是分段给的,我一开始没注意边界条件,结果从第17轮起就开始偏差。调试花了半天,最后发现是个右移位数写成了常量。算是交了一笔学费。
SM3选用的非线性函数FF和GG,其实跟SHA-256的Σ函数很接近,但细节上做了调整。FF用了三输入异或,GG用了选择函数。你对比下两个标准就会发现,SM3的轮函数混合程度比SHA-256更深,安全性理论上更优。
我用一个SVG图来展示SM3的整体流程,方便你对照理解:
一句话总结
SM3算法的整个流程可以概括为:消息经过填充对齐到512位分组,用8个32位字初始化状态,然后每个分组通过消息扩展和64轮迭代压缩,不断更新状态。所有分组处理完毕,当前状态就是256位的摘要输出。
我在项目里调试时,最喜欢用已知测试向量来验证。比如标准文档附录里给的“abc”的摘要,或者空串的摘要。你只要把IV写对,填充做对,64轮循环的每一步算一遍,结果就能对上。我曾经因为一个移位运算符优先级写错,浪费了一个下午。嗯,从那以后我开始用单元测试逐轮验证中间值。