2. SM3密码杂凑算法:从原理到落地
SM3,国密标准里的密码杂凑算法。说白了,它就是把任意长度的输入,揉成一个256位的固定输出。你想想看,数字签名、消息认证、密钥派生,哪个环节少得了它?
我最早接触SM3是在一个政务系统的改造项目里。客户要求从SHA-256迁移到国密,当时第一反应就是——这俩到底差在哪?性能会不会掉?踩过坑之后,今天我把这些经验掰开揉碎讲给你。
2.1 SM3算法原理:一句话讲清
SM3的核心逻辑,跟SHA-256很像,但细节上做了不少改动。它采用Merkle-Damgård结构,消息分组长度512位,输出256位摘要。
整个流程分三步:
- 消息填充:补位到512位的整数倍
- 迭代压缩:每512位一组,跑64轮压缩函数
- 输出摘要:最后8个32位字拼成256位结果
嗯,这里要注意——SM3的压缩函数里用了16轮异或逻辑,这是它和SHA-256最大的区别之一。后面我会细讲。
核心要点:SM3的压缩函数包含64轮迭代,每轮使用不同的消息字和常数。它的布尔函数和置换函数都经过专门设计,能抵抗差分攻击和线性攻击。
2.2 消息填充:别小看这一步
消息填充,听起来简单,但做错的人真不少。我曾经在代码审查时发现有人直接把消息末尾补零,结果摘要全错。
SM3的填充规则很明确:
- 先在消息末尾补一个'1'位
- 然后补k个'0'位,k满足 (l + 1 + k) ≡ 448 mod 512
- 最后附加64位的原始消息长度(大端序)
举个例子:假设原始消息是"abc",长度24位。填充后变成:
原始: 01100001 01100010 01100011
补1: 01100001 01100010 01100011 1
补0: ...后面补423个0...
加长度: 最后64位写入 0x00000000 00000018
我的习惯:处理填充时,我一般先算好总长度,再一次性分配缓冲区。避免逐位操作,性能会好很多。
2.3 迭代压缩:64轮到底在做什么
迭代压缩是SM3的心脏。每512位消息块进来,先扩展成132个32位字(W0~W67和W'0~W'63),然后跑64轮。
每轮做的事情:
- 用当前轮的消息字Wj和W'j
- 结合8个32位寄存器(A~H)
- 通过布尔函数FFj和GGj
- 再经过置换函数P0和P1
- 更新寄存器状态
你想想看,64轮下来,初始值被彻底打乱。这就是杂凑算法的本质——雪崩效应。
避坑指南:我曾经在实现时把布尔函数FFj的公式写反了。SM3的FFj在j≤15和j≥16时用的是不同逻辑,千万别搞混。建议写单元测试,用标准测试向量验证。
2.4 SM3 vs SHA-256:异同对比
很多人问我,SM3和SHA-256到底差在哪?我直接给你一张表:
| 对比项 | SM3 | SHA-256 |
|---|---|---|
| 输出长度 | 256位 | 256位 |
| 消息分组 | 512位 | 512位 |
| 迭代轮数 | 64轮 | 64轮 |
| 布尔函数 | FFj/GGj(分段不同) | Ch/Maj(统一) |
| 置换函数 | P0/P1(16位循环移位) | Σ0/Σ1(不同移位量) |
| 消息扩展 | W0~W67 + W'0~W'63 | W0~W63 |
| 安全强度 | 256位(国标) | 256位(国际) |
说白了,SM3在布尔函数和消息扩展上更复杂。它用了16轮异或来扩散消息,SHA-256只用4轮。代价是SM3软件实现比SHA-256慢10%~20%,但硬件实现差距不大。
我的观点:如果你的系统只在国内跑,用SM3完全够用。如果涉及跨境互认,可能需要同时支持两种算法。我在一个跨境支付项目里就做了双算法兼容,切换时注意摘要长度一致,接口设计要灵活。
2.5 SM3的代码实现:从零手写
下面我给出一份精简的SM3核心实现。注意,这是教学版本,生产环境建议用OpenSSL或GmSSL。
#include <stdint.h>
#include <string.h>
#define SM3_ROTL(x, n) (((x) << (n)) | ((x) >> (32 - (n))))
// 布尔函数
#define SM3_FF0(x, y, z) ((x) ^ (y) ^ (z))
#define SM3_FF1(x, y, z) (((x) & (y)) | ((x) & (z)) | ((y) & (z)))
#define SM3_GG0(x, y, z) ((x) ^ (y) ^ (z))
#define SM3_GG1(x, y, z) (((x) & (y)) | ((~x) & (z)))
// 置换函数
#define SM3_P0(x) ((x) ^ SM3_ROTL((x), 9) ^ SM3_ROTL((x), 17))
#define SM3_P1(x) ((x) ^ SM3_ROTL((x), 15) ^ SM3_ROTL((x), 23))
// 初始值
static const uint32_t SM3_IV[8] = {
0x7380166f, 0x4914b2b9, 0x172442d7, 0xda8a0600,
0xa96f30bc, 0x163138aa, 0xe38dee4d, 0xb0fb0e4e
};
// 64轮常数
static const uint32_t SM3_T[64] = {
0x79cc4519, 0x79cc4519, 0x79cc4519, 0x79cc4519,
0x79cc4519, 0x79cc4519, 0x79cc4519, 0x79cc4519,
0x79cc4519, 0x79cc4519, 0x79cc4519, 0x79cc4519,
0x79cc4519, 0x79cc4519, 0x79cc4519, 0x79cc4519,
0x7a879d8a, 0x7a879d8a, 0x7a879d8a, 0x7a879d8a,
0x7a879d8a, 0x7a879d8a, 0x7a879d8a, 0x7a879d8a,
0x7a879d8a, 0x7a879d8a, 0x7a879d8a, 0x7a879d8a,
0x7a879d8a, 0x7a879d8a, 0x7a879d8a, 0x7a879d8a,
0x7a879d8a, 0x7a879d8a, 0x7a879d8a, 0x7a879d8a,
0x7a879d8a, 0x7a879d8a, 0x7a879d8a, 0x7a879d8a,
0x7a879d8a, 0x7a879d8a, 0x7a879d8a, 0x7a879d8a,
0x7a879d8a, 0x7a879d8a, 0x7a879d8a, 0x7a879d8a,
0x7a879d8a, 0x7a879d8a, 0x7a879d8a, 0x7a879d8a,
0x7a879d8a, 0x7a879d8a, 0x7a879d8a, 0x7a879d8a,
0x7a879d8a, 0x7a879d8a, 0x7a879d8a, 0x7a879d8a,
0x7a879d8a, 0x7a879d8a, 0x7a879d8a, 0x7a879d8a
};
// 消息扩展
static void sm3_msg_expand(const uint8_t *block, uint32_t W[68], uint32_t Wp[64]) {
int j;
for (j = 0; j < 16; j++) {
W[j] = (block[j*4] << 24) | (block[j*4+1] << 16) |
(block[j*4+2] << 8) | block[j*4+3];
}
for (j = 16; j < 68; j++) {
W[j] = SM3_P1(W[j-16] ^ W[j-9] ^ SM3_ROTL(W[j-3], 15)) ^
SM3_ROTL(W[j-13], 7) ^ W[j-6];
}
for (j = 0; j < 64; j++) {
Wp[j] = W[j] ^ W[j+4];
}
}
// 压缩函数
static void sm3_compress(uint32_t V[8], const uint8_t *block) {
uint32_t W[68], Wp[64];
uint32_t A, B, C, D, E, F, G, H;
uint32_t SS1, SS2, TT1, TT2;
int j;
sm3_msg_expand(block, W, Wp);
A = V[0]; B = V[1]; C = V[2]; D = V[3];
E = V[4]; F = V[5]; G = V[6]; H = V[7];
for (j = 0; j < 64; j++) {
SS1 = SM3_ROTL((SM3_ROTL(A, 12) + E + SM3_ROTL(SM3_T[j], j)), 7);
SS2 = SS1 ^ SM3_ROTL(A, 12);
TT1 = (j < 16 ? SM3_FF0(A, B, C) : SM3_FF1(A, B, C)) + D + SS2 + Wp[j];
TT2 = (j < 16 ? SM3_GG0(E, F, G) : SM3_GG1(E, F, G)) + H + SS1 + W[j];
D = C;
C = SM3_ROTL(B, 9);
B = A;
A = TT1;
H = G;
G = SM3_ROTL(F, 19);
F = E;
E = SM3_P0(TT2);
}
V[0] ^= A; V[1] ^= B; V[2] ^= C; V[3] ^= D;
V[4] ^= E; V[5] ^= F; V[6] ^= G; V[7] ^= H;
}
调试建议:我习惯在每轮压缩后打印A~H的值,跟标准测试向量逐轮比对。这样定位问题最快。SM3的标准测试向量在GMT 0004-2012附录里可以找到。
2.6 SM3算法流程总览
下面这张图把SM3的完整流程画出来了。从消息填充到迭代压缩,再到最终输出,一目了然。
2.7 性能优化与工程实践
最后聊点工程上的经验。SM3的软件实现,性能瓶颈主要在消息扩展和64轮压缩。我做过一些优化:
- 查表法:把循环移位和布尔函数预计算,减少运行时开销
- SIMD指令:在x86上用SSE/AVX并行处理多个消息块
- 内存对齐:确保消息块按4字节对齐,避免非对齐访问
注意:SM3的硬件实现比软件快很多。如果你的系统对性能要求极高,建议用硬件加速模块。我在一个物联网网关项目里用过SM3硬件IP,吞吐量能到10Gbps以上。
好了,SM3的核心内容就这些。记住,理解原理是第一步,动手实现才是真功夫。建议你拿标准测试向量跑一遍,看看自己的实现对不对。