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

SM3 是国密标准里的杂凑算法。说白了,就是把任意长度的消息,压缩成一个固定 256 位的摘要。我们平时说“哈希”,指的就是这个。

我最早接触它时,总觉得跟 SHA-256 差不多。后来做国密改造项目,才发现细节上差别不小。踩过坑,才算真正搞明白。

3.1 算法核心流程

SM3 的处理过程可以分成四步。每一步都有讲究。我们一起捋一遍。

  1. 消息填充:补位到 512 位的整数倍。
  2. 初始值设定:用 8 个 32 位寄存器装初始向量。
  3. 迭代压缩:对每个 512 位分组做 64 轮计算。
  4. 输出杂凑值:最终 8 个寄存器的值就是 256 位摘要。

你想想看,核心就在迭代压缩那一步。64 轮,每轮都用布尔函数和置换运算。强度就在这里。

3.2 消息填充细节

填充规则很简单。但我在第一次实现时还真出过 bug。

  • 先在消息末尾追加一个 1 bit。
  • 然后补 0,直到长度模 512 等于 448。
  • 最后用 64 位表示原始消息长度。

我自己的经验:补位时最容易错的是字节序。SM3 采用大端序,跟你的平台字节序可能不一样。我曾经在 x86 机器上直接 memcpy,结果长度字段全反了。

3.3 初始值与迭代过程

SM3 的初始值 IV 是 8 个固定的 32 位常数。这里我直接写出来,方便你对照。

寄存器 初始值(十六进制)
V0 0x7380166F
V1 0x4914B2B9
V2 0x172442D7
V3 0xDA8A0600
V4 0xA96F30BC
V5 0x163138AA
V6 0xE38DEE4D
V7 0xB0FB0E4E

迭代时,每个 512 位分组会扩展成 132 个 32 位字(W0~W67 和 W'0~W'63)。压缩函数对这 132 个字做 64 轮运算,更新 8 个寄存器。

关键点:SM3 压缩函数里的布尔函数 FF 和 GG 会随轮数变化。前 16 轮和后 48 轮不同。我第一次写代码时忽略了这点,结果摘要全错。

3.4 压缩函数详解

压缩函数是这个算法的心脏。我习惯把它拆成三块来看。

  • 消息扩展:把 16 个消息字扩成 132 个字。
  • 64 轮计算:每轮用 SS1、SS2、TT1、TT2 四个中间变量。
  • 状态更新:每轮结束后,8 个寄存器循环移位赋值。

下面这段代码,是我在某次项目里实际用的核心循环。注释都在。

// 64 轮压缩循环
for (j = 0; j < 64; j++) {
    SS1 = rotl32(rotl32(A, 12) + E + rotl32(Tj, j), 7);
    SS2 = SS1 ^ rotl32(A, 12);
    TT1 = FFj(A, B, C) + D + SS2 + Wj[j];
    TT2 = GGj(E, F, G) + H + SS1 + Wj_1[j];
    D = C;
    C = rotl32(B, 9);
    B = A;
    A = TT1;
    H = G;
    G = rotl32(F, 19);
    F = E;
    E = P0(TT2);
}

你仔细看看,每轮只变了 A 和 E。其他寄存器只是跟着挪。这种设计,硬件实现时很省资源。

注意:P0 置换是 SM3 独有的。它跟 SHA-256 的 Σ 函数不一样。我的一个同事直接拿 SHA-256 的代码改,结果在 P0 上翻车了。千万别偷这个懒。

3.5 SM3 与 SHA-256 对比

很多资料说 SM3 跟 SHA-256 结构相似。这个没错。但差距就在那几个布尔函数和置换上。

对比项 SM3 SHA-256
消息分组 512 bit 512 bit
摘要长度 256 bit 256 bit
轮数 64 64
扩展字数 132 64
布尔函数 FFj / GGj(分段) Ch / Maj(固定)
置换函数 P0 / P1 Σ0 / Σ1

我个人感觉,SM3 的布尔函数分段设计,让前 16 轮和后 48 轮有不同的非线性度。这在密码分析上更难攻破。

SM3 密码杂凑算法处理流程 原始消息(任意长度) 步骤 1:消息填充(补 1 + 补 0 + 长度) 步骤 2:分组迭代(每 512 bit 为一块) 步骤 3:64 轮压缩函数 输出:256 位杂凑值(8 个 32 位字)

3.6 代码实现时的避坑指南

我写 SM3 也不是一次成功的。这里面有几个坑,你提前知道,能省很多时间。

  • 循环移位:SM3 里到处是循环左移。C/C++ 里标准左移不是循环的,要自己写 rotl32。
  • W' 的计算:W'j = Wj ^ Wj+4,这个异或顺序不能反。我亲眼见过有人调了好几天,结果就是这里写反了。
  • 多分组处理:最后一个分组填充完可能正好是 512 的整数倍,但还是要加一个完整的分组。这个规则跟 MD5 一样,别想当然地跳过。

一个小技巧:调试时可以打印每个分组的 W 数组,跟国密官方测试用例对照。前几个字对了,后面基本不会错。我自己都是这么干的。

3.7 总结

SM3 设计得很精巧。它跟 SHA-256 同属一个家族,但细节上做了强化。消息扩展用了 132 个字,布尔函数分段设计,这些都让它的安全性更高一点。

我在项目里用它做日志完整性校验。每次写入日志时计算一次 SM3 摘要,存储时把摘要附在后面。读取时重新计算对比。这样日志有没有被篡改,一眼就能看出来。

嗯,这里要注意一点。SM3 是逐分组计算的,流式处理时一定要维护好中间状态。我在做流式日志传输时,就因为这个中间状态没处理好,导致摘要不一致,排查了好久。