SM2签名算法:从流程到实战
SM2签名算法,说白了就是给数据打上一个「防伪标签」。谁签的名,数据有没有被篡改,一验便知。跟我前几年做过的CA认证系统一样,这套流程在密钥分层管理里至关重要——上层CA签下级证书,全指着它。
签名流程看着公式多,但拆开来看,核心就三步:预处理、签名生成、签名验证。每一步都有讲究,尤其是哈希函数的配合,稍有偏差,整个机制就垮了。
预处理阶段:Z值与哈希的第一次配合
签名之前,SM2不像RSA那样直接哈希消息完事。它多了一个步骤——生成用户标识的哈希摘要ZA。我刚开始接触SM2时觉得这步多余,后来才明白,这是为了防止「密钥替换攻击」。嗯,设计者考虑得很周全。
ZA的计算涉及以下参数拼接:
- ENTLA:用户标识的长度(2字节)
- IDA:用户标识(比如身份证号或设备序列号)
- a, b, xG, yG:椭圆曲线参数(SM2标准曲线固定)
- xA, yB:签名者的公钥坐标
把这些串成一个比特串,然后用SM3哈希一次,就得到ZA。我个人习惯是在代码里用结构体封装这段逻辑,清晰一点,不容易出错。
// 伪代码:计算Z值
function deriveZ(IDA, publicKey, curveParams):
entla = len(IDA) * 8; // 转换为比特长度
a = curveParams.a;
b = curveParams.b;
xG = curveParams.xG;
yG = curveParams.yG;
xA = publicKey.x;
yA = publicKey.y;
buffer = concat(entla, IDA, a, b, xG, yG, xA, yA);
return SM3(buffer);
签名生成:两个随机数一个签名
签名过程会生成一对签名值(r, s)。这里最关键的是那个临时随机数k——千万不能重复,也千万不能泄漏。我记得有一次研发同事把随机数生成器写错了,导致k值固定,结果私钥直接被恢复出来了……那场面,啧啧。
签名的步骤如下:
- 把消息M和ZA拼接成M' = ZA || M
- 计算e = SM3(M'),得到哈希摘要
- 生成随机数k ∈ [1, n-1](n是椭圆曲线的阶)
- 计算椭圆曲线点(x1, y1) = [k]G
- 计算r = (e + x1) mod n,如果r == 0或r + k == n则返回第3步重试
- 计算s = ((1 + dA)⁻¹ × (k - r × dA)) mod n,如果s == 0则返回第3步重试
- 输出签名(r, s)
这里我提一个坑:SM2的签名计算用到了模逆运算(1 + dA)⁻¹。有些密码库的模逆实现没处理边界情况,当(1 + dA)与n不互质时会崩。好在SM2标准曲线参数已经保证了这个值肯定存在,但如果你换曲线,就得留个心眼。
签名验证:三步确认,一个不能少
验证流程比生成简单——不需要私钥,但需要你的公钥和Z值。整个过程其实就是把签名流程倒过来走一遍,检查最后得到的R'是否等于r。
- 检查r, s是否在[1, n-1]范围内,不在直接拒掉
- 计算e = SM3(ZA || M)
- 计算t = (r + s) mod n,如果t == 0,验证失败
- 计算椭圆曲线点(x1, y1) = [s]G + [t]PA(PA是签名者的公钥)
- 计算R = (e + x1) mod n
- 如果R == r,验证通过;否则失败
你想想看,这里有个精妙之处:验证过程中不需要私钥,也不需要解密密文。只要能拿到公钥,任何人都能验证签名。这也是PKI体系能运转起来的基础。
| 步骤 | 签名者操作 | 验证者操作 |
|---|---|---|
| 预处理 | 计算ZA,拼接M',计算e | 使用同样的ZA,拼接M',计算e |
| 核心计算 | 生成随机数k,标量乘点,模逆运算 | 标量乘点,点加,模运算 |
| 结果 | 输出(r, s) | 比较R与r |
| 依赖密钥 | 私钥dA | 公钥PA |
哈希算法的选择:为什么必须用SM3?
SM2标准明确规定了哈希算法使用SM3。但现实项目中总有各种妥协——比如跟国际系统对接,对方只支持SHA-256。我见过不少方案强行把SM2的哈希换成SHA-256,结果出了兼容性问题。
国密标准里的规定很死:SM2签名用的哈希函数输出长度必须是256位。SM3刚好是256位,SHA-256也是256位,理论上可以换。但要注意两件事:
- SM3和SHA-256的填充方式不同:SM3采用Merkle–Damgård结构,SHA-256也是,但具体填充位有差异。直接换哈希可能导致Z值计算对不上。
- 合规性:在等保三级或密评场景下,用SHA-256代替SM3属于「未使用国密算法」,评测直接不通过。
我的建议是:如果系统全栈国产化,老老实实全用SM3,别给自己找事。如果必须跟国际系统互通,那就得在协议层约定好哈希算法的标识字段,签名时带上算法ID,验证时根据ID选择对应的哈希函数。
哈希配合的核心原则:签名者和验证者使用的哈希算法必须完全一致,包括Z值计算阶段和签名阶段的哈希。任何一方的哈希实现有偏差,签名都验证不过。
一张图串起整个流程
下面这张流程图,我画了签名和验证两个轨道。你会发现它们像镜像一样——签名时用私钥,验证时用公钥;签名时算点和模逆,验证时算点和比较。理解了这张图,SM2签名就算真正拿下了。
几个容易踩的坑
实战中我总结过几个高频问题,这里列出来,免得你重蹈覆辙:
- Z值缓存不一致:签名方和验证方如果用了不同的IDA(比如一个用设备ID,一个用用户ID),Z值对不上,签名永远过不了。我曾经排查过整整一天的问题,最后发现是配置中心里IDA字段写错了。
- 哈希调用时机不对:SM2的e值是在拼接Z和M之后才可哈希,不是直接哈希M。有些开源库的封装不一致,你得自己看清楚文档。
- 随机数k的生存周期:签名完成后k必须立即销毁,不能存到数据库或日志里。如果k泄漏,攻击者可以反推出私钥。这不是理论威胁——2011年索尼PS3就是因为k固定被破解的。