SM2 数字签名算法深度解析:从 ZA 计算到 ECDSA 变体

算法原理 · 2026-08-13

概述

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 中哈希函数选择不当的安全风险
本文将从数学公式出发,完整推导 SM2 签名的签名与验签流程,分析 ZA 值的安全意义,并与 ECDSA、Schnorr 签名进行横向对比。

算法背景与设计动机

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 签名在身份绑定和算法确定性两个维度上优于 ECDSA,同时也为密评合规提供了明确的检查项。

数学原理

曲线参数

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
这些参数定义于 GM/T 0003.5-2012 和 GB/T 32918.5-2016,且与 NIST P-256(secp256r1)的参数完全不同。二者不能互换使用,密钥也不兼容。

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 编码规则

$ENTL_A$ 是 $ID_A$ 长度的比特数,以 2 字节大端序存储。例如:

  • $ID_A = \text{"alice@example.com"}$(19 个字符)
  • $ENTL_A = 19 \times 8 = 152 = \text{0x0098}$(十六进制)
  • 编码为两个字节:0x00 0x98
注意:这里的长度是比特长度,不是字节长度。这是 SM2 实现中常见的错误来源之一。

#### 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 签署了某条消息"的证明
  • 每个签名在数学上唯一绑定到特定身份
这一设计使 SM2 签名天然适合身份认证场景(如数字证书签发、API 调用鉴权),而不仅仅是数据完整性验证场景。

签名算法流程

#### 输入与输出

项目说明
输入私钥 $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$ 安全参数长度否

安全性对比

属性ECDSASM2Schnorr
抗身份混淆攻击❌✅❌
哈希函数固定❌✅(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 实现:

常见错误:将 $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-2012SM2 椭圆曲线公钥密码算法 第 2 部分:数字签名算法第 4 章签名算法、附录 A 测试向量
GB/T 32918.2-2016SM2 椭圆曲线公钥密码算法 第 2 部分:数字签名算法同 GM/T 0003.2-2012,国家标准版本
GM/T 0009-2023SM2 密码算法使用规范§4.1 签名使用约束、§5.1 ZA 值计算
GM/T 0003.5-2012SM2 椭圆曲线公钥密码算法 第 5 部分:参数定义曲线参数 sm2p256v1
GM/T 0005-2012随机性检测规范签名随机数 $k$ 的质量要求

总结

SM2 数字签名算法是国密体系中最核心且应用最广泛的密码服务之一。本文从数学公式出发,完整推导了签名与验签流程,分析了 ZA 值在身份绑定和防混淆攻击中的关键作用,并与 ECDSA、Schnorr 进行了结构化对比。

实践中的核心要点:

  • ZA 值必须正确计算:特别注意 ENTL 是比特长度而非字节长度
  • 随机数 $k$ 严禁重用:一次重用即可完全泄露私钥
  • 签名公式不可与 ECDSA 混用:两者公式不同,互不兼容
  • 严格遵循 GM/T 0009-2023 使用规范:特别是 SM3 不可替换、ID 长度限制等约束
SM2 签名算法的设计体现了国密标准在安全性与实用性之间的平衡:通过身份绑定机制弥补了 ECDSA 的结构性缺陷,同时通过强制使用 SM3 消除了哈希函数选择的不确定性。对于从事国密改造的从业者而言,深入理解 SM2 签名的数学原理和实现细节,是确保密码应用合规的基础。

参考实践