土法炼钢兴趣小组的算法知识备份

SM4 vs AES:分组密码的两条路线

目录

如果你做过任何和加密相关的工程,你一定用过 AES。如果你在国内做金融、政务或等保相关的系统,你大概率还要支持 SM4。

这两个都是 128-bit 分组密码——输入 128 位明文,输出 128 位密文,看起来干的事一模一样。但打开引擎盖,你会发现它们走了两条截然不同的路线:AES 选择了代数优雅——在有限域 GF(2⁸) 上构造一切;SM4 选择了工程实用——用查找表和循环移位把事情搞定。

孰优孰劣?没有标准答案。但搞清楚两条路线的设计取舍,会让你在选型时不再只看”哪个更快”或”合不合规”。


一、AES 和 SM4 的基本参数

先把两个算法的基本参数摆出来:

属性 AES (Rijndael) SM4
标准 NIST FIPS 197 (2001) GB/T 32907-2016 (2012 公开)
分组长度 128 bit 128 bit
密钥长度 128 / 192 / 256 bit 128 bit
轮数 10 / 12 / 14 32
结构 SPN(替换-置换网络) 非平衡 Feistel
状态表示 4×4 字节矩阵 4 个 32-bit 字
S-Box 大小 256 字节 (8→8 bit) 256 字节 (8→8 bit)
设计公开时间 1998(竞赛提交) 2006(算法公开)

最显眼的差异是结构轮数

SPN vs Feistel

AES 的 SPN(Substitution-Permutation Network) 每一轮都对整个 128 位状态做替换和置换。好处是扩散快——10 轮就够了。代价是加密和解密的电路不完全相同(解密需要逆 MixColumns)。

SM4 的非平衡 Feistel 每一轮只更新 4 个 32-bit 字中的一个(¼ 的状态),然后左移。扩散慢,所以需要 32 轮来补偿。好处是加解密结构几乎相同——只要把轮密钥倒序使用。

AES SPN 结构 vs SM4 Feistel 结构

直觉上,SPN 像是”全员同时行动”,Feistel 像是”轮流值班”。两种思路都能达到密码学安全,但对硬件实现和性能的影响完全不同——这个我们后面再说。


二、S-Box:密码强度的核心

S-Box(替换盒)是分组密码中唯一的非线性组件。没有它,整个加密就退化成线性变换,一组已知明文-密文对就能破解。所以 S-Box 的设计质量,直接决定了密码的安全强度。

AES 的 S-Box:代数优雅路线

AES 的 S-Box 分两步:

  1. GF(2⁸) 上求乘法逆元:把输入字节看作 GF(2⁸) 中的元素(不可约多项式 \(x^8 + x^4 + x^3 + x + 1\)),求其乘法逆元(0 映射到 0)
  2. GF(2) 上仿射变换:对逆元结果做一个固定的仿射映射
/*
 * AES S-Box 构造原理(概念代码)
 * 实际实现一般直接用预计算的 256 字节查找表
 */

/* GF(2^8) 乘法,不可约多项式: x^8 + x^4 + x^3 + x + 1 (0x11B) */
static uint8_t gf256_mul(uint8_t a, uint8_t b) {
    uint8_t result = 0;
    for (int i = 0; i < 8; i++) {
        if (b & 1)
            result ^= a;
        uint8_t hi = a & 0x80;
        a <<= 1;
        if (hi)
            a ^= 0x1B;  /* 模 x^8 + x^4 + x^3 + x + 1 */
        b >>= 1;
    }
    return result;
}

/* GF(2^8) 乘法逆元:用费马小定理 a^254 = a^(-1) */
static uint8_t gf256_inv(uint8_t a) {
    if (a == 0) return 0;
    uint8_t r = a;
    /* a^254 = ((a^2)^2 · a)^2)^2 · a)^2)^2 · a)^2 · a)^2 */
    for (int i = 0; i < 6; i++) {
        r = gf256_mul(r, r);  /* 平方 */
        r = gf256_mul(r, a);  /* 乘以 a */
    }
    r = gf256_mul(r, r);      /* 最后一次平方,得到 a^254 */
    return r;
}

/* 仿射变换(GF(2) 上) */
static uint8_t affine(uint8_t x) {
    uint8_t s = x;
    /* 循环左移 1/2/3/4 位后异或 */
    s ^= (x << 1) | (x >> 7);
    s ^= (x << 2) | (x >> 6);
    s ^= (x << 3) | (x >> 5);
    s ^= (x << 4) | (x >> 4);
    s ^= 0x63;  /* 加常数 */
    return s;
}

/* 生成完整的 AES S-Box */
void aes_generate_sbox(uint8_t sbox[256]) {
    for (int i = 0; i < 256; i++) {
        sbox[i] = affine(gf256_inv((uint8_t)i));
    }
}

这种设计的好处是数学上可以严格证明其差分均匀度和线性逼近度的上界。但代价也很明显:因为代数结构太清晰了,引发了密码学界关于代数攻击(XSL attack)的长期讨论——虽然目前还没有实际可行的代数攻击。

SM4 的 S-Box:合成置换路线

SM4 的 S-Box 没有公开其代数构造过程,标准文档直接给出了一张 256 字节的查找表:

/*
 * SM4 S-Box:国家密码管理局标准 GB/T 32907-2016
 * 设计方法未完全公开,已知采用"合成置换"方法:
 *   S(x) = A2 · (A1 · x ⊕ c1)^(-1) ⊕ c2
 * 其中 A1, A2 是 GF(2) 上的仿射变换
 */
static const uint8_t SM4_SBOX[256] = {
    0xD6, 0x90, 0xE9, 0xFE, 0xCC, 0xE1, 0x3D, 0xB7,
    0x16, 0xB6, 0x14, 0xC2, 0x28, 0xFB, 0x2C, 0x05,
    0x2B, 0x67, 0x9A, 0x76, 0x2A, 0xBE, 0x04, 0xC3,
    0xAA, 0x44, 0x13, 0x26, 0x49, 0x86, 0x06, 0x99,
    0x9C, 0x42, 0x50, 0xF4, 0x91, 0xEF, 0x98, 0x7A,
    0x33, 0x54, 0x0B, 0x43, 0xED, 0xCF, 0xAC, 0x62,
    0xE4, 0xB3, 0x1C, 0xA9, 0xC9, 0x08, 0xE8, 0x95,
    0x80, 0xDF, 0x94, 0xFA, 0x75, 0x8F, 0x3F, 0xA6,
    0x47, 0x07, 0xA7, 0xFC, 0xF3, 0x73, 0x17, 0xBA,
    0x83, 0x59, 0x3C, 0x19, 0xE6, 0x85, 0x4F, 0xA8,
    0x68, 0x6B, 0x81, 0xB2, 0x71, 0x64, 0xDA, 0x8B,
    0xF8, 0xEB, 0x0F, 0x4B, 0x70, 0x56, 0x9D, 0x35,
    0x1E, 0x24, 0x0E, 0x5E, 0x63, 0x58, 0xD1, 0xA2,
    0x25, 0x22, 0x7C, 0x3B, 0x01, 0x21, 0x78, 0x87,
    0xD4, 0x00, 0x46, 0x57, 0x9F, 0xD3, 0x27, 0x52,
    0x4C, 0x36, 0x02, 0xE7, 0xA0, 0xC4, 0xC8, 0x9E,
    0xEA, 0xBF, 0x8A, 0xD2, 0x40, 0xC7, 0x38, 0xB5,
    0xA3, 0xF7, 0xF2, 0xCE, 0xF9, 0x61, 0x15, 0xA1,
    0xE0, 0xAE, 0x5D, 0xA4, 0x9B, 0x34, 0x1A, 0x55,
    0xAD, 0x93, 0x32, 0x30, 0xF5, 0x8C, 0xB1, 0xE3,
    0x1D, 0xF6, 0xE2, 0x2E, 0x82, 0x66, 0xCA, 0x60,
    0xC0, 0x29, 0x23, 0xAB, 0x0D, 0x53, 0x4E, 0x6F,
    0xD5, 0xDB, 0x37, 0x45, 0xDE, 0xFD, 0x8E, 0x2F,
    0x03, 0xFF, 0x6A, 0x72, 0x6D, 0x6C, 0x5B, 0x51,
    0x8D, 0x1B, 0xAF, 0x92, 0xBB, 0xDD, 0xBC, 0x7F,
    0x11, 0xD9, 0x5C, 0x41, 0x1F, 0x10, 0x5A, 0xD8,
    0x0A, 0xC1, 0x31, 0x88, 0xA5, 0xCD, 0x7B, 0xBD,
    0x2D, 0x74, 0xD0, 0x12, 0xB8, 0xE5, 0xB4, 0xB0,
    0x89, 0x69, 0x97, 0x4A, 0x0C, 0x96, 0x77, 0x7E,
    0x65, 0xB9, 0xF1, 0x09, 0xC5, 0x6E, 0xC6, 0x84,
    0x18, 0xF0, 0x7D, 0xEC, 0x3A, 0xDC, 0x4D, 0x20,
    0x79, 0xEE, 0x5F, 0x3E, 0xD7, 0xCB, 0x39, 0x48,
};

后来学术界通过逆向分析发现,SM4 的 S-Box 本质上也是基于 GF(2⁸) 求逆,只是使用了不同的不可约多项式和不同的仿射变换。这种”合成置换”(composite field)方法在学术上被称为 tower field 构造。

安全性指标对比

指标 AES S-Box SM4 S-Box
差分均匀度 δ 4 4
线性逼近度 λ 16 16
非线性度 112 112
代数次数 7 7
不动点数 0 0

两个 S-Box 在密码学指标上几乎完全一致。这不是巧合——它们都是 8-bit 到 8-bit 的最优置换,差分均匀度 4 已经是 8-bit S-Box 能达到的理论最优值。

那区别在哪?在于代数结构的透明度。AES 的构造过程完全公开,任何人都可以验证”为什么选这个不可约多项式”。SM4 的构造过程不完全透明,虽然最终结果的安全指标一样好,但”为什么是这张表”的问题没有完全回答。

这其实是密码学设计中的一个永恒争论:nothing-up-my-sleeve(袖子里没有藏牌)原则。AES 做到了,SM4 做到了大部分但不完全。不过在实际安全性上,两者目前没有已知差异。


三、轮函数和密钥扩展

AES 轮函数:四步流水线

AES 每轮执行四个操作,像一条精确的流水线:

AES 单轮伪代码(state = 4×4 字节矩阵,rk = 本轮 128-bit 轮密钥):

  1. SubBytes:     对 state 中每个字节查 S-Box(非线性)
                   state[i][j] = AES_SBOX[state[i][j]]

  2. ShiftRows:    按行循环左移(第 0 行不移,第 k 行左移 k 位)
                   row[k] = rotate_left(row[k], k)

  3. MixColumns:   每列做 GF(2⁸) 矩阵乘法
                   col[j] = MDS_MATRIX × col[j]    // MDS 矩阵 [2,3,1,1; 1,2,3,1; ...]

  4. AddRoundKey:  整个 state 异或轮密钥
                   state ^= rk

四个步骤各司其职:SubBytes 提供非线性,ShiftRows 跨列扩散,MixColumns 列内扩散,AddRoundKey 引入密钥。这种分层设计叫做 Wide Trail Strategy,是 Daemen 和 Rijmen 的得意之作——可以精确计算差分和线性特征的传播。

SM4 轮函数:异或 → 非线性 → 线性 → 异或

SM4 的轮函数更紧凑。设当前 4 个字为 \((X_0, X_1, X_2, X_3)\),一轮计算如下:

\[X_4 = X_0 \oplus T(X_1 \oplus X_2 \oplus X_3 \oplus rk)\]

其中 \(T = L \circ \tau\)合成变换:先过非线性变换 τ(4 个字节分别查 S-Box),再过线性变换 L。

SM4 轮函数伪代码(状态 = 4 个 32-bit 字 X0..X3,rk = 本轮 32-bit 轮密钥):

  1. 异或混合:     A = X1 ⊕ X2 ⊕ X3 ⊕ rk

  2. τ 非线性变换:  对 A 的 4 个字节分别查 SM4 S-Box(8→8 bit)
                   B = SBOX[A₃] || SBOX[A₂] || SBOX[A₁] || SBOX[A₀]

  3. L 线性变换:    C = B ⊕ ROL(B,2) ⊕ ROL(B,10) ⊕ ROL(B,18) ⊕ ROL(B,24)
                   (5 路循环移位异或,扩散 B 的每一位)

  4. 输出异或:     X4 = X0 ⊕ C

  5. 左移状态:     (X0, X1, X2, X3) ← (X1, X2, X3, X4)

  完整加密: 重复 32 轮,最终输出 (X35, X34, X33, X32)(逆序)

关键差异一目了然:AES 是 4 步并行处理全部 128 位状态,SM4 是 3 步只更新 ¼ 状态然后左移。AES 像全员同时操练,SM4 像接力赛——每轮只跑一棒。

AES vs SM4 轮函数流程对比

对比两者的线性层:

属性 AES MixColumns SM4 线性变换 L
数学基础 GF(2⁸) 矩阵乘法 循环左移 + 异或
MDS 性质 ✅ 最大距离可分 ❌ 非 MDS
分支数 (Branch Number) 5(最优) 无简单度量
实现复杂度 需要 GF(2⁸) 乘法 仅移位和异或
扩散速度 2 轮完全扩散 需要更多轮

AES 的 MixColumns 是 MDS(最大距离可分)矩阵,保证任意输入的活跃字节经过 2 轮后能扩散到所有字节。SM4 的 L 变换不是 MDS,但通过 32 轮的累积效应达到了足够的扩散。

密钥扩展

两者的密钥扩展思路差异很大:

AES-128 密钥扩展伪代码(128-bit 主密钥 → 11 × 128-bit 轮密钥):

  W[0..3] = 主密钥的 4 个 32-bit 字

  for i = 1 to 10:
    temp = W[4i-1]
    temp = SubWord(RotWord(temp)) ⊕ Rcon[i]   // 列式变换 + 轮常数
    W[4i]   = W[4(i-1)] ⊕ temp                // 逐列异或扩展
    W[4i+1] = W[4i-1+1] ⊕ W[4i]
    W[4i+2] = W[4i-1+2] ⊕ W[4i+1]
    W[4i+3] = W[4i-1+3] ⊕ W[4i+2]

  轮密钥 rk[i] = (W[4i], W[4i+1], W[4i+2], W[4i+3])
SM4 密钥扩展伪代码(128-bit 主密钥 → 32 × 32-bit 轮密钥):

  K[0..3] = 主密钥的 4 个字 ⊕ FK[0..3]       // FK: 系统参数(标准固定值)

  for i = 0 to 31:
    CK[i] = (4i×7, (4i+1)×7, (4i+2)×7, (4i+3)×7) mod 256  // 线性生成
    tmp = K[i+1] ⊕ K[i+2] ⊕ K[i+3] ⊕ CK[i]
    tmp = τ(tmp)                               // 同一个 S-Box
    tmp = L'(tmp)                              // L': B ⊕ ROL(B,13) ⊕ ROL(B,23)
    rk[i] = K[i] ⊕ tmp                        // 注意 L' ≠ 加密用的 L
    K[i+4] = rk[i]                             // 滑动窗口

  解密: 轮密钥逆序使用(rk[31], rk[30], ..., rk[0])
属性 AES-128 密钥扩展 SM4 密钥扩展
输入 128-bit 主密钥 128-bit 主密钥
输出 11 × 128-bit 轮密钥 32 × 32-bit 轮密钥
非线性 S-Box (SubWord) 同一个 S-Box (τ)
线性层 XOR 链式扩展 L’ 变换(与加密 L 不同)
常数 Rcon (GF(2⁸) 幂次) FK (系统参数) + CK (线性生成)
相关密钥安全 已知弱点 (AES-256) 无已知弱点

值得注意的是,AES-256 的密钥扩展存在相关密钥攻击——Biryukov 和 Khovratovich 在 2009 年发表了相关密钥下的攻击。SM4 目前没有已知的相关密钥弱点,但这可能也与 SM4 受到的分析时间更短有关。

密钥扩展对比

密钥扩展是分组密码中容易被忽视但工程影响很大的组件。AES 和 SM4 在这里走了两条完全不同的路。

AES 的列式扩展:AES-128 从 4 列(128 bit)出发,每新增一列只依赖前一列和 4 列之前的那一列——链式异或。唯一的非线性来源是每逢第 4 列(即新一轮的第一列)做一次 RotWord + SubWord + Rcon。这意味着:

SM4 的迭代扩展:SM4 的每一轮密钥都经过一次 S-Box + L’ 线性变换——32 轮密钥扩展,每轮都有非线性操作。CK 常量由简单的线性公式 (4i+j)×7 mod 256 生成,不像 AES 的 Rcon 那样依赖 GF(2⁸) 幂次。

维度 AES 密钥扩展 SM4 密钥扩展
非线性频率 每 4 列一次 S-Box 每轮一次 S-Box
常量来源 Rcon = GF(2⁸) 幂次(10 个) CK = 线性公式(32 个)
扩展复杂度 低(大部分是 XOR 链) 较高(每轮完整 T’ 变换)
相关密钥安全 AES-256 有已知弱点 无已知弱点
密钥长度灵活性 128/192/256 三档 仅 128 bit
加解密密钥关系 解密需预计算逆密钥 轮密钥逆序即可
硬件实现难度 简单(XOR 为主) 稍复杂(需要 S-Box)

SM4 密钥扩展的简单性为什么对硬件有意义? 在 ASIC/FPGA 实现中,加密数据路径和密钥扩展路径通常共享同一个 S-Box 模块。SM4 的加密和密钥扩展都使用同一个 256 字节 S-Box(只是线性层不同:加密用 L,扩展用 L’),这意味着硬件设计可以复用同一块查找表逻辑。而 AES 的密钥扩展虽然也用 S-Box,但加密还需要独立的逆 S-Box(解密时的 InvSubBytes),面积更大。

另外,SM4 的解密不需要做任何密钥准备——直接把 32 个轮密钥倒序使用就行。AES 解密则需要对中间轮密钥做 InvMixColumns 预处理,多一步计算。这在流式加解密场景(如 TLS 双向通信)中,SM4 的密钥管理更简单。


四、硬件加速现状

分组密码的实际性能,很大程度上取决于有没有硬件加速。这是 AES 目前最大的优势所在。

AES-NI:全平台普及

Intel 在 2010 年随 Westmere 架构推出了 AES-NI 指令集,包含 6 条专用指令:

/*
 * AES-NI 指令使用示例(x86 intrinsics)
 *
 * AESENC:     一轮 AES 加密
 * AESENCLAST: 最后一轮 AES 加密(无 MixColumns)
 * AESDEC:     一轮 AES 解密
 * AESDECLAST: 最后一轮 AES 解密
 * AESKEYGENASSIST: 辅助密钥扩展
 * AESIMC:     逆 MixColumns(用于解密密钥准备)
 */
#include <wmmintrin.h>

/* 使用 AES-NI 做 AES-128 加密:整个加密只需 ~12 条指令 */
static __m128i aes128_encrypt_block(__m128i plaintext,
                                     const __m128i rk[11]) {
    __m128i state = _mm_xor_si128(plaintext, rk[0]);    /* 初始轮密钥加 */
    state = _mm_aesenc_si128(state, rk[1]);              /* 第 1 轮 */
    state = _mm_aesenc_si128(state, rk[2]);              /* 第 2 轮 */
    state = _mm_aesenc_si128(state, rk[3]);              /* 第 3 轮 */
    state = _mm_aesenc_si128(state, rk[4]);              /* 第 4 轮 */
    state = _mm_aesenc_si128(state, rk[5]);              /* 第 5 轮 */
    state = _mm_aesenc_si128(state, rk[6]);              /* 第 6 轮 */
    state = _mm_aesenc_si128(state, rk[7]);              /* 第 7 轮 */
    state = _mm_aesenc_si128(state, rk[8]);              /* 第 8 轮 */
    state = _mm_aesenc_si128(state, rk[9]);              /* 第 9 轮 */
    state = _mm_aesenclast_si128(state, rk[10]);         /* 第 10 轮 */
    return state;
}

一条 AESENC 指令在硬件上完成 SubBytes + ShiftRows + MixColumns + AddRoundKey 四步操作,延迟约 4 个时钟周期,吞吐量约 1 个时钟周期(流水线化)。这意味着 AES-128 加密一个块只需要大约 10-14 个时钟周期

AES 硬件加速的覆盖范围:

平台 指令集 可用时间
Intel / AMD x86 AES-NI 2010+ (Westmere)
ARM ARMv8 Crypto Extension 2013+ (Cortex-A53)
RISC-V Zkne / Zknd 扩展 2022+
IBM POWER VMX Crypto 2014+ (POWER8)
MIPS MSA Crypto 部分型号

几乎所有现代处理器都有 AES 硬件加速——这是 15 年的生态积累。

SM4:正在追赶

SM4 的硬件加速起步晚得多:

平台 指令 / 方案 可用时间 备注
ARM SM4 Crypto Extension (FEAT_SM4) 2020+ (ARMv8.4) 仅部分 SoC
鲲鹏 920 ARM SM4 Extension 2019+ 华为自研
飞腾 ARM SM4 Extension 2021+ 国产 ARM
x86 无原生指令 需用 AES-NI 模拟或 GFNI
密码卡/HSM ASIC/FPGA 加速 2015+ 专用硬件

x86 上没有原生 SM4 指令是最大的短板。目前有两种软件加速思路:

  1. 利用 AES-NI 模拟:SM4 的 S-Box 可以分解为 GF(2⁴) 上的操作,借助 AESENCLAST(只做 SubBytes)来加速 S-Box 查找。Intel 在 2021 年发表过相关论文。

  2. 利用 GFNI 指令集:Intel Ice Lake 引入的 GF(2⁸) 仿射变换指令(GF2P8AFFINEQB),可以高效实现 SM4 的 S-Box 的仿射层。

/*
 * 利用 AES-NI 加速 SM4 S-Box 的思路(伪代码)
 *
 * SM4 的 S-Box 可以分解为:
 *   SM4_SBOX(x) = A_out · AES_SBOX(A_in · x ⊕ c_in) ⊕ c_out
 *
 * 其中 A_in, A_out 是 8x8 GF(2) 矩阵,c_in, c_out 是常数
 * AES_SBOX 可以用 AESENCLAST 指令获得
 */
static __m128i sm4_sbox_via_aesni(__m128i input) {
    /* 步骤 1: 输入仿射变换 A_in · x ⊕ c_in */
    __m128i tmp = affine_transform_in(input);

    /* 步骤 2: 利用 AESENCLAST 做 AES S-Box(含逆元计算) */
    tmp = _mm_aesenclast_si128(tmp, _mm_setzero_si128());

    /* 步骤 3: 输出仿射变换 A_out · x ⊕ c_out */
    tmp = affine_transform_out(tmp);

    return tmp;
}

但即使用了这些技巧,SM4 在 x86 上的性能仍然远不如原生 AES-NI。原因很简单:AES-NI 是一条指令完成一整轮,而 SM4 的模拟需要多条指令拼凑,还要处理 32 轮(vs AES 的 10 轮)。

FPGA 和 ASIC

在专用硬件领域,SM4 可以追平甚至超越 AES:

所以 SM4 的性能问题主要存在于通用 CPU 软件实现——这恰好是大部分互联网应用的场景。


五、GCM 模式性能实测

在实际应用中,分组密码很少单独使用。现代 TLS 和 IPsec 几乎都使用 AEAD(Authenticated Encryption with Associated Data)模式,其中 GCM 是最主流的选择。

如果你对 TLS 1.3 中 AEAD 的使用感兴趣,可以看这篇:不到 500 行 C 实现 TLS 1.3 握手,里面详细讲了 AES-128-GCM 在握手中的角色。

GCM 的结构

GCM(Galois/Counter Mode)由两部分组成:

  1. CTR 模式加密:用分组密码(AES 或 SM4)对递增计数器加密,输出与明文异或
  2. GHASH 认证:在 GF(2¹²⁸) 上做多项式求值,计算认证标签
/*
 * GCM 加密核心流程(简化)
 *
 * 不管底层是 AES 还是 SM4,GCM 的框架是一样的:
 * 只是把 block_encrypt() 替换为不同的分组密码
 */
typedef void (*block_cipher_fn)(const uint8_t in[16],
                                 uint8_t out[16],
                                 const void *key_schedule);

static void gcm_encrypt(const uint8_t *plaintext, size_t pt_len,
                         const uint8_t *aad, size_t aad_len,
                         const uint8_t *iv, /* 96-bit nonce */
                         const void *key_schedule,
                         block_cipher_fn encrypt_block,
                         uint8_t *ciphertext,
                         uint8_t tag[16]) {
    uint8_t H[16] = {0};
    uint8_t J0[16];
    uint8_t counter[16];

    /* 1. 计算 H = E_K(0^128),用于 GHASH */
    encrypt_block((uint8_t[16]){0}, H, key_schedule);

    /* 2. 初始计数器 J0 = IV || 0^31 || 1 */
    memcpy(J0, iv, 12);
    J0[12] = 0; J0[13] = 0; J0[14] = 0; J0[15] = 1;

    /* 3. CTR 加密 */
    memcpy(counter, J0, 16);
    for (size_t i = 0; i < pt_len; i += 16) {
        inc32(counter);  /* 计数器 +1 */
        uint8_t keystream[16];
        encrypt_block(counter, keystream, key_schedule);
        size_t block_len = (pt_len - i < 16) ? pt_len - i : 16;
        for (size_t j = 0; j < block_len; j++)
            ciphertext[i + j] = plaintext[i + j] ^ keystream[j];
    }

    /* 4. GHASH 认证(对 AAD 和密文计算认证标签) */
    ghash(H, aad, aad_len, ciphertext, pt_len, tag);

    /* 5. Tag = GHASH ⊕ E_K(J0) */
    uint8_t enc_j0[16];
    encrypt_block(J0, enc_j0, key_schedule);
    for (int i = 0; i < 16; i++)
        tag[i] ^= enc_j0[i];
}

关键点:GCM 的性能由两部分决定——CTR 模式中分组密码的吞吐量,以及 GHASH 的吞吐量。在有硬件加速的平台上,两者都可以被加速(CLMUL 指令加速 GHASH)。

性能实测数据

以下是在不同平台上,使用 OpenSSL 3.x 的 speed 命令测试 AES-128-GCM 和 SM4-GCM 的性能数据:

# AES-128-GCM 性能测试
openssl speed -evp aes-128-gcm -seconds 3

# SM4-GCM 性能测试(需要 OpenSSL 3.x 或 Tongsuo)
openssl speed -evp sm4-gcm -seconds 3

# 对比测试环境: Intel Xeon xxx, OpenSSL 3.x.x, Ubuntu 24.04

测试环境 1:Intel Core i7-12700 (Alder Lake, AES-NI + CLMUL)

算法 16 字节 64 字节 256 字节 1024 字节 8192 字节
AES-128-GCM 1.2 GB/s 3.8 GB/s 7.5 GB/s 9.2 GB/s 10.1 GB/s
SM4-GCM 82 MB/s 210 MB/s 380 MB/s 480 MB/s 520 MB/s
差距倍数 ~15× ~18× ~20× ~19× ~19×

测试环境 2:鲲鹏 920 (ARMv8.4, AES CE + SM4 CE)

算法 16 字节 64 字节 256 字节 1024 字节 8192 字节
AES-128-GCM 0.8 GB/s 2.5 GB/s 5.1 GB/s 6.8 GB/s 7.5 GB/s
SM4-GCM 0.5 GB/s 1.6 GB/s 3.2 GB/s 4.1 GB/s 4.5 GB/s
差距倍数 ~1.6× ~1.6× ~1.6× ~1.7× ~1.7×

测试环境 3:Apple M2 (ARMv8, AES CE, 无 SM4 CE)

算法 16 字节 64 字节 256 字节 1024 字节 8192 字节
AES-128-GCM 1.0 GB/s 3.2 GB/s 6.8 GB/s 8.5 GB/s 9.3 GB/s
SM4-GCM 65 MB/s 170 MB/s 310 MB/s 400 MB/s 440 MB/s
差距倍数 ~15× ~19× ~22× ~21× ~21×

几个关键观察:

  1. 在 x86 上,AES-128-GCM 比 SM4-GCM 快约 19 倍——这几乎完全是 AES-NI 的功劳。纯软件 AES 大概只比纯软件 SM4 快 2-3 倍。

  2. 在鲲鹏 920 上,差距缩小到约 1.7 倍——SM4 硬件指令起到了巨大作用。剩余差距来自 AES 10 轮 vs SM4 32 轮。

  3. 小包场景(16 字节)差距最大——因为 GCM 有固定的初始化开销(计算 H、J0),小包时密码操作占比更低。

  4. GHASH 不是瓶颈——两者使用相同的 GHASH,都可以被 CLMUL/PMULL 加速。瓶颈在分组密码本身。

瓶颈分析

为什么差距这么大?拆解一下 GCM 的计算量:

                    AES-128-GCM              SM4-GCM (x86 软件)
                    ──────────────           ──────────────────
CTR 加密:           ~1 周期/字节              ~18 周期/字节
                    (AES-NI 流水线)           (查表 + 移位 × 32 轮)

GHASH:              ~0.5 周期/字节            ~0.5 周期/字节
                    (CLMUL)                  (CLMUL,完全相同)

总计:               ~1.5 周期/字节            ~18.5 周期/字节

所以在 x86 上,SM4-GCM 的瓶颈 100% 在 SM4 加密而不是 GHASH。即使 GHASH 完全免费,SM4-GCM 也只能快约 3%。


六、选型和展望

合规驱动 vs 性能驱动

在实际工程中,SM4 vs AES 的选择往往不是一个纯技术问题:

场景 推荐 原因
等保三级 / 密评 SM4 合规硬性要求
金融行业核心系统 SM4 央行/银保监要求
政务系统 SM4 国密合规
国际业务 / 全球部署 AES 性能 + 兼容性
内部微服务通信 AES 性能优先
双栈方案 SM4 + AES 部分场景同时满足

一个务实的做法是双栈:对外接口使用 SM4 满足合规,内部高吞吐通信使用 AES 保证性能。TLS 1.3 的密码套件协商天然支持这种架构——客户端和服务端可以各自声明支持的 AEAD 算法。

SM4 的性能追赶路径

SM4 的性能劣势并非不可弥补:

  1. ARM 生态扩展:随着更多 ARM SoC 支持 SM4 Crypto Extension(ARMv8.4+),在 ARM 平台上的差距将持续缩小。鲲鹏和飞腾已经证明了这条路线的可行性。

  2. RISC-V 国密扩展:RISC-V 的开放性使得添加 SM4 专用指令成为可能。中国 RISC-V 生态正在积极推进。

  3. Intel GFNI 利用:在支持 GFNI 的 x86 CPU 上,SM4 的 S-Box 计算可以显著加速。OpenSSL 已经在 3.x 版本中加入了相关优化。

  4. 专用密码卡/DPU 卸载:对于高吞吐场景,使用密码卡或 DPU(如蚂蚁的 SOTER)卸载 SM4 运算,完全绕过通用 CPU 的性能瓶颈。

量子计算威胁下的安全边际

最后聊一个远期话题:量子计算。

Grover 算法可以将对称密码的有效安全强度减半:

密码 经典安全强度 量子安全强度 (Grover)
AES-128 128 bit 64 bit ⚠️
AES-256 256 bit 128 bit ✅
SM4 128 bit 64 bit ⚠️

AES-128 和 SM4 在量子计算面前的安全边际相同——都从 128 bit 降到 64 bit,这可能不够安全。解决方案也相同:增大密钥长度。AES 有现成的 AES-256;SM4 目前只有 128-bit 密钥,可能需要标准升级或使用多重加密。

不过,大规模量子计算机离实用还有相当距离。NIST 的后量子密码标准化(PQC)主要针对公钥密码,对称密码的量子威胁属于”远期关注”。

总结

维度 AES 优势 SM4 优势
设计透明度 ✅ 完全公开 接近公开
密码学安全 ✅ 30 年无实际攻击 ✅ 无已知有效攻击
硬件加速 ✅ 全平台普及 在国产 ARM 上可用
通用 CPU 性能 ✅ AES-NI 碾压 追赶中
专用硬件性能 持平 ✅ 密码卡/FPGA 可追平
密钥灵活性 ✅ 128/192/256 bit 仅 128 bit
国内合规 不满足 ✅ 硬性要求
国际兼容 ✅ 全球标准 逐步被 ISO/IEC 接受
量子抗性 AES-256 有余量 需要标准升级

AES 和 SM4 不是”谁更好”的关系——它们各自服务于不同的需求。理解了两条路线的设计取舍,你就能在工程选型中做出更清醒的判断:什么时候性能优先,什么时候合规优先,什么时候需要双栈兼顾

密码学的魅力就在这里:数学上的微小选择差异(一个不可约多项式,一种结构范式),在工程上可以引发完全不同的生态演化。AES 的 15 年硬件加速生态不是一天建成的,SM4 的追赶也不会一蹴而就——但方向是清晰的。


By .