By Jonathan Corbet
June 3, 2019
fs-verity机制起源于Android项目,目的是希望把部分文件配置成read-only,并且让kernel检测到任何对这个文件的修改,甚至包括offline改动(例如有人把eMMC chip取下来用其他方法更改了里面的block,再焊回手机)。此前的多种fs-verity实现方案在社区这边收到一些反对意见,所以都没能合入kernel。2019年5月23日又有了一个最新版本的实现方案被提了出来,这次使用了一个更改过的user-space API,可能更有机会被合入Linux主干代码。
fs-verity会对文件生成一组hash值。hash值就能用来监测文件是否被改动过。目前的实现方案里,hash都存在一个Merkle tree里面,这样在文件被访问的时候能以最高效率来验证hash。这个Merkle tree本身也被hash以及签名过,这样每次有hash值被修改了也能第一时间被监测到(此时对文件的访问会被阻塞住)。实用场景就是在Android里面对一些非常重要的package进行保护,万一有攻击者能修改本地存储设备的话也能立刻报出警报。
此前fs-verity的其他实现方式在社区这边被搁置,都是因为大家对这个API的工作方式很有意见。为了要保护某个文件,user space就需要首先生成一个Merkle tree并签名,然后把这个tree追加到这个文件末尾,并跟对应的文件系统块的起始地址和block size要align。再利用一个ioctl()调用通知kernel来隐藏这个tree,这样这个文件看起来就比它的真实长度要短一点,然后kernel此后就可以利用这个tree来验证文件内容了。这个机制很明显跟一些文件系统管理各个文件结束位置的方式不兼容。内核开发者还抱怨说这个API事实上把fs-verity的内部实现细节暴露过多给user space了。总之,此前的版本本来希望被加入Linux 5.0的,最终没能成功。
这组新patch set把Merkle tree的建立过程挪到kernel里面了,相当于把tree的存储位置细节信息都隐藏起来了。这样user-space application如果想对某个文件打开fs-verity,就先用read-only模式打开这个文件,获取到file descriptor(事实上文件其实会被改变,因为加上了protection并且被设为read-only)。然后调用新加的FS_IOC_ENABLE_VERITY ioctl(),传入这样一个结构给kernel:
这里的version值必须要设为1,主要是为了今后万一fs-verity的实现细节有改动之后的兼容性而准备的。同样的,reserved必须全部设为0。hash_algorithm就是告诉kernel需要用哪种hash算法来对文件内容进行哈希处理。目前唯一有意义的值是FS_VERITY_HASH_ALG_SHA256和FS_VERITY_HASH_ALG_SHA512。block_size就是表示hash的block size,必须跟文件系统的block size要对的上。如果salt_size和salt_ptr也设置了,就意味着要对每个block进行hash处理前要先添加个salt值(对数据加盐,增加hash的逆向破解的难度)。还有可以用sig_ptr和sig_size来指定数字签名,这是个可选项,下面也会有详细介绍。
这个ioctl()调用会把整个文件全部read一遍,生成Merkle tree,然后根据所使用的文件系统的特点来决定把tree存放到哪里。如果文件很大,这个操作就会花费不少时间,这个过程中如果有fatal signal(例如被kill -9)的话会中断,文件本身不会被改动。如果这个目标文件当前被人打开并且是可以写入的模式的话,对它打开fs-verity就会失败。
ioctl如果成功了,这个文件就能支持fs-verity了,之后再用write模式打开此文件就会失败,哪怕文件权限属性里面允许此用户使用write权限打开这个文件也没有用。不过文件的部分metadata还是能够被改动的,也能被重命名或者删除。所有的读操作,如果度出来的数据跟存好的hash值对不上,那么读文件的操作也会fail(返回EIO)。不过如果user space很依赖fs-verity保护的话,它还是应该在打开文件之后主动用FS_IOC_MEASURE_VERITY这个ioctl()来验证fs-verity是打开的。此ioctl传入数据指针是这样的结构:
struct fsverity_digest {
__u16 digest_algorithm;
__u16 digest_size; /* input/output */
__u8 digest[];
};
假设文件已经用fs-verity保护起来了,hash summary信息就会填到这个结构里返回出来。User space可以用这个信息来验证digest数据是否符合预期。如果不这样测试一下的话,攻击者很可能把一个文件彻底替换成恶意文件并且同时使用相应的Merkle tree值。当然,digest也能被签名来保护起来,这样kernel就会在每次访问的时候验证签名确保安全。需要签名的就是这个结构:
struct fsverity_signed_digest {
char magic[8]; /* must be "FSVerity" */
__le16 digest_algorithm;
__le16 digest_size;
__u8 digest[];
};
这些digest摘要信息都可以用上述的FS_IOC_MEASURE_VERITY ioctl()来从kernel获取。因此,要想对一个fs-verity文件加上签名,一个可行的方法就是:创建这个文件,打开fs-verity不过此时不要使用签名,然后获取digest摘要信息,最后再用签名信息第二次创建这个文件并打开fs-verity。真实使用场景里面,通过fs-verity保护起来的文件(就像Andriod的package文件)通常都是生成文件系统镜像文件时就已经配好了相应的签名数据,因此上述的两步创建签名保护的方法一般用不上。
要想完成签名以及验证,还需要提供一组公钥来做签名验证。fs-verity子系统会创建一个新的keyring(称为.fs-verity),有相应权限的用户可以往这个keyring钥匙环里添加证书用于文件验证。当然,用于签名的秘钥绝对不能在目标系统(target system)上存放。假设攻击者没有办法通过其他方式拿到秘钥,那么通过公钥进行的验证就能确保文件没有被更改过。
目前的patch set提供了对ext4和F2FS文件系统的支持。相应的也提供了详细的文档介绍如何使用以及它的实现原理。有些kernel feature在添加时没有提供足够的document,fs-verity这方面表现的非常非常不错。
此前的各版patch set都引起了很多热火朝天的讨论。这次,似乎大家都表现的很安静,因此作者Eric Biggers都不得不在mailinglist里提醒大家是否有comment要说。除非有人提出反对意见,否则之后意味着这回fs-verity真的很有可能已经扫清了大家的担忧,进而可能会被合入Linux 5.3版本中。
全文完
LWN文章遵循CC BY-SA 4.0许可协议。
极度欢迎将文章分享到朋友圈长按下面二维码关注:Linux News搬运工,希望每周的深度文章以及开源社区的各种新近言论,能够让大家满意~