SM3密码杂凑算法原理归纳总结

说实话,很多人刚接触国密算法时,总觉得SM3就是SHA‑256的复制品。我在项目里调试过这两个算法,细节上其实差别不小。今天咱们就掰开揉碎了聊聊SM3的核心原理。

3.1 SM3算法总体流程

SM3属于迭代型哈希函数,说白了就是把任意长度的输入消息,经过填充、分组、迭代压缩,最后输出一个256位的摘要。整个流程可以概括成四个步骤:

  • 消息填充 – 补位 + 补长度,使消息是512位的整数倍
  • 消息分组 – 按512位/64字节切块
  • 迭代压缩 – 每块经过64轮运算,更新中间状态
  • 输出摘要 – 最后的状态拼接成256位结果

我个人习惯把算法拆成“填充”和“压缩”两大块来理解。压缩那块最复杂,包含消息扩展和压缩函数。

核心要点:SM3的输出长度为256位(32字节),安全强度与SHA‑256相当,但内部设计有差异,尤其在布尔函数和置换运算上。

SM3算法整体流程图 SM3 算法迭代压缩流程 输入消息 M 消息填充 分组 (512位/块) 迭代压缩(64轮) 消息扩展 → 压缩函数(GG / FF) 中间状态 V(i) → V(i+1) 输出 256位杂凑值 每个分组 直至所有分组

注意:SM3的初始值IV是固定的8个32位字,迭代过程中不会写入外部依赖。你如果在HSM中实现,一定要用硬件寄存器保存这些常数,别放RAM里,容易被侧信道攻击抓到规律。

3.2 消息填充:细节决定成败

SM3填充与SHA‑256非常相似,但我不建议直接复用。我曾经在初版硬件设计里偷懒,直接把SHA‑256的填充逻辑改了下长度字段,结果综合时发现高位截断问题。所以还是老老实实按标准来:

  • 先在消息末尾补一个“1”比特
  • 然后补“0”比特,直到长度 mod 512 = 448(即还剩64位留给长度)
  • 最后附加64位原始消息长度(大端模式,单位比特)

举个简单的例子:

原始消息填充后(十六进制)说明
0x61 (a)0x6180…00000008补1加0,长度8 bits
空消息0x8000…00000000比特长度0

你想想看,要是消息长度恰好是512的整数倍,是不是就不用填充了?不对,标准要求必须填充——即使长度已经对齐,也要再补一个完整分组。这样做是为了防止长度可变导致的碰撞。我见过有些软件实现漏填这种情况,测完向量能过,但实际用起来就出问题。

3.3 迭代压缩:64轮才是真功夫

SM3的压缩流程分两步:消息扩展和压缩函数。每进来一个分组,扩展模块会生成132个字(W[0]~W[67] 和 W'[0]~W'[63]),然后压缩函数用这些字循环更新8个工作寄存器。

3.3.1 消息扩展

扩展过程可以用下面几个公式概括:

W[j] = M[j]              , 0 ≤ j ≤ 15
W[j] = P₁(W[j-16] ⊕ W[j-9] ⊕ (W[j-3] <<< 15)) ⊕ 
       (W[j-13] <<< 7) ⊕ W[j-6]   , 16 ≤ j ≤ 67

W'[j] = W[j] ⊕ W[j+4]   , 0 ≤ j ≤ 63

这里P₁是置换函数:P₁(X) = X ⊕ (X <<< 15) ⊕ (X <<< 23)。我在FPGA上实现时,把W[j]的生成做成流水线,每周期出一个新值,64轮要67个周期才能把W[67]算完。为什么?因为W[67]供最后几轮使用,不能少。

3.3.2 压缩函数(轮函数)

每轮的处理可以看作是“以字为单位的非线性更新”。核心是三个布尔函数和两个置换:

轮次布尔函数 FFj布尔函数 GGj
0 ≤ j ≤ 15FF = X⊕Y⊕ZGG = X⊕Y⊕Z
16 ≤ j ≤ 63FF = (X∧Y)∨(X∧Z)∨(Y∧Z)GG = (X∧Y)∨(¬X∧Z)

你可能注意到了,前16轮是纯异或,后48轮变成了“多数投票”和“选择”逻辑。这样设计的好处是在前几轮快速扩散比特,后面则增强抗碰撞能力。嗯,这里要注意,置换P₀在每轮最后用到:

P₀(X) = X ⊕ (X <<< 9) ⊕ (X <<< 17)

小技巧:硬件实现时,布尔函数FF/GG都可以用LUT或简单逻辑门完成,但P₀的循环移位要特别注意——因为SM3用的是32位字,移位量9和17都不满32,用组合逻辑就能搞定。我在一个安全芯片项目里把P₀和消息扩展的P₁合并成一个模块,省了不少门电路,时序也没崩。

3.4 与SHA‑256的关键差异

很多工程师问过我:既然SM3和SHA‑256都叫256位哈希,那区别在哪?我列个表你就明白了:

对比项SM3SHA‑256
消息扩展反馈深度16→68字,且使用P₁16→64字,无置换
布尔函数种类2种(FF/GG,分前后)2种(Ch/Maj,固定)
置换操作P₀、P₁各一个无(用σ函数)
常数个数64个(含j的线性函数)64个(固定素数小数部分)
初始值IV0x7380166f…0x6a09e667…

说白了一个最大的感受:SM3的扩散性比SHA‑256更强,因为多了一层P₁和P₀置换。但也因为这个,硬件实现时关键路径会比SHA‑256长一点点。我在HSM设计里把压缩函数的关键路径拆成两级流水,这样能跑到400MHz以上。

3.5 实践中的避坑指南

最后分享两个我踩过的坑:

  • 字节序陷阱:SM3标准规定所有多字节数据采用大端序。很多嵌入式系统默认小端,转换时容易忘。我以前在填充长度字段时忘做大小端转换,结果算出的摘要只有前几个字节是对的,后面全错。
  • 寄存器初始化:8个工作寄存器A~H必须在每个新的消息分组开始时拷贝当前IV或上一轮结果,千万不要误用压缩函数内部的临时值。我在一个早期RTL版本里,把中间变量直接当新一轮初始状态送了进去,导致一个分组算出来就漂移了。

总结:SM3的算法结构清晰,但细节决定安全性。作为硬件设计者,建议把填充、消息扩展和压缩函数做成独立的子模块,这样便于测试和优化。记住:国密算法的正确实现不仅靠标准文档,还得靠反复的向量验证。