4、SM4算法在安全世界的实现:SM4加解密核心逻辑、密钥扩展算法、CBC/ECB模式实现与测试
SM4算法,说白了就是咱们国家自己的分组密码标准。我在TrustZone项目里用过很多次,它跟AES有点像,但结构上更简洁。今天咱们就把它的核心逻辑、密钥扩展、还有CBC/ECB模式,一个一个掰开揉碎了讲清楚。
4.1 SM4算法概述
SM4是一个分组密码算法,分组长度128位,密钥长度也是128位。它采用32轮非线性迭代结构。嗯,这里要注意,它不是Feistel结构,而是类似AES的SPN结构,但轮函数更轻量。
核心参数速查表
| 参数 | 值 | 说明 |
|---|---|---|
| 分组长度 | 128 bit (16字节) | 输入输出都是16字节 |
| 密钥长度 | 128 bit (16字节) | 加密密钥和解密密钥相同 |
| 轮数 | 32轮 | 每轮使用一个轮密钥 |
| 轮密钥长度 | 32 bit (4字节) | 共32个轮密钥 |
| 基本运算 | 异或、S盒、循环移位 | 硬件实现友好 |
我个人习惯把SM4看作一个「轻量级AES」。为什么这么说?因为它没有AES那么复杂的列混合和行移位,轮函数里主要就是异或、查S盒、再加一个线性变换。在TrustZone的安全世界里,这种简洁性意味着更少的侧信道攻击面。
4.2 SM4加解密核心逻辑
SM4的加密和解密,结构完全一样,只是轮密钥的使用顺序相反。加密时用rk0, rk1, ..., rk31,解密时用rk31, rk30, ..., rk0。这一点跟AES的加解密对称性很像。
咱们先看加密流程。输入128位明文,分成4个32位字:X0, X1, X2, X3。然后执行32轮迭代,每轮更新一个32位字。
第i轮的公式很简单:
X_{i+4} = X_i ⊕ T(X_{i+1} ⊕ X_{i+2} ⊕ X_{i+3} ⊕ rk_i)
这里的T函数是SM4的核心,它由两部分组成:非线性变换τ和线性变换L。
- τ变换:把32位输入分成4个字节,每个字节查S盒。SM4的S盒是固定的8位输入8位输出,跟AES的S盒不同,但设计原则类似。
- L变换:对τ的输出做循环移位和异或。公式是:
L(B) = B ⊕ (B <<< 2) ⊕ (B <<< 10) ⊕ (B <<< 18) ⊕ (B <<< 24)
32轮结束后,把X32, X33, X34, X35拼接起来,就是密文。
我的经验:在TrustZone安全世界里实现SM4时,我建议把S盒放在安全内存里,不要放在普通DRAM。我曾经见过一个项目,S盒被侧信道攻击通过缓存时间泄露了,嗯,那之后我就把S盒锁在TZ的SRAM里了。
4.3 密钥扩展算法
密钥扩展,说白了就是把16字节的原始密钥,变成32个32位的轮密钥。SM4的密钥扩展算法跟加密轮函数很像,但用了不同的常数。
流程是这样的:
- 把128位密钥分成4个32位字:
MK0, MK1, MK2, MK3 - 用系统参数FK做初始异或:
K0 = MK0 ⊕ FK0, K1 = MK1 ⊕ FK1, K2 = MK2 ⊕ FK2, K3 = MK3 ⊕ FK3 - 然后迭代生成轮密钥:
rk_i = K_{i+4} = K_i ⊕ T'(K_{i+1} ⊕ K_{i+2} ⊕ K_{i+3} ⊕ CK_i)
这里的T'函数跟加密的T函数类似,但L变换不同:L'(B) = B ⊕ (B <<< 13) ⊕ (B <<< 23)
FK和CK都是固定常数。FK有4个,CK有32个。这些常数在标准文档里都有,我就不列了,你写代码时直接查表就行。
避坑指南:我曾经在移植SM4时,把密钥扩展的CK常数搞错了一个字节,结果加解密结果对不上。调试了整整一天才发现。建议你实现完后,先用标准测试向量验证一下密钥扩展的正确性。
4.4 CBC/ECB模式实现
SM4本身只定义了分组加密,怎么处理长数据呢?这就需要工作模式了。最常用的就是ECB和CBC。
ECB模式
ECB模式最简单:把明文分成16字节一块,每块独立用SM4加密。但我不推荐你在生产环境用ECB。为什么?因为相同的明文块会生成相同的密文块,容易泄露数据模式。我在一个金融项目里见过用ECB加密头像的,结果头像轮廓在密文里还能看出来,嗯,那画面太美我不敢看。
CBC模式
CBC模式引入了IV(初始向量),每个明文块先跟上一个密文块异或,再加密。这样相同的明文块会生成不同的密文块。
CBC加密流程:
C0 = IV
Ci = SM4_Encrypt(Pi ⊕ C_{i-1})
CBC解密流程:
Pi = SM4_Decrypt(Ci) ⊕ C_{i-1}
模式对比
| 特性 | ECB | CBC |
|---|---|---|
| 并行加密 | 支持 | 不支持 |
| 并行解密 | 支持 | 支持 |
| 错误传播 | 仅影响当前块 | 影响当前块和下一块 |
| 数据模式泄露 | 有 | 无 |
| IV需求 | 不需要 | 需要,且必须随机 |
| 推荐场景 | 测试向量验证 | 生产环境数据加密 |
4.5 代码实现示例
下面我给出SM4核心加解密和CBC模式的C语言实现片段。注意,这是简化版,去掉了内存管理和边界检查,但核心逻辑是完整的。
// SM4 S盒
static const uint8_t sm4_sbox[256] = {
0xD6,0x90,0xE9,0xFE,0xCC,0xE1,0x3D,0xB7,0x16,0xB6,0x14,0xC2,0x28,0xFB,0x2C,0x05,
// ... 完整S盒请参考标准文档
};
// 轮函数T
static uint32_t sm4_t(uint32_t x) {
uint8_t b[4];
b[0] = sm4_sbox[(x >> 24) & 0xFF];
b[1] = sm4_sbox[(x >> 16) & 0xFF];
b[2] = sm4_sbox[(x >> 8) & 0xFF];
b[3] = sm4_sbox[x & 0xFF];
uint32_t y = (b[0] << 24) | (b[1] << 16) | (b[2] << 8) | b[3];
// 线性变换L
return y ^ rotl(y, 2) ^ rotl(y, 10) ^ rotl(y, 18) ^ rotl(y, 24);
}
// 加密一个分组
void sm4_encrypt_block(uint32_t rk[32], uint8_t in[16], uint8_t out[16]) {
uint32_t x[36];
for (int i = 0; i < 4; i++) {
x[i] = (in[i*4] << 24) | (in[i*4+1] << 16) | (in[i*4+2] << 8) | in[i*4+3];
}
for (int i = 0; i < 32; i++) {
x[i+4] = x[i] ^ sm4_t(x[i+1] ^ x[i+2] ^ x[i+3] ^ rk[i]);
}
for (int i = 0; i < 4; i++) {
out[i*4] = (x[35-i] >> 24) & 0xFF;
out[i*4+1] = (x[35-i] >> 16) & 0xFF;
out[i*4+2] = (x[35-i] >> 8) & 0xFF;
out[i*4+3] = x[35-i] & 0xFF;
}
}
// CBC模式加密
void sm4_cbc_encrypt(uint32_t rk[32], uint8_t iv[16], uint8_t *in, uint8_t *out, size_t len) {
uint8_t prev[16];
memcpy(prev, iv, 16);
for (size_t i = 0; i < len; i += 16) {
for (int j = 0; j < 16; j++) {
prev[j] ^= in[i+j];
}
sm4_encrypt_block(rk, prev, out+i);
memcpy(prev, out+i, 16);
}
}
我的建议:在TrustZone里实现时,记得把轮密钥rk数组放在安全世界的内存里,不要泄露到普通世界。另外,IV必须随机生成,我习惯用安全世界的TRNG来生成IV,而不是用伪随机数。
4.6 测试与验证
实现完了怎么验证对不对?用标准测试向量。SM4的标准测试向量在GMT 0002-2012里有。我贴一个常用的:
密钥: 0123456789abcdeffedcba9876543210
明文: 0123456789abcdeffedcba9876543210
密文: 681edf34d206965e86b3e94f536e4246
你实现完后,用这个向量跑一下。如果结果对不上,先检查密钥扩展,再检查轮函数。我当年调试时,就是先用这个向量把密钥扩展的中间结果打印出来,跟标准值逐轮对比,才找到bug的。
4.7 知识体系总览
下面这张图把SM4在TrustZone安全世界里的实现脉络梳理清楚了。你可以把它当作一个快速索引。
本章小结:SM4的核心就三件事——密钥扩展、32轮迭代、工作模式。在TrustZone里实现时,记住把敏感数据(S盒、轮密钥、IV)放在安全世界,加解密操作在安全世界完成,普通世界只传密文和明文。这样既保证了性能,又守住了安全底线。