3. 签名生成审计:随机数k的生成质量、杂凑值计算流程、签名参数合法性
签名生成这个环节,是SM2安全审计里最容易出问题的地方之一。我这些年做安全审计,见过太多「算法实现没问题,但签名就是能被伪造」的案例。说白了,问题往往不出在算法本身,而出在实现细节上。
今天咱们就聚焦三个核心点:随机数k的生成质量、杂凑值计算流程、签名参数合法性。这三个点任何一个出问题,整个签名体系就形同虚设。
核心观点:SM2签名的安全性,本质上依赖于随机数k的不可预测性。k一旦泄露或可预测,私钥就等于公开了。
3.1 随机数k的生成质量审计
先说说随机数k。SM2签名算法里,k是每次签名时临时生成的随机数,范围在[1, n-1]之间。n是SM2椭圆曲线的阶,一个256位的大素数。
为什么k这么重要?因为签名值(r, s)中的r = (k * G).x,s = (1 + dA)^(-1) * (k - r * dA) mod n。如果你能猜出k,或者两次签名用了同一个k,私钥dA就能被直接算出来。
我记得有一次审计某个金融系统的SM2签名模块,发现他们的随机数生成器用的是C语言的rand()函数。嗯,你没看错,就是那个伪随机数生成器。我当时就跟他们说:「你们这个签名,跟直接把私钥写在代码里没什么区别。」
审计要点清单
| 审计项 | 检查内容 | 常见问题 |
|---|---|---|
| 随机源 | 是否使用密码学安全的随机数生成器(CSPRNG) | 使用rand()、random()等非安全随机源 |
| 熵源质量 | 是否从操作系统熵池(如/dev/urandom)获取种子 | 固定种子、时间戳作为种子 |
| k的唯一性 | 每次签名是否生成新的k,且不重复 | k重用导致私钥泄露 |
| k的比特长度 | k的比特长度是否等于n的比特长度(256位) | k比特长度不足,降低安全性 |
| k的偏置检测 | k的分布是否均匀,无统计偏置 | 高位或低位存在偏置 |
⚠️ 我曾经遇到过一个案例:某厂商的SM2签名实现,为了「性能优化」,把k的生成改成了「预先生成一批k,存起来,签名时取用」。结果这批k是用同一个种子生成的,而且没有做重复检测。审计时发现,1000个签名里,有3个用了相同的k。私钥直接暴露。
排查方法
- 源码审查:检查随机数生成器的调用链。确认是否调用了操作系统提供的安全随机接口(如Linux的getrandom()、Windows的BCryptGenRandom())。
- 日志审计:如果系统有签名日志,检查是否有相同的r值出现。r相同意味着k相同,这是最直接的证据。
- 统计测试:收集大量签名中的r值,做卡方检验或KS检验,看分布是否均匀。我习惯用NIST SP 800-22的随机性测试套件来跑一遍。
- 代码插桩:在测试环境里,对k的生成过程插桩,打印k的十六进制值,人工检查是否有规律。
💡 个人习惯:我审计时,会要求厂商提供「签名过程中k的生成流程图」。如果对方拿不出来,或者流程图里没有标注随机源,我会直接标记为高风险。
3.2 杂凑值计算流程审计
SM2签名不是直接对消息签名,而是对消息的杂凑值签名。这个杂凑值计算流程,比你想的要复杂一些。
SM2标准里,杂凑值计算分两步:
- 计算ZA值:ZA = H256(ENTLA || IDA || a || b || xG || yG || xA || yA)
- 计算e值:e = H256(ZA || M)
其中,ENTLA是用户IDA的比特长度,IDA是用户标识,a、b是椭圆曲线参数,xG、yG是基点坐标,xA、yA是用户公钥坐标,M是原始消息。
你想想看,这个ZA值里包含了公钥信息。这意味着什么?意味着同一个消息,不同用户签名时,杂凑值是不同的。这是SM2区别于ECDSA的一个重要设计。
审计要点清单
| 审计项 | 检查内容 | 常见问题 |
|---|---|---|
| ZA计算完整性 | ZA是否包含了所有必需字段 | 遗漏IDA或公钥坐标 |
| 字节序处理 | ZA和e计算中的字节序是否与标准一致 | 大端小端混淆 |
| 哈希算法 | 是否使用SM3(国密哈希算法) | 误用SHA-256或其他算法 |
| 消息预处理 | 消息M是否原样输入,无额外填充 | 对消息做了不必要的编码转换 |
| IDA默认值 | IDA是否使用标准默认值(1234567812345678) | IDA为空或长度错误 |
避坑指南:我曾经审计过一个项目,他们的ZA计算里把IDA写死了空字符串。结果就是,所有用户的ZA值都一样,签名失去了用户关联性。更严重的是,攻击者可以用自己的签名冒充其他用户——因为杂凑值计算里没有区分用户身份。
排查方法
- 向量测试:使用SM2标准附录中的测试向量,逐字节比对ZA和e的计算结果。这是最可靠的方法。
- 代码走读:检查ZA计算函数的输入参数列表,确认是否传入了IDA、公钥等必要信息。
- 日志对比:在测试环境中,打印ZA和e的中间值,与标准实现(如GmSSL)的输出做对比。
3.3 签名参数合法性审计
签名生成完成后,输出的(r, s)对需要满足一系列合法性条件。这些条件不是可有可无的,它们是安全性的最后一道防线。
SM2标准规定,签名(r, s)必须满足:
- r ∈ [1, n-1]
- s ∈ [1, n-1]
- r + s ≠ n
为什么要有这些限制?我简单解释一下:
- 如果r=0或s=0,签名验证时会出现除零错误,或者直接绕过验证逻辑。
- 如果r+s=n,某些实现中会导致验证公式出现特殊情况,可能被利用。
⚠️ 注意:有些实现为了「性能」,跳过了这些合法性检查。我见过一个嵌入式设备上的SM2实现,s=0时直接返回「验证通过」。攻击者只需要构造一个s=0的签名,就能通过任何验证。这简直是在给攻击者开后门。
审计要点清单
| 审计项 | 检查内容 | 常见问题 |
|---|---|---|
| r值范围 | r是否在[1, n-1]区间内 | r=0或r≥n |
| s值范围 | s是否在[1, n-1]区间内 | s=0或s≥n |
| r+s条件 | r+s是否等于n | r+s=n时未做特殊处理 |
| 签名输出格式 | 签名是否按标准DER编码或固定长度格式输出 | 格式错误导致解析失败 |
排查方法
- 边界测试:构造r=0、r=n、s=0、s=n、r+s=n等边界情况,看签名生成函数是否会正确处理。
- 代码审查:检查签名生成函数的末尾,是否有对(r, s)的合法性判断逻辑。
- 模糊测试:使用随机输入,看签名生成是否会崩溃或输出非法值。
💡 我建议:在代码里加一个断言(assert),在签名生成后立即检查(r, s)的合法性。这样在测试阶段就能发现问题,而不是等到上线后被攻击者利用。
3.4 知识体系总览
下面这张图,把签名生成审计的三个核心维度串起来了。你可以把它当作审计时的检查清单。
好了,以上就是签名生成审计的三个核心维度。你审计时,按这个框架去查,基本不会漏掉关键点。记住:随机数k是命门,杂凑值计算是基础,参数合法性是底线。三条线都守住,签名安全才有保障。