「常量时间」是那种你今天做对了、过段时间却会悄悄丢掉的性质。一段本来不按密钥分支的代码,下个月加了条快路径,依赖升了级,评审一挥手就过了——时序泄漏又溜回来了,还没人盯着。

这正是 gm-crypto-rs 要应对的事。这个库实现了 SM2、SM3、SM4,涉密路径都是按常量时间设计的——可「设计成常量时间」只是评审那一刻签下的字,之后会慢慢松动。所以我真正在意的,不是「它今天是不是常量时间」,而是「靠什么保证每次提交之后,它还是常量时间」。

答案是挂在 CI 里的一套 dudect(时序泄漏检测工具)。dudect 的思路是这样:拿一个涉密操作,喂它两类输入——比如固定密钥和随机密钥——量出两组执行时间的分布,再把「两组差多少」压成一个统计量 t(下面记成 τ)。耗时要是跟密钥无关,两组分布就大致重叠,|τ| 很小;要是有关,|τ| 就往上走。

这套 harness 覆盖 18 条涉密路径。核心那一组每个 PR 都带门禁:|τ| 一旦越过 0.20,构建直接挂掉。不是日志里没人看的一句 warning,而是 PR 上的一个红叉。泄漏得先处理掉,改动才进得来。

我信 dudect 的原因很简单:它只报检测,不报证明。|τ| 低,只说明在这一轮给定的测量预算下没测到泄漏,并不等于泄漏不存在——这是 dudect 自己的说法,我原样留着。统计能告诉你「我没看见」,却没法把这句话变成「它不存在」。常量时间门禁是个烟雾报警器,不是保证——因为另一种做法,是让一个绿勾替你含糊其辞。

也不是每条路径都适合放进 PR 门禁。一次完整的泄漏测量很贵,而一个 PR 的时间是有限的,于是 harness 把测量分成两层。核心那层不达标就让构建失败;另一层——域求逆诊断、k-class 签名、缓冲式 GCM——只当遥测来量:放进更重的 nightly 里跑(阈值松一点,0.25),而不是每个 PR 都拦。这是一笔诚实的折中。这些路径是看着、不是拦着,这个划分也得长期维护——但它让每个 PR 的信号够快,也让门禁还算数。

这些都没法让这个库「可证明是常量时间」。在真实硬件上,没有什么能做到;要是 CPU 的乘法耗时本身就跟操作数有关,这个保证连原则上都不成立。harness 换来的,是一根绊线:哪天一次改动把跟密钥相关的时序差异又带了回来,构建会先变红,而不是让一个弱点悄无声息地发出去。这套 harness、那 18 条路径的清单、还有那个阈值,都在公开仓库里——你不想听我空口说,可以自己去翻 CI 配置和实现。