4. 签名验证审计:公钥点有效性验证、签名值范围检查、验证公式推导

签名验证这块,说实话是 SM2 安全审计里最容易出纰漏的地方。很多人觉得验签嘛,套个标准库就完事了。但我在项目里见过太多次——库是标准库,调用方式不对,照样出大问题。

今天咱们就掰开揉碎,把验签的三个核心审计点讲清楚:公钥点有效性、签名值范围、验证公式推导。这三个点任何一个出问题,攻击者就能伪造签名。

4.1 公钥点有效性验证

先问个问题:你拿到一个公钥,直接拿去验签,你觉得安全吗?

答案是不安全。公钥可能是个无效点,甚至可能是无穷远点。攻击者可以构造一个不在椭圆曲线上的点,让你验签通过。我当年审计一个金融系统时就遇到过——对方用了第三方库,但没检查公钥是否在曲线上,结果被注入了一个低阶点攻击。

4.1.1 必须检查的三件事

我个人习惯,公钥点有效性验证至少要做这三步:

  1. 检查公钥点不是无穷远点——无穷远点验签永远返回真,这你想想看,多可怕。
  2. 检查公钥点坐标在合法范围内——x 和 y 必须在 [1, p-1] 之间,p 是 SM2 的素域模数。
  3. 检查公钥点满足椭圆曲线方程——y² ≡ x³ + ax + b (mod p),这是硬道理。

核心原则:公钥点必须是椭圆曲线上的非无穷远点,且阶为大素数 n 的倍数。

4.1.2 代码示例

下面是我常用的验证逻辑,用伪代码表示,你审计时可以对照:

// 步骤1:检查无穷远点
if (P == O) {
    return INVALID_PUBKEY;
}

// 步骤2:检查坐标范围
if (x < 1 || x >= p || y < 1 || y >= p) {
    return INVALID_PUBKEY;
}

// 步骤3:检查曲线方程
if ( (y * y) % p != (x * x * x + a * x + b) % p ) {
    return INVALID_PUBKEY;
}

// 步骤4(可选但推荐):检查点的阶
if (n * P != O) {
    return INVALID_PUBKEY;
}

注意:步骤4不是所有实现都做,但如果你审计的是高安全等级系统,我建议加上。我曾经审计过一个区块链项目,就是因为没检查阶,被小群攻击把私钥给泄露了。

4.2 签名值范围检查

签名值 (r, s) 的范围检查,看起来简单,但坑不少。SM2 标准里明确写了:r 和 s 都必须在 [1, n-1] 范围内,n 是 SM2 的基点阶。

但我在实际审计中发现,很多实现只检查了 r 的范围,忘了检查 s。或者检查了 s,但用的是 <= n 而不是 < n。这些细节,攻击者都能利用。

4.2.1 检查清单

检查项 正确条件 常见错误
r 的范围 1 ≤ r ≤ n-1 漏检 r=0 或 r=n
s 的范围 1 ≤ s ≤ n-1 只检 r 不检 s
r 与 s 的关系 r + s ≠ n(部分场景) 完全忽略

小技巧:审计时可以直接看代码里有没有 if (r < 1 || r >= n) 和 if (s < 1 || s >= n) 这两个判断。少一个,就是漏洞。

4.3 验证公式推导

SM2 的验签公式,说白了就是三步走:

  1. 计算消息摘要 e = H(M)
  2. 计算 t = (r + s) mod n,如果 t = 0 则验签失败
  3. 计算点 (x₁, y₁) = sG + tP,验证 R = (e + x₁) mod n 是否等于 r

这个公式推导,我建议你审计时自己手算一遍。为什么?因为很多库的实现有细微偏差,比如把 e 和 x₁ 的顺序搞反了,或者模运算写成了普通加法。

3.3.1 公式推导的审计要点

我个人习惯,审计验证公式时重点看这几个地方:

  • 消息摘要 e 的计算方式——SM2 用的是 ZA || M 的哈希,不是直接对 M 哈希。这个 ZA 包含用户身份信息,很多人会漏掉。
  • t 的计算——t = (r + s) mod n,注意是模 n 不是模 p。我见过有人写成模 p,结果验签全乱套。
  • 点乘的顺序——sG + tP,先算 sG 和 tP,再相加。有些实现为了性能用了联合点乘,但结果必须等价。
  • 最终比较——R = (e + x₁) mod n,比较 R 和 r。注意 e 是整数,x₁ 是点的 x 坐标,两者相加前要确保类型一致。

避坑指南:我曾经审计过一个硬件钱包,它的验签实现把 e 和 x₁ 的加法写成了 e + x₁ % n,由于运算符优先级问题,实际计算的是 e + (x₁ % n)。虽然结果一样,但代码可读性极差,容易在后续维护中引入 bug。

4.4 核心流程总结

下面这张图,是我做审计时贴在工位上的。每次验签审计,我就对着这张图逐项检查:

SM2 签名验证审计流程 输入:M, (r,s), P 步骤1:公钥点有效性验证 非无穷远 · 坐标范围 · 曲线方程 · 阶检查 步骤2:签名值范围检查 1 ≤ r ≤ n-1 · 1 ≤ s ≤ n-1 步骤3:计算消息摘要 e e = H(ZA || M) 步骤4:计算 t 和点 (x₁, y₁) t = (r+s) mod n · (x₁,y₁) = sG + tP 步骤5:R = (e+x₁) mod n == r ? 常见审计发现 • 公钥未检查曲线方程 • s 的范围漏检 • ZA 缺失导致摘要错误 • 模运算用错模数

4.5 实战避坑清单

最后,我把自己这些年踩过的坑整理成一份清单,你审计时可以直接对照:

  • 公钥点没检查无穷远点——攻击者传个 O 进来,验签直接返回真。我见过一个 IoT 设备因此被批量伪造固件签名。
  • 签名值 s 没检查范围——s=0 时,验签公式里 t = r,点乘结果可控,攻击者可以构造特殊签名。
  • 摘要计算漏了 ZA——SM2 的摘要不是 H(M),而是 H(ZA || M)。ZA 包含用户 ID、曲线参数等。漏掉 ZA,签名可被跨用户复用。
  • 模运算用错模数——t 的计算是模 n,不是模 p。n 和 p 不一样,SM2 的 n 略小于 p。
  • 点乘结果没检查——sG + tP 如果算出来是无穷远点,验签应该直接失败。有些实现没做这个检查。

严重警告:以上任何一个点出问题,都可能导致签名伪造。这不是理论风险,是我在真实项目中验证过的。审计时请逐项核对,不要跳过任何一步。

嗯,签名验证审计就讲到这里。记住三个核心:公钥点有效性、签名值范围、验证公式推导。每个点都检查到位,SM2 验签这块就不会出大问题。