如何在 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 是内核中的一个设备映射器目标, 在读取数据时验证块设备的完整性它的工作原理是针对预先计算的哈希树计算和验证每个块(通常为 4K)的哈希值,通常使用 SHA-256。

这种设计允许 重启期间或执行期间无法静默修改文件它是将信任的启动链扩展到操作系统、限制恶意软件持久性、加强安全策略以及确保启动期间的加密和 MAC 机制的关键。

在 Android(自 4.4 起)和 Linux 上, 信任锚定在树的根哈希中,并使用位于受保护位置(例如,启动分区或安全启动签名的 UKI)的公钥进行签名和验证。破解任何区块都需要破解底层加密哈希值。

验证是通过区块和按需进行的: 与 I/O 成本相比,增加的延迟微乎其微如果检查失败,内核会返回 I/O 错误,并且文件系统会显示损坏,这在数据不可靠的情况下是正常的。应用可以根据自身的容错能力决定是否继续运行。

验证树的内部工作原理

验证树是分层构建的。 第 0 层是来自设备的原始数据,分为 4K 块;每个区块都会计算一个 SHA-256(加盐)哈希值。这些哈希值随后被连接起来,形成第一层。第一层随后被分组为多个区块,并重新计算哈希值以形成第二层,依此类推,直到所有内容都合并到一个区块中:该区块经过哈希运算后,将生成根哈希值。

如果任何一层没有完全完成一个区块, 用零填充,直到达到 4K 以避免歧义。树的总大小取决于被检查分区的大小;实际上,对于典型的系统分区,它通常小于 30 MB。

大致流程是: 选择随机盐,哈希到 4K,用每个块的盐计算 SHA-256,连接形成层级,用零填充区块边界,并重复上一层,直到剩下一个根哈希。该根哈希与所使用的盐一起,被输入到 dm-verity 表和签名中。

磁盘格式版本和算法

磁盘上的哈希块的格式有一个版本。 版本 0 是 Chromium OS 中使用的原始版本:在散列过程结束时添加盐,摘要连续存储,块的其余部分用零填充。

La 建议新设备使用版本 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(Reed–Solomon)与隔行扫描技术相结合 减少空间占用,并提高恢复损坏数据块的能力。此功能与 dm-verity 配合使用:如果检查失败,子系统可以在声明无法恢复之前尝试纠正。

性能与优化

为了减少影响: 在 ARMv7 上通过 NEON 启用 SHA-2 加速,在 ARMv8 上启用 SHA-2 扩展 从内核。调整硬件的预读和 prefetch_cluster 参数;每个块的验证通常不会增加 I/O 成本,但这些设置会有所不同。

Linux(systemd、veritysetup)和 Android 入门

在 Linux 和 Android 上配置 dm-verity

在具有 systemd 的现代 Linux 上, dm-verity 允许已验证的只读根 使用 veritysetup(cryptsetup 的一部分)、systemd-veritysetup.generator 和 systemd-veritysetup@.service。建议包含安全启动和签名的 UKI(统一内核映像),尽管它们并非严格要求。

准备和建议的分区

功能性和调整性系统的一部分。 为哈希树保留一个卷 (通常 8-10% 的根目录大小就足够了),如果需要写入,请考虑将 /home 和 /var 分开。典型的方案包括:ESP(用于引导加载程序)、XBOOTLDR(用于 UKI)、根目录(加密或不加密)、VERITY 分区,以及可选的 /home 和 /var。

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

必须可写的文件

使用 root ro,一些程序希望写入 /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 中,很方便 将 Pacman 数据库移动到 /usr/lib/pacman 这样 rootfs 就会始终镜像已安装的软件包。然后,将缓存重定向到 /var/lib/pacman 并进行链接。如果要在不影响根目录的情况下更改镜像列表,请将其移动到 /var/etc 并进行链接。

使用 NetworkManager, 将系统连接移至 /var/etc/NetworkManager 并从 /etc/NetworkManager/system-connections 链接。这样可以保持根目录不变,并使配置保持在可写入的位置。

真实性构建与测试

从现场,一切完美,并安装在ro,创建树和roothash与 veritysetup 格式:运行时,它会打印 Root Hash 行,您可以将其保存到 roothash.txt。使用 veritysetup open root-device root verity-device $(cat roothash.txt) 并 mount /dev/mapper/root 运行它进行测试。

如果你更喜欢, 首先将树生成到文件中 (verity.bin)然后将其写入 VERITY 分区。结果集包括:根映像、Verity 树以及启动时需要锁定的根哈希值。

配置内核行

添加以下参数: systemd.verity=1、roothash=contents_of_roothash.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.紧急=重启 y rd.shell=0 (如果启动失败,则防止未经授权的shell) 封锁=保密 保护内核内存不被访问。

具有 verity 的附加分区

不仅仅是根: 您可以在 /etc/veritytab 中定义其他映射 systemd-veritysetup@.service 会在启动时组装它们。请记住:非 root 分区更容易进行读写挂载,而且 root 用户可能会在这些分区上禁用 Verity,因此这些分区的安全性较低。

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

dm-verity 并不是灵丹妙药。 使用您自己的密钥对 UKI 进行签名并启用安全启动 防止任何人覆盖 kernel/initramfs/cmdline(其中包含根哈希值)。sbupdate-git 或 sbctl 等工具有助于保持镜像签名并保证启动链完整。

如果启用内核锁定或模块签名验证, DKMS 或树外模块必须签名 否则它们将无法加载。请考虑为您的管道添加支持签名的自定义内核(请参阅签名内核模块)。

加密、TPM 和计量

dm-verity 保护完整性, 非保密性如果 root 不包含任何机密信息且启动链受到保护,则可以不加密。如果您使用 root 的密钥文件解锁其他卷,则最好对其进行加密。

使用 TPM 2.0, systemd-cryptenroll 允许将密钥绑定到 PCR 0、1、5、7 (固件、选项、GPT、安全启动状态)。添加 rd.luks.options=LUKS_UUID=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 对 roothash 进行签名,允许您选择持久模式或临时模式,并保留最新的 rootfs 作为备份。注意:向已验证的根文件系统添加持久性功能的应用场景有限;请尝试将应用数据持久化到单独的分区中。

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

从 Android 10 开始, RootFS 停止在 RAM 磁盘上运行并与 system.img 集成。 (system-as-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 不同,没有 boot_a/boot_b 备份,因此如果启动更新失败,在非 A/B 中删除恢复可能会导致您无法使用恢复模式。

内核通过两条路径将system.img挂载到/converity: vboot 1.0 (内核补丁用于解析 /system 中的 Android 元数据并派生 dm-verity 参数;cmdline 包括 root=/dev/dm-0、skip_initramfs 和 init=/init 以及 dm=…)或 vboot 2.0/AVB其中引导加载程序集成了 libavb,读取哈希树描述符(在 vbmeta 或系统中),构造参数并在 cmdline 中将它们传递给内核,并支持 FEC 和 restart_on_corruption 等标志。

使用系统作为 root, 不要使用 BOARD_ROOT_EXTRA_FOLDERS 用于设备专用的根文件夹:刷写 GSI 时,这些文件夹将会消失。请在 /mnt/vendor/ 下定义特定的挂载点,由 fs_mgr 自动创建,并在设备树的 fstab 中引用它们。

Android 允许 来自 /product/vendor_overlay/ 的供应商覆盖:init 将在 /vendor 中挂载符合 SELinux 上下文要求且存在 /vendor/ 的子目录. 需要 CONFIG_OVERLAY_FS=yy,在较旧的内核上,需要 override_creds=off 补丁。

典型实现: 在设备/中安装预编译文件/ /vendor_overlay/将它们添加到 PRODUCT_COPY_FILES 中,并使用 find-copy-subdir-files 将其复制到 $(TARGET_COPY_OUT_PRODUCT)/vendor_overlay,在 file_contexts 中为 etc 和 app 定义上下文(例如 vendor_configs_file 和 vendor_app_file),并在 init.te 中允许在这些上下文上挂载。在 userdebug 中使用最新的 vfs_mgr_vendor_overlay_test 进行测试。

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

在具有 A/B 插槽的设备上,更改插槽或 刷写 vbmeta/boot 时与 roothash 不一致 这可能会触发警告:dm-verity 损坏,您的设备不受信任。类似 fastboot flash –disable-verity –disable-verification vbmeta vbmeta.img 的命令会禁用验证,但会使系统失去任何完整性保障。

一些引导加载程序支持 快速启动 oem disable_dm_verity 以及与之相反的 enable_dm_verity。它在某些型号上有效,但在其他型号上无效;并且可能需要调整内核/magisk 的参数。使用风险自负:谨慎的做法是 对齐启动、vbmeta 和系统,签署或重新生成树并确保预期的根哈希与配置的根哈希匹配。

如果在警告之后您可以继续按下电源,系统将启动,但 你不再拥有完整的信任链要在不牺牲安全性的情况下删除消息,请恢复原始签名图像或使用正确的哈希树重建/验证 vbmeta,而不是禁用 verity。

i.MX 和 OpenWrt 平台

在 i.MX6 上(例如 sabresd), 使用 DM_VERITY 和 FEC 支持配置内核,使用 veritysetup 生成树,安全存储根哈希,并在命令行中传递适当的参数,或通过 initramfs 与 systemd-veritysetup 集成。如果您不使用 dm-crypt,则无需 CAAM 来实现 verity;重点在于完整性。

在 OpenWrt 和 带有 OpenEmbedded 的嵌入式 Linux 系统, 正在努力整合 dm-verity 和 SELinux (Bootlin 作业已修改,旨在纳入支持)。这是一种自然的契合:路由器和网络设备受益于不可变的、经过验证的、MAC 强化的根。

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

cryptsetup 可以为您生成树,但如果您更喜欢了解格式,紧凑表行定义包括: 映射名称、数据设备、数据块和哈希大小、块中的图像大小、hash_start 位置(如果是连接层,则为区块镜像 + 8)、根哈希值和盐值。生成连接层(从上到下,不包括第 0 层)后,将树写入磁盘。

打包所有东西, 编写 dm-verity 表,对其进行签名(典型的 RSA-2048)并在元数据中对签名+表进行分组 带有版本化标头和魔数。然后,它将系统映像、Verity 元数据和哈希树连接起来。在 fstab 中,它将 fs_mgr 标记为“verify”,并将公钥放在 /boot/verity_key 中以验证签名。

优化 SHA-2 加速您的 CPU 并调整预读/预取簇。在 ARM 硬件上,NEON SHA-2(ARMv7)和 SHA-2 扩展(ARMv8)显著降低了验证开销。

在任何部署中,请记住 根哈希值必须受到保护:无论是编译成已签名的 UKI、已签名的启动分区,还是由引导加载程序使用 AVB 进行验证。此后的所有内容都将继承该信任。

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

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