「常量时间」是那种你做对一次、过段时间却会悄悄丢掉的性质。一段本来不按密钥分支的代码,下个月加了条快路径,依赖升了级,评审一挥手就过了——时序泄漏又溜回来了,还没人盯着。
gm-crypto-rs 就卡在这个缝里。这个库实现了 SM2、SM3、SM4,涉密路径都是按常量时间设计的——可「设计成常量时间」只是评审那一刻签下的字,之后会慢慢松动。所以我真正在意的,不是「它今天是不是常量时间」,而是「靠什么让每次提交之后,它还是常量时间」。
答案是挂在 CI 里的一套 dudect(时序泄漏检测工具)。dudect 的思路是这样:拿一个涉密操作,喂它两类输入——比如固定密钥和随机密钥——量出两组执行时间的分布,再把「两组差多少」压成一个统计量 t(下面记成 τ)。耗时要是跟密钥无关,两组分布就大致重叠,|τ| 很小;要是有关,|τ| 就往上走。
到 v1.11.0,这套 harness 覆盖 20 条涉密路径。核心那一组每个 PR 都带门禁:|τ| 一旦越过 0.20,构建直接挂掉。不是日志里没人看的一句 warning,而是 PR 上的一个红叉。泄漏得先处理掉,改动才进得来。
要紧的正是这条但书:它是检测,不是证明。|τ| 低,只说明在这一轮给定的测量预算下没测到泄漏,并不等于泄漏不存在——这是 dudect 自己的说法,我原样留着。统计能告诉你「我没看见」,却没法把这句话变成「它不存在」。常量时间门禁是个烟雾报警器,不是保证——因为另一种做法,是让一个绿勾悄悄说满。
也不是每条路径都适合放进 PR 门禁。一次完整的泄漏测量很贵,而一个 PR 的时间是有限的,于是 harness 把测量分成两层。核心那层不达标就让构建失败;另一层——域求逆诊断、k-class 签名,以及 2026-06-17 起的 HMAC-SM3——只当遥测来量:放进 nightly(夜间)任务里跑,对照一个 0.55 的粗回归哨兵值看,而不是每个 PR 都按 0.20 拦。k-class 那条原先在 nightly 上挂的是更紧的 0.25,2026-06-07 松开了:共享 runner 上的类分割噪声老是把它误触发;HMAC-SM3 十天后因为同一种噪声也降了级。每一次放松都把理由一起写出来,不是悄悄改掉阈值。这是一笔诚实的折中。这些路径是看着、不是拦着,这个划分也得长期维护——但它让每个 PR 的信号够快,也让门禁还算数。
这些都没法让这个库「可证明是常量时间」。在真实硬件上,没有什么能做到;要是 CPU 的乘法耗时本身就跟操作数有关,这个保证连原则上都不成立。harness 换来的,是一根绊线:哪天一次改动把跟密钥相关的时序差异又带了回来,构建会先变红,而不是让一个弱点悄无声息地发出去。这套 harness、那份路径清单、还有那些阈值,都在公开仓库里——你不想听我空口说,可以自己去翻 CI 配置和实现。
以上讲的是这道门禁「做了什么」。更难的那一半是:凭什么相信「没测到泄漏」这个结论本身——那需要一个你能亲眼看着它报警的检测器。这部分写在另一篇。