SM4 分组密码填充模式:PKCS7、ANSI X9.23、ISO/IEC 10126 的原理与实现
SM4 是中国国家密码管理局发布的分组密码标准(GB/T 32907-2016),块长度为 128 位(16 字节)。分组密码的工作模式(如 ECB、CBC)要求输入数据长度为块大小的整数倍。当明文长度不满足此条件时,必须使用填充方案。
填充方案是密码学基础组件,其设计必须满足可逆性和唯一性两个核心性质:
- 可逆性:从填充后的数据能够准确还原原始数据
- 唯一性:填充方案产生的结果不会与原始数据本身产生歧义
一、填充方案的数学定义
1.1 形式化描述
设 $M$ 为明文消息,$n$ 为块大小(SM4 中 $n = 16$ 字节)。填充函数 $Pad: \{0,1\}^* \rightarrow \{0,1\}^{kn}$ 将任意长度消息映射到块大小的整数倍长度,其中 $k = \lceil |M|/n \rceil + 1$(至少填充一块)。
去填充函数 $Unpad: \{0,1\}^{kn} \rightarrow \{0,1\}^*$ 满足 $Unpad(Pad(M)) = M$。
1.2 安全性要求
填充方案不应泄露明文长度的信息。在理想的填充方案中,密文长度应独立于明文长度(除了块大小对齐带来的最小膨胀)。
二、PKCS7 填充
2.1 算法描述
PKCS7 填充(RFC 5652)是最广泛使用的填充方案,其定义如下:
填充过程:
令 pad_len = n - (|M| mod n)
若 pad_len = 0,则 pad_len = n
填充字节序列为 pad_len 个值为 pad_len 的字节形式化定义: $$Pad(M) = M \parallel \underbrace{pad\_len \parallel pad\_len \parallel \cdots \parallel pad\_len}_{pad\_len \text{ 次}}$$
去填充过程:
读取最后一个字节值为 p
若 1 ≤ p ≤ n,则去除最后 p 个字节
否则,填充无效2.2 示例演示
以 SM4(n=16)为例:
示例 1:明文长度 12 字节
明文: M = "Hello World!" (12 字节)
pad_len = 16 - (12 mod 16) = 4
填充后: "Hello World!\x04\x04\x04\x04" (16 字节)示例 2:明文长度恰好 16 字节
明文: M = "Hello World!!!" (16 字节)
pad_len = 16 - (16 mod 16) = 0 → 实际 pad_len = 16
填充后: "Hello World!!!" + 16 个 0x10 (32 字节)示例 3:空消息
明文: M = "" (0 字节)
pad_len = 16 - (0 mod 16) = 0 → 实际 pad_len = 16
填充后: 16 个 0x10 (16 字节)2.3 安全性分析
PKCS7 填充具有以下性质:
- 确定性:相同的明文产生相同的填充结果
- 可逆性:最后一个字节明确指示填充长度,去填充无歧义
- 长度隐藏:加密后密文长度为 $\lceil |M|/n \rceil \times n$,泄露明文长度的上界
- _padding oracle 攻击_:某些实现中,解密端对无效填充返回不同的错误信息,攻击者可利用此侧信道逐字节解密。防护措施包括:统一错误响应、使用 AEAD 模式(如 SM4-GCM)。
三、ANSI X9.23 填充
3.1 算法描述
ANSI X9.23 填充(ANSI X9.23-1998)是 ANSI 标准化的填充方案,主要应用于 IBM 系统和 .NET 框架。
填充过程:
令 pad_len = n - (|M| mod n)
若 pad_len = 0,则 pad_len = n
前 (pad_len - 1) 个字节填充 0x00
最后一个字节填充 pad_len形式化定义: $$Pad(M) = M \parallel \underbrace{0x00 \parallel 0x00 \parallel \cdots \parallel 0x00}_{pad\_len - 1 \text{ 次}} \parallel pad\_len$$
去填充过程:
读取最后一个字节值为 p
若 1 ≤ p ≤ n,则去除最后 p 个字节
否则,填充无效3.2 示例演示
示例:明文长度 12 字节
明文: M = "Hello World!" (12 字节)
pad_len = 16 - (12 mod 16) = 4
填充后: "Hello World!\x00\x00\x00\x04" (16 字节)示例:明文长度恰好 16 字节
明文: M = "Hello World!!!" (16 字节)
pad_len = 16
填充后: 15 个 0x00 + 0x10 (16 字节)3.3 与 PKCS7 的区别
| 特性 | PKCS7 | ANSI X9.23 |
|---|---|---|
| 填充字节 | 全部为 pad_len | 前 (pad_len-1) 个为 0x00 |
| 最后一字节 | pad_len | pad_len |
| 实现复杂度 | 低 | 低 |
| 跨平台支持 | 广泛 | 中等 |
四、ISO/IEC 10126 填充
4.1 算法描述
ISO/IEC 10126 填充(ISO/IEC 10126-2:2007)使用随机字节进行填充,是 ISO 标准化的填充方案。
填充过程:
令 pad_len = n - (|M| mod n)
若 pad_len = 0,则 pad_len = n
前 (pad_len - 1) 个字节为随机值
最后一个字节为 pad_len形式化定义: $$Pad(M) = M \parallel R \parallel pad\_len$$ 其中 $R$ 是长度为 $(pad\_len - 1)$ 的随机字节序列。
去填充过程:
读取最后一个字节值为 p
若 1 ≤ p ≤ n,则去除最后 p 个字节
否则,填充无效4.2 示例演示
示例:明文长度 12 字节
明文: M = "Hello World!" (12 字节)
pad_len = 4
填充后: "Hello World!\xA3\x7F\x02\x04" (16 字节)
(假设随机字节为 0xA3, 0x7F, 0x02)4.3 安全性分析
ISO/IEC 10126 填充的主要优势是随机性:
- 相同明文产生不同密文:每次填充使用不同的随机字节,即使加密相同明文,密文也不同
- 抗模式分析:填充字节的随机性增加了密文的熵
- 防填充预言攻击:由于填充内容不可预测,攻击者难以利用填充结构进行侧信道分析
- 需要安全的随机数生成器
- 解密时必须丢弃随机填充字节,无法验证原始填充内容
五、Zero Padding(零填充)
5.1 算法描述
Zero Padding 是最简单的填充方案,用 0x00 字节填充至块大小的整数倍。
填充过程:
令 pad_len = n - (|M| mod n)
若 pad_len = 0,则 pad_len = n
填充 pad_len 个 0x00 字节5.2 不可逆性问题
Zero Padding 存在根本性的不可逆性问题:
示例:
情况 1: M = "Hello" (5 字节)
填充后: "Hello\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" (16 字节)
情况 2: M = "Hello\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" (16 字节)
填充后: 同上(32 字节)解密后无法区分这两种情况。因此,Zero Padding 仅适用于已知明文长度或配合长度前缀的场景。
5.3 适用场景
Zero Padding 适合以下场景:
- 加密前已附加长度字段(如 TLV 编码)
- 明文内容已知不包含尾部 0x00
- 内部协议且双方约定固定长度
六、填充方案的选择原则
6.1 决策矩阵
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| TLS/HTTPS 通信 | PKCS7 | RFC 5246 标准,广泛支持 |
| 数据库字段加密 | PKCS7 或 ISO/10126 | PKCS7 兼容性好,ISO/10126 更安全 |
| IoT 嵌入式设备 | ANSI X9.23 | 实现简单,内存开销小 |
| 消息队列(高安全) | ISO/10126 | 随机填充,抗模式分析 |
| 内部协议(已知长度) | Zero Padding | 最简单,无额外开销 |
6.2 安全性比较
安全性排序(从高到低):
ISO/10126 > PKCS7 ≈ ANSI X9.23 > Zero Padding
理由:
- ISO/10126 的随机填充使得相同明文产生不同密文,增加攻击难度
- PKCS7 和 ANSI X9.23 是确定性填充,但广泛应用于标准协议
- Zero Padding 完全不可逆,仅在受控场景使用6.3 互操作性考虑
不同语言和库对填充方案的支持程度不同:
| 语言/库 | PKCS7 | ANSI X9.23 | ISO/10126 | Zero Padding |
|---|---|---|---|---|
| Python (cryptography) | ✅ | ✅ | ✅ | ❌ |
| Python (gmssl) | ✅ | ❌ | ❌ | ✅ |
| Java (Cipher) | ✅ | ✅ | ✅ | ✅ |
| Go (crypto/cipher) | ✅ | ❌ | ❌ | ❌ |
| .NET (RijndaelManaged) | ✅ | ✅ | ✅ | ❌ |
七、常见实现错误
7.1 填充长度计算错误
错误示例:
# 错误:当明文长度是块大小的倍数时,pad_len 应为 n 而非 0
pad_len = n - (len(data) % n) # 当 len(data) % n == 0 时,pad_len = 0正确实现:
pad_len = n - (len(data) % n)
if pad_len == 0:
pad_len = n7.2 去填充时未验证填充有效性
错误示例:
# 危险:直接信任最后一个字节,可能被攻击者利用
def unpad(data):
pad_len = data[-1]
return data[:-pad_len]正确实现:
def unpad(data):
pad_len = data[-1]
if pad_len < 1 or pad_len > n:
raise ValueError("Invalid padding")
# 验证所有填充字节是否符合预期
if all(b == pad_len for b in data[-pad_len:]):
return data[:-pad_len]
raise ValueError("Invalid padding")7.3 混淆参数单位
在 Python 的 cryptography 库中,PKCS7 构造函数的参数是位数而非字节数:
# 错误:16 字节被误解为 16 位
PKCS7(16) # ValueError: Invalid key length
# 正确:128 位 = 16 字节
PKCS7(128) # ✅ SM4 块大小八、与 SM4 工作模式的配合
填充方案通常与分组密码工作模式配合使用:
8.1 ECB 模式
ECB(Electronic Codebook)模式是最简单的工作模式,每个块独立加密。由于相同的明文块产生相同的密文块,ECB 模式不适合加密结构化数据,但在填充方案的演示中常用。
8.2 CBC 模式
CBC(Cipher Block Chaining)模式通过引入 IV(初始化向量)使相同的明文产生不同的密文,是目前最常用的模式。填充在 CBC 模式中同样必要。
8.3 填充与 AEAD
高级加密模式(如 SM4-GCM、SM4-CCM)是认证加密(AEAD)方案,不需要填充。它们直接处理任意长度的数据,并提供加密和完整性认证。对于新系统设计,推荐优先使用 AEAD 模式。
九、总结
SM4 填充方案的选择应综合考虑以下因素:
- 兼容性:PKCS7 是最广泛支持的方案,适合大多数场景
- 安全性:ISO/10126 提供最好的抗侧信道能力
- 性能:ANSI X9.23 和 PKCS7 实现简单,性能相当
- 标准遵循:遵循相应行业标准(如 TLS 使用 PKCS7)
- 优先使用成熟的密码学库(如 cryptography、gmssl)
- 避免自行实现填充逻辑
- 对于新系统,考虑使用 AEAD 模式(如 SM4-GCM)避免填充问题
参考标准
- GB/T 32907-2016《信息安全技术 SM4 分组密码算法》
- RFC 5246《The Transport Layer Security (TLS) Protocol Version 1.2》
- ANSI X9.23-1998《Data Encryption Alphabetical Pad Method》
- ISO/IEC 10126-2:2007《Information technology — Security techniques — Encryption algorithms — Part 2: Mechanisms using a block cipher algorithm》