如何恒定时间比较 MAC?

解读

在国内 Rust 岗位面试中,恒定时间比较 MAC 是一道高频安全题,考察候选人是否理解“时间侧信道攻击”以及 Rust 标准库提供的密码学安全比较原语
MAC(Message Authentication Code)通常用于验证消息完整性,如果比较实现是逐字节短路逻辑(==),攻击者可以通过测量响应时间差异逐字节爆破出正确 MAC。
面试官期望你给出零成本、无分支、不依赖 CPU 缓存命中的解决方案,并能指出 Rust 生态中官方认证的 crate,而不是自己手写汇编。

知识点

  1. 时间侧信道(Timing Side-Channel):攻击者利用 if a != b { return early; } 引入的时间差,逐字节推断密钥。
  2. 恒定时间(Constant-Time):算法执行路径、内存访问模式、分支预测均与密钥内容无关,耗时仅与输入长度有关
  3. Rust 标准库 std::hint::black_box 仅防止编译器优化,不提供 CT 保证;真正需要的是密码学级 CT 比较
  4. 官方答案:ring::constant_time::verify_slices_are_equalhmac::Mac::verify_truncate_left(底层调用 crypto::subtle::constant_time_eq)。
  5. 自实现模板:使用 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 }
}

拓展思考

  1. 长度不一致怎么办:恒定时间比较要求先补齐到统一长度(如 32 B),否则仍可泄露长度信息;国内合规场景(国密 SM4-CMAC)同样遵循此原则。
  2. 性能权衡ring 的 CT 实现已用 SIMD 掩码,比手写 Rust 更快;在嵌入式 no_std 环境可启用 subtlenightly feature 获得内联汇编版本。
  3. 与零拷贝结合:在高性能网关(如 Envoy-Rust 插件)中,MAC 位于 Bytes 切片头部,验证前禁用 slice::eq 短路,否则再快的网络栈也会被侧信道击穿。
  4. 审计要点:国内等保 3 级要求密码运算侧信道报告,面试官可能让你现场画出CT 比较在 TLS 1.3 Finished 消息中的调用路径,务必指出 verify_slice 之后才能更新状态机,防止MAC 通过后仍泄露响应时间差