如何在 Linux 上使用 dm-verity:完整实用指南

  • dm-verity 使用签名的根哈希树动态验证区块,从而固定信任的引导链。
  • 其现代部署结合了 veritysetup、systemd-veritysetup、Secure Boot 和 UKI 来保护内核、initramfs 和 cmdline。
  • Android 使用 system-as-root 和 AVB 传递 dm-verity 参数;FEC 和反应策略增强了稳健性。
  • 不可变根需要分离可写数据(/var、/home)并使用图像或 A/B 方案安排更新。

Linux 上的 dm-verity

如果您担心系统的完整性,dm-verity 是 Linux 生态系统中用于可靠启动和存储篡改检测的关键组件。它最初是内核设备映射器的一部分,现在已成为 Android、OpenWrt 以及其他寻求增强安全性的发行版中经过验证的启动过程的基础。

dm-verity并非抽象概念,而是与 veritysetup 和 systemd-veritysetup 等实际工具配合使用。它利用哈希树动态验证数据块,并能根据数据损坏情况采取相应的策略,包括记录事件、重启或阻塞系统。让我们深入了解一下,不放过任何细节。

什么是 dm-verity 以及您为什么应该关注

使用 dm-verity 进行完整性验证

`dm-verity` 是 `device-mapper` 的一个内核目标,用于在读取数据时验证块设备的完整性。它的工作原理是计算并检查每个数据块(通常为 4K)的哈希值,并将其与预先计算的校验树进行比较,通常使用 SHA-256 算法。

这种设计可以防止文件在重启之间或执行过程中被静默修改。它是扩展操作系统启动信任链、限制恶意软件持久性、加强安全策略以及在启动过程中为加密机制和消息认证码 (MAC) 提供保障的关键。

在 Android(自 4.4 版本起)和 Linux 系统中,信任机制基于数据树的根哈希值,该哈希值使用存储在受保护位置(例如,启动分区或使用安全启动签名的 UKI)的公钥进行签名和验证。破解任何数据块都需要破解其底层加密哈希值。

验证是按块按需进行的:与 I/O 开销相比,增加的延迟微乎其微。如果检查失败,内核会返回 I/O 错误,文件系统会显示为已损坏,这在数据不可信时是预期行为。应用程序随后可以根据自身的容错能力决定是否继续执行。

验证树的内部工作原理

验证树是分层构建的。第 0 层是原始设备数据,被分割成 4K 个数据块;每个数据块都会计算一个带盐值的 SHA-256 哈希值。这些哈希值连接起来形成第 1 层。第 1 层的数据又被分组并重新哈希,形成第 2 层,依此类推,直到所有数据都能放入一个单独的数据块中:该数据块的哈希值即为根哈希值。

如果任何一层未能完全填满一个数据块,则会用零填充至 4K,以避免歧义。树的总大小取决于已验证分区的大小;实际上,对于典型的系统分区,通常小于 30 MB 左右。

一般流程如下:选择一个随机盐值,将其分割成 4K 个区块,使用每个区块的盐值计算 SHA-256 哈希值,将这些哈希值连接起来形成不同的层级,在区块边界处填充零,然后重复上述步骤,直到只剩下一个根哈希值。该根哈希值连同所使用的盐值一起,用于生成 dm-verity 表和签名。

磁盘格式版本和算法

磁盘上哈希块的格式有版本号。版本 0 是 Chromium OS 最初使用的格式:哈希过程中在末尾添加盐值,摘要连续存储,区块的其余部分用零填充。

建议新设备使用版本1 :盐值会添加到哈希值中,并且每个摘要都会用零填充到 2 的幂次方,从而提高对齐性和鲁棒性。dm-verity 表还会指定算法(例如 sha1 或 sha256),尽管当前安全级别使用的是 sha256。

dm-verity 表和基本参数

dm-verity 目标表描述了数据所在位置、哈希树所在位置以及如何验证数据。典型表字段:

  • 开发:需要验证数据的设备(路径类型 /dev/sdXN 或 greater:lesser)。
  • hash_dev:具有哈希树的设备(可以相同;如果相同,则 hash_start 必须在检查范围之外)。
  • 数据块大小:数据块大小(以字节为单位)(例如 4096)。
  • hash_block_size:哈希块大小(以字节为单位)。
  • 数据块数量:可验证数据块的数量。
  • hash_start_block:到树的根块的偏移量(以 hash_block_size 块为单位)。
  • 算法:哈希算法(例如 sha256)。
  • 消化:根块哈希的十六进制编码(包括根据格式版本的盐);这个值是值得信任的。
  • 盐:十六进制盐。

此外,还有一些非常有用的可选参数可以用来调整其行为:

  • 忽略腐败:记录损坏的块,但允许继续读取。
  • 重启损坏:在检测到损坏时重新启动(与ignore_corruption不兼容,并且需要用户空间支持以避免循环)。
  • 腐败恐慌: : 检测到损坏时会引起恐慌(与以前的版本不兼容)。
  • 重新启动错误 y 错误时恐慌:除了 I/O 错误之外的反应相同。
  • 忽略零块:不检查预期为零的块并返回零。
  • 使用_fec_from_device + fec_roots + fec_块 + fec_start:当验证失败时,启用 Reed–Solomon (FEC) 来恢复数据;数据、哈希和 FEC 区域不得重叠,并且块大小必须匹配。
  • 最多检查一次:仅在第一次读取时检查每个数据块(以实时攻击中的安全性为代价来减少开销)。
  • root_hash_sig_key_desc:在创建映射时引用密钥环中的密钥来验证根哈希的 PKCS7 签名(需要适当的内核配置和受信任的密钥环)。
  • 尝试验证_tasklet:如果哈希值被缓存并且 I/O 大小允许,则检查下半部分以减少延迟;使用 /sys/module/dm_verity/parameters/use_bh_bytes 根据 I/O 类进行调整。

签名、元数据和信任锚定

为了确保 dm-verity 的可靠性,根哈希值必须可信,并且通常需要签名。在经典的 Android 系统中,启动分区中包含一个公钥,制造​​商会对其进行外部验证;这可以验证根哈希签名,并确保系统分区未被篡改。

Verity 元数据增加了结构和版本控制。元数据块包含魔数 0xb001b001(字节 b0 01 b0 01)、版本(当前为 0)、PKCS1.5 格式的表签名(RSA-2048 通常为 256 字节)、表长度、表本身以及填充至 32K 的零。

在 Android 实现中,验证依赖于fs_mgr 和 fstab:在相应的条目上添加勾选标记,并将密钥放置在 /boot/verity_key 中。如果魔数没有出现在应该出现的位置,验证就会停止,以避免验证错误信息。

已验证开始运行

保护机制存在于内核中:如果内核在启动前被攻破,攻击者就能保持控制权。因此,制造商通常会对每个阶段进行严格验证:设备中烧录的密钥用于验证第一个引导加载程序,该程序再验证下一个引导加载程序(应用程序引导加载程序),最后验证内核。

内核验证通过后,当已验证的块设备挂载时,dm-verity 会被激活。它不会对整个设备进行哈希运算(这样做速度慢且耗能),而是在访问每个数据块时进行验证。验证失败会导致 I/O 错误,服务和应用程序会根据其容忍度做出相应的反应:要么在没有数据的情况下继续运行,要么彻底失败。

前向纠错(FEC)

自 Android 7.0 起,FEC(里德-所罗门纠错)与交错技术相结合,以减少空间占用并提高损坏数据块的恢复能力。这与 dm-verity 机制共存:如果校验失败,子系统可以在宣布数据块无法恢复之前尝试进行纠错。

性能与优化

为了降低影响:在 ARMv7 上通过 NEON 启用 SHA-2 加速,在 ARMv8 上通过内核启用 SHA-2 扩展。调整硬件的预读和预取集群参数;块验证通常与 I/O 开销相比增加不多,但这些调整确实有效。

Linux(systemd、veritysetup)和 Android 入门

在 Linux 和 Android 上配置 dm-verity

在现代 Linux 系统中,如果安装了 systemd, dm-verity 可以使用 veritysetup(cryptsetup 的一部分)、systemd-veritysetup.generator 和[email protected]来验证只读 root 用户。建议使用安全启动和已签名的 UKI(统一内核映像),但并非绝对必要。

准备和建议的分区

这是功能完善且经过优化的系统的一部分。为哈希树预留一个卷(通常根目录大小的 8-10% 就足够了),如果需要写入 /home 和 /var 目录,请考虑将它们分开。典型的方案包括:ESP(用于引导加载程序)、XBOOTLDR(用于 UKI)、根目录(可选择是否加密)、VERITY 分区,以及可选的 /home 和 /var 分区。

作为 root 文件系统,EROFS 是 ext4 或 squashfs 的一个非常有趣的替代方案:它采用只读设计,在闪存/SSD 上性能非常好,默认使用 lz4 压缩,并且在带有 dm-verity 的 Android 手机中广泛使用。

必须可写的文件

拥有 root 权限后,某些程序需要在初始化期间写入/etc 目录。您可以将必要的文件移动到 /var/etc 目录,并创建符号链接(例如,将 NetworkManager 连接链接到 /etc/NetworkManager/system-connections)。请注意,systemd-journald 要求 /etc/machine-id 文件必须存在于根目录中(而不仅仅是符号链接),以避免影响早期启动。

要了解运行时发生的变化,请使用 `dracut-overlayroot`:它会在根目录上覆盖一个 tmpfs 文件,所有写入的内容都会出现在 `/run/overlayroot/u` 目录中。将该模块添加到 `/usr/lib/dracut/modules.d/` 目录,在 `dracut` 中包含 `overlayroot`,并在内核命令行中设置 `overlayroot=1`;这将显示哪些内容需要迁移到 `/var` 目录。

有用的例子:pacman 和 NetworkManager

在 Arch Linux 系统中,建议将 pacman 数据库移动到 `/usr/lib/pacman`,以便根文件系统始终反映已安装的软件包。然后,将缓存重定向到 `/var/lib/pacman` 并链接它。要在不影响根目录的情况下更改镜像列表,请将其移动到 `/var/etc` 并以相同的方式链接它。

使用 NetworkManager 时,将 system-connections 文件移动到 /var/etc/NetworkManager 目录,并从 /etc/NetworkManager/system-connections 目录创建链接。这样可以保持根目录的不可变性,并确保配置信息在需要写入的位置仍然有效。

真实性构建与测试

在所有设备都完美运行且已配置为只读 (RO) 的实时环境中,使用`veritysetup format`创建根树和根哈希:执行后,它会打印根哈希行,您可以将其保存到 `roothash.txt` 中。使用 `veritysetup open root-device root verity-device $(cat roothash.txt)` 打开根设备进行测试,并挂载 `/dev/mapper/root`。

如果您愿意,可以先将系统树生成到文件(verity.bin),然后将其写入 VERITY 分区。生成的数据集包含:根镜像、verity 系统树以及您将在启动时锚定的根哈希值。

配置内核行

添加以下参数:systemd.verity=1、roothash=roothash_contents.txt、systemd.verity_root_data=ROOT-PATH(例如,LABEL=OS)和 systemd.verity_root_hash=VERITY-PATH(例如,LABEL=VERITY)。对于严格策略,请将 systemd.verity_root_options 设置为 restart-on-corruption 或 panic-on-corruption。

其他推荐选项:ro(如果您不使用 EROFS/squashfs),rd.emergency=reboot和rd.shell=0(以防止在启动失败时出现未经授权的 shell),以及lockdown=confidentiality以保护内核内存免受访问。

具有 verity 的附加分区

不仅限于根分区:您可以在 /etc/veritytab 中定义其他映射,[email protected]会将它们集成到启动过程中。请记住:使用读写权限重新挂载非根分区更容易,而且 root 用户可以禁用这些分区上的 Verity,因此这些分区的安全性较低。

安全性:安全启动、UKI 和签名模块

dm-verity 并非万能灵药。它使用自身的密钥对 UKI 进行签名并启用安全启动,以防止任何人替换内核/initramfs/cmdline(其中包含根哈希值)。诸如 sbupdate-git 或 sbctl 之类的工具有助于维护已签名的镜像和完整的启动链。

如果启用内核锁定或模块签名验证,则 DKMS 或外部内核模块必须经过签名才能加载。请考虑使用支持签名的自定义内核来优化您的工作流程(请参阅已签名内核模块)。

加密、TPM 和计量

dm-verity 保护的是完整性,而非机密性。如果根卷不包含任何敏感信息且引导链已受到保护,则可以不对其进行加密。如果您使用根卷中的密钥文件来解锁其他卷,则建议对其进行加密。

使用 TPM 2.0 时,systemd-cryptenroll 允许您将密钥绑定到 PCR 0、1、5 和 7(固件、选项、GPT、安全启动状态)。添加 `rd.luks.options=UUID_of_LUKS=tpm2-device=auto` 并确保在 initramfs 中包含 TPM2 支持。systemd-boot 会检查 PCR4 中的 kernel.efi 文件,这在 UKI 或其命令行发生更改时可用于使密钥失效。

更新和部署模型

经过验证的只读根目录不会使用传统的包管理器进行更新。理想情况下,应该使用Yocto 项目等工具构建新镜像,然后再进行部署。Systemd 提供了 `systemd-sysupdate` 和 `systemd-repart` 工具,用于可靠地下载和刷写镜像。

另一种策略是A/B 交换:维护两个根目录和两个验证文件。将活动目录复制到非活动目录,应用更改,然后重建验证文件。下次启动时,切换目录。如果您使用的是 UKI,请记住在命令行中更新根目录哈希值或重建已签名的 UKI。

对于可选的持久化,可以在根目录使用 OverlayFS,并通过 tmpfs 或磁盘上的 upper 参数进行验证。您也可以传递 systemd.volatile=overlay 参数来实现临时持久化。Flatpak 可以轻松地在 /var 和 /home 目录下安装应用程序,而无需修改根目录 /。

有些软件包可以自动完成这个过程(例如 AUR 中的 verity-squash-root),它们通过构建根 squashfs 并使用内核和 initramfs 对根哈希进行签名,允许您选择持久模式或临时模式,并将最新的根文件系统作为备份。注意:为已验证的根文件系统添加持久性功能的应用场景较为有限;请尝试将应用程序数据持久化到单独的分区上。

Android:system-as-root、AVB 和供应商覆盖

从 Android 10 开始,rootfs 不再存储在 ramdisk 中,而是集成到 system.img 中(系统即 root)。所有预装 Android 10 的设备都采用这种方案,并且需要 ramdisk 来支持 dm-linear。在这一代系统中,BOARD_BUILD_SYSTEM_ROOT_IMAGE 被设置为 false,以区分使用 ramdisk 和直接激活 system.img。

Android 10 引入了动态分区和第一阶段初始化进程,该进程会激活逻辑系统分区;内核不再直接挂载该分区。仅系统级 OTA 更新需要系统即 root 的设计,这在 Android 10 设备上是强制性的。

在非 A/B 配置中,应将恢复分区与启动分区分开。与 A/B 配置不同,非 A/B 配置没有 boot_a/boot_b 备份,因此,如果启动更新失败,移除非 A/B 配置中的恢复分区可能会导致您无法进入恢复模式。

内核使用两种方法将 system.img 挂载到 / 目录并启用 verity:vboot 1.0(为内核打补丁,以解析 /system 中的 Android 元数据并导出 dm-verity 参数;命令行包括 root=/dev/dm-0、skip_initramfs 和 init=/init with dm=…)或vboot 2.0/AVB,其中引导加载程序集成了 libavb,读取哈希树描述符(在 vbmeta 或 system 中),构造参数并通过命令行将其传递给内核,并支持 FEC 和 restart_on_corruption 等标志。

使用系统作为根目录时,不要使用 BOARD_ROOT_EXTRA_FOLDERS来创建设备特定的根文件夹:它们在刷写 GSI 时会消失。在 /mnt/vendor/<点> 下定义特定的挂载点(fs_mgr 会自动创建),并在设备树的 fstab 文件中引用它们。

Android 允许从 `/product/vendor_overlay/<version>` 配置供应商覆盖层:`init` 会挂载 `/vendor` 目录下满足 SELinux 上下文要求且存在 `/vendor/<overlay_dir>` 的子目录。这需要设置 `CONFIG_OVERLAY_FS=yy`,并且在较旧的内核中,还需要 `override_creds=off` 补丁。

典型实现:将预编译文件安装到 device/<vendor>/<target>/vendor_overlay/<version> 目录下,使用 find-copy-subdir-files 命令将它们添加到 PRODUCT_COPY_FILES 中,指向 $(TARGET_COPY_OUT_PRODUCT)/vendor_overlay,在 file_contexts 中为 etc 和 app 定义上下文(例如 vendor_configs_file 和 vendor_app_file),并在 init.te 中启用这些上下文的 mounton 功能。使用 userdebug 中的 atest vfs_mgr_vendor_overlay_test 进行测试。

故障排除:Android 上的 dm-verity 损坏消息

对于具有 A/B 插槽的设备,在未兼容 root 哈希的情况下更换插槽或刷写 vbmeta/boot 镜像可能会触发警告:“dm-verity 已损坏,您的设备不受信任”。类似 `fastboot flash --disable-verity --disable-verification vbmeta vbmeta.img` 的命令会禁用验证,但会使系统失去完整性保证。

某些引导加载程序支持`fastboot oem disable_dm_verity`及其相反的 `enable_dm_verity` 命令。它在某些型号上有效,但在其他型号上无效;并且可能需要调整内核/Magisk 的标志。使用风险自负:谨慎的做法是对齐 `boot`、`vbmeta` 和 `system`,对内核树进行签名或重新生成,并确保预期的根哈希值与配置的哈希值匹配。

如果警告出现后您仍能继续按电源按钮,系统将启动,但您的信任链将不再完整。要移除此消息而不牺牲安全性,请恢复原始签名镜像或使用正确的哈希树重建/验证 vbmeta,而不是禁用 verity。

i.MX 和 OpenWrt 平台

在 i.MX6(例如 sabresd)中,配置内核以支持 DM_VERITY 和 FEC,使用 veritysetup 生成树,可靠地存储根哈希值,并在命令行中传递相应的参数,或者通过 initramfs 集成 systemd-veritysetup。如果您不使用 dm-crypt,则不需要 CAAM 进行验证;重点在于完整性。

在 OpenWrt 和使用 OpenEmbedded 的嵌入式 Linux 系统中,目前正在努力集成 dm-verity 和 SELinux(Bootlin 的工作已通过审查,旨在将其纳入支持)。这可谓天作之合:路由器和网络设备受益于具有 MAC 地址的不可更改、经过验证且经过强化的根证书。

手动树和元数据构建(详细视图)

cryptsetup 可以为您生成树,但如果您希望了解其格式,紧凑的表行定义包括:映射名称、数据设备、数据块大小和哈希值、镜像大小(以块为单位)、哈希起始位置(如果是拼接的,则为镜像大小加 8)、根哈希值和退出位置。生成拼接的层(从上到下,不包括第 0 层)后,将树写入磁盘。

为了打包所有内容,请创建 dm-verity 表,对其进行签名(通常使用 RSA-2048 签名),并将签名和表组合成元数据,其中包含版本化的头部和魔数。然后,将系统镜像、verity 元数据和哈希树连接起来。在 fstab 文件中,将 fs_mgr 标记为 verify,并将公钥放在 /boot/verity_key 中以验证签名。

利用CPU 的 SHA-2 加速进行优化,并调整预读/预取集群。在 ARM 硬件上,NEON SHA-2 (ARMv7) 和 SHA-2 扩展 (ARMv8) 可显著降低验证负载。

在任何部署中,务必记住必须保护根哈希值:可以通过将其编译到已签名的 UKI 中、将其存储在已签名的引导分区中,或者使用 AVB 通过引导加载程序对其进行验证。此后发生的一切操作都将继承这种信任。

有了以上所有条件,dm-verity 就成为不可变、移动和嵌入式系统的坚实基础,支持事务性更新、配置覆盖和现代安全模型,该模型可以减少攻击面并防止持久化,而不会牺牲性能。

Yocto 项目是什么?
相关文章:
Yocto 项目是什么:完整的嵌入式指南

在 Google 中将其添加为首选来源