如何恒定时间比较?
解读
在国内 Rust 岗位面试中,"恒定时间比较"并不是考排序复杂度,而是考密码学安全与侧信道防御意识。
面试官真正想问的是:
- 你知道常规
==会按字节短路比较,时间随匹配长度变化,可被计时攻击(Timing Attack)利用; - 你能给出 Rust 标准库/第三方 crate 中已加固的恒定时间实现,并解释其汇编级保证;
- 你能权衡性能、可移植性与 FFI 安全,写出零开销抽象的 Rust 代码,而不是简单贴一段 C 移植版。
知识点
- 计时攻击原理:攻击者通过测量响应时间差异推断密钥或 token 的逐字节匹配情况。
- 恒定时间(constant-time)三要素:
- 无分支(branch-free)
- 无内存访问依赖(memory access pattern 固定)
- 无数据依赖的指令周期(固定 CPU 流水线行为)
- Rust 生态方案:
subtlecrate 的ConstantTimeEqtrait,编译期保证无分支,支持u8数组到CtOption<T>的整套抽象;ring/rust-crypto内部均依赖subtle;- 标准库暂无公开恒定时间比较,不可直接用
==;
- 底层实现:使用
xor+or+wrapping_sub掩码技巧,LLVM 无法优化掉,且通过#[inline(never)]与volatile读屏障防止编译器重排。 - 零成本抽象:Rust 泛型 +
const GENERIC长度参数,编译期展开循环,无运行时长度变量,真正做到 O(1) 时间且零堆分配。 - 国内合规:国密算法 SM4-GCM、双证书 TLS 场景下,商密产品认证明确要求 MAC 比较必须恒定时间,否则无法过检。
答案
use subtle::ConstantTimeEq;
/// 恒定时间比较两个等长密钥,返回 true 仅当完全一致
/// 时间只与 len 有关,与内容无关
pub fn secure_compare(a: &[u8], b: &[u8]) -> bool {
// 长度不一致直接拒绝,但耗时仍恒定:先比较 len 再做 CtEq
if a.len() != b.len() {
return false;
}
// subtle 内部使用 branch-free 汇编,保证 O(1) 时间
a.ct_eq(b).into()
}
/// 若不想依赖第三方,可手写核心 64 字节以内无分支版本
#[inline(never)]
pub fn ct_compare_32(a: &[u8; 32], b: &[u8; 32]) -> bool {
let mut acc = 0u8;
for i in 0..32 {
acc |= a[i] ^ b[i];
}
// 0 表示相等,非零表示不等;通过 WrappingSub 把结果映射到 0/1
// 编译器无法优化掉 acc 使用,因其参与 volatile 风格的黑盒
(acc.wrapping_sub(1) >> 7) == 0
}
要点说明:
- subtle 已屏蔽所有 LLVM 优化陷阱,生产环境优先使用;
- 手写版必须
#[inline(never)]防止跨过程优化,且数组长度固定为 const,循环展开后指令数恒定; - 禁止提前 return,保证最坏情况时间等于最好情况时间。
拓展思考
- 可变长度前缀攻击:若先比较长度再比较内容,攻击者仍可测量总时间差。正确做法是先复制到固定长度缓冲区(如 128 B),再恒定时间比较,牺牲内存换安全。
- 异步场景:在 Tokio 任务中做 MAC 校验,务必使用
blocking::unblock把恒定时间比较 offload 到独立线程,防止调度器计时噪声被攻击者采样。 - 国密双证书:SM2 签名结果 DER 编码长度可变,需先解析出固定 64 B 的 (r,s),再做 CtEq,否则恒定时间比较会失去意义。
- 侧信道升级:恒定时间仅防时序攻击,无法防御功耗/电磁分析。国内高等级密码模块需再叠加掩码与盲化技术,Rust 可通过
zeroizecrate 在比较后立即清零中间状态,减少信息残留。