3. SM4分组密码算法:从Feistel结构到实战加固

SM4,国密标准里的分组密码算法。说实话,它跟AES走的是完全不同的路子。AES用的是SPN结构,而SM4沿用了Feistel网络——跟DES一个家族。我最早接触SM4是在一个金融数据加密项目里,当时客户要求必须用国密,我第一反应是:这玩意儿性能行不行?后来测完发现,在ARM芯片上SM4的吞吐量其实相当能打。

3.1 SM4算法结构:Feistel网络

SM4的分组长度是128位,密钥长度也是128位。它采用32轮Feistel结构,每一轮只加密一半数据。你想想看,这种结构最大的好处是什么?加解密几乎一样,只是轮密钥的使用顺序相反。硬件实现时,加解密可以共用一套电路。

核心参数一览:

  • 分组长度:128 bits(16字节)
  • 密钥长度:128 bits(16字节)
  • 轮数:32轮
  • 结构:Feistel网络
  • S盒:8×8 S盒,共256个元素

Feistel网络每轮的工作流程是这样的:将128位输入分成左右两半,各64位。右半部分直接成为下一轮的左半部分,左半部分则与轮函数F的输出做异或,成为下一轮的右半部分。轮函数F包含S盒替换、线性变换L和轮密钥异或。嗯,这里要注意,SM4的S盒是固定的,不像AES那样可以动态生成。

SM4单轮Feistel结构 128位输入 左半 64位 (L₀) 右半 64位 (R₀) 直接传递 轮函数 F(R₀, RK₀) RK₀ ⊕ 新左半 64位 (L₁) 新右半 64位 (R₁) 轮函数F内部展开 F(R, RK) = L( S( R ⊕ RK ) ) ① 与轮密钥异或:R ⊕ RK ② S盒替换:4个S盒并行,每个8位输入→8位输出 ③ 线性变换L:32位循环左移 + 异或组合 ④ 输出32位结果,与左半64位中的高32位异或

3.2 SM4密钥扩展:32轮子密钥的生成

密钥扩展这块,SM4比AES简单不少。128位主密钥经过32轮迭代,生成32个32位的轮密钥。我见过不少开发者直接拿主密钥当轮密钥用——这是致命的错误。

避坑指南:我曾经在一个IoT固件里发现,有人把SM4的主密钥直接复制了32份当轮密钥。结果呢?加密强度直接降到了跟单轮加密差不多。密钥扩展不是摆设,它是Feistel网络安全性的基石。

密钥扩展流程如下:

  1. 将128位主密钥MK拆成4个32位字:(MK₀, MK₁, MK₂, MK₃)
  2. 与系统参数FK异或:(K₀, K₁, K₂, K₃) = (MK₀⊕FK₀, MK₁⊕FK₁, MK₂⊕FK₂, MK₃⊕FK₃)
  3. 对i=0到31,迭代计算:Kᵢ₊₄ = Kᵢ ⊕ T'(Kᵢ₊₁ ⊕ Kᵢ₊₂ ⊕ Kᵢ₊₃ ⊕ CKᵢ)
  4. 轮密钥RKᵢ = Kᵢ₊₄

系统参数FK是固定值,CK是32个固定常数。说白了,密钥扩展就是把主密钥打散、扩散,让每一轮的密钥看起来都跟随机数一样。

参数 值(十六进制) 说明
FK₀ A3B1BAC6 系统参数,用于初始化
FK₁ 56AA3350 系统参数,用于初始化
FK₂ 677D9197 系统参数,用于初始化
FK₃ B27022DC 系统参数,用于初始化
CKᵢ 0x00070E15...(32个) 固定常数,每轮不同

3.3 SM4加解密流程:32轮迭代

加密时,128位明文经过32轮Feistel迭代,最后将左右两半交换输出。解密时,用同样的轮密钥,但顺序反过来——从RK₃₁用到RK₀。这就是Feistel网络最优雅的地方。

// SM4加密核心流程(伪代码)
void sm4_encrypt(uint8_t plaintext[16], uint8_t ciphertext[16], uint32_t rk[32]) {
    uint32_t X[36];
    // 将128位明文拆成4个32位字
    bytes_to_words(plaintext, X);
    
    // 32轮迭代
    for (int i = 0; i < 32; i++) {
        X[i+4] = X[i] ^ F(X[i+1] ^ X[i+2] ^ X[i+3] ^ rk[i]);
    }
    
    // 反序输出(最后一轮后左右交换)
    uint32_t output[4] = {X[35], X[34], X[33], X[32]};
    words_to_bytes(output, ciphertext);
}

// 轮函数F
uint32_t F(uint32_t x) {
    // S盒替换
    uint8_t b[4];
    b[0] = SBOX[(x >> 24) & 0xFF];
    b[1] = SBOX[(x >> 16) & 0xFF];
    b[2] = SBOX[(x >> 8) & 0xFF];
    b[3] = SBOX[x & 0xFF];
    uint32_t z = (b[0] << 24) | (b[1] << 16) | (b[2] << 8) | b[3];
    
    // 线性变换L
    return z ^ (z << 2) ^ (z << 10) ^ (z << 18) ^ (z << 24);
}

个人经验:我习惯在嵌入式平台上用查表法实现S盒替换。SM4的S盒只有256个元素,查表比计算快得多。但要注意查表不能有缓存侧信道泄漏——这是另一个故事了。

3.4 SM4工作模式:ECB/CBC/GCM

SM4本身只定义了单块加密。要加密多块数据,必须搭配工作模式。国密标准里明确支持ECB、CBC、GCM三种模式。我建议你直接跳过ECB,除非你只是做测试。

3.4.1 ECB模式(电子密码本模式)

每个明文块独立加密,同样的明文块得到同样的密文块。这会导致什么?如果你加密一张图片,ECB模式会在密文里保留图片的轮廓信息。我曾经在渗透测试中,仅凭ECB模式的密文就还原出了原始图片的轮廓——这完全失去了加密的意义。

警告:ECB模式不适合加密结构化数据。如果你必须用,至少确保每个数据块都是唯一的。但说实话,我建议你直接别用ECB。

3.4.2 CBC模式(密码分组链接模式)

CBC模式引入了初始向量IV。每个明文块先与前一个密文块异或,再加密。这样相同的明文块会得到不同的密文块。CBC模式是国密项目里最常见的模式。

CBC模式的关键点:

  • IV必须随机生成,长度16字节
  • IV不需要保密,但必须保证每次加密用不同的IV
  • 加密是串行的,不能并行
  • 解密可以并行(因为密文块都已知)
  • 需要填充(PKCS#7填充)

3.4.3 GCM模式(伽罗瓦计数器模式)

GCM模式是我个人最喜欢的模式。它结合了CTR模式的加密和GMAC的认证。一次操作同时提供机密性和完整性校验。国密标准里的GCM模式,SM4作为底层分组密码,GMAC使用GHASH算法。

特性 ECB CBC GCM
是否需要IV 否 是(16字节) 是(12字节推荐)
是否需要填充 是 是 否(流模式)
并行加密 支持 不支持 支持
完整性校验 无 无 有(GMAC)
安全性等级 低 中 高
国密标准 GB/T 32907 GB/T 32907 GB/T 38636

实战建议:在启动校验场景中,我推荐使用GCM模式。原因很简单:启动过程中,你不仅要保证固件不被窃取,还要保证固件没有被篡改。GCM一次搞定加密和认证,省掉额外的HMAC计算。我在一个车载ECU的启动校验方案里就是这么做的,性能开销只比CBC多了不到15%。

3.5 实战中的SM4加固要点

讲完理论,说说我在项目中踩过的坑:

  • 密钥存储:SM4的128位密钥不能明文存在Flash里。我习惯用PUF(物理不可克隆函数)或SE(安全元件)来保护密钥。如果硬件不支持,至少用密钥派生函数从主密钥派生SM4密钥。
  • IV管理:CBC和GCM的IV必须唯一。我曾经见过一个系统,每次重启都用固定的IV——结果就是同样的明文产生同样的密文,跟ECB没区别。
  • 时序攻击:S盒查表如果依赖密钥,可能被时序攻击。我建议在安全等级高的场景里,用bitslice实现或者使用带恒定时间实现的密码库。
  • 模式选择:能上GCM就别用CBC,能上CBC就别用ECB。如果性能敏感,CTR模式也是不错的选择,但别忘了加MAC。

SM4作为国标算法,在嵌入式系统里表现相当稳定。它的Feistel结构让硬件实现变得简单,密钥扩展也不复杂。但简单不代表可以随便用——工作模式选错、密钥管理不当,再强的算法也白搭。嗯,这部分内容够你应付大部分SM4相关的实战场景了。