3、SM3算法原理:密码杂凑算法结构、填充规则、迭代过程
SM3 是国密标准里的密码杂凑算法。说白了,它就是一把“数字指纹生成器”——任意长度的输入,都能压成一个 256 位的固定摘要。我最早接触 SM3 是在一个政务系统的签名项目里,当时总想着“不就是个 hash 嘛”,结果调试时因为填充细节没注意,死活对不上标准向量。从那以后,我再也不敢小看填充规则了。
3.1 算法总体结构
SM3 的结构其实很经典:先对消息做填充,再按 512 位分组,然后用一个压缩函数反复迭代。我个人习惯把它拆成四个阶段:
- 消息预处理:填充 + 长度编码,保证分组完整。
- 初始化:设置 8 个 32 位初始寄存器(IV)。
- 消息扩展:每个 512 位分组扩展出 132 个 32 位字。
- 迭代压缩:64 轮置换,更新寄存器值。
你想想看,整个流程就像工厂流水线——进来一条消息,先“整形”,然后“切块”,再“锻造”,最后出成品。下面这张图能帮你把全貌看清楚:
3.2 填充规则
SM3 的填充跟 SHA-256 很像,但细节有差异。我曾在一次跨平台联调中,因为某个 SDK 把大端字节序搞反了,导致填充后的长度字段解析错误,哈希值整个不对。那感觉……嗯,查了一整天。
填充分三步:
- 补一个“1”:在消息末尾添加一个比特 1,即字节 0x80。
- 补“0”:直到消息长度与 448 模 512 同余(即剩余 64 位留给长度字段)。
- 加 64 位长度:原始消息长度(比特数)用大端序写在最后 8 个字节。
举个例子:若原始消息只有 1 字节 “0x61”(字符 'a'),填充后是这样:
原始: 0x61
补1: 0x61 0x80
补0至448位: 0x61 0x80 后跟 54 个 0x00
加长度: 最后8字节表示原始长度 8 比特 → 0x00 0x00 ... 0x00 0x08
3.3 迭代过程与消息扩展
填充完成后,消息被切成 n 个 512 位的分组。每个分组都要经历一次“消息扩展”和“压缩函数”。我习惯把迭代逻辑写成伪代码,这样更直观:
// 假设 V0 为初始 IV(8 个 32 位字)
for i = 0 to n-1:
// 1. 消息扩展:将 B_i 扩展成 132 个字
W[0..15] = B_i // 直接按字拷贝
for j = 16 to 67:
W[j] = P1(W[j-16] ⊕ W[j-9] ⊕ (W[j-3] << 15))
⊕ (W[j-13] << 7) ⊕ W[j-6]
for j = 0 to 63:
W'[j] = W[j] ⊕ W[j+4]
// 2. 压缩函数:64 轮迭代
V_{i+1} = CF(V_i, B_i) // 里面跑 64 轮布尔+置换
// 最终 V_{i+1} 作为下一组的初始值
// 输出 V_n 作为摘要
这里的 P1 是 SM3 特有的线性置换函数:
P1(X) = X ⊕ (X << 9) ⊕ (X << 17) (循环左移)。
压缩函数 CF 每轮用到两个消息字 Wj 和 W'j,加上 8 个寄存器 A~H。具体轮函数我就不贴全了,网上标准文档都有。但我可以分享一个调试技巧:
3.4 压缩函数核心
SM3 的压缩函数本质是一个 64 轮的 Feistel 结构。每轮由三部分组成:
- 布尔函数 FFj 与 GGj:前 16 轮用 FF1/GG1(异或为主),后 48 轮用 FF2/GG2(与/或为主)。
- 置换函数 P0:对 32 位字做
X ⊕ (X << 9) ⊕ (X << 17)。 - 常数 Tj:前 16 轮固定
0x79cc4519,后 48 轮固定0x7a879d8a。
每一轮更新寄存器的方式可以概括为:
SS1 = ((A << 12) + E + (Tj << j)) << 7
SS2 = SS1 ⊕ (A << 12)
TT1 = (FFj(A,B,C) + D + SS2) + W'[j]
TT2 = (GGj(E,F,G) + H + SS1) + W[j]
D = C; C = B << 9; B = A; A = TT1
H = G; G = F << 19; F = E; E = P0(TT2)
注意这里循环左移的位数(12、7、9、19)都是 SM3 特有的。我记得早期有个开源实现把 B << 9 写成了 B << 8,结果只在最后几轮出错,排查起来特别痛苦。
3.5 参数速查表
为了方便你快速查阅,我把几个核心参数列成了表格:
| 参数 | 值 | 说明 |
|---|---|---|
| 摘要长度 | 256 位(32 字节) | 最终输出固定 |
| 分组大小 | 512 位(64 字节) | 迭代处理的基本单位 |
| 初始 IV | 0x7380166f, 0x4914b2b9, ... | 共 8 个字,国标明文 |
| 消息扩展字数 | 132 个 (W0..67 + W'0..63) | 每组扩展一次 |
| 压缩轮数 | 64 轮 | 每轮消耗 2 个消息字 |
| 循环左移位数 | 7,9,12,15,17,19 | 分布在扩展和压缩中 |
嗯,SM3 的原理就聊到这儿。记住:填充是地基,扩展是骨架,压缩是核心算法。把这三部分拆开实现、分开验证,基本不会出大问题。