2. SM3密码杂凑算法:从原理到落地

SM3,国密标准里的密码杂凑算法。说白了,它就是把任意长度的输入,揉成一个256位的固定输出。你想想看,数字签名、消息认证、密钥派生,哪个环节少得了它?

我最早接触SM3是在一个政务系统的改造项目里。客户要求从SHA-256迁移到国密,当时第一反应就是——这俩到底差在哪?性能会不会掉?踩过坑之后,今天我把这些经验掰开揉碎讲给你。

2.1 SM3算法原理:一句话讲清

SM3的核心逻辑,跟SHA-256很像,但细节上做了不少改动。它采用Merkle-Damgård结构,消息分组长度512位,输出256位摘要。

整个流程分三步:

  1. 消息填充:补位到512位的整数倍
  2. 迭代压缩:每512位一组,跑64轮压缩函数
  3. 输出摘要:最后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的完整流程画出来了。从消息填充到迭代压缩,再到最终输出,一目了然。

SM3算法完整流程 原始消息 消息填充 补1位 + 补0位 + 附加长度 分组:每512位一块 迭代压缩(64轮) 消息扩展 → 布尔函数 → 置换函数 → 状态更新 256位摘要 说明 SM3采用Merkle-Damgård结构,输出256位摘要。安全强度与SHA-256相当,但布尔函数和消息扩展更复杂。

2.7 性能优化与工程实践

最后聊点工程上的经验。SM3的软件实现,性能瓶颈主要在消息扩展和64轮压缩。我做过一些优化:

  • 查表法:把循环移位和布尔函数预计算,减少运行时开销
  • SIMD指令:在x86上用SSE/AVX并行处理多个消息块
  • 内存对齐:确保消息块按4字节对齐,避免非对齐访问

注意:SM3的硬件实现比软件快很多。如果你的系统对性能要求极高,建议用硬件加速模块。我在一个物联网网关项目里用过SM3硬件IP,吞吐量能到10Gbps以上。

好了,SM3的核心内容就这些。记住,理解原理是第一步,动手实现才是真功夫。建议你拿标准测试向量跑一遍,看看自己的实现对不对。