为 LLVM 引入常量时间支持以保护加密代码

Trail of Bits 为 LLVM 开发了常量时间编码支持,为开发者提供编译器级别的保证,确保其加密实现免受与分支相关的定时攻击。此项工作引入了 __builtin_ct_select 系列内在函数,可防止 Clang 编译器破坏精心设计的常量时间代码。__builtin_ct_select 保证了选择操作将会编译为常量时间机器代码,无论优化级别如何。

Trail of Bits 已经开发了 LLVM 的恒定时间编码支持,为开发者提供了编译器级别的保证,确保他们的加密实现能够抵御与分支相关的时序攻击。这些更改正在审查中,并将添加到即将发布的 LLVM 22 版本中。这项工作引入了 __builtin_ct_select 系列的内联函数和支持基础设施,以防止 Clang 编译器(以及可能使用 LLVM 构建的其他编译器)无意中破坏精心设计的恒定时间代码。本文将带你了解我们构建的内容、它的工作原理以及它支持的内容。我们还将讨论我们扩展这项工作的一些未来计划。

编译器优化问题

现代编译器擅长让代码运行得更快。它们消除冗余操作,向量化循环,并巧妙地重构算法,以榨取每一丝性能。但这种优化热情在处理加密代码时变成了一种负担。

考虑一下来自 Sprenkels (2019) 的这个看似无辜的恒定时间查找:

uint64_t constant_time_lookup(const size_t secret_idx,
  const uint64_t table[16]) {
    uint64_t result = 0;
    for (size_t i = 0; i < 8; i++) {
        const bool cond = i == secret_idx;
        const uint64_t mask = (-(int64_t)cond);
        result |= table[i] & mask;
    }

    return result;}

这段代码小心地避免了在 secret index(秘密索引)上进行分支。无论秘密值如何,每次迭代都执行相同的操作。然而,由于编译器的构建目的是让你的代码运行得更快,它们会看到一个机会,通过将这段精心设计的代码优化成包含分支的版本来改进它。

问题在于,编译后的代码中的任何数据相关行为都会产生时序侧信道。如果编译器引入一个分支,比如 if (i == secret_idx),CPU 将根据分支是否被采用而花费不同的时间。现代 CPU 具有分支预测器,可以学习模式,从而使正确预测的分支比错误预测的分支更快。一个可以测量多次执行中这些时间差异的攻击者可以统计地确定哪个索引正在被访问,从而有效地恢复秘密。即使是几个 CPU 周期的小的时间变化也可以通过足够的测量来利用。

我们构建了什么

我们的解决方案为加密开发者提供了显式的编译器内联函数,可以在整个编译管道中保持恒定时间属性。核心添加是 __builtin_ct_select 系列的内联函数:

// 恒定时间条件选择
result = __builtin_ct_select(condition, value_if_true, value_if_false);

这个内联函数保证了上面的选择操作将被编译成恒定时间的机器代码,而与优化级别无关。当你在你的 C/C++ 代码中编写这个时,编译器会把它翻译成一个特殊的 LLVM 中间表示内联函数 (llvm.ct.select.*),它带有语义含义:“这个操作必须保持恒定时间。”

与优化器可以自由地重新排列和转换的常规代码不同,这个内联函数充当了一个屏障。优化器将其识别为安全关键操作,并在从源代码到汇编的每个编译阶段都保留其恒定时间属性。

现实世界的影响

在他们最近的研究“ Breaking Bad: How Compilers Break Constant-Time Implementations”中,Srdjan Čapkun 和他的研究生 Moritz Schneider 和 Nicolas Dutly 发现,编译器破坏了许多生产加密库中的恒定时间保证。他们对五个编译器上的 19 个库的分析揭示了在编译过程中引入的系统性漏洞。

有了我们的内联函数,有问题的查找函数就变成了这个恒定时间版本:

uint64_t
constant_time_lookup(const size_t secret_idx,
                     const uint64_t table[16]) {
  uint64_t result = 0;

  for (size_t i = 0; i < 8; i++) {
    const bool cond = i == secret_idx;
    result |= __builtin_ct_select(cond, table[i], 0u);
  }
  return result;
}

内联函数的使用可以防止编译器对其进行任何修改,从而确保选择保持恒定时间。没有任何优化过程会将其转换为易受攻击的内存访问模式。

社区参与和采用

要使这些更改进入上游,需要广泛的社区参与。我们在 2025 年 8 月的 LLVM Discourse 论坛上发布了我们的 [RFC]。

该 RFC 收到了来自编译器和密码学社区的大量反馈。来自 Rust Crypto、BearSSL 和 PuTTY 的开源维护者表示有浓厚的兴趣采用这些内联函数来取代他们当前的内联汇编解决方法,同时提供了关于实现方法和未来原语的宝贵反馈。LLVM 开发者帮助确保这些内联函数能够与自动向量化和其他优化过程正确地协同工作,并提供了特定于架构的实现指导。

在现有工作的基础上构建

我们的方法综合了之前多个工作的经验教训:

  • Simon 和 Chisnall __builtin_ct_choose (2018):这项工作为保留恒定时间属性的编译器内联函数提供了概念基础,但从未上游。
  • Jasmin (2017):这项工作展示了编译器感知的恒定时间原语的价值,但需要一种新的语言。
  • Rust 的 #[optimize(never)] 实验:这些实验强调了对细粒度优化控制的需求。

它如何在各种架构上工作

我们的实现确保 __builtin_ct_select 在每个平台上都编译为恒定时间代码:

x86-64: 此内联函数直接编译为 cmov(条件移动)指令,该指令始终以恒定时间执行,而与条件值无关。

i386: 由于 i386 缺少 cmov,我们使用带掩码的算术模式和按位运算来实现恒定时间选择。

ARM 和 AArch64: 对于 AArch64,内联函数被降低为 CSEL 指令,该指令提供恒定时间执行。对于 ARM,由于 ARMv7 没有像 AAarch64 那样的恒定时间指令,因此该实现生成一个使用按位运算的掩码算术模式。

其他架构: 通用的回退实现使用按位算术来确保恒定时间执行,即使是在我们尚未以原生方式添加支持的平台上也是如此。

每个架构都需要不同的指令来实现恒定时间行为。我们的实现透明地处理这些差异,因此开发者可以编写可移植的恒定时间代码,而不必担心特定于平台的细节。

基准测试结果

我们在苏黎世联邦理工学院的合作伙伴正在使用他们来自“Breaking Bad”研究的测试套件进行全面的基准测试。初步结果如下:

  • 大多数加密操作的性能开销最小
  • 在所有测试的优化级别上100% 保留恒定时间属性
  • 与包括 HACL*、Fiat-Crypto 和 BoringSSL 在内的主要加密库成功集成

接下来是什么

虽然 __builtin_ct_select 解决了最关键的需求,但我们的 RFC 概述了其他内联函数的路线图:

恒定时间操作

我们未来计划扩展恒定时间实现,特别是针对算术或字符串操作,并评估表达式以使其为恒定时间。

_builtin_ct<op> // 用于恒定时间算术或字符串操作
__builtin_ct_expr(expression)  // 强制整个表达式在没有分支的情况下进行计算

其他语言的采用路径

我们的 LLVM 实现的模块化特性意味着任何以 LLVM 为目标的语言都可以利用这项工作:

Rust: Rust 编译器团队正在探索如何通过其 core::intrinsics 模块公开这些内联函数,可能会在标准库中提供安全的包装器。

Swift: Apple 的安全团队表示有兴趣为其加密框架采用这些原语。

WebAssembly: 这些内联函数对于基于浏览器的密码学特别有用,尽管存在沙盒,但时序攻击仍然是一个问题。

致谢

这项工作是与苏黎世联邦理工学院的 系统安全组 合作完成的。特别感谢 Laurent Simon 和 David Chisnall 在恒定时间编译器支持方面的开创性工作,以及 LLVM 社区在 RFC 过程中提供的建设性反馈。

我们特别感谢我们的 Trail of Bits 密码学团队的技术审查。

资源

本博文中提到的工作由 Trail of Bits 根据 DARPA 在合同编号 N66001-21-C-4027 下支持的工作进行(发布声明 A,批准公开发布:无限制发布)。 本材料中表达的任何意见、发现、结论或建议均为作者的意见,不一定反映美国政府或 DARPA 的观点。

庆祝我们 2024 年的开源贡献 2025 年 1 月 23 日\ 虽然 Trail of Bits 以开发 Slither、Medusa 和 Fickling 等安全工具而闻名,但我们的工程努力……EuroLLVM 2024 旅行报告 2024 年 6 月 21 日\ EuroLLVM 是一次开发者会议,重点关注 LLVM 基金会旗下的项目,这些项目位于 LLVM GitHub 中……了解 AddressSanitizer:为你的代码提供更好的内存安全 2024 年 5 月 16 日\ 这篇文章将指导你使用 AddressSanitizer (ASan),这是一个编译器插件,可以帮助开发者检测内存……

  • 原文链接: blog.trailofbits.com/202...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Trail of Bits
Trail of Bits
https://www.trailofbits.com/