3. SM3 密码杂凑算法:算法原理、消息填充、迭代压缩函数、输出长度

好,咱们来聊聊 SM3。说实话,做国密接口这几年,SM3 是我打交道最多的算法之一。它不像 SM2 那么多弯弯绕绕的数学原理,也不像 SM4 那样需要盯着密钥扩展。SM3 的本质很简单——把任意长度的消息,给你“揉”成一个固定长度(256位)的摘要。但揉得不好,就会出大问题。

3.1 算法原理

SM3 是中国国家密码管理局发布的密码杂凑算法标准(GM/T 0004-2012)。它的输出长度是 256 位,也就是 32 个字节。你想想看,这 32 个字节的摘要,就是整条消息的“指纹”。

它的核心思想是什么?说白了,就是三个步骤:

  • 消息填充:把原始消息补成 512 位的整数倍。
  • 迭代压缩:把填充后的消息分成一块一块(512 位一组),然后一块一块地“加密”处理。
  • 输出摘要:所有块处理完后,最终得到 256 位的杂凑值。

关键点:SM3 的强度和 SHA-256 相当。国家密码管理局在发布时,对它的抗碰撞性、抗第二原像性都有严格的要求。我个人习惯把 SM3 和 SHA-256 做对比,它们结构类似,但 SM3 在压缩函数的设计上更“保守”,比如使用了更多的“加常数”和“异或”操作,理论上对某些密码分析攻击有更强的抵抗力。

3.2 消息填充

嗯,这里要注意。为什么需要消息填充?因为 SM3 是按 512 位(64 字节)的分组来处理消息的。如果消息不是 512 位的整数倍,你就没法分组了。

填充规则如下:

  1. 在消息末尾追加一个比特“1”。
  2. 然后追加 k 个比特“0”,使得填充后的消息长度(原始长度 + 1 + k)模 512 等于 448。
  3. 最后,再追加 64 位的比特串,表示原始消息的长度(大端序)。

举个例子:如果原始消息是 100 位,填充 1 位的“1”后是 101 位。我们需要填充到 (100 + 1 + k) ≡ 448 (mod 512),算出来 k = 347 位。最后再追加 64 位的原始长度(100)。

我曾经踩过一个坑:在实现消息填充时,把原始消息长度(64 位)的字节序搞反了。我用的是小端序,但 SM3 标准明确要求大端序。结果算出来的摘要跟标准测试向量完全对不上。查了整整半天,才发现是长度字段的字节序问题。各位同学,字节序是国密接口里最容易出错的地方,没有之一。

3.3 迭代压缩函数

这是 SM3 最核心的部分。我说“迭代压缩”,你脑子里要有几个关键词:消息扩展、64 轮迭代、状态变量 A-H。

SM3 的压缩函数接收两个输入:

  • 当前 512 位的消息分组 B
  • 上一轮压缩得到的 256 位中间状态 V

输出是新的 256 位中间状态 V'。

它的处理流程如下:

  1. 消息扩展:将 512 位的分组 B,扩展成 132 个 32 位字(W0~W67 和 W'0~W'63)。扩展公式我不展开,但你要知道它引入了复杂的异或、循环移位和异或组合,目的是打乱位模式。
  2. 64 轮迭代:从初始状态 A、B、C、D、E、F、G、H(8 个 32 位字,共 256 位)开始,每一轮根据 Wj 和 W'j 更新这些状态变量。
  3. 压缩输出:64 轮结束后,把最终的状态变量与输入的中间状态(V)进行“加法”(模 2^32 加法),得到新的 V'。

我的一个小习惯:在调试 SM3 实现时,我总是在 64 轮迭代的每一轮末尾,都把 8 个状态变量打印出来。这样可以快速定位是哪一轮开始出错。特别是跟标准测试向量做对比时,这种逐轮调试能让你精确到具体的某次运算有误。

下面我用一段伪代码,来展示 64 轮压缩函数的核心:


// 假设 A-H 已经初始化为 IV
// Wj 和 W'j 已经通过消息扩展得到
for j = 0; j < 64; j++:
    // 1. 计算 SS1、SS2
    SS1 = ((A <<< 12) + E + (Tj <<< j)) <<< 7
    SS2 = SS1 ^ (A <<< 12)
    // 2. 计算 TT1、TT2
    TT1 = FFj(A, B, C) + D + SS2 + W'j
    TT2 = GGj(E, F, G) + H + SS1 + Wj
    // 3. 更新 8 个状态变量(循环右移)
    D = C
    C = B <<< 9
    B = A
    A = TT1
    H = G
    G = F <<< 19
    F = E
    E = P0(TT2)

这里面的 FFj 和 GGj 是布尔函数,不同的 j 范围(前 16 轮和后 48 轮)使用不同的函数。P0 是置换函数。

3.4 输出长度

SM3 的输出长度是固定的 256 位,即 32 字节。这也意味着,无论你输入的消息是 1 个字节,还是 1GB 的文件,最终得到的摘要都是 32 个字节。

为了更好地理解,我把 SM3 与常见的其他杂凑算法做个对比:

算法 输出长度 标准
SM3 256 位 (32 字节) GM/T 0004-2012
SHA-256 256 位 (32 字节) FIPS PUB 180-4
SHA-1 160 位 (20 字节) FIPS PUB 180-4(已弃用)
MD5 128 位 (16 字节) RFC 1321(已弃用)

输出长度决定了算法的安全强度。256 位的输出,按目前的理论,要找到两条有相同摘要的消息(碰撞),需要进行约 2^128 次运算。这个量级,在可预见的未来都是安全的。

3.5 SM3 结构图

明白了原理,再来看这张整体结构图,就顺了:

SM3 算法核心流程 原始消息 M 消息填充(补1+补0+长度) 分为 512 位分组 [B1, B2, ... Bn] 迭代压缩函数 (消息扩展 + 64 轮压缩 + 模加) 输出 256 位杂凑值

这张图把整个 SM3 流程串起来了。你从最上面的“原始消息”开始,经过填充、分组、迭代压缩,最后输出 256 位的杂凑值。迭代压缩那块,就是前面说的“消息扩展 + 64 轮压缩 + 模加”。

3.6 总结

SM3 密码杂凑算法,说白了就是一个“确定性压缩器”。你给它任意长度的输入,它固定给你 256 位输出。它的安全核心在于迭代压缩函数的 64 轮运算,以及消息扩展的复杂性。

我在各个项目中,把 SM3 用于:

  • 数据完整性校验:比如文件上传时的秒传逻辑,用 SM3 摘要作为文件标识。
  • 数字签名:与 SM2 配合,用 SM3 对消息摘要进行签名。
  • HMAC-SM3:用 SM3 构造消息认证码,用于 API 接口的防篡改。

它在国密体系里,是出镜率最高的算法之一。理解它,你就拿到了国密接口的第一张入场券。