SM2算法部署实践全流程解析 - 第4节

4. SM2数字签名算法:签名与验签流程

数字签名,说白了就是给一段数据盖个「电子印章」。SM2的签名算法和ECDSA长得很像,但细节上处处不同。我刚开始接触时也经常搞混,后来踩了几个坑才彻底理清楚。

4.1 签名流程

签名方持有私钥 dA,要对消息 M 生成签名 (r, s)。流程如下:

  1. 计算 Z = SM3(ENTLA || IDA || a || b || xG || yG || xA || yA)
  2. 计算 e = SM3(Z || M)
  3. 生成随机数 k ∈ [1, n-1]
  4. 计算椭圆曲线点 (x1, y1) = [k]G
  5. 计算 r = (e + x1) mod n,若 r = 0 或 r + k = n 则回到步骤3
  6. 计算 s = ((1 + dA)^(-1) * (k - r * dA)) mod n,若 s = 0 则回到步骤3
  7. 输出签名 (r, s)

关键点:SM2的签名方程是 s = (1+dA)^(-1) * (k - r*dA),而ECDSA是 s = k^(-1) * (e + r*dA)。结构完全不同,千万别套用ECDSA的公式。

4.2 验签流程

验签方持有公钥 PA,收到消息 M' 和签名 (r', s'):

  1. 检查 r' ∈ [1, n-1],s' ∈ [1, n-1],不通过则验签失败
  2. 计算 Z = SM3(ENTLA || IDA || a || b || xG || yG || xA || yA)
  3. 计算 e' = SM3(Z || M')
  4. 计算 t = (r' + s') mod n,若 t = 0 则验签失败
  5. 计算椭圆曲线点 (x1', y1') = [s']G + [t]PA
  6. 计算 R = (e' + x1') mod n,若 R = r' 则验签通过,否则失败

我的经验:验签时最容易出错的是步骤4的 t = (r' + s') mod n。ECDSA验签用的是 u1 = e' * s'^(-1) 和 u2 = r' * s'^(-1),SM2直接相加就行。我第一次实现时习惯性写了除法,结果验签死活过不了。

4.3 SM2签名与ECDSA的区别

两者虽然都基于椭圆曲线,但差异不小。我整理了一张对比表:

对比项 SM2 ECDSA
哈希算法 SM3(256位) SHA-1/256/384/512
消息预处理 需要计算Z值(包含用户身份) 直接对消息哈希
签名方程 s = (1+dA)^(-1)*(k - r*dA) s = k^(-1)*(e + r*dA)
验签方程 [s']G + [t]PA,其中 t = r'+s' [e'*s'^(-1)]G + [r'*s'^(-1)]PA
r的计算 r = (e + x1) mod n r = x1 mod n
安全性依赖 SM3抗碰撞 + 椭圆曲线离散对数 SHA抗碰撞 + 椭圆曲线离散对数
签名长度 64字节(r和s各32字节) 64字节(r和s各32字节,256位曲线)

避坑指南:我曾经在对接国密网关时,对方把SM2签名当ECDSA验签,结果r值算出来总是差一点。查了两天才发现是 r = (e + x1) mod n 和 r = x1 mod n 的区别。SM2把消息摘要e揉进了r,ECDSA没有。这个差异会导致签名完全不兼容。

4.4 Z值计算与消息预处理

Z值是SM2签名里最容易被忽略的部分。说白了,Z值就是把签名者的身份信息、曲线参数、公钥统统哈希一遍,然后拼到消息前面再哈希。这样做的好处是:签名和签名者的身份绑定了,不能张冠李戴。

4.4.1 Z值计算步骤

  1. ENTLA:用户身份IDA的比特长度,用2字节表示。比如IDA是"Alice",长度5字节=40比特,ENTLA=0x0028。
  2. IDA:用户身份标识,通常用字符串,国密标准推荐默认值"1234567812345678"。
  3. 曲线参数:a、b、xG、yG,都是256位大数,按字节串拼接。
  4. 公钥:xA、yA,也是256位大数。
  5. SM3哈希:把上面所有字节串拼起来,做一次SM3,得到32字节的Z值。

Z值公式:
Z = SM3(ENTLA || IDA || a || b || xG || yG || xA || yA)

4.4.2 消息预处理

预处理后的消息摘要 e 是这样算的:

e = SM3(Z || M)

其中 M 是原始消息。也就是说,SM2签名实际上是对 Z || M 这个组合做签名。验签时也要用同样的方式计算 e'。

我踩过的坑:有一次做跨平台联调,Android端签名,服务端验签。两边Z值算出来不一样。查了半天,发现是IDA的编码方式不同——Android端用了UTF-8,服务端用了GBK。SM3对字节敏感,编码不一致Z值就不一样。后来统一用UTF-8才解决。

4.4.3 为什么需要Z值?

你想想看,如果没有Z值,签名只和消息本身有关。那攻击者拿到你的签名后,可以把你的公钥换掉,声称这个签名是他用自己的私钥签的。有了Z值,签名里嵌入了公钥的哈希,公钥被篡改后验签必然失败。

说白了,Z值就是给签名加了一把「身份锁」。ECDSA没有这个设计,所以ECDSA签名本身不绑定签名者身份,需要靠证书链来保证公钥的归属。SM2把这个绑定做到了算法层面,更安全,但也更复杂。

4.5 完整签名与验签流程图

下面这张图把签名和验签的完整流程画出来了,我建议你保存下来,实现时对照着看:

SM2 数字签名与验签完整流程 签名方 私钥 dA + 消息 M 计算 Z = SM3(ENTLA||IDA||...) 计算 e = SM3(Z || M) 生成随机数 k ∈ [1, n-1] 计算 (x1,y1) = [k]G 计算 r = (e + x1) mod n 计算 s = (1+dA)^(-1)*(k - r*dA) 输出签名 (r, s) 验签方 公钥 PA + 消息 M' + 签名 (r',s') 检查 r',s' ∈ [1, n-1] 计算 Z' = SM3(ENTLA||IDA||...) 计算 e' = SM3(Z' || M') 计算 t = (r' + s') mod n 计算 (x1',y1') = [s']G + [t]PA 计算 R = (e' + x1') mod n R == r' ? 通过 : 失败 传输 (r, s) 和 M 注意:签名方和验签方必须使用相同的 IDA、曲线参数和公钥,否则 Z 值不同,验签必然失败

4.6 实现中的常见问题

  • Z值计算不一致:IDA编码、字节序、大数转字节串的方式必须统一。我建议在项目文档里写清楚「所有大数采用大端序,IDA采用UTF-8编码」。
  • 随机数k的生成:必须使用密码学安全的随机数生成器。我曾经见过有人用 rand() 生成k,结果签名被攻破。嗯,那是个惨痛的教训。
  • 验签时的点乘顺序:[s']G + [t]PA 是先分别做标量乘,再做点加。不要写成 [s' + t]G,那是错的。
  • 边界条件:r = 0 或 s = 0 时必须重新生成k。虽然概率极低,但必须处理。

安全警告:SM2签名算法对侧信道攻击比较敏感。如果是在嵌入式设备或移动端实现,建议使用常数时间实现,避免k的生成时间泄露。我曾经在ARM Cortex-M4上做过测试,不加防护时通过功耗分析可以在1000次签名内恢复出私钥。