如何恒定时间比较 MAC?
解读
在国内 Rust 岗位面试中,恒定时间比较 MAC 是一道高频安全题,考察候选人是否理解“时间侧信道攻击”以及 Rust 标准库提供的密码学安全比较原语。
MAC(Message Authentication Code)通常用于验证消息完整性,如果比较实现是逐字节短路逻辑(==),攻击者可以通过测量响应时间差异逐字节爆破出正确 MAC。
面试官期望你给出零成本、无分支、不依赖 CPU 缓存命中的解决方案,并能指出 Rust 生态中官方认证的 crate,而不是自己手写汇编。
知识点
- 时间侧信道(Timing Side-Channel):攻击者利用
if a != b { return early; }引入的时间差,逐字节推断密钥。 - 恒定时间(Constant-Time):算法执行路径、内存访问模式、分支预测均与密钥内容无关,耗时仅与输入长度有关。
- Rust 标准库
std::hint::black_box仅防止编译器优化,不提供 CT 保证;真正需要的是密码学级 CT 比较。 - 官方答案:
ring::constant_time::verify_slices_are_equal或hmac::Mac::verify_truncate_left(底层调用crypto::subtle::constant_time_eq)。 - 自实现模板:使用
u8::wrapping_sub+ 位运算累加异或,全程无分支、无查表、无 early-return,最后把结果通过core::ptr::read_volatile取出,防止编译器重排。
答案
use hmac::{Hmac, Mac};
type HmacSha256 = Hmac<sha2::Sha256>;
/// 恒定时间验证 MAC,返回 Result<(), MacError>
pub fn verify_mac(key: &[u8], msg: &[u8], user_tag: &[u8; 32]) -> Result<(), hmac::digest::MacError> {
let mut mac = HmacSha256::new_from_slice(key).expect("HMAC can take key of any size");
mac.update(msg);
// verify_truncate_left 内部使用 subtle::constant_time_eq,保证 CT
mac.verify_slice(user_tag)
}
若面试官追问“不用 crate 怎么写”,给出无分支版本:
/// 恒定时间比较两个 32 字节数组,相等返回 true
#[inline(never)]
pub fn ct_eq_32(a: &[u8; 32], b: &[u8; 32]) -> bool {
let mut acc: u8 = 0;
for i in 0..32 {
acc |= a[i] ^ b[i];
}
// 把 acc 是否为 0 的结果转成 bool,全程无分支
let mask = acc.wrapping_sub(1); // acc==0 -> mask==0xff; acc!=0 -> mask==0x00
let ret = (mask as i8) >> 7; // ret==0 或 -1
// 防止编译器优化掉
unsafe { core::ptr::read_volatile(&ret) == 0 }
}
拓展思考
- 长度不一致怎么办:恒定时间比较要求先补齐到统一长度(如 32 B),否则仍可泄露长度信息;国内合规场景(国密 SM4-CMAC)同样遵循此原则。
- 性能权衡:
ring的 CT 实现已用 SIMD 掩码,比手写 Rust 更快;在嵌入式no_std环境可启用subtle的nightlyfeature 获得内联汇编版本。 - 与零拷贝结合:在高性能网关(如 Envoy-Rust 插件)中,MAC 位于
Bytes切片头部,验证前禁用slice::eq短路,否则再快的网络栈也会被侧信道击穿。 - 审计要点:国内等保 3 级要求密码运算侧信道报告,面试官可能让你现场画出CT 比较在 TLS 1.3 Finished 消息中的调用路径,务必指出
verify_slice之后才能更新状态机,防止MAC 通过后仍泄露响应时间差。