OpenSSL 4.0 把 SM2 的默认区分标识符改成 1234567812345678:老签名的国密验签会突然不通过
OpenSSL 4.0 的 CHANGES 里有一条容易被忽略的破坏性变更:验证 SM2 签名证书时,默认 distinguishing identifier(区分标识符)从空串改成了常量 ASCII 串 "1234567812345678"。同一份 CHANGES 里还有 ASN1_STRING 变为不透明、X509 系列函数 const 化、移除 engine 支持、c_rehash 换成 openssl rehash 等改动,但真正让存量国密系统在升级后验签失败的,往往是这一条。
distid 是什么,为什么一改就验不过
SM2 签名不是直接对消息做哈希。签名预处理会先算一个 Z 值:
Z = SM3(ENTL || ID || a || b || xG || yG || xA || yA)
e = SM3(Z || M)
其中 ID 就是 distinguishing identifier,ENTL 是它的比特长度。Z 参与了最终摘要的计算,所以 distid 一变,e 就变,签名和验签用的就不是同一个摘要。私钥、公钥、曲线参数、消息全都没变,签名依然对不上。
"1234567812345678" 是 GB/T 32918 里给出的默认用户标识,16 字节 ASCII,ENTL 为 0x0080。3.x 用的空串并不是这个标准默认值,4.0 只是把默认值改成了标准值。问题在于:存量签名几乎都是按空串签出来的。
为什么表现成“证书链正常但签名校验失败”
证书链验证和应用层签名验证是两条路径。链上如果混有 RSA/ECDSA 签名的证书,这部分照常通过;而真正用 SM2 私钥签的数据——TLS 的 CertificateVerify、或者业务报文签名——验签端在 4.0 上会用新的默认 distid 重算摘要,结果直接不匹配。
典型现象是:握手走到证书验证阶段报签名错误,或者应用层验签函数返回失败,但抓包看证书链、公钥、算法标识全都正常。排查时先确认签名方和验签方的 distid 是否一致,不要先去查证书有效期或曲线支持。
复现:3.x 与 4.0 交叉对照
同一段数据,在两个版本上分别签名、交叉验签:
# 3.x:默认空 distid 签名
openssl dgst -sm3 -sign sm2.key -out sig_old.bin msg.txt
# 4.0:默认 distid 签名
openssl dgst -sm3 -sign sm2.key -out sig_new.bin msg.txt
# 4.0 验证 4.0 的签名:通过
openssl pkeyutl -verify -pubin -inkey sm2.pub -in msg.txt -sigfile sig_new.bin
# 4.0 验证 3.x 的签名:失败
openssl pkeyutl -verify -pubin -inkey sm2.pub -in msg.txt -sigfile sig_old.bin
如果 sig_old.bin 在 3.x 上验签通过、在 4.0 上失败,而 sig_new.bin 在 4.0 上通过,基本就可以确定是 distid 默认值变化。pkeyutl 对 SM2 可能还需要补 -digest sm3 或 -rawin 之类的参数,具体取决于你的调用方式,先保证两边参数完全对称。
命令行:两个方向的兼容写法
空值通过 distid: 传入——注意冒号后面什么都不写,这才是空串。
4.0 生成与旧版本兼容的签名,用 openssl dgst 的 -sigopt:
openssl dgst -sm3 -sign sm2.key -sigopt distid: -out sig_compat.bin msg.txt
4.0 验证旧版本产生的签名,用 openssl pkeyutl 的 -pkeyopt:
openssl pkeyutl -verify -pubin -inkey sm2.pub -in msg.txt \
-sigfile sig_old.bin -pkeyopt distid:
反过来,如果要让 3.x 验证 4.0 默认参数签出的数据,同样得让 3.x 侧显式使用 "1234567812345678" 作为 distid;由于 3.x 的默认值是空串,这一步必须写到命令行或代码里。
应用层:别依赖默认值
命令行只是排查手段,真正的修复在 EVP 调用里。规则很简单:凡是 SM2 签名/验签,都显式设置 distid,不使用库的默认值。
- 调用 EVP 签名或验签时,在 pkey 上下文里设置 distid 参数,键名与命令行的
distid对应;空串也要显式设置,不能靠“不设置”来表达空值。 - 新系统之间统一用
"1234567812345678"。 - 需要兼容 4.0 之前的存量数据时,走兼容分支:把 distid 显式设为空串。
- 版本差异可以用
OPENSSL_VERSION_NUMBER判断(4.0 起为0x40000000L及以上),但判断只用于选默认策略,不要用它跳过显式设置。 - 证书链上的 SM2 签名同样受影响。CA 用旧版本签发、验证端是 4.0 时,验签入口要一并处理 distid,
openssl verify和应用层 EVP 两条路径都要覆盖。
新旧系统混跑时的约定
升级窗口期最容易出问题的是新旧版本同时在跑。建议把 distid 当成配置项而不是代码默认值:
- 在配置里写死一个值,新老两端都显式读取并设置,避免一端用库默认、一端用配置。
- 存量签名验证端先开兼容开关(空 distid),等存量数据自然过期或重签后再切到
"1234567812345678"。 - 升级前用 3.x 和 4.0 各跑一遍交叉验签矩阵:3.x 签 / 3.x 验、3.x 签 / 4.0 验(加兼容参数)、4.0 签 / 3.x 验(两侧显式设值)、4.0 签 / 4.0 验。
- 新增数据一律按标准默认值签,别为了省事继续用空串,否则下一次迁移还要再付一遍代价。