SM3密码杂凑算法原理归纳总结
说实话,很多人刚接触国密算法时,总觉得SM3就是SHA‑256的复制品。我在项目里调试过这两个算法,细节上其实差别不小。今天咱们就掰开揉碎了聊聊SM3的核心原理。
3.1 SM3算法总体流程
SM3属于迭代型哈希函数,说白了就是把任意长度的输入消息,经过填充、分组、迭代压缩,最后输出一个256位的摘要。整个流程可以概括成四个步骤:
- 消息填充 – 补位 + 补长度,使消息是512位的整数倍
- 消息分组 – 按512位/64字节切块
- 迭代压缩 – 每块经过64轮运算,更新中间状态
- 输出摘要 – 最后的状态拼接成256位结果
我个人习惯把算法拆成“填充”和“压缩”两大块来理解。压缩那块最复杂,包含消息扩展和压缩函数。
核心要点:SM3的输出长度为256位(32字节),安全强度与SHA‑256相当,但内部设计有差异,尤其在布尔函数和置换运算上。
注意:SM3的初始值IV是固定的8个32位字,迭代过程中不会写入外部依赖。你如果在HSM中实现,一定要用硬件寄存器保存这些常数,别放RAM里,容易被侧信道攻击抓到规律。
3.2 消息填充:细节决定成败
SM3填充与SHA‑256非常相似,但我不建议直接复用。我曾经在初版硬件设计里偷懒,直接把SHA‑256的填充逻辑改了下长度字段,结果综合时发现高位截断问题。所以还是老老实实按标准来:
- 先在消息末尾补一个“1”比特
- 然后补“0”比特,直到长度 mod 512 = 448(即还剩64位留给长度)
- 最后附加64位原始消息长度(大端模式,单位比特)
举个简单的例子:
| 原始消息 | 填充后(十六进制) | 说明 |
|---|---|---|
| 0x61 (a) | 0x6180…00000008 | 补1加0,长度8 bits |
| 空消息 | 0x8000…00000000 | 比特长度0 |
你想想看,要是消息长度恰好是512的整数倍,是不是就不用填充了?不对,标准要求必须填充——即使长度已经对齐,也要再补一个完整分组。这样做是为了防止长度可变导致的碰撞。我见过有些软件实现漏填这种情况,测完向量能过,但实际用起来就出问题。
3.3 迭代压缩:64轮才是真功夫
SM3的压缩流程分两步:消息扩展和压缩函数。每进来一个分组,扩展模块会生成132个字(W[0]~W[67] 和 W'[0]~W'[63]),然后压缩函数用这些字循环更新8个工作寄存器。
3.3.1 消息扩展
扩展过程可以用下面几个公式概括:
W[j] = M[j] , 0 ≤ j ≤ 15
W[j] = P₁(W[j-16] ⊕ W[j-9] ⊕ (W[j-3] <<< 15)) ⊕
(W[j-13] <<< 7) ⊕ W[j-6] , 16 ≤ j ≤ 67
W'[j] = W[j] ⊕ W[j+4] , 0 ≤ j ≤ 63
这里P₁是置换函数:P₁(X) = X ⊕ (X <<< 15) ⊕ (X <<< 23)。我在FPGA上实现时,把W[j]的生成做成流水线,每周期出一个新值,64轮要67个周期才能把W[67]算完。为什么?因为W[67]供最后几轮使用,不能少。
3.3.2 压缩函数(轮函数)
每轮的处理可以看作是“以字为单位的非线性更新”。核心是三个布尔函数和两个置换:
| 轮次 | 布尔函数 FFj | 布尔函数 GGj |
|---|---|---|
| 0 ≤ j ≤ 15 | FF = X⊕Y⊕Z | GG = X⊕Y⊕Z |
| 16 ≤ j ≤ 63 | FF = (X∧Y)∨(X∧Z)∨(Y∧Z) | GG = (X∧Y)∨(¬X∧Z) |
你可能注意到了,前16轮是纯异或,后48轮变成了“多数投票”和“选择”逻辑。这样设计的好处是在前几轮快速扩散比特,后面则增强抗碰撞能力。嗯,这里要注意,置换P₀在每轮最后用到:
P₀(X) = X ⊕ (X <<< 9) ⊕ (X <<< 17)
小技巧:硬件实现时,布尔函数FF/GG都可以用LUT或简单逻辑门完成,但P₀的循环移位要特别注意——因为SM3用的是32位字,移位量9和17都不满32,用组合逻辑就能搞定。我在一个安全芯片项目里把P₀和消息扩展的P₁合并成一个模块,省了不少门电路,时序也没崩。
3.4 与SHA‑256的关键差异
很多工程师问过我:既然SM3和SHA‑256都叫256位哈希,那区别在哪?我列个表你就明白了:
| 对比项 | SM3 | SHA‑256 |
|---|---|---|
| 消息扩展反馈深度 | 16→68字,且使用P₁ | 16→64字,无置换 |
| 布尔函数种类 | 2种(FF/GG,分前后) | 2种(Ch/Maj,固定) |
| 置换操作 | P₀、P₁各一个 | 无(用σ函数) |
| 常数个数 | 64个(含j的线性函数) | 64个(固定素数小数部分) |
| 初始值IV | 0x7380166f… | 0x6a09e667… |
说白了一个最大的感受:SM3的扩散性比SHA‑256更强,因为多了一层P₁和P₀置换。但也因为这个,硬件实现时关键路径会比SHA‑256长一点点。我在HSM设计里把压缩函数的关键路径拆成两级流水,这样能跑到400MHz以上。
3.5 实践中的避坑指南
最后分享两个我踩过的坑:
- 字节序陷阱:SM3标准规定所有多字节数据采用大端序。很多嵌入式系统默认小端,转换时容易忘。我以前在填充长度字段时忘做大小端转换,结果算出的摘要只有前几个字节是对的,后面全错。
- 寄存器初始化:8个工作寄存器A~H必须在每个新的消息分组开始时拷贝当前IV或上一轮结果,千万不要误用压缩函数内部的临时值。我在一个早期RTL版本里,把中间变量直接当新一轮初始状态送了进去,导致一个分组算出来就漂移了。
总结:SM3的算法结构清晰,但细节决定安全性。作为硬件设计者,建议把填充、消息扩展和压缩函数做成独立的子模块,这样便于测试和优化。记住:国密算法的正确实现不仅靠标准文档,还得靠反复的向量验证。