功能说明
普通的字节比较在遇到第一个不同字节时立即返回,攻击者可以反复测量响应时间,逐位推断出正确的令牌或签名。常量时间比较在长度相同时始终遍历全部字节,把所有差异按位或累积到一个变量,最后只判断一次,因此耗时只与长度有关。
这类比较适用于 API 令牌、HMAC 签名、一次性验证码等场景。示例同时提供字符串包装函数,直接比较两段文本,并展示相同、不同、长度不一致和空串四种结果。
关键代码
函数先比较长度,长度不同直接返回 false,这是可以接受的泄露,因为长度本身通常不是秘密。循环用 zip 同时遍历两段字节,把异或结果累积到 difference,任何一处不同都会让最终值非零。black_box 告诉编译器这个值必须真实计算,防止优化器把循环化简或提前退出。最后一次性比较是否为零。
使用注意
长度不同会立即返回,若长度也必须保密,应先对输入做固定长度哈希再比较。编译器优化可能影响常量时间性质,生产环境建议使用经过审计的库,例如 subtle 提供的 ConstantTimeEq。该函数只处理字节,比较字符串前要确认编码一致。比较结果不要用于日志中的逐字节诊断,否则又泄露了信息。空串等于空串返回 true。
验证方式
相同令牌输出 true,只差最后一个字符时输出 false,长度不同输出 false,两个空串输出 true。可以在基准测试中比较不同前缀长度下的耗时,常量时间实现的耗时曲线应基本平坦;普通比较则会随匹配前缀变长而增加。
代码示例
use std::hint::black_box;
fn constant_time_eq(left: &[u8], right: &[u8]) -> bool {
if left.len() != right.len() {
return false;
}
let mut difference = 0u8;
for (a, b) in left.iter().zip(right.iter()) {
difference |= a ^ b;
}
black_box(difference) == 0
}
fn compare_tokens(expected: &str, provided: &str) -> bool {
constant_time_eq(expected.as_bytes(), provided.as_bytes())
}
fn main() {
println!("equal: {}", compare_tokens("token-abc", "token-abc"));
println!("different: {}", compare_tokens("token-abc", "token-abd"));
println!("length mismatch: {}", compare_tokens("token-abc", "token"));
println!("empty: {}", compare_tokens("", ""));
}