fs-verity: 只读的基于文件的真实性保护

简介

fs-verity (fs/verity/) 是一个支持层,文件系统可以连接到它,以支持只读文件的透明完整性和真实性保护。目前,它受 ext4、f2fs 和 btrfs 文件系统支持。与 fscrypt 类似,支持 fs-verity 不需要太多文件系统特定的代码。

fs-verity 类似于 dm-verity,但它作用于文件而不是块设备。在支持 fs-verity 的文件系统上的常规文件上,用户空间可以执行一个 ioctl 调用,使文件系统为文件构建一个 Merkle 树,并将其持久化到与文件关联的文件系统特定位置。

此后,文件被设置为只读,并且所有对文件的读取都会自动根据文件的 Merkle 树进行验证。读取任何损坏的数据,包括 mmap 读取,都将失败。

用户空间可以使用另一个 ioctl 调用来检索 fs-verity 正在为文件强制执行的根哈希(实际上是“fs-verity 文件摘要”,它是一个包含 Merkle 树根哈希的哈希值)。无论文件大小如何,此 ioctl 都以恒定时间执行。

fs-verity 本质上是一种以恒定时间哈希文件的方式,但有一个注意事项:运行时,如果读取会违反哈希,则读取将失败。

用例

fs-verity 本身只提供完整性保护,即检测意外(非恶意)损坏。

然而,由于 fs-verity 使文件哈希的检索效率极高,因此它主要用作支持认证(检测恶意修改)或审计(在使用前记录文件哈希)的工具。

可以使用标准文件哈希而不是 fs-verity。但是,如果文件很大且只访问一小部分,则效率低下。例如,Android 应用程序包 (APK) 文件经常出现这种情况。这些文件通常包含许多翻译、类和其他资源,这些资源在特定设备上很少或甚至从不访问。在启动应用程序之前读取并哈希整个文件将是缓慢且浪费的。

与提前哈希不同,fs-verity 还会每次分页数据时重新验证数据。这确保了恶意磁盘固件无法在运行时不被检测地更改文件内容。

fs-verity 不会取代或废弃 dm-verity。dm-verity 仍应在只读文件系统上使用。fs-verity 适用于必须存在于读写文件系统上的文件,因为这些文件是独立更新的且可能是用户安装的,因此无法使用 dm-verity。

fs-verity 不强制要求其文件哈希的特定认证方案。(类似地,dm-verity 也不强制要求其块设备根哈希的特定认证方案。)认证 fs-verity 文件哈希的选项包括:

  • 受信任的用户空间代码。通常,访问文件的用户空间代码可以被信任来认证它们。例如,一个应用程序希望在使用数据文件之前认证它们,或者一个作为操作系统一部分(已经通过其他方式认证,例如从使用 dm-verity 的只读分区加载)的应用程序加载器希望在加载应用程序之前认证它们。在这些情况下,此受信任的用户空间代码可以通过使用 FS_IOC_MEASURE_VERITY 检索其 fs-verity 摘要,然后使用任何支持数字签名的用户空间加密库来验证其签名,从而认证文件的内容。

  • 完整性测量架构 (IMA)。IMA 支持 fs-verity 文件摘要作为其传统完整文件摘要的替代方案。“IMA 评估”强制要求文件在其“security.ima”扩展属性中包含有效且匹配的签名,这由 IMA 策略控制。有关更多信息,请参阅 IMA 文档。

  • 完整性策略强制执行 (IPE)。IPE 支持根据文件的不可变安全属性(包括受 fs-verity 内置签名保护的属性)强制执行访问控制决策。“IPE 策略”专门允许使用属性 fsverity_digest(用于通过其 verity 摘要识别文件)和 fsverity_signature(用于授权具有已验证 fs-verity 内置签名的文件)来授权 fs-verity 文件。有关配置 IPE 策略和理解其操作模式的详细信息,请参阅 IPE 管理指南

  • 受信任的用户空间代码与 内置签名验证 结合使用。此方法应非常谨慎地使用。

用户 API

FS_IOC_ENABLE_VERITY

FS_IOC_ENABLE_VERITY ioctl 在文件上启用 fs-verity。它接受一个指向 struct fsverity_enable_arg 结构体的指针,定义如下:

struct fsverity_enable_arg {
        __u32 version;
        __u32 hash_algorithm;
        __u32 block_size;
        __u32 salt_size;
        __u64 salt_ptr;
        __u32 sig_size;
        __u32 __reserved1;
        __u64 sig_ptr;
        __u64 __reserved2[11];
};

此结构体包含为文件构建 Merkle 树的参数。它必须按如下方式初始化:

  • version 必须为 1。

  • hash_algorithm 必须是用于 Merkle 树的哈希算法标识符,例如 FS_VERITY_HASH_ALG_SHA256。有关可能值的列表,请参阅 include/uapi/linux/fsverity.h

  • block_size 是 Merkle 树的块大小,以字节为单位。在 Linux v6.3 及更高版本中,这可以是 1024 到系统页面大小和文件系统块大小的最小值之间(包含)的任何 2 的幂。在早期版本中,只允许页面大小。

  • salt_size 是盐值的字节大小,如果未提供盐值则为 0。盐值是一个预先添加到每个哈希块的值;它可用于为特定文件或设备个性化哈希。目前,最大盐值大小为 32 字节。

  • salt_ptr 是指向盐值的指针,如果未提供盐值则为 NULL。

  • sig_size 是内置签名的字节大小,如果未提供内置签名则为 0。目前,内置签名(有点随意地)限制为 16128 字节。

  • sig_ptr 是指向内置签名的指针,如果未提供内置签名则为 NULL。只有在使用 内置签名验证 功能时才需要内置签名。IMA 评估不需要它,如果文件签名完全在用户空间中处理,也不需要它。

  • 所有保留字段必须清零。

FS_IOC_ENABLE_VERITY 使文件系统为文件构建一个 Merkle 树,并将其持久化到与文件关联的文件系统特定位置,然后将文件标记为 verity 文件。此 ioctl 在大文件上可能需要很长时间才能执行,并且可被致命信号中断。

FS_IOC_ENABLE_VERITY 检查 inode 的写入权限。但是,它必须在 O_RDONLY 文件描述符上执行,并且不能有任何进程打开文件进行写入。在执行此 ioctl 期间尝试打开文件进行写入将失败并返回 ETXTBSY。(这对于保证在启用 verity 后不会存在可写文件描述符,以及保证在构建 Merkle 树期间文件内容稳定是必要的。)

成功时,FS_IOC_ENABLE_VERITY 返回 0,并且文件成为 verity 文件。失败时(包括被致命信号中断的情况),文件不会发生任何更改。

FS_IOC_ENABLE_VERITY 可能因以下错误而失败:

  • EACCES:进程没有文件的写入权限

  • EBADMSG:内置签名格式不正确

  • EBUSY:此 ioctl 已在该文件上运行

  • EEXIST:文件已启用 verity

  • EFAULT:调用者提供了不可访问的内存

  • EFBIG:文件太大,无法启用 verity

  • EINTR:操作被致命信号中断

  • EINVAL:不支持的版本、哈希算法或块大小;或设置了保留位;或文件描述符既不是常规文件也不是目录。

  • EISDIR:文件描述符指向一个目录

  • EKEYREJECTED:内置签名与文件不匹配

  • EMSGSIZE:盐值或内置签名太长

  • ENOKEY:“.fs-verity”密钥环不包含验证内置签名所需的证书

  • ENOPKG:fs-verity 识别哈希算法,但它在当前配置的内核加密 API 中不可用(例如,对于 SHA-512,缺少 CONFIG_CRYPTO_SHA512)。

  • ENOTTY:此类型的文件系统未实现 fs-verity

  • EOPNOTSUPP:内核未配置 fs-verity 支持;或文件系统超级块未启用“verity”特性;或文件系统不支持此文件上的 fs-verity。(请参阅 文件系统支持。)

  • EPERM:文件是只追加的;或者,需要内置签名但未提供。

  • EROFS:文件系统是只读的

  • ETXTBSY:有人打开文件进行写入。这可以是调用者的文件描述符,另一个打开的文件描述符,或者可写内存映射持有的文件引用。

FS_IOC_MEASURE_VERITY

FS_IOC_MEASURE_VERITY ioctl 检索 verity 文件的摘要。fs-verity 文件摘要是一个加密摘要,用于识别在读取时强制执行的文件内容;它通过 Merkle 树计算,与传统的完整文件摘要不同。

此 ioctl 接受一个指向可变长度结构体的指针:

struct fsverity_digest {
        __u16 digest_algorithm;
        __u16 digest_size; /* input/output */
        __u8 digest[];
};

digest_size 是一个输入/输出字段。在输入时,它必须初始化为为可变长度 digest 字段分配的字节数。

成功时,返回 0,内核按如下方式填充结构体:

  • digest_algorithm 将是用于文件摘要的哈希算法。它将与 fsverity_enable_arg::hash_algorithm 匹配。

  • digest_size 将是摘要的字节大小,例如 SHA-256 为 32。(这可能与 digest_algorithm 重复。)

  • digest 将是摘要的实际字节。

FS_IOC_MEASURE_VERITY 保证以恒定时间执行,无论文件大小如何。

FS_IOC_MEASURE_VERITY 可能因以下错误而失败:

  • EFAULT:调用者提供了不可访问的内存

  • ENODATA:文件不是 verity 文件

  • ENOTTY:此类型的文件系统未实现 fs-verity

  • EOPNOTSUPP:内核未配置 fs-verity 支持,或文件系统超级块未启用“verity”特性。(请参阅 文件系统支持。)

  • EOVERFLOW:摘要长于指定的 digest_size 字节。尝试提供更大的缓冲区。

FS_IOC_READ_VERITY_METADATA

FS_IOC_READ_VERITY_METADATA ioctl 从 verity 文件读取 verity 元数据。此 ioctl 自 Linux v5.12 起可用。

此 ioctl 对于需要在当前运行的内核之外执行 verity 验证的情况很有用。

一个例子是服务器程序接收 verity 文件并将其提供给客户端程序,以便客户端可以自行对文件进行 fs-verity 兼容的验证。这只有在客户端不信任服务器且服务器需要为客户端提供存储时才有意义。

另一个例子是在用户空间中创建文件系统镜像时(例如使用 mkfs.ext4 -d)复制 verity 元数据。

这是一个相当特殊的用例,大多数 fs-verity 用户不需要此 ioctl。

此 ioctl 接受一个指向以下结构体的指针:

#define FS_VERITY_METADATA_TYPE_MERKLE_TREE     1
#define FS_VERITY_METADATA_TYPE_DESCRIPTOR      2
#define FS_VERITY_METADATA_TYPE_SIGNATURE       3

struct fsverity_read_metadata_arg {
        __u64 metadata_type;
        __u64 offset;
        __u64 length;
        __u64 buf_ptr;
        __u64 __reserved;
};

metadata_type 指定要读取的元数据类型

  • FS_VERITY_METADATA_TYPE_MERKLE_TREE 读取 Merkle 树的块。块按从根级别到叶级别的顺序返回。在每个级别内,块按其哈希本身进行哈希的相同顺序返回。有关更多信息,请参阅 Merkle 树

  • FS_VERITY_METADATA_TYPE_DESCRIPTOR 读取 fs-verity 描述符。请参阅 fs-verity 描述符

  • FS_VERITY_METADATA_TYPE_SIGNATURE 读取传递给 FS_IOC_ENABLE_VERITY 的内置签名(如果有)。请参阅 内置签名验证

语义类似于 pread()offset 指定要从元数据项读取的偏移量(以字节为单位),length 指定要从元数据项读取的最大字节数。buf_ptr 是指向要读取到的缓冲区的指针,转换为 64 位整数。__reserved 必须为 0。成功时,返回读取的字节数。在元数据项结束时返回 0。返回的长度可能小于 length,例如当 ioctl 被中断时。

FS_IOC_READ_VERITY_METADATA 返回的元数据不保证根据 FS_IOC_MEASURE_VERITY 将返回的文件摘要进行认证,因为元数据预期无论如何都用于实现 fs-verity 兼容验证(尽管在没有恶意磁盘的情况下,元数据确实会匹配)。例如,为了实现此 ioctl,文件系统可以只从磁盘读取 Merkle 树块,而无需实际验证到根节点的路径。

FS_IOC_READ_VERITY_METADATA 可能因以下错误而失败:

  • EFAULT:调用者提供了不可访问的内存

  • EINTR:在读取任何数据之前 ioctl 被中断

  • EINVAL:设置了保留字段,或 offset + length 溢出

  • ENODATA:文件不是 verity 文件,或者请求了 FS_VERITY_METADATA_TYPE_SIGNATURE 但文件没有内置签名

  • ENOTTY:此类型的文件系统未实现 fs-verity,或此 ioctl 尚未实现

  • EOPNOTSUPP:内核未配置 fs-verity 支持,或文件系统超级块未启用“verity”特性。(请参阅 文件系统支持。)

FS_IOC_GETFLAGS

现有的 ioctl FS_IOC_GETFLAGS(并非 fs-verity 专用)也可用于检查文件是否启用了 fs-verity。为此,请检查返回标志中的 FS_VERITY_FL (0x00100000)。

verity 标志不能通过 FS_IOC_SETFLAGS 设置。您必须使用 FS_IOC_ENABLE_VERITY 代替,因为必须提供参数。

statx

自 Linux v5.5 起,如果文件启用了 fs-verity,则 statx() 系统调用会设置 STATX_ATTR_VERITY。这比 FS_IOC_GETFLAGS 和 FS_IOC_MEASURE_VERITY 性能更好,因为它不需要打开文件,而打开 verity 文件可能很昂贵。

访问 verity 文件

应用程序可以像访问非 verity 文件一样透明地访问 verity 文件,但有以下例外:

  • Verity 文件是只读的。它们不能以写入模式打开或被 truncate(),即使文件模式位允许。尝试执行这些操作之一将失败并返回 EPERM。但是,对所有者、模式、时间戳和 xattr 等元数据的更改仍然允许,因为 fs-verity 不会测量这些。Verity 文件也可以被重命名、删除和链接。

  • verity 文件不支持直接 I/O。尝试在此类文件上使用直接 I/O 将回退到缓冲 I/O。

  • verity 文件不支持 DAX(直接访问),因为这会规避数据验证。

  • 读取与 verity Merkle 树不匹配的数据将失败并返回 EIO (对于 read()) 或 SIGBUS (对于 mmap() 读取)。

  • 如果 sysctl “fs.verity.require_signatures” 设置为 1,并且文件未由“.fs-verity”密钥环中的密钥签名,则打开文件将失败。请参阅 内置签名验证

不支持直接访问 Merkle 树。因此,如果复制 verity 文件,或备份和恢复,它将失去其“verity”特性。fs-verity 主要用于可执行文件等由包管理器管理的文件。

文件摘要计算

本节描述 fs-verity 如何使用 Merkle 树哈希文件内容以生成加密识别文件内容的摘要。此算法对于所有支持 fs-verity 的文件系统都相同。

只有当用户空间需要自行计算 fs-verity 文件摘要(例如为了签署文件)时,才需要了解此算法。

Merkle 树

文件内容被划分为块,其中块大小是可配置的,但通常为 4096 字节。如果需要,最后一个块的末尾会用零填充。然后,每个块都会被哈希,生成第一级哈希。接着,此第一级中的哈希会被分组为“块大小”字节的块(如果需要,末尾用零填充),然后这些块被哈希,生成第二级哈希。这个过程一直向上进行,直到只剩下一个块。此块的哈希即为“Merkle 树根哈希”。

如果文件适合一个块且不为空,则“Merkle 树根哈希”简单地是单个数据块的哈希。如果文件为空,则“Merkle 树根哈希”全部为零。

这里的“块”不一定与“文件系统块”相同。

如果指定了盐值,则它会被零填充到哈希算法压缩函数的输入大小的最近倍数,例如 SHA-256 为 64 字节,或 SHA-512 为 128 字节。填充后的盐值会预先添加到每个被哈希的数据或 Merkle 树块。

块填充的目的是使每个哈希都在相同数量的数据上进行,这简化了实现并为硬件加速提供了更多可能性。盐值填充的目的是在预计算加盐哈希状态后(然后为每个哈希导入)使加盐“免费”。

示例:在 SHA-256 和 4K 块的推荐配置中,每个块可容纳 128 个哈希值。因此,Merkle 树的每个级别大约比前一个级别小 128 倍,对于大文件,Merkle 树的大小大约收敛到原始文件大小的 1/127。但是,对于小文件,填充很重要,使得空间开销按比例更大。

fs-verity 描述符

Merkle 树根哈希本身是模糊的。例如,它无法区分一个大文件和一个小型第二文件,该小型文件的数据恰好是第一个文件的顶层哈希块。填充到下一个块边界的约定也会导致歧义。

为了解决这个问题,fs-verity 文件摘要实际上是计算以下结构体的哈希,该结构体包含 Merkle 树根哈希以及文件大小等其他字段:

struct fsverity_descriptor {
        __u8 version;           /* must be 1 */
        __u8 hash_algorithm;    /* Merkle tree hash algorithm */
        __u8 log_blocksize;     /* log2 of size of data and tree blocks */
        __u8 salt_size;         /* size of salt in bytes; 0 if none */
        __le32 __reserved_0x04; /* must be 0 */
        __le64 data_size;       /* size of file the Merkle tree is built over */
        __u8 root_hash[64];     /* Merkle tree root hash */
        __u8 salt[32];          /* salt prepended to each hashed block */
        __u8 __reserved[144];   /* must be 0's */
};

内置签名验证

CONFIG_FS_VERITY_BUILTIN_SIGNATURES=y 增加了对内核中 fs-verity 内置签名验证的支持。

重要!在使用此功能之前,请务必非常小心。这不是使用 fs-verity 进行签名的唯一方法,替代方案(例如用户空间签名验证和 IMA 评估)可能要好得多。也很容易陷入认为此功能解决了比实际更多问题的陷阱。

启用此选项将增加以下功能:

  1. 在引导时,内核创建一个名为“.fs-verity”的密钥环。root 用户可以使用 add_key() 系统调用将受信任的 X.509 证书添加到此密钥环中。

  2. FS_IOC_ENABLE_VERITY 接受指向以 PKCS#7 格式的 DER 格式的文件 fs-verity 摘要的分离签名的指针。成功时,ioctl 将签名与 Merkle 树一起持久化。然后,无论 sysctl 变量“fs.verity.require_signatures”(在下一项中描述)的状态如何,只要文件签名存在,内核就会使用“.fs-verity”密钥环中的证书验证文件的实际摘要与此签名是否匹配。IPE LSM 依赖此行为来识别并标记包含已验证内置 fsverity 签名的 fsverity 文件。

  3. 一个新的 sysctl “fs.verity.require_signatures” 可用。当设置为 1 时,内核要求所有 verity 文件都具有如 (2) 中所述的正确签名的摘要。

(2) 中描述的签名所必须签名的数据是以下格式的 fs-verity 文件摘要:

struct fsverity_formatted_digest {
        char magic[8];                  /* must be "FSVerity" */
        __le16 digest_algorithm;
        __le16 digest_size;
        __u8 digest[];
};

就是这样。需要再次强调的是,fs-verity 内置签名不是使用 fs-verity 进行签名的唯一方法。有关 fs-verity 使用方式的概述,请参阅 用例。fs-verity 内置签名存在一些主要限制,在使用它们之前应仔细考虑:

  • 内置签名验证会强制内核强制任何文件实际启用 fs-verity。因此,它不是一个完整的身份验证策略。目前,如果使用它,完成身份验证策略的一种方法是让受信任的用户空间代码在访问文件之前明确检查文件是否启用了带有签名的 fs-verity。(在 fs.verity.require_signatures=1 的情况下,只需检查是否启用了 fs-verity 就足够了。)但是,在这种情况下,受信任的用户空间代码可以直接将签名与文件一起存储,并使用加密库自行验证,而不是使用此功能。

  • 另一种方法是将 fs-verity 内置签名验证与 IPE LSM 结合使用,IPE LSM 支持定义一个由内核强制执行的系统范围身份验证策略,该策略只允许具有已验证 fs-verity 内置签名的文件执行某些操作,例如执行。请注意,IPE 不要求 fs.verity.require_signatures=1。有关更多详细信息,请参阅 IPE 管理指南

  • 文件的内置签名只能在文件启用 fs-verity 的同时设置。以后更改或删除内置签名需要重新创建文件。

  • 内置签名验证对系统上所有启用 fs-verity 的文件使用同一组公钥。不能为不同文件信任不同的密钥;每个密钥都是全有或全无的。

  • sysctl fs.verity.require_signatures 适用于系统范围。将其设置为 1 只有在系统上所有 fs-verity 用户都同意将其设置为 1 时才有效。此限制可能会阻止 fs-verity 在其会有帮助的情况下使用。

  • 内置签名验证只能使用内核支持的签名算法。例如,内核尚不支持 Ed25519,尽管这通常是新加密设计推荐的签名算法。

  • fs-verity 内置签名采用 PKCS#7 格式,公钥采用 X.509 格式。这些格式被广泛使用,包括被一些其他内核功能(这就是 fs-verity 内置签名使用它们的原因)使用,并且功能非常丰富。不幸的是,历史表明,解析和处理这些格式(它们来自 1990 年代,基于 ASN.1)的代码通常由于其复杂性而存在漏洞。这种复杂性并非加密本身固有的。

    不需要 X.509 和 PKCS#7 高级功能的 fs-verity 用户应强烈考虑使用更简单的格式,例如纯 Ed25519 密钥和签名,并在用户空间中验证签名。

    选择使用 X.509 和 PKCS#7 的 fs-verity 用户仍应考虑在用户空间中验证这些签名更灵活(出于本文前面提到的其他原因),并消除了启用 CONFIG_FS_VERITY_BUILTIN_SIGNATURES 及其相关内核攻击面增加的需要。在某些情况下,甚至可能是必要的,因为高级 X.509 和 PKCS#7 功能并非总是按预期与内核一起工作。例如,内核不检查 X.509 证书的有效期。

    注意:支持 fs-verity 的 IMA 评估不使用 PKCS#7 进行签名,因此它部分避免了此处讨论的问题。IMA 评估确实使用 X.509。

文件系统支持

fs-verity 受以下几个文件系统支持。必须启用 CONFIG_FS_VERITY kconfig 选项才能在这些文件系统上使用 fs-verity。

include/linux/fsverity.h 声明了 fs/verity/ 支持层和文件系统之间的接口。简而言之,文件系统必须提供一个 fsverity_operations 结构体,该结构体提供读写 verity 元数据到文件系统特定位置的方法,包括 Merkle 树块和 fsverity_descriptor。文件系统还必须在特定时间调用 fs/verity/ 中的函数,例如当文件打开或页面已读入页缓存时。(请参阅 验证数据。)

ext4

ext4 自 Linux v5.4 和 e2fsprogs v1.45.2 起支持 fs-verity。

要在 ext4 文件系统上创建 verity 文件,文件系统必须已使用 -O verity 格式化或已在其上运行 tune2fs -O verity。“verity”是一个 RO_COMPAT 文件系统特性,因此一旦设置,旧内核将只能以只读方式挂载文件系统,旧版本的 e2fsck 将无法检查文件系统。

最初,带有“verity”特性的 ext4 文件系统只能在其块大小等于系统页面大小(通常为 4096 字节)时挂载。在 Linux v6.3 中,此限制已移除。

ext4 在 verity 文件上设置 EXT4_VERITY_FL 磁盘 inode 标志。它只能由 FS_IOC_ENABLE_VERITY 设置,并且不能清除。

ext4 还支持加密,可以与 fs-verity 同时使用。在这种情况下,验证的是明文数据而不是密文。这是使 fs-verity 文件摘要有意义所必需的,因为每个文件都以不同的方式加密。

ext4 将 verity 元数据(Merkle 树和 fsverity_descriptor)存储在文件末尾之后,从 i_size 之后的第一个 64K 边界开始。这种方法之所以有效,是因为 (a) verity 文件是只读的,并且 (b) 完全超出 i_size 的页面对用户空间不可见,但 ext4 可以在内部读/写它们,只需对 ext4 进行一些相对较小的更改。这种方法避免了依赖 EA_INODE 特性以及重新设计 ext4 的 xattr 支持以支持将多吉字节的 xattr 分页到内存中,以及支持加密 xattr。请注意,当文件被加密时,verity 元数据必须被加密,因为它包含明文数据的哈希值。

ext4 只允许对基于 extent 的文件进行 verity。

f2fs

f2fs 自 Linux v5.4 和 f2fs-tools v1.11.0 起支持 fs-verity。

要在 f2fs 文件系统上创建 verity 文件,文件系统必须已使用 -O verity 格式化。

f2fs 在 verity 文件上设置 FADVISE_VERITY_BIT 磁盘 inode 标志。它只能由 FS_IOC_ENABLE_VERITY 设置,并且不能清除。

与 ext4 类似,f2fs 将 verity 元数据(Merkle 树和 fsverity_descriptor)存储在文件末尾之后,从 i_size 之后的第一个 64K 边界开始。请参阅上面 ext4 的解释。此外,f2fs 每个 inode 最多支持 4096 字节的 xattr 条目,这通常甚至不足以容纳单个 Merkle 树块。

f2fs 不支持对当前有原子或易失性写入待处理的文件启用 verity。

btrfs

btrfs 自 Linux v5.15 起支持 fs-verity。启用 verity 的 inode 会被标记为 RO_COMPAT inode 标志,并且 verity 元数据存储在单独的 btree 项中。

实现细节

验证数据

fs-verity 确保对 verity 文件的所有数据读取都经过验证,无论使用哪个系统调用进行读取(例如 mmap()、read()、pread()),也无论它是第一次读取还是后续读取(除非后续读取可以返回已验证的缓存数据)。下面,我们描述文件系统如何实现这一点。

页缓存

对于使用 Linux 页缓存的文件系统,必须修改 ->read_folio()->readahead() 方法以在 folio 被标记为 Uptodate 之前验证它们。仅仅挂接 ->read_iter() 是不够的,因为 ->read_iter() 不用于内存映射。

因此,fs/verity/ 提供了函数 fsverity_verify_blocks(),用于验证已读入 verity inode 页缓存的数据。包含的 folio 仍必须被锁定且未 Uptodate,因此用户空间尚无法读取。根据需要进行验证,fsverity_verify_blocks() 将通过 fsverity_operations::read_merkle_tree_page() 回调到文件系统以读取哈希块。

如果验证失败,fsverity_verify_blocks() 返回 false;在这种情况下,文件系统不得设置 folio Uptodate。在此之后,根据通常的 Linux 页缓存行为,用户空间尝试从包含 folio 的文件部分 read() 将失败并返回 EIO,而对内存映射中 folio 的访问将引发 SIGBUS。

原则上,验证数据块需要验证 Merkle 树中从数据块到根哈希的整个路径。然而,为了效率,文件系统可以缓存哈希块。因此,fsverity_verify_blocks() 只向上遍历树读取哈希块,直到看到一个已经验证的哈希块。然后它验证到该块的路径。

这种优化也被 dm-verity 使用,可以实现出色的顺序读取性能。这是因为通常(例如,对于 4K 块和 SHA-256,128 次中有 127 次)来自树底层的哈希块将已缓存并从读取前一个数据块中检查过。然而,随机读取的性能较差。

基于块设备的文件系统

Linux 中基于块设备的文件系统(例如 ext4 和 f2fs)也使用页缓存,因此上述小节也适用。然而,它们通常也一次从文件中读取许多数据块,这些块被分组到一个名为“bio”的结构中。为了使这些类型的文件系统更容易支持 fs-verity,fs/verity/ 还提供了一个函数 fsverity_verify_bio(),用于验证 bio 中的所有数据块。

ext4 和 f2fs 也支持加密。如果一个 verity 文件同时也被加密,则数据必须在验证之前解密。为了支持这一点,这些文件系统为每个 bio 分配一个“读取后上下文”并将其存储在 ->bi_private 中:

struct bio_post_read_ctx {
       struct bio *bio;
       struct work_struct work;
       unsigned int cur_step;
       unsigned int enabled_steps;
};

enabled_steps 是一个位掩码,指定是否启用了解密、verity 或两者。在 bio 完成后,对于每个所需的后处理步骤,文件系统将 bio_post_read_ctx 入队到一个工作队列,然后工作队列执行解密或验证。最后,没有发生解密或 verity 错误的 folio 被标记为 Uptodate,并且 folio 被解锁。

在许多文件系统上,文件可以包含空洞。通常,->readahead() 只是将空洞块清零并认为相应的数据是最新的;不会发出 bio。为了防止这种情况绕过 fs-verity,文件系统使用 fsverity_verify_blocks() 来验证空洞块。

文件系统还禁用 verity 文件上的直接 I/O,因为否则直接 I/O 将绕过 fs-verity。

用户空间工具

本文档侧重于内核,但 fs-verity 的用户空间工具可以在以下位置找到:

有关详细信息,包括设置 fs-verity 保护文件的示例,请参阅 fsverity-utils 源代码树中的 README.md 文件。

测试

要测试 fs-verity,请使用 xfstests。例如,使用 kvm-xfstests

kvm-xfstests -c ext4,f2fs,btrfs -g verity

常见问题

本节回答了本文件中未直接回答的有关 fs-verity 的常见问题。

问:

为什么 fs-verity 不是 IMA 的一部分?

答:

fs-verity 和 IMA(完整性测量架构)关注点不同。fs-verity 是一种文件系统级别的机制,用于使用 Merkle 树哈希单个文件。相比之下,IMA 指定了一个系统范围的策略,该策略规定了哪些文件被哈希以及如何处理这些哈希,例如记录它们、认证它们或将它们添加到测量列表中。

IMA 支持 fs-verity 哈希机制作为完整文件哈希的替代方案,适用于那些希望获得基于 Merkle 树的哈希的性能和安全优势的用户。然而,强制所有 fs-verity 的使用都通过 IMA 是没有意义的。即使作为独立的 文件系统特性,fs-verity 也已经满足了许多用户的需求,并且可以像其他文件系统特性一样进行测试,例如使用 xfstests。

问:

fs-verity 不是没用吗,因为攻击者可以修改存储在磁盘上的 Merkle 树中的哈希?

答:

为了验证 fs-verity 文件的真实性,您必须验证“fs-verity 文件摘要”的真实性,其中包含 Merkle 树的根哈希。请参阅 用例

问:

fs-verity 不是没用吗,因为攻击者可以把 verity 文件替换成非 verity 文件?

答:

请参阅 用例。在最初的用例中,真正认证文件的是受信任的用户空间代码;fs-verity 只是一个工具,可以高效安全地完成这项工作。受信任的用户空间代码会将非 verity 文件视为非真实文件。

问:

为什么 Merkle 树需要存储在磁盘上?难道不能只存储根哈希吗?

答:

如果 Merkle 树不存储在磁盘上,那么即使只读取一个字节,也必须在文件首次访问时计算整个树。这是 Merkle 树哈希工作方式的基本结果。要验证叶节点,您需要验证到根哈希的整个路径,包括根节点(根哈希是其哈希的对象)。但如果根节点不存储在磁盘上,您必须通过哈希其子节点来计算它,依此类推,直到实际哈希了整个文件。

这使得基于 Merkle 树的哈希的大部分意义都丧失了,因为如果您无论如何都必须提前哈希整个文件,那么您可以简单地进行 sha256(文件) 代替。那会简单得多,而且速度也稍快。

诚然,内存中的 Merkle 树仍然可以提供每次读取时进行验证的优势,而不仅仅是第一次读取。然而,它会效率低下,因为每次哈希页被逐出时(您无法将整个 Merkle 树固定在内存中,因为它可能非常大),为了恢复它,您需要再次哈希树中其下方的一切。这再次使得基于 Merkle 树的哈希的大部分意义都丧失了,因为单个块读取可能触发重新哈希数千兆字节的数据。

问:

但是不能只存储叶节点并计算其余的吗?

答:

请参阅上一个答案;这实际上只是上移了一个级别,因为可以替代地将数据块解释为 Merkle 树的叶节点。诚然,如果存储叶级别而不是只存储数据,树的计算速度会快得多,但这只是因为每个级别的大小都小于下一级大小的 1%(假设推荐的 SHA-256 和 4K 块设置)。出于完全相同的原因,通过存储“仅叶节点”,您已经存储了树的 99% 以上,所以您不妨简单地存储整个树。

问:

Merkle 树可以提前构建吗,例如作为安装到多台计算机的软件包的一部分进行分发?

答:

目前不支持。这是原始设计的一部分,但为了简化内核 UAPI 和因为它不是一个关键用例而被移除。文件通常只安装一次并使用多次,并且加密哈希在大多数现代处理器上速度相对较快。

问:

为什么 fs-verity 不支持写入?

答:

写入支持将非常困难,并且需要完全不同的设计,因此它完全超出了 fs-verity 的范围。写入支持将需要:

  • 一种维护数据和哈希之间一致性的方法,包括所有级别的哈希,因为崩溃后(特别是可能影响整个文件!)的损坏是不可接受的。解决此问题的主要选项是数据日志、写时复制和日志结构卷。但要改造现有文件系统以适应新的持久性机制非常困难。数据日志在 ext4 上可用,但非常慢。

  • 每次写入后重建 Merkle 树,这将极其低效。或者,可以使用不同的认证字典结构,例如“认证跳表”。然而,这将复杂得多。

将其与 dm-verity 与 dm-integrity 进行比较。dm-verity 非常简单:内核只验证只读数据与只读 Merkle 树。相比之下,dm-integrity 支持写入但速度慢,复杂得多,并且实际上不支持全设备认证,因为它独立认证每个扇区,即没有“根哈希”。让相同的设备映射器目标支持这两种非常不同的情况并没有多大意义;同样适用于 fs-verity。

问:

既然 verity 文件是不可变的,为什么不设置不可变位?

答:

现有的“不可变”位 (FS_IMMUTABLE_FL) 已经有一套特定的语义,它不仅使文件内容只读,还阻止文件被删除、重命名、链接或更改其所有者或模式。fs-verity 不需要这些额外的属性,因此重用不可变位不合适。

问:

为什么 API 使用 ioctl 调用而不是 setxattr() 和 getxattr()?

答:

滥用 xattr 接口进行基本任意的系统调用受到大多数 Linux 文件系统开发者的强烈反对。xattr 应该真正只是磁盘上的 xattr,而不是一个神奇地触发 Merkle 树构建的 API。

问:

fs-verity 支持远程文件系统吗?

答:

到目前为止,所有已实现 fs-verity 支持的文件系统都是本地文件系统,但原则上任何能够存储每文件 verity 元数据的文件系统都可以支持 fs-verity,无论它是本地还是远程。某些文件系统可能存储 verity 元数据的选项较少;一种可能性是将其存储在文件末尾之后,并通过操作 i_size 从用户空间“隐藏”它。fs/verity/ 提供的 数据验证函数 还假设文件系统使用 Linux 页缓存,但本地和远程文件系统通常都这样做。

问:

为什么还有任何文件系统特定的内容?fs-verity 不应该完全在 VFS 层实现吗?

答:

有很多原因导致这不可能或会非常困难,包括:

  • 为了防止绕过验证,在 folio 被验证之前不得将其标记为 Uptodate。目前,每个文件系统都负责通过 ->readahead() 将 folio 标记为 Uptodate。因此,目前 VFS 无法自行进行验证。更改此项将需要对 VFS 和所有文件系统进行重大更改。

  • 它将需要定义一种与文件系统无关的方式来存储 verity 元数据。扩展属性不适用于此,因为 (a) Merkle 树可能达到千兆字节,但许多文件系统假设所有 xattr 都适合单个 4K 文件系统块,并且 (b) ext4 和 f2fs 加密不加密 xattr,但当文件内容加密时,Merkle 树必须加密,因为它存储明文文件内容的哈希。

    因此,verity 元数据必须存储在实际文件中。使用单独的文件会非常难看,因为元数据从根本上是受保护文件的一部分,并且可能导致用户删除实际文件但没有元数据文件,反之亦然。另一方面,如果它在同一个文件中,则会破坏应用程序,除非文件系统对 i_size 的概念与 VFS 脱钩,这将很复杂并需要更改所有文件系统。

  • 理想情况下,FS_IOC_ENABLE_VERITY 使用文件系统的事务机制,以便文件要么最终启用 verity,要么没有进行任何更改。允许在崩溃后出现中间状态可能会导致问题。