SM4 的数学基石:有限域与 S 盒

说实话,很多人学 SM4 上来就啃 S 盒,觉得就是一张 16×16 的查找表。但我个人习惯,先把有限域搞清楚。没有这个基础,你后面做白盒实现或者侧信道防护时,一定会踩坑。我就在一个嵌入式项目里吃过亏 —— 没搞懂字节在 GF(2⁸) 下的乘法规则,导致 S 盒查表结果跟标准库对不上。折腾了两天才发现,其实是域生成元选错了。

好了,咱们一步步来。

有限域 GF(2⁸) 是什么?

有限域,说白了就是一组数,个数有限,还能做加减乘除(除法除了 0 都能做)。SM4 用的是 GF(2⁸),也就是 256 个元素。每个元素可以用一个字节表示,比如 0x57 代表多项式 x⁶ + x⁴ + x² + x + 1。

注意,这里的加法和我们平时用的整数加法不一样。在 GF(2⁸) 里,加法就是按位异或。为什么?因为系数只有 0 和 1,加法不进位,直接 xor 就行。你想想看,这样设计的好处就是硬件实现时特别快,一个异或门就能搞定。

运算整数环GF(2⁸)
加法带进位加按位异或 (⊕)
乘法普通乘法后取模多项式乘法后模不可约多项式
模数256x⁸ + x⁴ + x³ + x + 1

我记得最初看 SM4 标准文档时,那个不可约多项式是 0x11B(二进制 1 0001 1011)。这个多项式是固定的,不能改,改了 S 盒就全变了。

一个小窍门: 如果你要手动算某两个字节的 GF(2⁸) 乘法,可以用“移位加异或”的算法。我一般会写一个函数,每次看乘数最低位,决定要不要异或被乘数,然后被乘数左移一位,如果溢出就异或 0x11B。

S 盒的构造逻辑

S 盒是 SM4 唯一的非线性部件。它接收一个字节,输出另一个字节。怎么来的?两步走:

  1. 求逆元:在 GF(2⁸) 里找输入字节的乘法逆元(0 的逆元定义为 0)。
  2. 仿射变换:对逆元做一个线性变换,再异或一个常数 0xD3。

为什么要加仿射变换?如果不加,S 盒就只是求逆,代数性质太差,容易被人用代数攻击攻破。加了仿射,好比给密码套了层“马甲”,让输入输出关系变得更复杂。

这里我贴一段用 C 语言生成 S 盒的核心代码,你自己可以跑跑看,验证结果是否跟标准一致。

// 构建 GF(2⁸) 乘法逆元表 (利用指数表和对数表)
uint8_t inv[256];
uint8_t exp[512], log[256];
// 首先构造指数表:exp[i] = 3^i (生成元 3)
exp[0] = 1;
for (int i = 1; i < 511; i++) {
    exp[i] = (exp[i-1] << 1) ^ (exp[i-1] & 0x80 ? 0x11B : 0);
}
// 然后对数表:log[exp[i]] = i (i < 256)
for (int i = 0; i < 255; i++) log[exp[i]] = i;
log[0] = 0; // 0 没有逆元
// 求逆元:inv[x] = exp[255 - log[x]],x≠0
for (int x = 1; x < 256; x++) {
    inv[x] = exp[255 - log[x]];
}
inv[0] = 0;

// 仿射变换
uint8_t sbox[256];
for (int x = 0; x < 256; x++) {
    uint8_t y = inv[x];
    uint8_t z = 0;
    // 矩阵乘法 + 常数 0xD3
    z ^= (y & 0x01) ? 0x01 : 0;
    z ^= (y & 0x02) ? 0x83 : 0;
    // ... 省略中间 6 行
    z ^= (y & 0x80) ? 0x8B : 0;
    z ^= 0xD3;
    sbox[x] = z;
}
曾经踩过的坑: 我在一次 FPGA 验证时,发现 S 盒的高 4 位和低 4 位搞反了。标准 S 盒的索引规则是:输入字节的高 4 位作为行号,低 4 位作为列号。而你用 C 代码生成时,要注意数组下标就是输入字节本身(0~255),不需要分行列。查表时如果按矩阵方式 sbox[row][col] 构造,一定要确认 row = (input >> 4),col = (input & 0x0F)。

S 盒的属性验证

为了确认你生成的 S 盒是对的,通常要检查几个性质:

  • 双射性:每个输出值唯一,256 个输出互不重复。
  • 差分均匀性:理想值是 4,实际 SM4 S 盒最大差分概率是 4/256。
  • 线性逼近:最大线性偏差大概在 80/512 左右,略低于理论最优,但够用。

我习惯用 Python 快速算一遍:假如输出不是双射,肯定是你仿射矩阵写错了。

核心结论: SM4 S 盒的代数性质决定了整个算法的抗攻击能力。如果你在做密码分析相关的项目,请务必确认你用的是标准 S 盒,不要自己另起炉灶。市场上有些“轻量级 SM4”改动了 S 盒,安全性未经验证,慎用。

一张图看懂有限域与 S 盒的关系

下面这张 SVG 图总结了从输入字节到 S 盒输出的完整数据流,以及有限域在其中的角色。

有限域与 SM4 S 盒构造流程图 输入字节 x GF(2⁸) 求逆元 (0的逆元为0) 仿射变换 (矩阵·逆元) ⊕ 0xD3 S(x) 有限域 GF(2⁸) 要素 不可约多项式: x⁸ + x⁴ + x³ + x + 1 (0x11B) | 生成元: 3 (0x03) 流程说明 1. 输入字节首先映射到 GF(2⁸) 中的多项式表示。 2. 利用离散对数(生成元 3)计算乘法逆元,0 的逆元强制为 0。 3. 逆元字节经过一个 8×8 二进制矩阵乘法,再加常量 0xD3。 4. 最终输出即为 S 盒查表值,存储于 256 字节的 ROM 中。

实践中的注意事项

最后聊几个实战中容易忽略的点:

  • 查表速度 vs 算力:如果你在 ARM Cortex-M 上跑,S 盒预计算好,查表最省事。但在 ASIC 或 FPGA 上,有时你会用组合逻辑直接算 S 盒,省掉一个 256 字节的 ROM。两种做法我都试过,查表版功耗更低。
  • 字节序:SM4 自身用小端序,但 S 盒定义时输入输出都是大端表示(高 4 位为行)。我见过有人把 0x01 当作最高位是 0 的行,结果全错。
  • 侧信道防护:如果直接查表,很容易被能量分析攻破。我通常在 S 盒外部加掩码,或者改用门限实现。这部分后面章节会详细讲。
你问我怎么快速验证? 用已知的测试向量:输入 0x00 应该得到 0xD3,输入 0x01 得到 0xED。如果两个都对,基本 S 盒没问题。

嗯,有限域和 S 盒就讲到这里。它们构成了 SM4 最核心的代数基础,后面的密钥扩展、轮函数都绕不开这两个概念。你如果能亲手把 S 盒生成一遍,对这个算法理解就深一层。