4. SM2 验签过程:验签算法步骤、公钥恢复、验证流程
验签,说白了就是对方扔给你一条消息、一个签名,还有他自称用的公钥,你得判断这到底是不是他签的。我刚开始做密码应用的时候,总觉得验签就是反过来跑一遍签名流程,结果踩了不少坑——后面我会细说。
SM2 的验签流程跟签名流程有对应关系,但不是简单的“反过来”。它依赖椭圆曲线上的配对验证。你想想看,签名时我们用私钥做了个点的乘法,验签时就要用公钥把这个点“解”回来,然后比较数值。
先看整体流程的骨架,我习惯用这张图来理解:
4.1 验签算法步骤拆解
上面那张图已经把五步串起来了,我再带你把每一步的数学细节过一遍。我个人习惯把验签分成三个阶段:预处理 → 组合验证 → 判等。
- 计算消息摘要 e:用 SM3 对原始消息 M 做哈希,得到 256 位的 e。注意这里要带上身份标识和椭圆曲线参数的哈希吗?SM2 签名时用了 ZA 和 ZB 来绑定用户身份,验签时也要用相同的 ZA(或 ZB)参与运算。我在项目中遇过很多人漏掉这个,导致验签死活不过。
- 验证签名值 (r, s) 的范围:检查 r 和 s 是否都在区间 [1, n-1] 内,并且 (r + s) mod n 不能为 0。为什么要检查?因为如果 r 或 s 等于 0,或者 (r+s) ≡ 0 mod n,那后面椭圆曲线点计算会出问题,攻击者可能伪造签名。
- 计算 t:t = (r + s) mod n。如果 t == 0,立刻返回验签失败。
- 计算椭圆曲线点 (x1', y1') = s·G + t·P:这里 G 是 SM2 标准曲线上的基点,P 是对方的公钥。这是一个标量乘和点加的组合运算。我建议用带坐标变换的雅可比坐标来算,纯仿射坐标在大数运算时速度慢一个数量级。
- 计算 R = (e + x1') mod n,然后判断 R 是否等于 r。相等则验签通过,否则失败。
我的小技巧:在实现验签时,可以复用签名模块里的点乘和点加函数。但要注意验签时的 s·G + t·P 是两个标量乘一次点加,签名时是 k·G(一个标量乘)。别搞混了。
4.2 公钥恢复 —— 到底能不能恢复?
你可能会问:验签时公钥 P 从哪来?是不是可以从签名 (r,s) 里把公钥反推出来?我明确告诉你:SM2 不支持签名中的公钥恢复。这是它跟 ECDSA 的一个区别。ECDSA 的某些变体(如 secp256k1 的 ECDSA 恢复公钥)允许你不带公钥只传签名,但 SM2 不允许。为什么?因为 SM2 签名公式里没给公钥留“解方程”的冗余信息。
那么在实战中,公钥怎么来?有三种常用方式:
| 方式 | 说明 | 适用场景 |
|---|---|---|
| 1. 证书携带 | 签名方提供数字证书(如 X.509),验签方从证书中提取公钥并验证证书链 | 需要身份认证的正式系统 |
| 2. 密钥库读取 | 双方预先交换并存储对方公钥在本地密钥库 | 嵌入式设备、物联网 |
| 3. 在线获取 | 验签时通过网络向公钥服务器请求对方公钥 | 即时通信、动态组网 |
我曾经踩过的坑:有一次做网关验签,对方直接传了公钥的十六进制字符串过来,我直接用那个字符串验签,结果总是失败。后来发现公钥坐标的格式不对(比特位顺序反了)。记住:SM2 公钥有两种标准表示——完整坐标(04||X||Y)和非压缩坐标(03/02||X)。验签时务必确认使用的格式跟签名生成时一致。
4.3 验证流程 —— 完整代码片段
下面我用 Python 演示验签的核心逻辑。为了突出流程,省略了椭圆曲线底层的大数运算(你可以调用像 gmssl 或 sm 这类库)。
def sm2_verify(M, r, s, P, ZA):
# 1. 计算 e = SM3(ZA || M)
e = sm3(ZA + M)
# 2. 检查 r,s 范围
n = sm2_params['n']
if not (1 <= r <= n-1 and 1 <= s <= n-1):
return False
# 3. 计算 t = (r + s) % n
t = (r + s) % n
if t == 0:
return False
# 4. 计算点 (x1', y1') = s*G + t*P
point = point_add(scalar_mult(s, G), scalar_mult(t, P))
# 5. 计算 R = (e + x1') % n
R = (e + point[0]) % n
return R == r
重点提醒:上面代码中的 ZA 是签名方的身份杂凑值,由签名方 ID、椭圆曲线参数等拼接后 SM3 得到。验签时一定要用与签名方相同的 ID 和参数重新计算 ZA,否则摘要 e 对不上。
4.4 实战中容易出错的细节
- 哈希顺序:SM3(M) 还是 SM3(ZA||M)?SM2 签名标准明确是 SM3(ZA||M),ZA 在前,消息在后。我见过有人用 SM3(M||ZA) 做验签,结果自然不对。
- 签名值格式:网络传输中 (r,s) 通常是大端字节序的 32 字节整数。记得在验签前转换成 Python 整数。
- 点加计算时公钥有效性:公钥 P 必须在椭圆曲线上且不是无穷远点。最好在验签前做一次 SM2 椭圆曲线群上的点合法性校验。
- 性能优化:如果频繁验签,可以预计算 G 的倍数点(比如使用固定基点的 wNAF 方法),能显著加速 s·G 运算。
嗯,到这里验签的核心内容就讲完了。下一部分我们会在实战项目里把签名和验签串起来,到时候再聊具体工程化的细节。