使用Nix实现SGX飞地的可复现构建,增强信任链

本文探讨了如何使用Nix包管理器实现Intel SGX飞地的可复现构建,解决隐私导向部署中的信任问题。通过分析Signal和MobileCoin的案例,详细介绍了SGX远程认证机制及Nix在构建透明性方面的优势。

增强SGX飞地的信任 - The Trail of Bits博客

背景

Intel SGX于2015年推出,是机密(或可信)计算的一种实现。更具体地说,它是一种可信执行环境(TEE),允许用户在由不受信任方拥有和维护的远程计算机上运行机密计算。用户信任硬件制造商(此处为Intel)保护执行环境免受篡改,即使是最高特权级别的代码(如内核恶意软件)也无法干预。SGX代码和数据存在于称为飞地(enclave)的特殊加密和认证内存区域中。

在Trail of Bits工作期间,我观察到使用SGX的系统存在一个未被充分解决的信任缺口:飞地用户不一定信任飞地作者。相反,用户可以自由审计飞地的开源代码以验证其功能性和安全性。例如,在Signal的联系人发现服务或MobileCoin的共识协议等隐私导向部署中可以观察到这种设置。为了验证信任,用户必须检查飞地是否由可信代码构建。不幸的是,这被证明是一项困难的任务,因为构建往往难以复现,并且依赖大量预编译的二进制代码。实际上,几乎没有人验证构建,只能选择信任飞地作者。

从另一个角度来看,类似的情况也发生在区块链世界中,智能合约以字节码形式部署。例如,Etherscan会尝试复现链上EVM字节码,以证明其由声称的Solidity源代码编译而成。如果用户不信任Etherscan,可以自由执行相同的操作。

解决这个问题的方法是以可复现和透明的方式构建SGX飞地,以便多方可以独立获得相同的结果,并审计构建中任何与供应链相关的问题。为了实现这一目标,我帮助将Intel的SGX SDK移植到Nixpkgs,这使得可以使用Nix包管理器以完全可复现的方式构建SGX飞地,从而任何用户都可以验证构建基于可信代码。

要了解可复现构建如何完善信任链,首先需要理解SGX提供的保证。

飞地如何证明其身份?

除了上述TEE保护(无信息泄漏且执行不可更改)外,SGX可以远程证明飞地的身份,包括其代码哈希、签名和运行时配置。此功能称为远程认证(remote attestation),对于不熟悉此类技术的人来说可能有些陌生。

当飞地加载时,其初始状态(包括代码)由CPU哈希化为测量哈希(measurement hash),也称为MRENCLAVE。只有当飞地代码更改时,此哈希才会改变。该哈希与其他数据(如签名者和环境详细信息)一起被放置在一个仅SGX实现可访问的特殊内存区域中。飞地可以请求CPU生成包含所有这些数据的报告(report),包括一段飞地定义的数据(称为report_data),然后将其传递给特殊的Intel签名的引用飞地(quoting enclave)以签署报告(从此称为quote),以便可以传递给远程方并进行验证。

接下来,验证者使用Intel和quote中的相关信息检查quote的真实性。尽管此时还有一些额外的检查和步骤,但在我们的案例中,最重要的是检查测量哈希,这是信任验证的关键组成部分。

我们根据什么验证哈希?最简单的解决方案是将可信的MRENCLAVE值硬编码到客户端应用程序本身。例如,Signal使用了此解决方案,其中MRENCLAVE被放置在客户端的构建配置中,并根据Signal服务器发送的签名quote中的哈希进行验证。将客户端和MRENCLAVE捆绑在一起是有意义的;毕竟,我们还需要审计和信任客户端应用程序代码。缺点是当飞地代码更改时,客户端应用程序必须重新构建和重新发布。如果预计飞地修改频繁,或者需要快速将客户端移动到另一个飞地(例如,在安全事件发生时),客户端可以使用更动态的方法,从可信第三方获取MRENCLAVE值。

安全通信通道

SGX可以证明飞地的身份及其生成的一段report_data,但建立可信和安全通信通道取决于飞地和验证者。由于SGX飞地是灵活的,可以通过ECALL和OCALL通过网络自由与外界通信,SGX本身并未对通道施加任何特定协议或实现。飞地开发者可以自由决定,只要通道是加密的、经过认证的并在飞地内部终止。

例如,SGX SDK实现了一个用于远程认证的认证密钥交换方案示例。然而,该方案假设了一个类似DRM的系统,其中飞地的签名者受信任,服务器的公钥硬编码在飞地代码中,因此不适合用于Signal等隐私导向的SGX部署。

如果我们不信任飞地作者,可以利用report_data建立这样的通道。此时SGX的保证基本上结束,从此我们必须信任飞地的源代码做正确的事情。这一事实起初并不明显,但如果我们查看例如关于如何在飞地内部建立安全TLS通道的RA-TLS论文,就会变得显而易见:

飞地在每次启动时生成一个新的公私RA-TLS密钥对。RA-TLS密钥不需要持久化,因为在启动时生成新密钥的成本相当低。不持久化密钥减少了密钥的暴露,并避免了与持久化相关的常见问题,如状态回滚保护。相关方可以检查源代码,以确信密钥从未在飞地外部暴露。

为了维护信任链,RA-TLS使用来自quote的report_data,该quote承诺了飞地的公钥哈希。在实现Noise Pipes并在report_data中承诺握手哈希的Signal协议中可以观察到类似的方法。

SGX加密和认证飞地的内存,但保护数据取决于在飞地中运行的代码。没有什么能阻止飞地代码向外界披露任何信息。如果我们不知道在飞地中运行什么代码,任何事情都可能发生。

幸运的是,我们知道代码是开源的,但我们如何确保特定Git提交的代码映射到飞地呈现的MRENCLAVE哈希?我们必须复现飞地构建,计算其MRENCLAVE哈希,并将其与从quote获得的哈希进行比较。如果构建无法复现,我们剩下的选择要么信任确认飞地安全可用的人,要么审计飞地的二进制代码。

为什么可复现构建困难?

我们关心的可复现性是比特级可复现性。某些软件可能在语义上相同,尽管其工件存在微小差异。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镜像相当困难,我们基本上需要信任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详解
  • SGX 101

如果您喜欢这篇文章,请分享: Twitter LinkedIn GitHub Mastodon Hacker News

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计