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 和补 0:在消息末尾加一个比特 1,接着补若干个比特 0,直到长度模 512 等于 448。
- 补原始长度:留出最后的 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。整个迭代过程是这样的:
初始值 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,别直接用拼接方式。