SM2算法部署实践全流程解析 - 第4节
4. SM2数字签名算法:签名与验签流程
数字签名,说白了就是给一段数据盖个「电子印章」。SM2的签名算法和ECDSA长得很像,但细节上处处不同。我刚开始接触时也经常搞混,后来踩了几个坑才彻底理清楚。
4.1 签名流程
签名方持有私钥 dA,要对消息 M 生成签名 (r, s)。流程如下:
- 计算
Z = SM3(ENTLA || IDA || a || b || xG || yG || xA || yA) - 计算
e = SM3(Z || M) - 生成随机数
k ∈ [1, n-1] - 计算椭圆曲线点
(x1, y1) = [k]G - 计算
r = (e + x1) mod n,若r = 0或r + k = n则回到步骤3 - 计算
s = ((1 + dA)^(-1) * (k - r * dA)) mod n,若s = 0则回到步骤3 - 输出签名
(r, s)
关键点:SM2的签名方程是 s = (1+dA)^(-1) * (k - r*dA),而ECDSA是 s = k^(-1) * (e + r*dA)。结构完全不同,千万别套用ECDSA的公式。
4.2 验签流程
验签方持有公钥 PA,收到消息 M' 和签名 (r', s'):
- 检查
r' ∈ [1, n-1],s' ∈ [1, n-1],不通过则验签失败 - 计算
Z = SM3(ENTLA || IDA || a || b || xG || yG || xA || yA) - 计算
e' = SM3(Z || M') - 计算
t = (r' + s') mod n,若t = 0则验签失败 - 计算椭圆曲线点
(x1', y1') = [s']G + [t]PA - 计算
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值计算步骤
- ENTLA:用户身份IDA的比特长度,用2字节表示。比如IDA是"Alice",长度5字节=40比特,ENTLA=0x0028。
- IDA:用户身份标识,通常用字符串,国密标准推荐默认值"1234567812345678"。
- 曲线参数:
a、b、xG、yG,都是256位大数,按字节串拼接。 - 公钥:
xA、yA,也是256位大数。 - 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 完整签名与验签流程图
下面这张图把签名和验签的完整流程画出来了,我建议你保存下来,实现时对照着看:
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次签名内恢复出私钥。