fs-verity:基于文件的只读真实性保护¶
简介¶
fs-verity (fs/verity/
) 是一个支持层,文件系统可以挂钩到该层以支持只读文件的透明完整性和真实性保护。目前,ext4、f2fs 和 btrfs 文件系统支持它。与 fscrypt 类似,支持 fs-verity 不需要太多特定于文件系统的代码。
fs-verity 类似于 dm-verity,但它作用于文件而不是块设备。在支持 fs-verity 的文件系统上的常规文件上,用户空间可以执行一个 ioctl,该 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
:该文件已启用 verityEFAULT
:调用方提供了无法访问的内存EFBIG
:文件太大,无法在其上启用 verityEINTR
:操作被致命信号中断EINVAL
:不支持的版本、哈希算法或块大小;或者设置了保留位;或者文件描述符既不是指常规文件也不是指目录。EISDIR
:文件描述符指向目录EKEYREJECTED
:内置签名与文件不匹配EMSGSIZE
:盐或内置签名太长ENOKEY
:“.fs-verity”密钥环不包含验证内置签名所需的证书ENOPKG
:fs-verity 识别哈希算法,但它在内核的加密 API 中未按当前配置提供(例如,对于 SHA-512,缺少 CONFIG_CRYPTO_SHA512)。ENOTTY
:此类型的文件系统未实现 fs-verityEOPNOTSUPP
:内核未配置为支持 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-verityEOPNOTSUPP
:内核未配置 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 文件并将其提供给客户端程序,以便客户端可以自行进行与 fs-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,或者尚未在此文件系统上实现此 ioctlEOPNOTSUPP
:内核未配置 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 起,statx() 系统调用会在文件启用 fs-verity 时设置 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 字节。如果需要,最后一个块的末尾将填充零。然后,对每个块进行哈希,生成第一级哈希值。然后,将第一级中的哈希值分组到 ‘blocksize’ 字节的块中(根据需要填充零),并对这些块进行哈希,生成第二级哈希值。此过程在树中向上进行,直到只剩下一个块。此块的哈希值是 “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 评估)可能会更好。而且,很容易陷入一种误区,认为此功能解决的问题比实际解决的要多。
启用此选项会添加以下内容:
在启动时,内核会创建一个名为“.fs-verity”的密钥环。root 用户可以使用 add_key() 系统调用将受信任的 X.509 证书添加到此密钥环。
FS_IOC_ENABLE_VERITY 接受一个指向文件 fs-verity 摘要的 DER 格式 PKCS#7 分离签名的指针。成功后,ioctl 会将签名与 Merkle 树一起持久保存。然后,每次打开文件时,内核都会使用“.fs-verity”密钥环中的证书验证文件的实际摘要与此签名。只要文件的签名存在,就会进行此验证,而与下一项中描述的 sysctl 变量“fs.verity.require_signatures”的状态无关。IPE LSM 依赖此行为来识别和标记包含已验证内置 fsverity 签名的 fsverity 文件。
一个新的 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 应用于整个系统。只有当系统上所有使用 fs-verity 的用户都同意将其设置为 1 时,将其设置为 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¶
自 Linux v5.4 和 e2fsprogs v1.45.2 起,ext4 支持 fs-verity。
要在 ext4 文件系统上创建 verity 文件,必须使用 -O verity
格式化文件系统,或者在其上运行 tune2fs -O verity
。“verity”是一个 RO_COMPAT 文件系统功能,因此一旦设置,旧内核将只能以只读方式挂载文件系统,而旧版本的 e2fsck 将无法检查文件系统。
最初,只有当 ext4 文件系统的块大小等于系统页面大小(通常为 4096 字节)时,才能挂载具有 “verity” 功能的 ext4 文件系统。在 Linux v6.3 中,此限制已删除。
ext4 在 verity 文件上设置磁盘 inode 标志 EXT4_VERITY_FL。它只能由 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¶
自 Linux v5.4 和 f2fs-tools v1.11.0 起,f2fs 支持 fs-verity。
要在 f2fs 文件系统上创建 verity 文件,必须使用 -O verity
格式化文件系统。
f2fs 在 verity 文件上设置磁盘 inode 标志 FADVISE_VERITY_BIT。它只能由 FS_IOC_ENABLE_VERITY 设置,并且不能清除。
与 ext4 类似,f2fs 将 verity 元数据(Merkle 树和 fsverity_descriptor)存储在文件末尾之后,从 i_size 之后的第一个 64K 边界开始。请参阅上面对 ext4 的解释。此外,f2fs 每个 inode 最多支持 4096 字节的 xattr 条目,这通常甚至不足以容纳单个 Merkle 树块。
f2fs 不支持对当前有原子或易失性写入挂起的文件启用 verity。
btrfs¶
自 Linux v5.15 起,btrfs 支持 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 页面缓存的通常行为,用户空间尝试通过 read()
从包含 folio 的文件部分读取数据时会失败并返回 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
是一个位掩码,指定是否启用了解密、验证或两者都启用。bio 完成后,对于每个需要的后处理步骤,文件系统会将 bio_post_read_ctx 加入工作队列,然后工作队列执行解密或验证。最后,没有发生解密或 verity 错误的数据页会被标记为 Uptodate,并且这些数据页会被解锁。
在许多文件系统中,文件可能包含空洞。通常,->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 指定一个系统范围的策略,该策略指定要哈希哪些文件以及如何处理这些哈希值,例如记录它们、验证它们或将它们添加到度量列表中。
对于那些希望获得基于 Merkle 树的哈希的性能和安全优势的人,IMA 支持 fs-verity 哈希机制作为完整文件哈希的替代方案。但是,强制所有 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(file) 。这将简单得多,而且速度也稍快一些。
诚然,内存中的 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()?
- 答:
对于大多数 Linux 文件系统开发人员来说,滥用 xattr 接口来执行基本上任意的系统调用是极不受欢迎的。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,或者没有进行任何更改。允许在崩溃后出现中间状态可能会导致问题。