SM2 签名 ZA 与 Entail 陷阱:80% 国密开发者都在犯的致命错误

国密算法 · 2026-09-26 · 2 阅读


title: "SM2 签名 ZA 与 Entail 陷阱:80% 国密开发者都在犯的致命错误" slug: "sm2-sign-za-entail-trap" excerpt: "SM2 签名的 ZA 计算涉及 ENT_L、标识 ID、椭圆曲线参数等多个字段,任何一步出错都会导致签名在密评中被判定为不合规。本文从源码级剖析 ZA 的计算过程,分析 8 个常见陷阱,并给出完整的修复方案和验证工具。" category: gm tags: - SM2 - ZA - GM/T 0003 - 国密 - 签名

前言

如果你在国密项目中做过 SM2 签名,你大概率遇到过这样的报错:

signature verification failed invalid signature format ZA hash mismatch
90% 的原因出在 ZA 计算——也就是 GM/T 0003.2-2012 规定的签名预处理步骤。

ZA 是什么?为什么每次签名前都要先算一遍?哪些地方最容易踩坑?本文从标准原文出发,结合生产实战,给出完整解答。

阅读前提:你需要理解基本的 SM2 签名流程(哈希→ZA 拼接→签名),但对 ZA 内部的各个字段可能不太清楚。

一、ZA 是什么,为什么要算它

1.1 标准原文

GM/T 0003.2-2012 第 5.1 节定义 SM2 签名算法:

CODE
输入:消息 M,发送方 A 的私钥 dA
输出:签名对 (r, s)

步骤:
1. 计算 ZA = SM3(ZA')
2. 计算 e = SM3(ENTL_A || ZA || M)
3. 生成随机数 k ∈ [1, n-1]
4. 计算 (x1, y1) = [k]G
5. r = (e + x1) mod n
6. s = ((1 + dA)^(-1) * (k - r * dA)) mod n
7. 返回 (r, s)

第 1 步的 ZA' 就是 ZA 的原始数据,ZA 是其 SM3 哈希值。

1.2 ZA 的设计意图

ZA 解决的核心问题是:防止重放攻击。

如果没有 ZA,攻击者可以截取合法的签名 (r, s),将其应用到另一条消息上。加入 ZA 后:

  • ZA 包含了发送方的身份标识(ID)
  • ZA 还包含了椭圆曲线参数(a, b, xG, yG)
  • 这意味着签名不仅绑定消息,还绑定了签名者的身份和使用的曲线
简单说:ZA 让签名具有身份属性,这是 SM2 与 ECDSA 的关键区别之一。

二、ZA 的结构拆解

2.1 ZA 的完整构成

CODE
ZA = SM3(ZA')
其中 ZA' = ENTL_A || ID_A || a || b || xG || yG || xA || yA

每个字段的含义:

字段长度说明
ENTL_A2 字节ID 的长度(比特数)
ID_A可变发送方标识,默认 A53A(ASCII 的"A")
a32 字节椭圆曲线方程 y² = x³ + ax + b 中的 a
b32 字节椭圆曲线方程中的 b
xG32 字节基点 G 的 x 坐标
yG32 字节基点 G 的 y 坐标
xA32 字节发送方公钥的 x 坐标
yA32 字节发送方公钥的 y 坐标

2.2 字段字节顺序

所有整数型字段(a、b、xG、yG、xA、yA)均为 大端序(Big-Endian),不足 32 字节时前面补零。

例如 GM/T 0003.5-2012 定义的 SM2 曲线参数:

CODE
a = 0xFFFCFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC
b = 0x28E9FA9E9D9F5E344D5A9E4BCF6509A7F39789F515AB8F92DDBCBD414D940E93
p = 0xFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF
n = 0xFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123
xG = 0x32C4AE2C1F1981195F9904466A39C9948FE30BBFF2660BE1715A4589334C74C7
yG = 0xBC3736A2F4F6779C59BDCEE36B692153D0A9877CC62A474002DF32E52139F0A0

2.3 ENTL_A 的计算

PYTHON
# ENTL_A = len(ID_A) in bits
id_bytes = b"A53A"  # 默认标识,ASCII
entl_a = len(id_bytes) * 8  # 4 * 8 = 32 = 0x0020
# 占 2 字节大端序
entl_a_bytes = entl_a.to_bytes(2, 'big')  # b'\x00\x20'

⚠️ 常见错误:将 ENTL_A 写成字节数(4)而非比特数(32)。

三、8 个常见陷阱与修复方案

陷阱 1:Entail 拼写错误

很多人把 Entail 写成 Entail、Ental、Entailment 等。实际上 GM/T 0003.2-2012 标准中的术语是 ENT_L(Entry Length,标识长度),但在代码中通常直接称为 ENT_L 或 entl。

代码中的实际名称:

PYTHON
# gmssl 库中:
from gmssl import sm2
# 签名时自动处理 ZA,开发者无需手动计算
sig = cryptsm2.sign_with_sm3(data, key)

# cryptography 库中:
# 不支持 SM2,无法直接签名

陷阱 2:ID 编码错误

GM/T 0003.2-2012 规定默认 ID 为 A53A(十六进制),对应 ASCII 字符 "A"。

PYTHON
# ✅ 正确:使用 A53A
ID_A = bytes.fromhex('A53A')

# ❌ 错误:直接使用 ASCII "A"
ID_A = b"A"  # 长度相同但含义不同

注意:bytes.fromhex('A53A') 和 b"A" 虽然长度都是 1 字节,但 字节值不同:

写法字节值说明
bytes.fromhex('A53A')0xA5, 0x3A2 字节,标准默认值
b"A"0x411 字节,ASCII A
实际工程中,很多开发者错误地使用 b"A" 代替 bytes.fromhex('A53A'),导致 ZA 计算错误。

陷阱 3:密钥格式错误

gmssl 库要求私钥为 64 字符(32 字节)的十六进制字符串:

PYTHON
# ✅ 正确:64 字符 hex string
private_key = "6d8e5c3a2b1f4e7d9c8b7a6f5e4d3c2b1a0f9e8d7c6b5a4f3e2d1c0b9a8f7e6d"

# ❌ 错误:缺少前导零
private_key = "6d8e5c3a2b1f4e7d9c8b7a6f5e4d3c2b1a0f9e8d7c6b5a4f3e2d1c0b9a8f7e"  # 62字符

修复方案:

PYTHON
def normalize_sm2_key(key: str) -> str:
    """确保私钥为 64 字符 hex 字符串"""
    key = key.replace("0x", "").replace(" ", "")
    if len(key) < 64:
        key = key.zfill(64)
    return key[:64]

陷阱 4:gmssl CryptSM2 初始化错误

gmssl 3.2.x 的 CryptSM2 必须同时传入私钥和公钥:

PYTHON
# ❌ 错误:只传私钥
cryptsm2 = sm2.CryptSM2(private_key)

# ✅ 正确:同时传入私钥和公钥
cryptsm2 = sm2.CryptSM2(private_key, public_key)

公钥格式:65 字节(0x04 + x + y),即 130 字符 hex 字符串。

陷阱 5:手动 SM3 哈希后签名

这是最高频的错误:

PYTHON
# ❌ 错误:手动先做 SM3 哈希
from gmssl import sm3, func
data_hash = sm3.sm3_hash(func.bytes_to_list(message))
sig = cryptsm2.sign(data_hash, k)  # 再次哈希!

# ✅ 正确:使用 sign_with_sm3
sig = cryptsm2.sign_with_sm3(message, k)

原因:sign() 方法内部会再次对输入做 SM3 哈希。如果你传入的已经是哈希值,相当于做了双重哈希,验证必然失败。

陷阱 6:k 值复用攻击

PYTHON
# ❌ 危险:固定 k 值
k = "0000000000000000000000000000000000000000000000000000000000000001"
sig = cryptsm2.sign_with_sm3(message, k)

# 攻击者获取两条相同 k 值的签名后,可推导私钥:
# s1 = (k - r1*dA) / (1+dA) mod n
# s2 = (k - r2*dA) / (1+dA) mod n
# 两式相减可解出 dA

修复:每次签名使用密码学安全的随机数:

PYTHON
import os
k = os.urandom(32).hex()
sig = cryptsm2.sign_with_sm3(message, k)

陷阱 7:公钥格式混淆

SM2 公钥有两种格式:

格式长度说明
压缩格式33 字节0x02/0x03 + x 坐标
非压缩格式65 字节0x04 + x + y
gmssl 库要求 非压缩格式(带 0x04 前缀)。

PYTHON
# ❌ 错误:传入压缩公钥
public_key_compressed = "02" + x_coord.hex()  # 65字符,缺少y

# ✅ 正确:使用完整公钥
public_key_uncompressed = "04" + x_coord.hex() + y_coord.hex()  # 130字符

陷阱 8:gmssl 与 cryptography 库混用

cryptography 库不支持 SM2 签名,但很多开发者误以为支持:

PYTHON
# ❌ 错误:cryptography 不支持 SM2
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature

# 这段代码会报 AttributeError 或 ImportError

正确做法:

PYTHON
# ✅ 使用 gmssl 进行 SM2 签名
from gmssl import sm2, func

# ✅ 或使用 Tongsuo/BabaSSL 的 Python 绑定
# pip install tongsuo  # 需厂商 SDK

四、完整的 ZA 计算验证工具

以下是一个完整的 ZA 计算验证脚本,可用于排查签名问题:

运行结果(默认 ID A53A,无发送方公钥):

CODE
ZA' (134 bytes): 0010a53afffcfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc28e9fa9e9d9f5e344d5a9e4bcf6509a7f39789f515ab8f92ddbcbd414d940e9332c4ae2c1f1981195f9904466a39c9948fe30bbff2660be1715a4589334c74c7bc3736a2f4f6779c59bdcee36b692153d0a9877cc62a474002df32e52139f0a0
ZA (hex): f413f66b21e5a47e074f52968d9fc9b087e11608f2970fb03b7dc54bcb13d3d0

五、生产环境中的最佳实践

5.1 使用标准 API,不要手动计算

PYTHON
from gmssl import sm2

# ✅ 推荐:使用 sign_with_sm3,库内部自动处理 ZA
cryptsm2 = sm2.CryptSM2(private_key, public_key)
signature = cryptsm2.sign_with_sm3(message, None)  # k 由库自动生成

# ✅ 验签同样使用标准 API
is_valid = cryptsm2.verify(signature, message)

5.2 密钥管理的三条红线

PYTHON
import os
import secrets

# 红线 1:私钥必须使用密码学安全的随机数生成
private_key = secrets.token_hex(32)  # 32 字节 = 64 字符 hex

# 红线 2:不要硬编码私钥
# 红线 3:密钥存储必须加密
# 使用 HSM 或 KMIP 管理私钥

5.3 签名前的预检查清单

六、密评中的 ZA 检查要点

根据 GM/T 0034-2014 和密评指南,ZA 相关的检查项:

检查项标准要求常见问题
ZA 计算符合 GM/T 0003.2手动计算错误
ID 编码默认 A53A 或协商值使用错误的 ID
曲线参数GM/T 0003.5-2012使用错误的 a/b 值
签名格式R+S 64 字节或 DER 编码格式不一致
随机数 k密码学安全随机固定 k 值或弱随机数
密评常见问题 TOP 3:

  • ZA 计算错误:手动拼接 ZA 时字段顺序或字节序错误
  • 双重哈希:先用 SM3 哈希消息,再传给 sign(),导致签名验证失败
  • k 值重用:使用固定或弱随机数生成 k,存在私钥推导风险
陷阱问题修复方案
ENTL 单位错误用字节数而非比特数entl = len(id_bytes) * 8
双重哈希先 SM3 再 sign()用 sign_with_sm3()
k 值固定可推导私钥用 os.urandom(32)
公钥格式压缩/非压缩混淆用 0x04 前缀的 65 字节
一句话记住:ENTL 是比特数(4 字节 = 32 bit = 0x0020),不是字节数(4 = 0x0004)。

总结

SM2 签名的 ZA 计算看似简单,实则是国密工程中最容易出错的环节之一。本文总结了 8 个常见陷阱,核心教训:

  • 优先使用库函数:sign_with_sm3() 自动处理 ZA,不要手动计算
  • 注意字节顺序:所有整数字段使用大端序,不足补零
  • 密钥格式检查:私钥 64 字符,公钥 130 字符(非压缩格式)
  • 随机数安全:k 值必须使用密码学安全的随机数生成器
记住:签名验证失败 90% 是 ZA 问题,ZA 问题 90% 是字段顺序或字节序错误。


参考资料