增强SGX飞地的信任 - The Trail of Bits博客
Artur Cygan
2024年1月26日
机密计算、生态系统安全、开源、供应链
为面向隐私的部署创建可重现的SGX飞地构建是一项困难的任务,缺乏方便且稳健的解决方案。我们描述了使用Nix实现可重现和透明的飞地构建,以便任何人都可以审计飞地是否运行其所声称的源代码,从而增强SGX系统的安全性。
在本博客文章中,我们将通过以下步骤解释如何增强SGX飞地的信任:
- 分析了Signal和MobileCoin飞地的可重现构建
- 分析了Intel的可重现SGX SDK构建
- 将SGX SDK打包到Nixpkgs中
- 准备了reproducible-sgx存储库,演示如何使用Nix构建SGX飞地
背景
Intel SGX于2015年推出,是机密(或可信)计算的一种实现。更具体地说,它是一个可信执行环境(TEE),允许用户在由不受信任方拥有和维护的远程计算机上运行机密计算。用户信任硬件(CPU)的制造商(在此情况下为Intel)来保护执行环境免受篡改,即使是最高特权级别的代码(如内核恶意软件)也无法干预。SGX代码和数据存在于称为飞地的特殊加密和认证内存区域中。
在Trail of Bits工作期间,我观察到使用SGX的系统存在一个未被充分解决的信任差距,即飞地的用户不一定信任飞地的作者。相反,用户可以自由审计飞地的开源代码以验证其功能性和安全性。例如,在面向隐私的部署中(如Signal的联系人发现服务或MobileCoin的共识协议)可以观察到这种设置。为了验证信任,用户必须检查飞地是否由可信代码构建。不幸的是,这被证明是一项困难的任务,因为构建往往难以重现,并且依赖于大量预编译的二进制代码。在实践中,几乎没有人验证构建,只能选择信任飞地作者。
从另一个角度来看,类似的情况发生在区块链世界中,智能合约以字节码形式部署。例如,Etherscan会尝试重现链上EVM字节码,以证明其是从声称的Solidity源代码编译而来。如果用户不信任Etherscan,他们可以自由执行相同的操作。
解决这个问题的方法是以可重现和透明的方式构建SGX飞地,以便多方可以独立地达成相同的结果,并审计构建以发现任何与供应链相关的问题。为了实现这一目标,我帮助将Intel的SGX SDK移植到Nixpkgs,这使得可以使用Nix包管理器以完全可重现的方式构建SGX飞地,任何用户都可以验证构建是基于可信代码的。
要了解可重现构建如何完成信任链,首先需要理解SGX提供的保证。
飞地如何证明其身份?
除了上述TEE保护(无信息泄漏且执行无法被更改)之外,SGX可以远程证明飞地的身份,包括其代码哈希、签名和运行时配置。这一功能称为远程认证,对于不熟悉此类技术的人来说可能有些陌生。
当飞地被加载时,其初始状态(包括代码)由CPU哈希化为测量哈希,也称为MRENCLAVE。只有当飞地的代码发生变化时,哈希才会改变。该哈希与其他数据(如签名者和环境详细信息)一起被放置在一个仅SGX实现可访问的特殊内存区域中。飞地可以请求CPU生成包含所有这些数据的报告,包括一段飞地定义的数据(称为report_data),然后将其传递给特殊的Intel签名的引用飞地以签署报告(从此称为引用),以便可以将其传递给远程方并进行验证。
接下来,验证者使用Intel和引用中的相关信息检查引用的真实性。尽管此时还有一些额外的检查和步骤,但在我们的情况下,最重要的是检查测量哈希,这是信任验证的关键组成部分。
我们如何验证哈希?最简单的解决方案是将可信的MRENCLAVE值硬编码到客户端应用程序本身中。例如,Signal使用了这种解决方案,其中MRENCLAVE被放置在客户端的构建配置中,并与Signal服务器发送的签名引用中的哈希进行验证。将客户端和MRENCLAVE捆绑在一起是有意义的;毕竟,我们还需要审计和信任客户端应用程序代码。缺点是当飞地代码更改时,客户端应用程序必须重新构建和重新发布。如果预计飞地修改频繁,或者需要快速将客户端移动到另一个飞地(例如,在安全事件发生时),客户端可以使用更动态的方法,从可信第三方获取MRENCLAVE值。
安全通信通道
SGX可以证明飞地的身份及其生成的一段report_data,但建立可信和安全通信通道的责任在于飞地和验证者。由于SGX飞地是灵活的,可以通过ECALL和OCALL通过网络自由与外界通信,SGX本身并未对通道施加任何特定的协议或实现。飞地开发者可以自由决定,只要通道是加密的、经过认证的,并在飞地内部终止。
例如,SGX SDK实现了一个用于远程认证的认证密钥交换方案示例。然而,该方案假设了一个类似DRM的系统,其中飞地的签名者受信任,服务器的公钥被硬编码在飞地的代码中,因此不适合用于面向隐私的SGX部署(如Signal)。
如果我们不信任飞地的作者,我们可以利用report_data来建立这样的通道。这是SGX保证 essentially结束的地方,从现在开始,我们必须信任飞地的源代码来做正确的事情。这一事实起初并不明显,但如果我们查看例如关于如何在飞地内部终止安全TLS通道的RA-TLS论文,就会变得明显:
飞地在每次启动时生成一个新的公私RA-TLS密钥对。RA-TLS密钥不需要持久化,因为在启动时生成新密钥的成本相对较低。不持久化密钥减少了密钥的暴露,并避免了与持久化相关的常见问题,如状态回滚保护。相关方可以检查源代码以确信密钥从未在飞地外部暴露。
为了维护信任链,RA-TLS使用来自引用的report_data,该引用承诺了飞地的公钥哈希。类似的方法可以在实现Noise Pipes的Signal协议中观察到,并在report_data中承诺握手哈希。
SGX加密和认证飞地的内存,但保护数据的责任在于在飞地中运行的代码。没有什么可以阻止飞地代码向外界披露任何信息。如果我们不知道在飞地中运行什么代码,任何事情都可能发生。
幸运的是,我们知道代码是开源的,但我们如何确保特定Git提交的代码映射到飞地呈现的MRENCLAVE哈希?我们必须重现飞地构建,计算其MRENCLAVE哈希,并将其与从引用中获得的哈希进行比较。如果构建无法重现,我们剩下的选择要么是信任确认飞地安全使用的人,要么是审计飞地的二进制代码。
为什么可重现构建困难?
我们关心的可重现类型是逐位可重现性。一些软件可能在语义上相同,尽管其工件存在微小差异。SGX飞地使用SGX SDK构建为.dll或.so文件,并且必须用作者的RSA密钥签名。由于我们计算工件的哈希,即使一位差异也会产生不同的哈希。我们可能可以容忍微小差异,因为测量过程省略了飞地可执行文件中的一些细节(如签名者),但拥有完整的文件可重现性是理想的。这是一项非平凡的任务,可以通过多种方式实现。
Signal和MobileCoin都认真对待这项任务,并旨在为其飞地提供可重现的构建。例如,Signal声称:
飞地代码构建是可重现的,因此任何人都可以验证发布的源代码是否与远程飞地的MRENCLAVE值对应。
Signal联系人发现服务构建的初始版本(存档于2023年初)基于Debian,并使用.buildinfo文件来锁定系统依赖项;然而,锁定是基于版本而不是哈希。这是Debian的一个限制,正如我们在BuildinfoFiles页面上读到的那样。SGX SDK和其他一些软件包是从获取的源代码构建的,而没有检查下载数据的哈希。虽然这些不一定是危险信号,但比必要的更多信任被放在了第三方(Debian和GitHub)上。
Signal联系人发现服务构建的当前版本略有不同。尽管我没有测试构建,但它基于一个Docker镜像,存在类似的问题,如从具有网络访问权限的系统包管理器安装依赖项,这不能保证可重现性。
另一个例子是MobileCoin,它提供了一个预构建的Docker镜像,其中包含飞地的构建环境。从Dockerfile构建相同的镜像很可能不会产生我们可以验证的可重现哈希,因此必须使用MobileCoin提供的镜像来重现飞地。这里的问题是,审计数百兆字节大的Docker镜像非常困难,我们 essentially需要信任MobileCoin认为镜像是安全的。
Docker是重现环境的流行选择,但它没有提供任何支持逐位可重现性的工具,而是专注于提供功能相似的环境。复杂的Docker镜像可能会在有限的时间内重现构建,但如果不采取特殊 care,由于文件系统时间戳、随机性和无限制的网络访问,构建将不可避免地出现分歧。
为什么Nix可以做得更好
Nix是一个跨平台的基于源的包管理器,具有Nix语言来描述包和一个大型社区维护的包集合称为Nixpkgs。NixOS是一个基于Nix和Nixpkgs构建的Linux发行版,从设计之初就专注于可重现性。它与传统的包管理器非常不同。例如,它不会将任何内容安装到常规系统路径(如/bin或/usr/lib)中。相反,它使用自己的/nix/store目录和指向安装在那里的包的符号链接。每个包都以一个哈希为前缀,该哈希捕获所有构建输入,如依赖图或编译选项。这意味着可以安装同一包的多个变体,仅因构建选项而异;从Nix的角度来看,这是一个不同的包。
Nix在暴露大多数可能导致构建不可重现的问题方面做得很好。例如,当遇到杂质(即未明确声明为构建输入的依赖项)时,Nix构建在开发过程中很可能会中断,迫使开发人员修复它。杂质通常从环境中捕获,包括环境变量或硬编码的系统范围目录(如/usr/lib)。Nix旨在通过沙盒化构建和修复文件系统时间戳来解决所有这些问题。Nix还要求所有从网络获取的输入都被固定。除此之外,Nixpkgs包含许多补丁(例如gnumake)以修复常见软件(如编译器或构建系统)中的可重现性问题。
减少杂质增加了构建可重现性的机会,这反过来增加了对源到工件对应关系的信任。然而,最终,可重现性不是可以证明或保证的东西。在底层,典型的Nix构建运行编译器,这些编译器可能依赖于某些随机性源,这些随机性可能泄漏到编译的工件中。理想情况下,应该持续跟踪可重现性。这种设置的一个例子是r13y.com网站,它跟踪NixOS镜像本身的可重现性。
除了强大的可重现性属性外,Nix在依赖透明度方面也表现出色。虽然Nix默认缓存构建输出,但每个包都可以从源代码构建,并且依赖图根植于易于审计的stage0引导程序,这将预编译二进制代码的信任降至最低。
Intel使用Nix的问题
还记得签署认证报告的引用飞地吗?为了提供所有SGX功能,Intel需要创建一组特权架构飞地,由Intel签名,这些飞地执行的任务太复杂,无法在CPU微码中实现。引用飞地就是其中之一。这些飞地是SGX的关键部分,因为它们可以访问烧录到CPU中的硬件密钥,并执行可信任务(如远程认证)。然而,引用飞地代码中的错误可能会使整个远程认证协议的安全保证无效。
意识到这一点,Intel准备了一个可重现的基于Nix的构建,该构建构建SGX SDK(构建任何飞地所必需)和所有架构飞地。该解决方案在Docker容器内使用Nix。我能够重现构建,但在仔细检查后,我发现了许多问题。
首先,构建没有固定Docker镜像或SDK源代码哈希。SDK可以从源代码构建,但架构飞地构建从Intel下载预编译的SDK安装程序,甚至不检查哈希。尽管使用了Nix,但有许多步骤发生在Nix构建之外。
不幸的是,构建的Nix部分不正确,并没有提供太多价值。依赖项是从预构建缓存中手动挑选的,这绕过了Nix提供的构建透明度。构建在nix-shell中运行,该shell应仅用于开发目的。该shell不提供与常规Nix构建相同的沙盒功能,并允许不同类型的杂质。事实上,在将SDK构建移植到Nixpkgs时,我发现了一些杂质。其中一些问题也被另一位研究人员注意到,但尚未解决。
将SGX SDK引入Nixpkgs
我得出结论,SGX SDK应该属于Nixpkgs,以实现真正可重现和透明的飞地构建。事实证明,已经有一个正在进行的努力,我加入并帮助完成了它。此后,该工作由社区扩展和维护。现在,任何SGX飞地都可以使用sgx-sdk包轻松地用Nix构建。我希望一旦这个解决方案成熟,Nixpkgs维护者可以与Intel一起维护它,并将其引入官方SGX SDK存储库。
我们准备了reproducible-sgx GitHub存储库,以展示如何使用Nix和移植的SDK构建Intel的示例飞地。虽然这展示了基础知识,但SGX飞地可以几乎任意复杂,并使用不同的库和编程语言。如果您希望看到另一个示例,请随时打开问题或拉取请求。
在本博客文章中,我们仅讨论了有关SGX飞地的可能安全问题的一小部分。例如,已经证明了许多针对SGX的安全侧信道攻击,如最近对Blu-ray DRM的攻击。如果您需要有关使用SGX或Nix的系统安全的帮助,请随时联系我们。
资源
- Intel SGX开发者指南
- Intel SGX Explained
- SGX 101
如果您喜欢这篇文章,请分享: Twitter LinkedIn GitHub Mastodon Hacker News
页面内容 背景 飞地如何证明其身份? 安全通信通道 为什么可重现构建困难? 为什么Nix可以做得更好 Intel使用Nix的问题 将SGX SDK引入Nixpkgs 资源 最近的帖子 Trail of Bits的Buttercup在AIxCC挑战赛中获得第二名 Buttercup现已开源! AIxCC决赛:故事的内幕 攻击者的提示注入工程:利用GitHub Copilot 在NVIDIA Triton中发现内存损坏(作为新员工) © 2025 Trail of Bits。 使用Hugo和Mainroad主题生成。