4. SM2 数字签名算法:签名、验签与随机数 k 的秘密
数字签名,说白了就是给一段数据盖个「电子印章」。你盖了章,别人就能确认这数据是你发的,而且中途没人动过手脚。SM2 的签名算法,就是国标里规定的盖章方式。
我个人习惯把签名和验签看作一对「锁与钥匙」的操作。签名是用私钥上锁,验签是用公钥开锁。但这里有个关键点——每次上锁,都得换一把新锁芯。这个锁芯,就是随机数 k。
4.1 签名流程:三步走,步步有坑
SM2 签名算法基于椭圆曲线离散对数难题。流程不复杂,但细节决定成败。我直接上流程,然后拆开讲。
签名输入:待签名消息 M、签名者私钥 dA、椭圆曲线参数(G、n 等)
签名输出:签名值 (r, s),其中 r 和 s 都是大整数
- 预处理消息:计算消息 M 的哈希值 e = H256(ZA || M)。ZA 是签名者的身份标识和公钥的杂凑值。
- 生成随机数:产生一个随机数 k ∈ [1, n-1]。
- 计算椭圆曲线点:计算点 (x1, y1) = [k]G。
- 计算 r:r = (e + x1) mod n。如果 r = 0 或 r + k = n,回到步骤 2。
- 计算 s:s = ((1 + dA)-1 · (k - r · dA)) mod n。如果 s = 0,回到步骤 2。
- 输出签名:(r, s)。
嗯,这里要注意步骤 4 和 5 里的「回到步骤 2」。这不是为了凑字数,而是安全需要。我曾经在项目里见过有人偷懒,把 r=0 的情况直接跳过不处理,结果被攻击者利用,伪造了签名。所以,这些检查一个都不能省。
4.2 验签流程:反向操作,验证真伪
验签是签名的逆过程。接收方拿到消息 M 和签名 (r, s),用签名者的公钥 PA 来验证。
验签输入:消息 M、签名 (r, s)、签名者公钥 PA、椭圆曲线参数
验签输出:是否通过验证(True/False)
- 检查范围:验证 r, s ∈ [1, n-1]。不在范围内直接返回 False。
- 计算哈希:同样计算 e = H256(ZA || M)。
- 计算 t:t = (r + s) mod n。如果 t = 0,返回 False。
- 计算椭圆曲线点:计算点 (x1, y1) = [s]G + [t]PA。
- 验证 R:计算 R = (e + x1) mod n。如果 R = r,验证通过;否则失败。
你想想看,验签过程里没有用到私钥,只用公钥就能完成。这就是非对称密码的魅力——私钥签名,公钥验签,各司其职。
4.3 为什么需要随机数 k?—— 没有它,签名就废了
这个问题我当年刚学的时候也困惑过。为什么每次签名都要生成一个新的随机数 k?直接用固定的不行吗?
答案是:绝对不行。
随机数 k 是 SM2 签名算法的安全基石。它的作用有三点:
| 作用 | 说明 | 如果缺失会怎样 |
|---|---|---|
| 防止私钥泄露 | k 参与 s 的计算,如果 k 固定或可预测,攻击者可以通过两个签名反推出私钥 dA | 私钥被直接算出,签名体系崩溃 |
| 保证签名唯一性 | 同一消息每次签名得到不同的 (r, s),防止重放攻击 | 签名可被复制,失去防伪能力 |
| 抵抗侧信道攻击 | 随机化计算过程,让攻击者无法通过功耗、时间等侧信道信息推断私钥 | 攻击者可通过物理手段逐步窃取私钥 |
我曾经踩过的坑:有一次在嵌入式设备上实现 SM2 签名,为了省时间,我用了伪随机数生成器(PRNG)且种子固定。结果测试时发现,两次签名同一消息,得到的 (r, s) 完全一样。这等于把私钥拱手送人。后来我换成了硬件真随机数发生器(TRNG),问题才解决。
教训:随机数 k 必须使用密码学安全的随机数生成器(CSPRNG),且每次签名独立生成。不要复用,不要预测,不要偷懒。
4.4 签名与验签的数学原理(快速回顾)
如果你对椭圆曲线点乘不熟,这里简单提一句。签名里的 [k]G 就是点 G 自加 k 次。椭圆曲线上的点加和倍点运算,构成了整个签名算法的基础。
验签时为什么 [s]G + [t]PA 能还原出 [k]G?因为:
[s]G + [t]PA = [s]G + [t·dA]G = [(s + t·dA)]G
把 s 和 t 的表达式代入,经过化简,最终得到 [k]G。这就是验签能成功的数学保证。
4.5 核心流程图:签名与验签的对照
下面这张 SVG 图,把签名和验签的流程放在一起对比。左边是签名,右边是验签,中间是共享的预处理步骤。你看一眼就能明白整个逻辑。
4.6 避坑指南:签名实现中的常见错误
我在多个项目中 review 过 SM2 签名代码,发现几个高频错误。列出来,你写代码时注意避开。
- 随机数 k 的生成质量不够:用
rand()或Math.random()生成 k。这些不是密码学安全的。必须用/dev/urandom、CryptGenRandom或硬件 TRNG。 - 忘记检查 r 和 s 是否为 0:如果 r=0 或 s=0,签名无效,必须重新生成 k。有些实现偷懒跳过,导致签名可被伪造。
- ZA 计算错误:ZA 包含签名者的 ID、曲线参数和公钥。不同实现如果 ID 编码方式不一致,会导致验签失败。国标规定 ID 默认是 "1234567812345678",但很多项目会自定义,必须提前约定好。
- 模逆运算实现不当:计算 (1+dA)-1 mod n 时,要用扩展欧几里得算法或费马小定理。不要用浮点数,不要用近似值。
我的建议:如果你是在生产环境中实现 SM2 签名,优先使用经过国密局认证的密码库(如 GmSSL、OpenSSL 1.1.1+ 的国密支持)。自己手写签名算法,很容易在细节上出错。除非你是为了学习原理,否则别重复造轮子。
4.7 小结
SM2 数字签名算法,核心就三件事:私钥签名、公钥验签、随机数 k 保命。签名流程里,k 的随机性直接决定了私钥的安全;验签流程里,椭圆曲线点乘的正确性决定了验签能否通过。两者配合,构成了国标级的数字签名方案。
下次你写签名代码时,记得多检查一遍随机数生成器。我曾经因为这个问题吃过亏,希望你不用再踩一遍。