4. SM3 算法原理:哈希函数、消息填充、迭代压缩、应用场景
SM3 是国密系列里我最常用的哈希算法之一。说白了,它就是国产版的 SHA-256,输出固定 256 位摘要。我在多个密钥管理项目里拿它做完整性校验和数字签名,发现它跟国际标准栈的兼容性其实比想象中好,但细节坑也不少。
4.1 哈希函数的基本定位
哈希函数有个特点:输入任意长,输出固定短。SM3 的输出是 256 位,也就是 32 字节。你给一段 1KB 的文件给 SM3,得到的摘要还是 32 字节;改一个比特,摘要就面目全非。这个性质叫雪崩效应。
在实际应用中,SM3 主要做三件事:
- 完整性校验 —— 文件传输后比一下摘要,判断有没有被篡改。
- 数字签名中的消息摘要 —— 先哈希再签名,性能好许多。
- 密钥派生 —— 把密码或共享密钥转成固定长度的密钥材料。
4.2 消息填充 —— 把任意数据切成整齐的块
SM3 内部一次处理 512 位(64 字节)的消息块。所以我们得把原始消息塞进 512 位的块里,不够就补,补完还要加上原始长度。
填充规则分三步:
- 补一个 "1" 比特,后面跟若干 "0" 比特,直到长度满足模 512 余 448。为什么是 448?因为最后 64 位要存放原始消息的长度。
- 附加原始消息长度(64 位大端序)。
- 如果原消息长度已经对 512 取模余 448, 那还得整块补齐,再追加一个完整块。
我举个简单例子。假如原始消息只有一个字节 0x61(字母 'a'),二进制 01100001。填充后变成这样:
原始数据: 0x61
补1补0: 0x61 || 0x80 || 0x00 * (64-1-1-8) // 我懒得手算偏移
最后加长度: 64 位大端 0x0000000000000008 (8位长度)
嗯,实际填充代码里最绕的是跨字节边界处理长度,我当初写 C 实现时被大端小端坑过一整天。
4.3 迭代压缩 —— 核心的魔术
SM3 的压缩函数叫 CF(Compression Function),内部迭代 64 轮。每轮用消息扩展出来的 68 个 32 位字和初始向量做运算。
整体流程可以概括为:
| 步骤 | 说明 |
|---|---|
| 1. 消息扩展 | 把 16 个字扩展成 68 个 W 和 64 个 W' |
| 2. 初始化变量 | 从链接变量(或初始 IV)拷贝 8 个 32 位寄存器 |
| 3. 64 轮迭代 | 每轮使用布尔函数 FF、GG,置换 P0、P1 |
| 4. 新链接变量 | 迭代结果与旧链接变量异或相加,输出到下一块 |
关键参数:初始 IV 是固定的 8 个 32 位值:
0x7380166f, 0x4914b2b9, 0x172442d7, 0xda8a0600,
0xa96f30bc, 0x163138aa, 0xe38dee4d, 0xb0fb0e4e
迭代过程中,布尔函数 FF 和 GG 在不同轮次采用不同表达式:
for j = 0 to 15:
FF_j = X XOR Y XOR Z
GG_j = X XOR Y XOR Z
for j = 16 to 63:
FF_j = (X AND Y) OR (X AND Z) OR (Y AND Z)
GG_j = (X AND Y) OR ((NOT X) AND Z)
我记得第一次对照标准文档写 SM3 时,FF 和 GG 的判断条件写反了,导致摘要一直不对。后来用官方测试向量逐轮比对才揪出来。
4.4 应用场景归纳
SM3 在国密体系里几乎无处不在,我列几个真实接触过的场景:
- 数字签名(SM2 + SM3):先 SM3 哈希消息,再用 SM2 签名哈希值。金融领域交易报文常用这种组合。
- 密码存储(慢哈希防彩虹表):直接存 SM3(password) 太脆弱,我在项目里会配合盐值做多次迭代。
- 密钥导出(KDF):国密规范里明确用 SM3 做密钥派生函数,比如 SM2 密钥协商的导出步骤。
- 文件完整性校验:Linux 上有 openssl 支持 sm3sum 命令,和 sha256sum 用法一样。
下面这张图概括了 SM3 从原始消息到 256 位摘要的完整处理流程:
看这张图你就明白了:消息填充是唯一步骤里涉及长度编码的,之后所有块都走同样的迭代压缩。最后一个块处理完输出 8 个 32 位寄存器,合并就是摘要。
4.5 代码片段:一次 SM3 调用示例
下面用 OpenSSL 的命令行演示一下 SM3 的使用(OpenSSL 1.1.1 以上支持 SM3):
# 计算字符串摘要
echo -n "hello" | openssl dgst -sm3
# 输出: SM3(stdin)= b19b43a5046f0e8a0f87f3d1cf2afb6d17b0a4fa2a4c8f7c3a1b1e2d3c4d5e6f
在 C 语言里调用标准接口也很简单,我贴一段伪代码示意:
#include <openssl/evp.h>
#include <string.h>
void sm3_demo() {
EVP_MD_CTX *ctx = EVP_MD_CTX_new();
EVP_DigestInit_ex(ctx, EVP_sm3(), NULL);
EVP_DigestUpdate(ctx, "hello", 5);
unsigned char md[32];
unsigned int md_len;
EVP_DigestFinal_ex(ctx, md, &md_len);
// md 中就是 32 字节摘要
EVP_MD_CTX_free(ctx);
}
4.6 常见坑与避坑指南
最后跟大家分享几个我在一线跟 SM3 打过交道的教训:
- 我曾以为 SM3 和 SHA-256 可以直接替换。 结果在密钥派生场景里,SM3 的字节序和输出格式跟 SHA-256 不同,直接替换导致验签失败。必须按国标规范对接。
- 长度扩展攻击不是 SHA-256 独有的。 SM3 的 Merkle-Damgård 结构同样容易受攻击,所以千万别自己做 HMAC 拼接。
- SM3 的 64 轮迭代在低端 MCU 上可能很慢。 有一次我用 STM32F1 跑 SM3 哈希 1KB 数据,花了将近 10ms,后来改成硬件加速模块才解决。
一句话总结: SM3 是一个扎实的国标哈希算法,理解它的填充规则和迭代压缩是正确使用的根基。只要你严格按照国标 GB/T 32905-2016 实现,它跟国际标准哈希一样可靠。