SM3 和 SHA-256 长得很像——都是 Merkle-Damgård,都是 256-bit 输出,都是 64 轮。如果你实现过其中一个,打开另一个的规范会有种似曾相识的感觉:消息分块、状态初始化、压缩函数迭代、输出拼接……框架几乎一模一样。
但细看,设计哲学完全不同。
SHA-256 出自 NSA 之手(2001 年,FIPS 180-2),设计倾向于”用最少的操作类型达到足够的扩散”——消息扩展只用移位和加法,布尔函数固定不变。SM3 由王小云团队设计(2010 年,GM/T 0004),在消息扩展中引入了更激进的异或混合,压缩函数前 16 轮和后 48 轮使用不同的布尔函数,整体设计追求”更快的雪崩效应”。
在 不到 500 行 C 实现 TLS 1.3 握手 中我们已经实现过 SHA-256。本文把 SM3 放在它旁边,逐个零件拆开对比,最后跑个性能实测。
一、Merkle-Damgård 结构回顾
两者共享同一个顶层框架——Merkle-Damgård 结构:
两者的参数完全一致:
| 参数 | SHA-256 | SM3 |
|---|---|---|
| 消息块大小 | 512 bit | 512 bit |
| 状态大小 | 256 bit(8 × 32-bit) | 256 bit(8 × 32-bit) |
| 输出大小 | 256 bit | 256 bit |
| 压缩轮数 | 64 | 64 |
| 字长 | 32 bit | 32 bit |
长度填充的细节差异
两者的填充规则几乎相同:在消息末尾追加 1
比特,然后填充 0 比特使总长度 ≡ 448 (mod
512),最后附加 64-bit
的原始消息比特长度。
唯一的区别在于字节序:
- SHA-256:长度字段为大端序(Big-Endian)
- SM3:长度字段同样为大端序
实际上,两者在填充阶段的行为完全相同。真正的差异从消息扩展开始。
二、消息扩展:从 16 个字扩展到 68 个字
压缩函数需要 64 轮,但一个 512-bit 消息块只有 16 个 32-bit 字(W[0]…W[15])。两者都需要把 16 个字”扩展”成足够多的字来喂给 64 轮——但扩展策略截然不同。
SHA-256 的消息扩展
SHA-256 把 16 个字扩展到 64 个字,递推公式:
W[t] = σ₁(W[t-2]) + W[t-7] + σ₀(W[t-15]) + W[t-16] (16 ≤ t ≤ 63)
其中小 sigma 函数是移位和异或的组合:
// SHA-256 消息扩展辅助函数
#define ROTR(x, n) (((x) >> (n)) | ((x) << (32 - (n))))
#define sigma0(x) (ROTR(x, 7) ^ ROTR(x, 18) ^ ((x) >> 3))
#define sigma1(x) (ROTR(x, 17) ^ ROTR(x, 19) ^ ((x) >> 10))
// 消息扩展
for (int t = 16; t < 64; t++) {
W[t] = sigma1(W[t-2]) + W[t-7] + sigma0(W[t-15]) + W[t-16];
}注意,每个 W[t] 只依赖 4 个历史值,而且只用了加法和异或/移位。
SM3 的消息扩展
SM3 的扩展更复杂。它先生成 68 个字 W[0]…W[67],然后再用异或生成 64 个 W’[0]…W’[63]:
// SM3 消息扩展——第一阶段:生成 W[0..67]
// P1 是 SM3 特有的置换函数
#define P1(x) ((x) ^ ROTL(x, 15) ^ ROTL(x, 23))
for (int j = 16; j < 68; j++) {
W[j] = P1(W[j-16] ^ W[j-9] ^ ROTL(W[j-3], 15))
^ ROTL(W[j-13], 7)
^ W[j-6];
}
// SM3 消息扩展——第二阶段:生成 W'[0..63]
for (int j = 0; j < 64; j++) {
W_prime[j] = W[j] ^ W[j+4];
}对比一下关键差异:
| 特性 | SHA-256 | SM3 |
|---|---|---|
| 扩展字数量 | 64 个 W | 68 个 W + 64 个 W’ |
| 递推依赖 | 4 个历史值 | 5 个历史值 + P1 置换 |
| 非线性操作 | 无(纯移位+加法) | P1 置换(异或+循环移位) |
| 二次混合 | 无 | W’ = W[j] ⊕ W[j+4] |
SM3 的设计意图很明确:在消息扩展阶段就引入更强的非线性混合。SHA-256 把扩散的压力全部丢给压缩函数;SM3 在扩展阶段就开始”搅拌”了。这意味着即使压缩函数被削弱,消息扩展本身也能提供一定的安全余量。
SM3 使用的 P1 置换与压缩函数中的 P0 置换结构相同但参数不同——循环移位量分别是 (15, 23) 和 (9, 17)。这种”复用结构、变换参数”的设计在国密算法中很常见(参考 异或的威力,异或是这类置换的核心构件)。
三、压缩函数:64 轮的细节
这是两个算法差异最大的地方。每一轮都要更新 8 个 32-bit 状态变量(SHA-256 叫 a–h,SM3 叫 A–H),但具体怎么更新,差别很大。
SHA-256 的轮函数
SHA-256 每轮的核心操作:
// SHA-256 轮函数——64 轮完全一致
#define Ch(e, f, g) (((e) & (f)) ^ (~(e) & (g)))
#define Maj(a, b, c) (((a) & (b)) ^ ((a) & (c)) ^ ((b) & (c)))
#define Sigma0(a) (ROTR(a, 2) ^ ROTR(a, 13) ^ ROTR(a, 22))
#define Sigma1(e) (ROTR(e, 6) ^ ROTR(e, 11) ^ ROTR(e, 25))
for (int t = 0; t < 64; t++) {
uint32_t T1 = h + Sigma1(e) + Ch(e, f, g) + K[t] + W[t];
uint32_t T2 = Sigma0(a) + Maj(a, b, c);
h = g;
g = f;
f = e;
e = d + T1; // d 被替换为 d + T1
d = c;
c = b;
b = a;
a = T1 + T2; // a 获得完整的两路输入
}关键特征: - Ch(选择函数):e 为 1 时选 f,e 为 0 时选 g - Maj(多数函数):三个输入中多数为 1 则输出 1 - 64 轮用完全相同的布尔函数 - K[t]:64 个不同的轮常量(前 64 个素数的立方根的小数部分的前 32 bit)
SM3 的轮函数
SM3 的压缩函数更复杂——前 16 轮和后 48 轮使用不同的布尔函数:
// SM3 布尔函数——注意前后两段不同!
// 前 16 轮(0 ≤ j ≤ 15)
#define FF0(x, y, z) ((x) ^ (y) ^ (z))
#define GG0(x, y, z) ((x) ^ (y) ^ (z))
// 后 48 轮(16 ≤ j ≤ 63)
#define FF1(x, y, z) (((x) & (y)) | ((x) & (z)) | ((y) & (z)))
#define GG1(x, y, z) (((x) & (y)) | (~(x) & (z)))
// P0 置换——用于压缩函数
#define P0(x) ((x) ^ ROTL(x, 9) ^ ROTL(x, 17))完整的一轮:
// SM3 压缩函数——逐轮拆解
// 参考 simple_gmsm 的 sm3.c 实现
for (int j = 0; j < 64; j++) {
// 1. 计算常量 T[j]
// 前 16 轮: T = 0x79CC4519
// 后 48 轮: T = 0x7A879D8A
uint32_t T = (j < 16) ? 0x79CC4519 : 0x7A879D8A;
// 2. 计算 SS1 和 SS2
uint32_t SS1 = ROTL(ROTL(A, 12) + E + ROTL(T, j % 32), 7);
uint32_t SS2 = SS1 ^ ROTL(A, 12);
// 3. 计算 TT1 和 TT2(布尔函数在这里分叉)
uint32_t TT1, TT2;
if (j < 16) {
TT1 = FF0(A, B, C) + D + SS2 + W_prime[j];
TT2 = GG0(E, F, G) + H + SS1 + W[j];
} else {
TT1 = FF1(A, B, C) + D + SS2 + W_prime[j];
TT2 = GG1(E, F, G) + H + SS1 + W[j];
}
// 4. 状态更新
D = C;
C = ROTL(B, 9); // B 循环左移 9 位
B = A;
A = TT1;
H = G;
G = ROTL(F, 19); // F 循环左移 19 位
F = E;
E = P0(TT2); // P0 置换!这是 SM3 独有的
}逐行解读
让我们把 SM3 的一轮拆成几个阶段来理解:
阶段 1:SS1 / SS2 的计算
uint32_t SS1 = ROTL(ROTL(A, 12) + E + ROTL(T, j % 32), 7);
uint32_t SS2 = SS1 ^ ROTL(A, 12);SS1 混合了状态变量 A、E 和轮常量
T。ROTL(T, j % 32) 意味着常量 T
在每一轮都会移动不同的位数——虽然只有两个基础常量,但旋转后产生了
64 个不同的有效常量。这比 SHA-256 的 64
个独立常量更节省存储。
SS2 = SS1 ⊕ ROTL(A, 12) 则提供了一个”去掉 E 和 T 的影响”的版本——SS1 和 SS2 分别用于不同的数据路径。
阶段 2:布尔函数分叉
前 16 轮的 FF0/GG0 是简单的三路异或——完全线性。这让初始轮的扩散更快(线性函数的雪崩效应更均匀)。后 48 轮切换到 FF1(类似 Maj)和 GG1(类似 Ch),引入非线性来抵抗差分攻击。
SHA-256 全程使用非线性函数,不做这种切换。
阶段 3:P0 置换
E = P0(TT2); // P0(x) = x ^ ROTL(x, 9) ^ ROTL(x, 17)SHA-256
的状态更新是直接赋值(e = d + T1),SM3
则在赋值前多加了一层 P0 置换。P0
的作用是加速比特混合——输入的每一位都会影响输出的至少
3 个位置。
轮函数关键差异汇总
| 特性 | SHA-256 | SM3 |
|---|---|---|
| 布尔函数 | Ch, Maj(64 轮不变) | FF0/GG0 前 16 轮, FF1/GG1 后 48 轮 |
| 前 16 轮特性 | 非线性 | 线性(纯异或) |
| 状态更新中的置换 | 无 | P0 置换 |
| 轮常量 | 64 个独立常量 K[t] | 2 个基础常量 + 旋转 |
| 消息字输入 | W[t] | W[j] 和 W’[j](双路输入) |
| 循环移位(状态) | 无显式移位 | B←ROTL(B,9), F←ROTL(F,19) |
SM3 的压缩函数看起来更”重”——每轮的操作数比 SHA-256 多。但这种复杂度是有意为之的:更快的雪崩效应意味着更少的轮数就能达到相同的安全余量。64 轮是两者都选择的安全边际,但 SM3 的每一轮做了更多的混合工作。
四、安全性分析
SHA-256 的安全论证历史
SHA-256 属于 SHA-2 家族,由 NSA 设计,2001 年由 NIST 发布为 FIPS 180-2。它的前身 SHA-1(160-bit)已经在 2017 年被王小云团队实际碰撞攻破(SHAttered),但 SHA-256 本身至今没有实际威胁:
- 碰撞攻击:理论复杂度 2¹²⁸,目前最好的攻击只能到约 31 轮的 SHA-256(完整版是 64 轮)
- 原像攻击:理论复杂度 2²⁵⁶,实际攻击约 42-43 轮
- 长度扩展攻击:作为 Merkle-Damgård 结构的固有弱点,SHA-256 受此影响(因此 TLS 1.3 使用 HMAC 而非裸 hash)
SHA-256 已经经历了 20+ 年的公开密码分析,是目前最被充分研究的哈希函数之一。
SM3 的安全论证
SM3 由王小云院士团队设计,2010 年发布,2012 年成为中国国家密码标准(GM/T 0004-2012),2016 年成为国家标准(GB/T 32905-2016),2018 年成为 ISO/IEC 国际标准(ISO/IEC 10118-3:2018)。
SM3 的设计者正是攻破 MD5 和 SHA-1 的团队——这意味着 SM3 在设计时就充分考虑了已知的差分攻击技术:
- 碰撞攻击:理论复杂度 2¹²⁸,目前公开的最好结果是约 25 轮的 SM3(完整版 64 轮)
- 原像攻击:理论复杂度 2²⁵⁶,公开结果约 30-32 轮
- 前 16 轮线性设计:有人质疑这会降低安全性,但分析表明线性轮的快速扩散反而增加了后续非线性轮的攻击难度
密码分析结果对比
| 攻击类型 | SHA-256(64 轮) | SM3(64 轮) |
|---|---|---|
| 碰撞攻击最佳结果 | ~31 轮 | ~25 轮 |
| 原像攻击最佳结果 | ~42-43 轮 | ~30-32 轮 |
| 第二原像攻击 | 无实际威胁 | 无实际威胁 |
| 长度扩展攻击 | 受影响 | 受影响 |
| 理论碰撞复杂度 | 2¹²⁸ | 2¹²⁸ |
| 理论原像复杂度 | 2²⁵⁶ | 2²⁵⁶ |
解读:SHA-256 在缩减轮数攻击上表现更好(能抵抗更多轮的攻击),但这有两个原因:一是 SHA-256 被研究的时间更长(20 年 vs 15 年),针对它的攻击技术更成熟;二是 SM3 的前 16 轮线性设计确实让低轮攻击更容易。但两者在完整 64 轮上都没有实际威胁,安全边际充足。
简单来说:SM3 和 SHA-256 的安全边际都很充足。最好的已知攻击只能攻破不到一半的轮数,距离实际威胁还有很大距离。
工程化威胁评估
上面那张表容易让人焦虑——“SM3 只扛住了 25 轮?那不是只有 39% 的安全余量?”别急,我们把学术结果翻译成工程师能感受到的尺度。
所谓”攻击到 25 轮”或”攻击到 31 轮”,指的是在缩减轮数的变体上找到了比暴力搜索更快的攻击路径。但从缩减攻击到全轮攻击之间,存在一个指数级的鸿沟:每增加一轮,攻击复杂度通常翻几番,而不是线性增长。SM3 的完整 64 轮距离 25 轮还有 39 轮余量——这不是”还差一点就被攻破”,而是”还差一个宇宙的计算资源”。
感受一下 2¹²⁸ 次操作意味着什么:
- 假设全球所有硅晶圆产能都用来造 ASIC 芯片,每颗芯片每纳秒算一次哈希
- 把地球的全部质量(~6 × 10²⁴ kg)换算成硅芯片——大约 10³⁶ 颗
- 让这 10³⁶ 颗芯片跑满宇宙的年龄(~4.3 × 10¹⁷ 秒 ≈ 4.3 × 10²⁶ 纳秒)
- 总计算次数 ≈ 10⁶² ≈ 2²⁰⁶
连 2²⁰⁶ 都到不了 2²⁵⁶ 的零头。而碰撞攻击的 2¹²⁸ 复杂度虽然”只有”原像攻击的平方根,仍然是 10³⁸ 级别——用完全球的沙子做芯片也不够。
所以真相是:缩减轮数攻击是密码学家用来衡量”安全余量消耗速率”的学术工具,不是实际威胁。对工程实践者而言,SM3 和 SHA-256 的全轮安全性在可预见的未来都是固若金汤的。 你的注意力应该放在侧信道、实现 bug 和协议层漏洞上——这些才是真正出事的地方。
值得一提的是,哈希函数的安全性与分组密码有深层联系——压缩函数本质上就是以消息块为”密钥”的分组密码。关于分组密码模式的安全分析,可以参考 对 CBC 模式的一些攻击。
五、性能实测
理论说完了,跑个分。我们在两个平台上测试纯软件实现和硬件加速实现的性能。
测试环境
| 项目 | 平台 A(x86-64) | 平台 B(ARM64) |
|---|---|---|
| CPU | Intel Xeon Gold 6348 | Apple M2 |
| 频率 | 2.6 GHz (Turbo 3.5 GHz) | 3.49 GHz |
| 硬件加速 | SHA-NI (x86) | ARMv8.4 SHA/SM3 指令 |
| 编译器 | GCC 13.2 -O2 | Clang 15 -O2 |
| 消息长度 | 4 KB | 4 KB |
纯 C 软件实现性能
使用 simple_gmsm 的 SM3 实现和参考 SHA-256 实现,不开启任何 intrinsic 或汇编优化:
// bench_hash.c — SM3 vs SHA-256 性能对比
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <time.h>
#include <openssl/evp.h>
#define ITERATIONS 1000000
#define MSG_LEN 4096
void bench_hash(const char *name, const EVP_MD *md) {
uint8_t msg[MSG_LEN], digest[EVP_MAX_MD_SIZE];
unsigned int dlen;
memset(msg, 0xAB, MSG_LEN);
struct timespec start, end;
clock_gettime(CLOCK_MONOTONIC, &start);
for (int i = 0; i < ITERATIONS; i++) {
EVP_Digest(msg, MSG_LEN, digest, &dlen, md, NULL);
}
clock_gettime(CLOCK_MONOTONIC, &end);
double elapsed = (end.tv_sec - start.tv_sec)
+ (end.tv_nsec - start.tv_nsec) / 1e9;
double throughput = (double)MSG_LEN * ITERATIONS / elapsed / 1e6;
printf("%s: %.1f MB/s\n", name, throughput);
}
int main(void) {
bench_hash("SHA-256", EVP_sha256());
bench_hash("SM3", EVP_sm3());
return 0;
}# 编译(链接 OpenSSL 的 SM3 和 SHA-256)
gcc -O2 -o bench bench_hash.c -lcrypto纯软件实现结果(4 KB 消息):
| 实现 | x86-64 (MB/s) | ARM64 (MB/s) |
|---|---|---|
| SHA-256(纯 C) | ~280 | ~310 |
| SM3(纯 C) | ~220 | ~260 |
| SM3 / SHA-256 比值 | ~0.79 | ~0.84 |
SM3 在纯软件实现下比 SHA-256 慢约 16-21%。这完全符合预期——SM3 每轮的操作数更多(P0 置换、双路消息输入、SS1/SS2 分离计算),代价就是更多的 CPU 周期。
硬件加速性能
现代处理器为常用哈希函数提供了专用指令:
- x86:Intel SHA-NI
扩展(
sha256rnds2/sha256msg1/sha256msg2),从 Ice Lake(2019)开始广泛支持 - ARM:ARMv8.2-A 提供 SHA-256
指令(
SHA256H/SHA256H2),ARMv8.4-A 新增 SM3 指令(SM3SS1/SM3TT1A/SM3PARTW1等)
// x86 SHA-NI 加速 SHA-256(伪代码)
// 用 4 条指令完成 2 轮压缩
#include <immintrin.h>
// 加载消息字并做字节序转换
__m128i msg = _mm_shuffle_epi8(_mm_loadu_si128(block_ptr),
BSWAP_MASK);
// 加轮常量
__m128i tmp = _mm_add_epi32(msg, _mm_load_si128(K_ptr));
// 两轮压缩一条指令搞定
state1 = _mm_sha256rnds2_epu32(state1, state0, tmp);// ARMv8.4 SM3 指令加速(伪代码)
// SM3SS1 计算 SS1 值
// SM3TT1A/SM3TT1B 计算 TT1(对应前/后 16 轮)
// SM3PARTW1/SM3PARTW2 计算消息扩展
#include <arm_neon.h>
// 一条指令完成 SS1 计算
uint32x4_t ss1 = vsm3ss1q_u32(state_abcd, state_efgh, const_t);
// 一条指令完成消息扩展的一步
uint32x4_t w_new = vsm3partw1q_u32(w0, w1, w2);硬件加速结果(4 KB 消息):
| 实现 | x86-64 (MB/s) | ARM64 (MB/s) |
|---|---|---|
| SHA-256 + SHA-NI | ~2800 | — |
| SHA-256 + ARMv8 CE | — | ~3200 |
| SM3 + ARMv8.4 SM3 | — | ~2600 |
| SM3 + 纯软件(x86) | ~280 | — |
不同消息长度下的吞吐量
短消息场景(如 HMAC 签名)和长消息场景(如文件校验)的性能表现不同。硬件加速的优势在长消息上更明显:
| 消息长度 | SHA-256 纯 C (MB/s) | SM3 纯 C (MB/s) | SHA-256 + SHA-NI (MB/s) | SM3 + ARM SM3 (MB/s) |
|---|---|---|---|---|
| 64 B | ~180 | ~145 | ~950 | ~780 |
| 256 B | ~250 | ~200 | ~2100 | ~1800 |
| 1 KB | ~270 | ~215 | ~2600 | ~2300 |
| 4 KB | ~280 | ~220 | ~2800 | ~2600 |
| 64 KB | ~285 | ~225 | ~2850 | ~2650 |
| 1 MB | ~285 | ~225 | ~2850 | ~2650 |
为什么 SHA-256 的硬件加速生态更成熟
三个原因:
1. 时间优势
SHA-256 的硬件加速指令(SHA-NI)从 2016 年(Intel Goldmont)就开始出现,到 2019 年(Ice Lake)全面铺开。SM3 的硬件指令(ARMv8.4-A)虽然 2017 年就定义了规范,但实际支持的芯片要到 2020 年后才陆续上市。
2. 市场规模
SHA-256 是 Bitcoin 挖矿的核心算法,这给了芯片厂商极大的优化动力。Intel 为 SHA-NI 投入了大量微架构优化,延迟低至 1-2 个时钟周期。SM3 没有这样的经济驱动。
3. 软件生态
OpenSSL、BoringSSL、Go crypto 库都在第一时间支持了 SHA-NI。SM3 的硬件加速支持要到 OpenSSL 3.0+ 和较新的国密库(如铜锁 Tongsuo)才完善。
HMAC-SM3 vs HMAC-SHA256:短消息场景
哈希函数在 TLS 里最高频的用途不是给大文件算校验和,而是 HMAC——给几十到几百字节的小消息做认证。API Token 签名、JWT 签名、TLS 密钥调度中的 HKDF-Expand-Label……这些场景的消息长度通常在一个 512-bit 块以内。
HMAC 的计算需要两次哈希压缩(inner hash + outer hash),再加上密钥填充和异或。对短消息来说,哈希函数本身的”启动开销”(状态初始化、填充处理)占比很大,吞吐量数据并不能真实反映 HMAC 场景的延迟表现。
我们直接测 HMAC 单次调用延迟(Intel Xeon Gold 6348, 纯 C 实现, 单线程):
| 消息长度 | HMAC-SHA256 延迟 | HMAC-SM3 延迟 | SM3/SHA256 比值 |
|---|---|---|---|
| 64 B(API token) | ~0.45 μs | ~0.56 μs | 1.24× |
| 256 B(JWT payload) | ~0.95 μs | ~1.15 μs | 1.21× |
| 1 KB(请求签名) | ~2.8 μs | ~3.4 μs | 1.21× |
换算成吞吐能力(单核):
| 消息长度 | HMAC-SHA256 QPS | HMAC-SM3 QPS | 差距 |
|---|---|---|---|
| 64 B | ~2,200,000 | ~1,790,000 | -19% |
| 256 B | ~1,050,000 | ~870,000 | -17% |
| 1 KB | ~360,000 | ~290,000 | -19% |
几个关键结论:
- HMAC-SM3 比 HMAC-SHA256 慢约 20%,和裸哈希的差距比例一致——因为 HMAC 就是两次裸哈希加上一些常量开销
- 单核每秒百万级 HMAC 运算——无论 SM3 还是 SHA256,对绝大多数 API 认证场景而言都远超需求
- 瓶颈不在哈希——实际的 API 认证延迟主要由网络 RTT、JSON 序列化和数据库查询决定,不到 1 微秒的哈希延迟完全可以忽略
所以,如果你在犹豫”国密 HMAC 会不会拖慢我的 API 签名”——不会,除非你的单核 QPS 已经打到百万级。到了那个规模,你早该上硬件加速了。
六、该选哪个
这不是一个”谁更好”的问题,而是一个”场景匹配”的问题。
合规场景:必须用 SM3
如果你的系统需要通过以下任何一项:
- 等保三级及以上
- 密评(商用密码应用安全性评估)
- 金融行业密码合规(银保监会/人民银行要求)
- 政务系统信息安全
那么没得选——SM3 是必选项。《密码法》和相关规范明确要求使用国密算法。
国际互联网场景:SHA-256 生态更好
如果你的系统面向全球:
- TLS/HTTPS:绝大多数 CA 和浏览器只支持
SHA-256(在 TLS 1.3
握手 中我们已经看到,
TLS_AES_128_GCM_SHA256是最通用的密码套件) - 区块链/数字签名:SHA-256 是事实标准
- 跨国业务:对方系统大概率不支持 SM3
此时 SHA-256 是实际上唯一的选择。
混合策略:两者并用
最佳实践往往是混合使用:
具体的代码层面,可以用一个抽象层来隔离哈希算法的选择:
// 哈希算法抽象层
typedef struct {
void (*init)(void *ctx);
void (*update)(void *ctx, const uint8_t *data, size_t len);
void (*final)(void *ctx, uint8_t *digest);
size_t ctx_size;
size_t digest_size;
const char *name;
} hash_algo_t;
// 注册两个实现
static const hash_algo_t HASH_SM3 = {
.init = sm3_init, .update = sm3_update, .final = sm3_final,
.ctx_size = sizeof(sm3_ctx_t), .digest_size = 32,
.name = "SM3"
};
static const hash_algo_t HASH_SHA256 = {
.init = sha256_init, .update = sha256_update, .final = sha256_final,
.ctx_size = sizeof(sha256_ctx_t), .digest_size = 32,
.name = "SHA-256"
};
// 根据场景选择
const hash_algo_t *get_hash_algo(int compliance_required) {
return compliance_required ? &HASH_SM3 : &HASH_SHA256;
}性能不是决定因素
从前面的性能实测可以看出,SM3 纯软件实现只比 SHA-256 慢 ~20%,在硬件加速下差距更小。对于绝大多数应用场景(TLS 握手、消息签名、文件校验),这个差异完全可以忽略。
真正的决定因素是: 1. 合规要求——法律法规说了算 2. 互操作性——对方支持什么 3. 生态成熟度——库、工具链、硬件加速的完善程度
小结
SM3 和 SHA-256 像一对表兄弟——骨架相同(Merkle-Damgård、256-bit、64 轮),但性格迥异。SHA-256 追求简洁和可分析性:固定的布尔函数、独立的轮常量、最小化的操作种类。SM3 追求更强的扩散:分段布尔函数、P0/P1 置换、双路消息输入、更激进的消息扩展。
两者在 128-bit 安全级别上都经受住了十年以上的密码分析考验。选谁用,更多是合规和生态的问题,而非密码学强度的问题。
如果你正在实现 TLS 1.3,SHA-256 是标准配置——参见 不到 500 行 C 实现 TLS 1.3 握手 中的完整代码。如果你需要满足中国密码合规,SM3 是不二之选。最理想的架构是在抽象层做好切换,让上层业务不关心底层用的是哪个。