更正(2026-10-12,UTC):这篇早先的版本把这项检查的约束力说过头了,下文已更正。时序检查只在改动密码代码、基准或依赖文件的 PR 上运行,PR 标题里带
[skip ci]之类的跳过标记也会跳过。核心路径在三轮测量中的 |τ| 中位数超过 0.20 时检查失败(其中 CBC 解密扇出在一种 runner CPU 上用 0.55)。它不是合并的必需检查:失败会把 PR 标红,但本身不会挡住改动合入。这项检查跑在 x86-64 Linux 的 CI runner 上。最新细节见案例页。
「常量时间」是那种你做对一次、过段时间却会悄悄丢掉的性质。一段本来不按密钥分支的代码,下个月加了条快路径,依赖升了级,评审一挥手就过了——时序泄漏又溜回来了,还没人盯着。
gm-crypto-rs 就卡在这个缝里。这个库实现了 SM2、SM3、SM4,涉密路径都是按常量时间设计的——可「设计成常量时间」只是评审那一刻签下的字,之后会慢慢松动。所以我真正在意的,不是「它今天是不是常量时间」,而是「哪次改动把时序泄漏带回来时,怎样能被发现」——范围只限测试覆盖的路径和硬件。
答案是挂在 CI 里的一套 dudect(时序泄漏检测工具)。dudect 的思路是这样:拿一个涉密操作,喂它两类输入——比如固定密钥和随机密钥——量出两组执行时间的分布,再把「两组差多少」压成一个统计量 t(下面记成 τ)。耗时要是跟密钥无关,两组分布就大致重叠,|τ| 很小;要是有关,|τ| 就往上走。
到 v1.11.0,这套 harness 覆盖 20 条涉密路径。凡是改动密码代码的 PR,核心那一组都要检查:某条路径三轮测量的 |τ| 中位数一旦超过 0.20,检查就失败。不是日志里没人看的一句 warning,而是 PR 上的一个红叉。这项检查不是合并的必需检查,所以红叉是该去处理的信号,而不是锁住合并的那把锁。
要紧的正是这条但书:它是检测,不是证明。|τ| 低,只说明在这一轮给定的测量预算下没测到泄漏,并不等于泄漏不存在——这是 dudect 自己的说法,我原样留着。统计能告诉你「我没看见」,却没法把这句话变成「它不存在」。常量时间检查是个烟雾报警器,不是保证——因为另一种做法,是让一个绿勾悄悄说满。
也不是每条路径都适合放进 PR 检查。一次完整的泄漏测量很贵,而一个 PR 的时间是有限的,于是 harness 把测量分成两层。核心那层不达标会让 PR 检查失败;另一层——域求逆诊断、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 换来的,是一根绊线:哪天一次改动把跟密钥相关的时序差异又带了回来,PR 上的检查会先变红(只限它测到的路径和硬件),而不是让这个弱点没人发现。这套 harness、那份路径清单、还有那些阈值,都在公开仓库里——你不想听我空口说,可以自己去翻 CI 配置和实现。
以上讲的是这项检查「做了什么」。更难的那一半是:凭什么相信「没测到泄漏」这个结论本身——那需要一个你能亲眼看着它报警的检测器。这部分写在另一篇。