4. SM3算法原理:消息填充、迭代压缩、输出长度

说到哈希算法,SM3 是国密体系的基石之一。简单理解,哈希算法就像把一段数据加工成一个独一无二的数字指纹。SM3 更是金融行业最重要的哈希标准,像签名验签、完整性校验、密码保护,都离不开它。

我刚开始搞这块时,觉得 SM3 不就是个哈希函数吗?不就是算个摘要吗?后来真到项目中,发现这里面的坑可不少。从消息填充、迭代压缩到最后输出,每一步都有讲究。你想想看,如果摘要算法出问题,整个签名体系就全面崩盘。所以今天我带你把它彻底弄明白。

SM3 核心指标:
  • 输出长度:256 比特(32 字节)
  • 消息分组:512 比特(64 字节)
  • 字长:32 比特
  • 压缩函数:64 轮迭代
  • 安全强度:与 SHA-256 相当

4.1 消息填充

SM3 处理消息的单位是 512 比特的分组。如果消息长度不是 512 的倍数,就需要填充。填充过程分两步,先补位,后补长度,说白了就是把原始消息拉长到合适的尺寸。

  • 第一步: 在消息末尾追加比特 "1",然后追加 k 个比特 "0",使得填充后的长度满足 (len + 1 + k) ≡ 448 mod 512。嗯,这里的 448 是固定的,也就是填充完后要留出 64 比特放长度信息。
  • 第二步: 把原始消息的长度(以比特为单位)用 64 比特无符号整数表示,高位在前。拼接到第一步的结果后面。最后的总长度就是 512 的整数倍。
小技巧: 消息长度不需要填充的溢出情况很少见。但如果是大文件哈希,注意原始消息长度可能超过 2^64 比特,这个问题我在项目中见过,有些实现直接用 64 位整型存长度,遇到超大文件会溢出。建议在文件级哈希中做长度校验。

我刚开始看规范时,觉得填充这部分太简单了。后来真动手写代码,才发现容易栽在两个地方:一个是长度编码时的字节序问题,另一个是填充后分组边界判定。我曾经在适配老版本国密设备时,因为双方填充逻辑的位数计数方式不同,导致校验失败。排查了好几天,最后发现是填充时把消息长度多算了 8 个比特。

4.2 迭代压缩

这是 SM3 最核心的部分。填充完的消息被切分成 512 比特分组。每个分组经历 64 轮迭代压缩。我个人习惯把这个过程看作是“数据搅拌机”——输入的中间状态和消息分组反复混合,产生新的中间状态。

具体流程是这样:

  1. 初始化:设置 8 个 32 比特字作为初始值 IV(V0)。规范里写得很清楚,这个 IV 是固定的。
  2. 消息扩展:每个 512 比特分组扩展成 132 个 32 比特字。前 16 个字直接取自分组,后面 116 个字通过线性变换生成。
  3. 压缩函数:扩展出的字与当前状态(Vj)进行 64 轮运算。每轮会用到布尔函数 FF、GG 以及循环移位。最后生成新的状态 Vj+1。
  4. 迭代:对所有分组重复步骤 2-3,直到处理完所有分组。最终状态 Vn 就是 256 比特的消息摘要。

压缩函数里,我特别想提一个点:布尔函数 FF 和 GG 的位置。你在看规范时,会发现 FF 和 GG 的定义里既有选择函数也有多数函数。它们在不同的轮中使用不同的常数。关于这些布尔函数的实现,我个人习惯用位运算直接写,比查表快得多。

注意: 压缩函数的循环左移操作在不同平台上的实现可能有微小的差异。特别是涉及大端序(big-endian)与小端序(little-endian)的转换时,一定要统一约定。很多接口报错的原因就在这。

我曾经在调试 SM3 性能时,发现每次迭代都要做很多循环移位和异或运算。我当时尝试把扩展后的 132 个字缓存在局部变量中,而不是每次都重新计算。结果性能提升了将近 40%。所以对于高并发场景,内存换时间是值得的。

4.3 输出长度

SM3 的输出是固定的 256 比特,也就是 32 字节。这个长度有什么讲究?

  • 安全强度:256 比特输出意味着抗碰撞安全强度为 2^128,抗原像安全强度为 2^256。这在金融行业里已经绰绰有余了。
  • 适用场景:数字签名、完整性校验、密钥派生、随机数生成。说白了,大部分需要哈希的场景 SM3 都能胜任。
  • 兼容性:很多现有系统(比如支付宝、微信的开放平台)都支持 256 比特摘要,所以 SM3 的 32 字节输出可以直接嵌入现有报文结构。
关键优势: 相比 SHA-1(160 位)和 MD5(128 位),SM3 的 256 位输出提供了更高的安全冗余。在金融交易场景下,即使未来 10-20 年,256 位哈希依然安全。

如果我有一个消息要哈希,SM3 给我的是一个 32 字节的二进制摘要。很多开发者在展示摘要时会转成十六进制字符串,也就是 64 个十六进制字符。和 SM2 签名配合使用时,这个 32 字节的统一长度让报文字段设计变得很简洁。

4.4 与 SHA-256 的对比

很多同学问我:SM3 和 SHA-256 有什么区别?哪个更好?我个人认为,两者在安全强度上基本相当,只是设计细节不同。我做一个对比表。

项目 SM3 SHA-256
输出长度 256 比特 256 比特
消息分组 512 比特 512 比特
压缩轮数 64 轮 64 轮
布尔函数 FF + GG(含选择/多数) Ch + Maj
字长与移位 32 位,循环左移 32 位,右移 + 循环右移
安全强度 2^128(抗碰撞) 2^128(抗碰撞)
国际认可 国密标准,部分 ISO NIST,全球通用

从表中能看出来,两者非常接近。SM3 的布尔函数设计上比 SHA-256 更复杂一些,但换来的不是本质上的安全提升,而是与国密其他算法(如 SM2、SM4)的配套使用更顺畅。我记得在迁移 SHA-256 到 SM3 时,主要是替换算法调用,报文结构基本不用动。

不过要留意一个区别:SM3 在扩展阶段的字生成方式和 SHA-256 不同。SM3 用到的是异或和循环移位的组合,而 SHA-256 用的是右移和异或的组合。这个差异导致 SM3 的扩散性略好,但对开发者来说感知不强。

下面我把整个 SM3 运算流程画了出来。从输入消息开始,经过填充、分组、迭代压缩,最终输出 256 比特摘要。这能帮你快速形成整体认知。

SM3 哈希运算整体流程图 输入消息(任意长度) 消息填充(补位 + 补长度) 分组(512比特/组) 初始值 IV(V₀) 迭代压缩(64轮/分组) 处理下一分组 最终摘要(256比特) 输出摘要(32字节) 说明 摘要长度固定为32字节 可用于签名验签、完整性校验

说实话,SM3 的原理并不复杂。但真正要把它用好,需要熟悉填充边界、压缩函数的实现细节以及字节序的处理。我建议你写一个测试用例,拿一个固定消息(比如 "abc")手工计算一遍摘要,然后和标准实现对比。这样做一次,你就能彻底理解 SM3 的全貌。