SM2 数字签名算法深度解析:从 ZA 计算到 ECDSA 变体
概述
SM2 数字签名算法是我国自主设计的椭圆曲线数字签名标准,定义于 GM/T 0003.2-2012《SM2 椭圆曲线公钥密码算法 第 2 部分:数字签名算法》 和对应的国家标准 GB/T 32918.2-2016。
该算法的核心定位是作为美国 NIST ECDSA(FIPS 186-4/5)的国密替代方案。相比 ECDSA,SM2 签名引入了两项关键改进:
- ZA 杂凑值:将用户身份标识(ID)与曲线参数绑定到签名前,防止身份混淆攻击
- 强制使用 SM3:杂凑算法不可替换,消除了 ECDSA 中哈希函数选择不当的安全风险
算法背景与设计动机
ECDSA 的安全局限
ECDSA 签名协议(FIPS 186-4 §4.3)的安全性依赖于两个参数:离散对数问题(DLP)和哈希函数的抗碰撞性。然而,ECDSA 存在两个结构性缺陷:
缺陷一:身份不绑定
ECDSA 签名仅对消息 $M$ 进行签名,不涉及签名者身份。攻击者可以用用户 A 的公钥对消息 $M$ 生成有效签名,然后声称"这是用户 B 的签名"。这种"身份混淆攻击"在多方场景中具有实际危害。
缺陷二:哈希算法可替换
ECDSA 允许使用任意安全哈希函数(SHA-256、SHA-3 等)。如果实现者错误选择了弱哈希函数(如 SHA-1),将直接削弱签名安全性。历史上多次因哈希函数退化导致的数字证书伪造事件(如 SHAttered 攻击)都与此相关。
SM2 的设计改进
SM2 签名针对上述缺陷进行了两项设计改进:
| 缺陷 | ECDSA 做法 | SM2 改进 | |
|---|---|---|---|
| 身份不绑定 | 仅对 $H(M)$ 签名 | 对 $H(Z_A \ | M)$ 签名,$Z_A$ 包含用户身份 |
| 哈希可替换 | 任意安全哈希函数 | 强制使用 SM3(GM/T 0009-2023 §4.1) |
数学原理
曲线参数
SM2 签名算法运行在 sm2p256v1 椭圆曲线上,曲线方程为:
$$E: y^2 = x^3 + ax + b \pmod{p}$$
其中各项参数(十六进制表示)如下:
| 参数 | 值 |
|---|---|
| 素数 $p$ | FFFFFFFE FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF 00000000 FFFFFFFF FFFFFFFF |
| 系数 $a$ | FFFFFFFE FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF 00000000 FFFFFFFF FFFFFFFC |
| 系数 $b$ | 28E9FA9E 9D9F5E34 4D5A9E4B CF6509A7 F39789F5 15AB8F92 DDBCBD41 4D940E93 |
| 基点 $G$ 的 $x$ 坐标 | 32C4AE2C 1F198119 5F990446 6A39C994 8FE30BBF F2660BE1 715A4589 334C74C7 |
| 基点 $G$ 的 $y$ 坐标 | BC3736A2 F4F6779C 59BDCEE3 6B692153 D0A9877C C62A4740 02DF32E5 2139F0A0 |
| 子群阶 $n$ | FFFFFFFE FFFFFFFF FFFFFFFF FFFFFFFF 7203DF6B 21C6052B 53BBF409 39D54123 |
| 余因子 $h$ | 1 |
ZA 杂凑值计算
ZA 值是 SM2 签名最具特色的设计,用于将用户身份绑定到签名过程中。
#### 计算公式
$$ZA = H_{256}(ENTL_A \| ID_A \| a \| b \| x_G \| y_G \| x_A \| y_A)$$
其中:
| 符号 | 含义 |
|---|---|
| $ENTL_A$ | 用户标识 $ID_A$ 的比特长度,2 字节大端序整数 |
| $ID_A$ | 用户唯一标识符(ASCII 字符串,最长 2040 字符) |
| $a, b$ | 椭圆曲线方程参数 |
| $(x_G, y_G)$ | 基点 $G$ 的坐标 |
| $(x_A, y_A)$ | 用户公钥 $P_A$ 的坐标 |
| $H_{256}$ | SM3 杂凑函数,输出 256 位 |
$ENTL_A$ 是 $ID_A$ 长度的比特数,以 2 字节大端序存储。例如:
- $ID_A = \text{"alice@example.com"}$(19 个字符)
- $ENTL_A = 19 \times 8 = 152 = \text{0x0098}$(十六进制)
- 编码为两个字节:
0x00 0x98
#### ZA 的安全意义
ZA 值的核心作用是防止身份混淆攻击(Identity Confusion Attack):
假设攻击者截获了用户 A 对消息 $M$ 的有效签名,能否将其冒充为用户 B 的签名?在 ECDSA 中,答案是肯定的——签名只验证消息完整性,不绑定身份。攻击者可以将 A 的签名直接作为 B 的签名提交,只要 B 的公钥已知。
在 SM2 中,答案是否定的——签名过程将 $ZA$(包含 $ID_A$)嵌入签名值 $r$ 的计算中。验证方必须使用 $ID_B$ 重新计算 $ZA_B$,只有当 $ID_B = ID_A$ 时,$r$ 值才会匹配。这意味着:
- 攻击者无法将 A 的签名冒充为 B 的签名
- 攻击者也无法伪造"B 签署了某条消息"的证明
- 每个签名在数学上唯一绑定到特定身份
签名算法流程
#### 输入与输出
| 项目 | 说明 |
|---|---|
| 输入 | 私钥 $d_A$、用户标识 $ID_A$、消息 $M$ |
| 输出 | 签名对 $(r, s)$,其中 $r, s \in [1, n-1]$ |
步骤 1:计算 ZA 杂凑值
$$Z_A = SM3(ENTL_A \| ID_A \| a \| b \| x_G \| y_G \| x_A \| y_A)$$
步骤 2:计算消息杂凑值
$$e = SM3(Z_A \| M)$$
这里 $e$ 是 256 位固定长度的杂凑值,作为签名的"消息摘要"。
步骤 3:生成随机数
$$k \in_R [1, n-1]$$
$k$ 必须通过密码学安全伪随机数生成器(CSPRNG)产生,且每次签名必须使用不同的 $k$ 值。$k$ 的重用是导致私钥泄露的最常见实现错误(详见下方"安全分析"章节)。
步骤 4:计算椭圆曲线点
$$(x_1, y_1) = [k]G$$
其中 $[k]G$ 表示标量乘法运算,结果是一个椭圆曲线点。
步骤 5:计算签名分量 r
$$r = (e + x_1) \bmod n$$
若 $r = 0$ 或 $r + k = n$,则回到步骤 3 重新选择 $k$。
步骤 6:计算签名分量 s
$$s = ((1 + d_A)^{-1} \cdot (k - r \cdot d_A)) \bmod n$$
若 $s = 0$,则回到步骤 3 重新选择 $k$。
步骤 7:输出签名
$$\sigma = (r, s)$$
签名 $(r, s)$ 为两个 256 位整数,总长度固定为 512 位(64 字节)。
#### 公式推导
SM2 签名的数学结构与 ECDSA 类似但存在关键差异。推导如下:
设签名方程为:
$$s = (1+d_A)^{-1}(k - r \cdot d_A) \bmod n$$
将等式两边同时乘以 $(1+d_A)$:
$$(1+d_A) \cdot s = k - r \cdot d_A \pmod{n}$$
移项:
$$k = s(1+d_A) + r \cdot d_A \pmod{n}$$
两边同时乘以 $G$:
$$[k]G = [s(1+d_A) + r \cdot d_A]G \pmod{n}$$
利用椭圆曲线的标量乘法性质 $[ab]G = [a][b]G$:
$$[s][1+d_A]G + [r][d_A]G = [s]G + [s]P_A + [r]P_A$$
其中 $P_A = [d_A]G$ 是用户公钥。整理得:
$$[k]G = [s]G + [r+s]P_A$$
这正是 SM2 验签公式的基础。
注意:SM2 的签名公式采用 $(1+d_A)^{-1}$ 而非 ECDSA 的 $d_A^{-1}$。这一设计变体避免了 $d_A = 0$ 时的除零问题,同时不影响安全性。
验签算法流程
#### 输入与输出
| 项目 | 说明 |
|---|---|
| 输入 | 公钥 $P_A$、用户标识 $ID_A$、消息 $M$、签名 $(r, s)$ |
| 输出 | 验证结果(通过 / 失败) |
步骤 1:参数范围验证
验证 $r, s \in [1, n-1]$。若不满足,直接判定验签失败。
步骤 2:重新计算 ZA 值
$$Z_A = SM3(ENTL_A \| ID_A \| a \| b \| x_G \| y_G \| x_A \| y_A)$$
步骤 3:重新计算消息杂凑值
$$e' = SM3(Z_A \| M)$$
步骤 4:计算辅助值 t
$$t = (r + s) \bmod n$$
若 $t = 0$,判定验签失败。
步骤 5:计算椭圆曲线点
$$(x_1, y_1) = [s]G + [t]P_A$$
步骤 6:计算 R 值
$$R = (e' + x_1) \bmod n$$
步骤 7:验证签名
验证通过当且仅当 $R = r$。
#### 验签公式的正确性证明
将 $t = r + s \pmod{n}$ 代入步骤 5:
$$[s]G + [r+s]P_A = [s]G + [r]P_A + [s]P_A$$
$$= s + [r]P_A$$
由于 $P_A = [d_A]G$,代入:
$$= s + [r][d_A]G$$
$$= [s][1+d_A]G + [rd_A]G$$
$$= [[s(1+d_A) + rd_A]]G$$
将签名公式 $s = (1+d_A)^{-1}(k - rd_A) \pmod{n}$ 代入:
$$= [[(k - rd_A) + rd_A]]G = [k]G = (x_1, y_1)$$
因此 $x_1$ 确实是 $[k]G$ 的横坐标,而:
$$R = (e' + x_1) \bmod n = (e + x_1) \bmod n = r$$
验签公式成立。∎
与 ECDSA、Schnorr 的对比
签名公式对比
| 算法 | 签名公式 | 签名长度 | 身份绑定 |
|---|---|---|---|
| ECDSA | $s = k^{-1}(e + d_A \cdot r) \bmod n$ | 变长(DER 编码) | 否 |
| SM2 | $s = (1+d_A)^{-1}(k - r \cdot d_A) \bmod n$ | 固定 64 字节 | 是(通过 $Z_A$) |
| Schnorr | $s = k - e \cdot d_A \bmod n$ | 固定 $2 \times$ 安全参数长度 | 否 |
安全性对比
| 属性 | ECDSA | SM2 | Schnorr |
|---|---|---|---|
| 抗身份混淆攻击 | ❌ | ✅ | ❌ |
| 哈希函数固定 | ❌ | ✅(SM3) | ❌(可选) |
| 私钥重用防御 | 依赖随机数 $k$ | 依赖随机数 $k$ | 依赖随机数 $k$ |
| 数学安全性证明 | IND-CPA(弱) | 基于 DLP 强模型 | 基于 DLP 强模型 |
| 签名确定性问题 | 是(随机化) | 是(随机化) | 否(可确定性版本) |
关键差异分析
差异一:身份绑定
SM2 签名的 $Z_A$ 值是最大差异化特征。在 PKI 体系中,证书本身已经包含身份绑定(通过 CA 签名),因此 ECDSA 的身份不绑定问题在实际应用中影响有限。但在无证书场景(如 IoT 设备轻量认证、区块链交易)中,SM2 的身份绑定机制具有显著优势。
差异二:哈希函数固定
SM2 强制使用 SM3,消除了"攻击者选择弱哈希"的可能性。这一设计在密码敏捷性(Cryptographic Agility)背景下是一个值得讨论的取舍:固定哈希函数简化了实现,但也降低了算法的适应性。GM/T 0009-2023 明确指出,在任何情况下都不允许替换 SM3。
差异三:签名公式变体
SM2 采用 $(1+d_A)^{-1}$ 而非 ECDSA 的 $d_A^{-1}$。这一变体在国际标准 ISO/IEC 14888-3 中也有收录,被称为"ECDSA 变体"。其核心优势是避免了 $d_A = 0$ 的边界情况,对工程实现更加友好。
安全分析
随机数 $k$ 重用的灾难性后果
SM2 签名使用随机数 $k$ 是一次性参数,绝对禁止重复使用。若两次签名使用了相同的 $k$,攻击者可以轻易恢复私钥。
推导:
假设攻击者获得两条签名 $(r_1, s_1)$ 和 $(r_2, s_2)$,对应消息 $M_1, M_2$,且 $k_1 = k_2 = k$。由于 $k$ 相同,则 $(x_1, y_1) = [k]G$ 也相同,因此 $r_1 = r_2 = r$。
从签名公式: $$s_1 = (1+d_A)^{-1}(k - r \cdot d_A) \bmod n$$ $$s_2 = (1+d_A)^{-1}(k - r \cdot d_A) \bmod n$$
两式相减: $$s_1 - s_2 = (1+d_A)^{-1}(e_1 - e_2) \bmod n$$
解得: $$d_A = (s_1 - s_2)^{-1}(e_1 - e_2) - 1 \bmod n$$
私钥 $d_A$ 被完全恢复。这就是 Sony PlayStation 3 私钥泄露事件的根本原因——开发者错误地硬编码了固定的 $k$ 值。
防护建议:
- 使用 CSPRNG 生成 $k$,禁止硬编码
- 参考 GM/T 0005-2012《随机性检测规范》 对随机数发生器进行定期检测
- 在实现中加入 $k$ 重用检测(缓存已使用的 $k$ 值,虽然会增加内存开销)
ZA 值的边界条件
$Z_A$ 的计算涉及多个参数,以下边界情况需要特别注意:
- $ID_A$ 为空字符串:$ENTL_A = 0$,$Z_A = SM3(0x0000 \| \text{空串} \| a \| b \| x_G \| y_G \| x_A \| y_A)$。此时签名仍然有效,但身份绑定作用减弱。
- $ID_A$ 超长:GM/T 0009-2023 规定 $ID_A$ 最长 2040 个字符(ASCII),超过此长度将导致 $ENTL_A > 16320$ 位,超出 SM3 单次处理的合理范围。实现时应做长度校验。
- $x_A, y_A$ 为零:若用户公钥坐标异常(如 $(0, 0)$),则 $Z_A$ 的计算结果可能泄露关于私钥的信息。实现时应验证公钥的有效性。
与 ECDSA 的侧信道差异
SM2 和 ECDSA 的签名计算都涉及椭圆曲线标量乘法和模逆运算,理论上都会受到侧信道攻击(时序攻击、功耗分析)的影响。但 SM2 的 $Z_A$ 计算引入了额外的 SM3 杂凑步骤,增加了实现复杂度。
防护建议:
- 使用恒定时间实现的 SM3 函数
- 椭圆曲线标量乘法采用蒙哥马利 ladder 算法
- 避免在签名过程中引入基于秘密值的分支
实现要点与常见错误
正确的 ZA 计算
以下是正确的 ZA 计算 Python 实现:
from gmssl import sm3, func
def compute_ZA(user_id: str, curve_a: int, curve_b: int,
Gx: int, Gy: int, Ax: int, Ay: int) -> bytes:
"""
计算 SM2 ZA 杂凑值
参照 GM/T 0003.2-2012 和 GM/T 0009-2023
"""
# 1. 计算 ENTL(比特长度,2字节大端)
id_bytes = user_id.encode('utf-8')
entl = len(id_bytes) * 8 # 比特长度
entl_bytes = func.int_to_bytes(entl, 2) # 2字节大端
# 2. 拼接ZA输入
za_input = entl_bytes + id_bytes
# 3. 添加曲线参数(每个32字节,大端)
za_input += func.int_to_bytes(curve_a, 32)
za_input += func.int_to_bytes(curve_b, 32)
za_input += func.int_to_bytes(Gx, 32)
za_input += func.int_to_bytes(Gy, 32)
za_input += func.int_to_bytes(Ax, 32)
za_input += func.int_to_bytes(Ay, 32)
# 4. SM3杂凑
return sm3.sm3_hash(func.bytes_to_list(za_input))常见错误:将 $ENTL_A$ 误算为字节长度(而非比特长度),这是最频繁的 Z_A 计算错误。
签名与验签的完整性
根据 GM/T 0009-2023 §4.1,SM2 签名的完整流程必须包括以下步骤:
- 计算 $Z_A$(包含完整的 ID、曲线参数、公钥坐标)
- 计算 $e = SM3(Z_A \| M)$
- 生成随机数 $k$,计算 $[k]G$
- 计算 $r = (e + x_1) \bmod n$,验证 $r \neq 0$ 且 $r + k \neq n$
- 计算 $s = ((1+d_A)^{-1} \cdot (k - r \cdot d_A)) \bmod n$,验证 $s \neq 0$
- 输出 $(r, s)$
与 ECDSA 互操作的陷阱
由于 SM2 和 ECDSA 的签名公式不同,两者的签名不能互相验证。以下是常见的互操作错误:
| 错误场景 | 后果 | 正确做法 |
|---|---|---|
| 用 ECDSA 验签 SM2 签名 | 验签失败(公式不匹配) | 使用 SM2 专用验签库 |
| 用 SM2 验签 ECDSA 签名 | 验签失败(缺少 $Z_A$ 绑定) | 区分签名类型,使用对应验签逻辑 |
| 混淆曲线参数 | 私钥生成错误,签名无效 | SM2 使用 sm2p256v1,ECDSA 使用 secp256r1 |
标准依据
本文所有算法描述均依据以下国密标准:
| 标准编号 | 名称 | 对应章节 |
|---|---|---|
| GM/T 0003.2-2012 | SM2 椭圆曲线公钥密码算法 第 2 部分:数字签名算法 | 第 4 章签名算法、附录 A 测试向量 |
| GB/T 32918.2-2016 | SM2 椭圆曲线公钥密码算法 第 2 部分:数字签名算法 | 同 GM/T 0003.2-2012,国家标准版本 |
| GM/T 0009-2023 | SM2 密码算法使用规范 | §4.1 签名使用约束、§5.1 ZA 值计算 |
| GM/T 0003.5-2012 | SM2 椭圆曲线公钥密码算法 第 5 部分:参数定义 | 曲线参数 sm2p256v1 |
| GM/T 0005-2012 | 随机性检测规范 | 签名随机数 $k$ 的质量要求 |
总结
SM2 数字签名算法是国密体系中最核心且应用最广泛的密码服务之一。本文从数学公式出发,完整推导了签名与验签流程,分析了 ZA 值在身份绑定和防混淆攻击中的关键作用,并与 ECDSA、Schnorr 进行了结构化对比。
实践中的核心要点:
- ZA 值必须正确计算:特别注意 ENTL 是比特长度而非字节长度
- 随机数 $k$ 严禁重用:一次重用即可完全泄露私钥
- 签名公式不可与 ECDSA 混用:两者公式不同,互不兼容
- 严格遵循 GM/T 0009-2023 使用规范:特别是 SM3 不可替换、ID 长度限制等约束
参考实践
- SM2 椭圆曲线公钥密码算法 — SM2 三大功能完整概述
- GM/T 0009-2023 SM2 密码算法使用规范 — SM2 使用规范的详细解读
- GM/T 0003-2012 SM2 密码算法标准深度解读 — 五个子标准的完整对照
- SM2 加密算法深度解析 — 公钥加密的 C1C3C2 格式详解
- SM2 密钥交换协议深度解析 — 密钥交换协议的 ZA 计算与协商流程