Systemd-boot 和 Tumbleweed 和 MicroOS 中的全盘加密
2023 年 12 月 20 日 | Alberto Planas | CC-BY-SA-3.0
Systemd-boot 和 Tumbleweed 和 MicroOS 中的全盘加密
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 稍后可以附加 LUKS2 设备,如果 TPM2 可以恢复密码,从而验证系统直到此时的健康状况。如果 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(系统所在的位置)的分区,并调整其大小以为 LUKS2 标头保留 32MB。之后,它将使用 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 值在构建服务中生成。
无论如何,我们还有很多工作要做。