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

SM3 是咱国标里定义的密码杂凑算法。说白了,它能把任意长度的消息,揉成一个固定长度的摘要。SM3 的输出是 256 位,也就是 32 个字节。这个长度设计得很巧妙,正好能跟 SM2 椭圆曲线公钥密码算法的密钥长度配套。

我经常把 SM3 比作「数字指纹」—— 消息哪怕改一个比特,摘要都会变得面目全非。这个特性在 Secrets Manager 里太重要了。你想想看,如果存储的密钥被篡改了一点点,我们用 SM3 一验就能发现。

3.1 SM3 算法核心参数

在开始拆解算法之前,我们先记住几个关键数字。我个人习惯先把这些参数背下来,后面看源码会顺很多。

参数 数值 说明
输出长度 256 bits (32 字节) 跟 SHA-256 一致
分组长度 512 bits (64 字节) 消息按此大小切块处理
字长 32 bits 内部运算的基本单位
迭代轮数 64 轮 每轮更新 8 个工作寄存器
初始值 IV 8 个 32 位常量 来自自然数的十六进制表示

3.2 消息填充:补位到 512 的倍数

SM3 处理消息之前,必须先填充。这个过程分两步:

  1. 补 1 和补 0:在消息末尾加一个比特 1,接着补若干个比特 0,直到长度模 512 等于 448。
  2. 补原始长度:留出最后的 64 位,写入原始消息的长度(以比特为单位,大端序)。

注意对齐问题:我在集成 SM3 到嵌入式设备时遇到过一个大坑 —— 有些平台的内存对齐要求很严格。你如果直接用 memcpy 去读未对齐的 32 位字,轻则性能下降,重则直接触发总线错误。我建议在填充阶段就确保数据对齐到 4 字节边界。

举个例子:如果原始消息是 "abc"(3 字节 = 24 比特),填充后消息变成:

原始:    01100001 01100010 01100011        (3 字节)
补1:     01100001 01100010 01100011 1       (25 比特)
补0:     ...后面补 0 到 448 比特...
补长度:  最后 64 位写入 0x00000000 00000018 (24 比特的十六进制)

3.3 迭代压缩:算法的核心心脏

填充完的消息被切成 512 比特的分组。每个分组经过一次 压缩函数 CF。整个迭代过程是这样的:

SM3 迭代压缩结构 初始值 IV (256 bit) V₀ 第 1 分组 扩展 + 64 轮压缩 V₁ = CF(V₀, B₁) 第 2 分组 扩展 + 64 轮压缩 V₂ = CF(V₁, B₂) 输出杂凑值 Vₙ 消息分组 B₁, B₂, …, Bₙ

初始值 V₀ 是标准里固定好的 8 个 32 位字。然后每一轮:

Vᵢ = CF(Vᵢ₋₁, Bᵢ)

其中 CF 是压缩函数,Bᵢ 是第 i 个消息分组。最后一次迭代的输出 Vₙ 就是最终的杂凑值。

3.4 压缩函数 CF 的内部细节

CF 函数是整个 SM3 最复杂的地方。我刚开始学的时候也觉得头大,后来拆开看,其实就两大块:消息扩展和轮函数压缩。

3.4.1 消息扩展

一个 512 比特的分组被扩展成 132 个 32 位字:

  • 前 16 个字 W₀ ~ W₁₅:直接取自消息分组。
  • 后 52 个字 W₁₆ ~ W₆₇:通过递推公式生成,混入了移位和异或运算。
  • W₀' ~ W₆₃':额外生成 64 个字,用于轮函数中的另一条路径。

为什么要扩展?说白了就是让消息的每一个比特都能影响更多轮的运算。这能增强雪崩效应。

3.4.2 64 轮压缩

SM3 内部维护 8 个 32 位寄存器 A, B, C, D, E, F, G, H。每轮用两个布尔函数做非线性变换:

  • FFⱼ(X, Y, Z):当 j 为 0~15 时,做 ((X ∧ Y) ∨ (¬X ∧ Z));当 j 为 16~63 时,做 ((X ∧ Y) ∨ (X ∧ Z) ∨ (Y ∧ Z))。
  • GGⱼ(X, Y, Z):当 j 为 0~15 时,做 ((X ∧ Y) ∨ (¬X ∧ Z));当 j 为 16~63 时,做 ((X ∨ Y) ∧ Z) ∨ (X ∧ Y) 之类的组合。

快速判断:0~15 轮的 FF 和 GG 都是「多路选择器」风格,16~63 轮则是「多数决」风格。我在代码里用宏或者内联函数来实现这两组,能减少分支预测失败。

每轮的流程大致是:

SS1 = ((A <<< 12) + E + (Tⱼ <<< j)) <<< 7
SS2 = SS1 ⊕ (A <<< 12)
TT1 = FFⱼ(A, B, C) + D + SS2 + Wⱼ'
TT2 = GGⱼ(E, F, G) + H + SS1 + Wⱼ
然后 B = A, C = B <<< 9, D = C, A = TT1, F = E, G = F <<< 19, H = G, E = P₀(TT2)

这里的 Tⱼ 是固定的轮常量。P₀ 是一个置换函数,做几个循环移位和异或。

3.5 SM3 与 SM2、SM4 的配合

在 Secrets Manager 里,SM3 经常跟 SM2 和 SM4 一起用:

组合场景 SM3 的角色 我在项目中怎么用
SM2 签名/验签 对消息生成摘要,SM2 再对这个摘要签名 用户上传密钥时,我先 SM3 哈希,再调用 SM2
SM4 密钥派生 对主密钥 + 上下文做哈希,派生出 SM4 会话密钥 曾经因为没加盐值导致派生结果一样,补上 SM3 哈希就安全了
完整性校验 计算密文或密钥的哈希值,跟存储的摘要比对 检测是否被篡改,效率比全量解密高多了

3.6 安全性考量与常见误区

SM3 被国家密码管理局认定为安全算法。截至目前,它还没有被攻破。但我见过一些实现上的问题:

我曾经犯过的错:

  • 忘记填充:直接拿原始消息做哈希。SM3 必须填充,否则结果完全错误。
  • 字节序搞反:SM3 内部使用大端序。我的一个项目在 x86 小端 CPU 上跑,没做转换,结果跟标准测试向量对不上。
  • 多线程共享上下文:SM3 的上下文结构里保存了中间状态,多个线程并发写入会互相覆盖。我建议每个线程都创建独立的上下文。

长度扩展攻击预警:SM3 的结构跟 SHA-256 类似,都存在长度扩展攻击的隐患。如果你用 SM3(secret || message) 来构造消息认证码,攻击者可以在不知道密钥的情况下,基于已有的哈希值伪造出 SM3(secret || message || padding || extra)。我在做 Secrets Manager 的 API 认证时,特意要求大家改用 HMAC-SM3,别直接用拼接方式。