if和==在密码学中不安全,因CPU分支预测、缓存命中差异会引入可测时间差,攻击者借此推断密钥;恒定时间编程要求所有路径的指令数、内存访问地址及分支行为与秘密输入无关,须避免条件跳转,使用掩码运算(如mask = -(a==b))和经验证库(如libsodium)。

为什么 if 和 == 在密码学里不安全
因为现代 CPU 会根据分支结果跳转、提前加载、缓存命中与否产生可测量的时间差,攻击者通过高精度计时(如 rdtscp)或缓存侧信道(如 Flush+Reload)就能推断密钥比特。哪怕一个 if (secret_key[i] == 0),编译器优化后仍可能生成条件跳转指令,这不是“写得小心”就能绕过的。
恒定时间代码的核心约束是:所有执行路径的指令数、内存访问地址、缓存行、分支预测行为必须完全一致——和输入(尤其是密钥)无关。
- 避免任何依赖秘密数据的分支:
if、?:、循环边界(如for (int i = 0; i ) - 避免依赖秘密数据的内存访问偏移:
table[secret_byte]是典型陷阱,即使table在 .data 段也会触发不同缓存行 - 慎用编译器内置函数:
__builtin_ctz、__builtin_popcount等在 x86 上通常非恒定时间,需查证文档或反汇编验证
用位运算替代条件逻辑:从 select 到 mask
最常用模式是把布尔条件转为全 0 或全 1 的掩码(mask),再用按位与/或做数据选择。例如实现恒定时间的字节比较:
// 恒定时间比较两个字节 a 和 b,返回 0xFF 表示相等,0x00 表示不等
uint8_t ct_eq(uint8_t a, uint8_t b) {
uint8_t diff = a ^ b;
// diff == 0 → 全 0;否则至少一位为 1
// 利用补码特性:-x 的最高位(符号位)在 x!=0 时为 1,在 x==0 时为 0
// 但更稳妥的是用无符号右移:(0 - diff) >> 24 → 仅当 diff==0 时为 0xFF
return 0 - (diff == 0); // 注意:C++ 中 bool 转 int 是 0/1,但需确保编译器不优化成条件跳转
}
实际项目中应使用已验证的恒定时间原语库(如 libsodium 的 sodium_memcmp),自己手写易出错。若必须自实现,优先用 std::equal 配合 std::array + 手动展开循环(固定长度),并检查生成的汇编是否无 je/jne。
立即学习“C++免费学习笔记(深入)”;
-
mask = -(a == b)在 GCC/Clang 下通常生成sete+movzbl+neg,是恒定时间的 - 但
(a == b) ? x : y即使加volatile也无法阻止优化器生成分支,必须改写为(mask & x) | (~mask & y) - 对数组索引,用
idx = ((mask & secret_idx) | (~mask & public_idx))强制访问同一地址(但注意:这不能解决缓存侧信道,只是避免地址泄露)
循环和内存访问的恒定时间陷阱
固定迭代次数的循环本身不是问题,但循环体内的行为必须恒定。常见错误包括:
- 在循环中调用
memcmp或std::string::compare—— 它们内部会提前退出,时间随第一个不匹配字节位置变化 - 用
std::vector存储中间密钥材料 —— 其operator[]不做越界检查,但底层内存分配和缓存行为不可控;应改用std::array或栈上固定大小缓冲区 - 调用
memset清零密钥 —— 标准库版本可能被优化掉(尤其memset(ptr, 0, len)),必须用explicit_bzero(glibc)或sodium_memzero
例如恒定时间的大数模乘中,蒙哥马利约简的每轮迭代必须执行完整位宽(如 256 次),即使高位已为 0;条件减法要写成 sub_mask = -(should_subtract); r = (r - m) & sub_mask | r & ~sub_mask;。
验证是否真恒定时间:别只信编译器
编译器不保证恒定时间,-O2 可能引入分支,-Os 可能拆分指令。必须实测:
- 用
objdump -d检查关键函数是否含je、jne、cmov(cmov是安全的,它不改变控制流) - 在真实硬件上跑微基准:用
RDTSCP测量同一函数不同密钥输入下的执行周期,标准差应接近时钟精度(如 - 用工具如
ctgrind(Valgrind 插件)或cachebench检测缓存访问模式是否依赖秘密数据 - 注意:ASLR、CPU 频率缩放、中断都会干扰测量,需关闭 turbo boost、绑定 CPU 核心、重复采样数千次取分布
真正棘手的不是写对一行 mask 运算,而是整条密码学路径——从密钥加载、S-box 查表(必须预计算为恒定时间多项式)、到最终输出——每个环节都得过筛。一个没注意到的 std::map::find 就能让整套防护失效。


















