Systemd-boot 和使用 TPM 和 FIDO2 进行全盘加密
2023年12月20日 | Alberto Planas | CC-BY-SA-3.0
Tumbleweed 和 MicroOS 中的 Systemd-boot 和全盘加密
openSUSE Tumbleweed 和 MicroOS 现在提供了一个使用 systemd-boot 作为引导加载程序,并且也基于 systemd 的全盘加密镜像。加密设备的解锁可以通过传统的密码、TPM2(一种已经存在于您系统中的加密设备,如果系统状态良好则会附加设备)或 FIDO2 密钥(将验证令牌的所有权)来完成。
这里有很多需要解释的地方,但基本上这些变化是朝着将发行版推向更安全地带的方向发展。一方面,它简化了发行版的架构,另一方面,它遵循其他发行版也在与之对齐的安全趋势。
那么,让我们从头开始…
systemd-boot
我们都知道并喜欢 GRUB2。它是一个优秀的引导加载程序。它也很庞大、复杂、丰富、巨大,并且在开发方面往往进展缓慢。
此引导加载程序的 openSUSE 包包含 200 多个补丁。其中一些补丁已经存在了 5 年、6 年……10 年。这既表明了维护者的才华,也可能预示着上游贡献过程缓慢的问题。
GRUB2 支持所有相关的系统,包括大型机、arm 或 powerpc。多种文件系统类型,包括 btrfs 或 NTFS。它包含完整的网络堆栈、USB 堆栈、终端,可以被脚本化……在某种程度上,它几乎是一个微型操作系统。
但随后 UEFI 在 18 年前出现,使得 GRUB2 提供的几乎所有功能都变得多余。系统固件已经以操作系统、引导加载程序或任何其他用户提供的应用程序可以使用的服务的形式提供了这些功能的大部分。当然,GRUB2 也支持 UEFI。
随着时间的推移,Linux 内核获得了作为 EFI 二进制文件编译的选项,通过可以附加到内核代码的存根。这意味着内核本身可以直接由固件启动,从而使引导加载程序在大多数情况下成为可选的。
随着时间的推移,新的、更直接的引导加载程序专注于 UEFI,例如 gummiboot。后来,此代码被集成到 systemd 中并重命名为 systemd-boot。
这段代码非常简单。比 GRUB2 简单很多个数量级。它基本上是一个非常小的 EFI 二进制文件,它呈现一个包含不同引导加载程序条目(在 引导加载程序规范 或简称 BLS 中描述的文本文件)的菜单,以及对 UEFI LoadImage 函数的调用,以将执行委托给所选内核。
此引导加载程序还可以与新的 统一内核镜像 (UKI) 一起工作,这些文件将内核、命令行和 initrd 聚合在一个单元中。这些 UKI 对于基于镜像的发行版非常有用,openSUSE 也计划支持它们。
将 systemd-boot 作为 GRUB2 的替代方案是 openSUSE 长期以来一直想要做的事情。2023 年 8 月,在 Factory 邮件列表中发布了一个 公告,宣布 Tumbleweed 支持 systemd-boot。
该公告引用了一个 wiki 条目,解释了如何手动将使用 GRUB2 的安装迁移到 systemd-boot。在公告之后不久,yast-bootloader 获得了对新安装的支持。
支持另一个引导加载程序会带来成本。如前所述,代码库更小,错误更少,更容易推理。但是 UEFI 依赖性降低了支持的架构数量(x86-64 和 aarch64)。通过为 GRUB2 提供支持 BLS 条目的补丁来缓解此问题,可以使引导加载程序之后的发行版架构独立于引导加载程序本身。好消息是该补丁已经存在,并且有可能将其添加到包中。
另一个问题是 systemd-boot 不支持 btrfs。作为 EFI 二进制文件,它只能从 FAT32 文件系统读取文件。可以通过将内核和 initrd 移动到 EFI 系统分区 (ESP) 来解决此限制。
最后,还需要考虑在 Tumbleweed 中支持快照,以及在 MicroOS 中支持事务。从引导加载程序,用户应该能够选择要从哪个快照启动,就像在使用 GRUB2 时可以做的那样。这两个概念都是使用 btrfs 子卷实现的,并且只有一部分内核、命令行、initrd 组合对于每个子卷有效。
例如,假设我们的系统中存在两个快照,每个快照都代表安装了两个内核的系统。这两个内核可能在所有快照中并不相同。也许其中一个升级用较新的版本替换了一个内核。我们需要某种工具来记录所需的信息,以将正确的组合关联起来,从而成功启动到任何这些快照,在这些限制下创建引导条目。
这个工具是 sdbootutil。每当 snapper 创建或销毁快照时(例如,当系统更新时),它将调用此工具,该工具将分析快照的内容,确保相应的内核安装在 ESP 中,存在有效的 initrd(如果不存在,将通过调用 mkinitrd 创建),并创建一个将内核、initrd 和快照通过命令行连接起来的引导条目。它还负责其他细节,例如检查分区上的可用空间。
通常此过程是透明的,但记住我们可以使用以下命令强制进入干净状态
sdbootutil add-all-kernels
sdbootutil remove-all-kernels
以防万一,你知道…
全盘加密
我们想要宣布的另一个方面是基于 systemd 的全盘加密 (FDE) 的支持。
FDE 并不是新手。 GRUB2 长期以来可以使用 cryptomount 命令解锁 LUKS 卷。传统上,这将在引导加载程序解锁时和 initrd 稍后解锁时向用户请求密码两次。可以通过将密码注入 initrd 或,如果您使用的是 openSUSE 包,它会透明地将其注入 initrd 来避免第二次请求。
最近,GRUB2 获得了两个新功能:对 LUKS2 加密设备的部分支持(使用 PBKDF2 作为密钥派生函数,而不是更安全和推荐的 Argon2id)以及一种可以将密钥存储在类似 TPM2 的设备中的密钥保护机制。
TPM2
详细解释 TPM2 的工作原理是另一篇文章的主题,但现在我们可以将其视为一个加密设备,仅当满足与系统状态相关的某些条件时,才能用于解锁密钥。如果系统状态良好,TPM2 将解锁密钥。
这个术语是一个技术术语,与断言系统处于已知良好状态有关。换句话说,我们确信固件没有被篡改,引导加载程序是我们安装的,并且没有被替换,内核是我们分发版中的内核,内核命令行是我们期望的,并且我们使用的 initrd 不包含我们不受控制的任何额外二进制文件。
在内部,TPM2 有一些寄存器,称为平台配置寄存器 (PCR)。在 TPM2 规范中,有 24 个,一个的大小足以存储哈希函数的值,例如 SHA1 或 SHA256。它们按组分隔:每个支持的哈希函数一个,但现在这太详细了。
这些寄存器有点特殊。我们可以重置它们,通常将值设置为 0。我们可以读取值,或者我们可以“扩展”它们。写入操作的设计方式是,我们无法设置寄存器中的任何随机值,除非是与关联的哈希函数连接当前 PCR 值和用户提供的新值的结果。
当前 PCR 值只能通过使用完全相同的序列扩展此寄存器来生成。如果我们更改其中一个值的哪怕一位,对于相同的 PCR,我们将产生截然不同的最终结果。
此功能用于一个称为 “度量启动” 的过程,其中在执行每个启动链阶段之前对其进行测量。这意味着在固件的初始阶段运行之前,有一个过程将计算内存中代码的哈希值,并使用此值扩展其中一个 PCR。在引导序列的最后(内核和 initrd)重复此操作。
当启用度量启动时,前 10 个 PCR 的最终值将包含只能通过使用已知版本的固件、引导加载程序和内核以及相关的证书、配置文件或内核参数来预测的值。如果其中一个元素发生更改(例如,使用不同的安全启动证书),它将生成与我们期望的不同 PCR 值。
TPM2 芯片是非常有趣的设备,其功能集远不止度量启动。如果您想了解更多信息,我建议您参考 此 或 此。
TPM2 用于 FDE
无论如何,这里的重点是我们可以创建一个“策略”,指示 TPM2 仅当某些 PCR 包含预期值时才解密密钥。细节略有不同,但现在让我们将此模型用作一个很好的近似值。
想法是我们可以使用某些 PCR 寄存器的值加密密码,以便 GRUB2 稍后可以在 TPM2 解锁密码的情况下附加 LUKS2 设备,从而验证系统直到此时的健康状况。如果 TPM2 解密失败,则意味着某些 PCR 没有预期的值,并且启动过程的某个阶段发生了变化。在这种情况下,GRUB2 将要求用户提供密码以继续加载内核和系统的其余部分。它将对新状态的信任委托给用户。
GRUB2 也提供了一种工具,用于根据当前 PCR 子集的值来密封密钥。这很好,但也带来了一些问题。其中一个问题是,我们可能正在以这样一种方式设置系统,即我们知道 PCR 值会在下一次启动期间发生变化(例如,在首次安装、引导加载程序升级或固件更新期间)。在这种情况下,使用当前寄存器值密封密码是没有用的:我们需要能够预测新的值,并使用这些假设值来进行密封。
另一个问题更隐蔽,并且稍后会变得至关重要。期望值可以频繁变化,并且不一定是唯一的。可能有一组有效的值。我们可以选择从不同的内核或不同的快照启动。 TPM2 使用称为授权策略的东西为此提供了一个解决方案。它们是一种可以改变的策略,但它们通过签名进行验证。本质上,我们创建一个公钥和一个私钥,并创建多个 PCR 策略,这些策略使用私钥进行签名。现在,TPM2 可以使用公共部分验证签名,并使用新策略中存储的 PCR 值来解封密钥。
自 2023 年初以来,openSUSE 提供了 pcr-oracle 工具,以帮助预测 PCR 寄存器的值,并使用 PCR 策略或授权策略使用这些值加密密钥。使用此工具,我们现在可以在一组可以改变的 PCR 值下密封密钥!
在 openSUSE wiki 上,我们可以找到更多关于这些主题的文档,包括有关如何在我们的安装中使用它的说明。
使用 systemd 进行磁盘加密
使用 GRUB2 时,FDE 运行良好,为什么还要寻找其他东西?一个原因非常明显:这种架构只有在使用了我们的 openSUSE GRUB2 版本时才能工作……它不适用于其他引导加载程序,如 systemd-boot。事实上,它甚至不适用于上游版本的 GRUB2 本身。
但还有一个原因:我们可以认为使用 GRUB2 没有完全实现测量启动。如果引导加载程序需要在加载内核之前解锁设备,那么评估系统健康状况的 PCR 策略无法对内核、命令行或将要使用的 initrd 进行断言。这些将在打开 LUKS2 设备之后加载。
使用 systemd-boot 为 FDE 提供了一种替代架构,可以与遵循 BLS 的任何引导加载程序一起正常工作(请记住,GRUB2 某个地方有一个支持它的补丁,因此不排除先验),并有机会在解锁设备之前进行完整的测量启动证明。
一个区别是,内核和 initrd 将被放置在未加密的 ESP 中,并且 sysroot 的解锁将从 initrd 内部使用 systemd-cryptsetup 提供的不同选项完成。目前,它可以使用普通密码、带有授权策略的 TPM2(可选地需要用户输入的 PIN)或 FIDO2 密钥设备解锁设备。在 /etc/crypttab 文件中,我们需要 描述解锁机制。
pcr-oracle 已扩展为支持创建 systemd 可以理解的授权策略。它们存储在一个 JSON 文件中,该文件包含多个预测,每个预测指示涉及的 PCR、TPM2 策略哈希、公钥的指纹和策略的签名。这与公共密钥 PEM 文件一起,构成了 systemd-cryptsetup 使用 TPM2 解封 LUKS2 密钥所需的所有数据。
用于签署策略的 RSA 2048 密钥可以使用 openssl 或 pcr-oracle 本身创建。需要注意的是:如果私钥泄露,那么 TPM2 可以提供的预期安全性就结束了。幸运的是,在这种情况下,解决方案很便宜:生成一个新密钥,使用 systemd-cryptenroll 将密钥重新注册到 LUKS2 密钥槽中,并使用 sdbootutil 为每个引导条目重新生成预测。是的……我们将记录所有过程在 “systemd-fde” wiki 页面 上,并提供更好的工具,但相信我,这确实是一项廉价的操作。
openSUSE 提供了一个 MicroOS 镜像,名为 kvm-and-xen-sdboot,展示了所有这些是如何工作的。该镜像包含一些已经提到的工具以及其他一些新工具
systemd-boot:代替默认GRUB2使用的引导加载程序sdbootutil:帮助脚本,用于同步系统的引导条目pcr-oracle:预测下一次启动的PCR值,并为systemd创建授权策略disk-encryption-tool:在首次启动时加密位于sysroot的设备dracut-pcr-signature:dracut模块,它将从ESP将预测加载到initrd中
这些工具被设计成一起用于这种新的 FDE 架构。以下是关于所有内容如何连接的简要说明。
一旦我们获得新的 MicroOS qcow2 镜像并设置了 VM,我们就可以继续启动过程。如果 VM 具有虚拟 TPM2 设备,它将开始测量执行的代码和数据,扩展相应的 PCR。一旦到达 systemd-boot,它将找到此会话的正确引导条目,并从中读取相应的内核和 initrd。
此时,该镜像尚未加密。在用于此首次启动的 initrd 内部,将调用 disk-encryption-tool 脚本。使用一些启发式方法,它将找到属于 sysroot(系统所在的位置)的分区,并调整其大小以保留 32MB 用于 LUKS2 标头。之后,它将使用 cryptsetup 提供的所有魔术来使用本地生成的密码重新加密设备。目前,此密码对应于在最后向用户呈现的恢复密钥,用户应记下并妥善保管。
重新加密后,系统 /etc/crypttab 将被更新,以告知此设备现在已加密,并且应稍后使用不同的工具进行管理。
在 initrd 的末尾,我们切换到新的 sysroot,现在终于位于加密设备中。 disk-encryption-tool 脚本已经完成了它的主要工作,但它安装了两个用于 jeos-firstboot 的模块,这些模块将在系统的首次启动时执行,而这正是目前正在发生的事情!
第一个模块,enroll,将检测是否插入了 FIDO2 密钥,以及是否可用 TPM2。如果是,它将呈现一个对话框,询问您想使用什么来解锁系统。第二个模块将询问用户是否将 root 密码也注册到 LUKS2 标头中作为新密钥,并显示之前生成的恢复密钥。
目前不建议同时注册两者。如前所述,如果您使用的是笔记本电脑或台式机,并且想要使用您拥有的令牌证明来解锁加密设备,则 FIDO2 密钥更有意义。这是一个交互式过程。 TPM2 在我们不想与系统交互并且只想在能够断言系统健康状况(启动链中没有篡改)时自动解锁设备的情况下更有意义。
如果我们注册 FIDO2 密钥,将调用 systemd-cryptenroll,我们将被要求两次按下按钮,安装过程就完成了。在下次启动时,将要求我们呈现密钥,如果密钥丢失,将要求提供恢复密码。
如果我们注册 TPM2 设备,将生成一个新 RSA 2048 密钥并存储(公钥和私钥部分)在 /etc/systemd 中,并使用 systemd-cryptenroll 来注册公钥并注释用于密封 LUKS2 密钥的 PCR。默认情况下,我们将使用 0、2、4、7 和 9。您可以在 此参考 中查看含义。 PCR 0 和 2 将测量所有 UEFI 固件代码。 PCR 4 将测量引导加载程序 (systemd-boot) 和内核(也为 UEFI 二进制文件)。 PCR 7 将注册所有安全启动证书,PCR 9 将由内核用于测量命令行和 initrd。
这涵盖了几乎所有有意义的内容,但用户拥有最终决定权,因为预测是在 sdbootutil 内部完成的,请记住,它将在系统每次更改后自动执行(更新、删除软件包、快照管理等),并且此工具将仅为 LUKS2 标头中注册的 PCR 生成预测。
无论选择哪种解锁机制,/etc/crypttab 文件都将使用此选择进行更新,并生成一个新的 initrd,以在下次启动时包含此信息。
最后,最后一个组件,dracut-pcr-signature 将负责在后续启动期间,systemd-cryptsetup 所需的所有信息都将“实时”存在于 initrd 内部。应该注意的是,initrd 需要 JSON 文件中的策略和密钥,但这些不能包含在 initrd 中!当我们对使用 initrd 的哈希扩展 PCR 进行预测时,就完成了,并且不能再触摸 initrd,因为这将产生新的哈希并自动使预测失效。
这个 dracut 模块将在任何加密设备的 systemd-cryptsetup 生成器启动之前执行,并且将在 ESP 分区中搜索一个 tpm2-pcr-signature.json 文件,该文件包含当前启动的所有有效预测。一旦此文件到位,systemd-crypsetup 就能断言设备处于预期状态,启动过程就可以一直进行到最后。
未来
该镜像已经存在,并且是一个健全的 PoC。它提供了一个更简单的架构,并将一些组件放置在正确的位置。这将在接下来的阶段中提供很大的帮助,因为我们还有一些其他事情想对发行版在 FDE 方面做。
一个非常明确的是 disk-encryption-tool 在基于镜像的安装之外用途有限。这部分代码应该存在于 YaST 和 Agama 中。安装程序已经在创建 LUKS2 设备,因此以对我们有用的方式扩展它应该“很容易”。
理想情况下,jeos-firstboot 模块也应该存在于安装程序中,但不知为何它们放在这里也说得过去。无论如何,功能不应被分离,两者都应该合并。
加密工具从一开始就做得很好:主密钥以及所有用户密钥都在安装时生成,但一个可能的改进是在稍后使用 systemd 工具生成恢复密钥。这是一个小细节,但将系统密钥与用户密钥分离可以简化架构。
另一个需要改进的方面是,用户可能希望同时使用 TPM2 和 FIDO2 密钥。例如,默认情况下使用 TPM2,如果启动阶段发生预测失败(或检测到安全漏洞),用户可以将解锁委托给 FIDO2 密钥,而不是使用密码。
sdbootutil 脚本包含许多也应该存在于 systemd 中的功能。与上游合作将使此工具随着时间的推移变得过时,这将是更好的消息。
我们可以在 systemd 中提供的另一个改进是,改进关于导致 TPM2 拒绝解封 LUKS2 密钥的原因的诊断。目前,我们有一个通用的失败消息,没有报告哪个 PCR 或 PCR 内部的哪个已测量组件报告了与预测不同的哈希值。这将有助于理解发生了什么问题。是启动加载程序被更改了吗?还是固件中的某些内容?
pcr-oracle 是一个非常好的工具,用于预测下一个 PCR 值。扩展它以解析与完整测量启动过程相关的日志中的新事件非常容易,包括内核、systemd-boot 在 PCR 12 上的扩展,或生成 systemd 所需的 JSON 文档。新的 systemd 255(在撰写本文时发布一周前)包含一个名为 systemd-pcrlock 的类似工具,它可以帮助我们提供我们正在寻找的改进诊断。评估此工具以进行预测也将很快完成。
目前,BLS 中的 Type#1 和 Type#2 条目不是同构的。UKI 格式的 EFI 文件中存在文本表示中不存在的部分。也许我们将来会决定使用 UKI,或者不使用。因此,一个好的改进是努力帮助实现这种统一,这将(在其他事情中)提供一种标准的方式来拆分 JSON 文件并将预测与每个启动加载程序条目关联起来。
生成和注册新密钥,或选择不同的 PCR 集目前是一个手动过程。可以扩展当前工具以帮助这些过程,或者可以提供更好的文档。
针对 FDE 的新方法不是要将 GRUB2 从等式中排除。而是提供使用遵循 BLS 的不同启动加载程序的机会。验证适当修补的(当然!)GRUB2 是否可以与所有这些一起工作仍然需要完成。
此外,还需要验证和改进使用多个加密磁盘的安装。原则上,设计和代码支持它(即使每个卷的 PCR 寄存器不同)。openQA 将在这里发挥奇迹。
最后,我们应该重新考虑 UKI 是否适合 openSUSE。如果我们朝那个方向发展,用于签署策略的私钥将保存在 OBS 中,并且这些策略也将在构建服务中使用不同的 PCR 值生成。
无论如何,我们还有很多工作要做。