| RFC-0157:Fxfs 加密和多卷支持 | |
|---|---|
| 状态 | 已接受 |
| 区域 |
|
| 说明 | 介绍了 Fxfs 中的加密和多卷支持。 |
| 问题 | |
| Gerrit 更改 | |
| 作者 | |
| 审核人 | |
| 提交日期(年-月-日) | 2022-04-07 |
| 审核日期(年-月-日) | 2022-04-28 |
摘要
此 RFC 介绍了 Fxfs 中的加密和多卷支持。
设计初衷
Fuchsia 需要能够支持使用与不同用户的密码绑定的不同密钥加密的多个卷。
利益相关方
审核人: abarth@google.com、enoharemaien@google.com、palmer@google.com、jfsulliv@google.com、jsankey@google.com、zarvox@google.com
咨询对象: Fuchsia 的安全、存储和隐私团队。
社交化: 在开始此 RFC 流程之前,此设计已与存储团队和上述审核人进行传阅和审核。
设计
RFC-0136 中介绍了 Fxfs。
要求
应该可以创建、枚举和删除卷。
每个卷都应使用不同的密钥进行加密。
应加密对象元数据,例如文件名、大小和时间戳。
必须能够在没有密钥的情况下删除卷。
应该可以支持密钥轮替,而无需付出巨大的迁移工作量。
应该对卷的大小进行限制(尽管此设计留待将来完成)。
应该可以在没有密钥的情况下查询卷的大小。
应该能够防范基于块计数的指纹攻击。这种攻击是指,通过了解一组文件中若干文件的加密大小(向上舍入到最接近的块),可以确定该组文件是否存在于文件系统中。
职责范围以外
此设计不涵盖以下方面:
在设备(包括宿主设备和目标设备)之间双向传输加密图片 - 虽然能够这样做有助于调试,但在生产设备上不太可能实现,也没有先例。
crypt 服务的实现。此设计在其他地方介绍。
该设计应支持密钥轮替,但组件之间要使用的确切 API 不在此范围内。
概览
Fxfs 加密
Fxfs 内置加密将支持单独的文件密钥。文件通常只有一个加密密钥,但为了支持密钥轮替和文件克隆,该格式将支持每个文件使用多个密钥。密钥将由 Fxfs 将与之通信的加密服务进行封装和解封。
根父级和根存储区中的以下对象将采用某种形式的加密:
日志(存在于根父级存储区中)将具有使用流密码加密的子存储区中元数据的突变。此密码的密钥将按存储区设置。
子存储区的层文件(存在于根存储区中)将被加密。加密这些文件的机制将与所有其他文件相同。密钥将使用按存储区设置的密钥进行封装,并与其他未加密的存储区信息一起存储。
根父级和根存储区中的其他对象将不会加密,即超级块、与分配器相关的所有对象、支持根对象存储区(用于保存根存储区对象的元数据)的层文件以及包含有关子存储区基本信息的对象。这些都不构成用户数据。
最终结果是:
- 用户数据将被加密
- 所有元数据(包括文件名、文件大小、范围信息和目录信息)都将被加密。
某些信息将不会加密 :
- 卷中的文件数。
- 分配给卷的范围集。
对象 ID
在 Fxfs 中,对象 ID 目前以单调递增的方式分配,这可以用作侧信道。为了解决这个问题,对象 ID 的最低有效 32 位将使用 ff1 加密(在分配时)进行加密。密钥将在循环使用 32 位对象 ID 后轮替。密钥将以封装形式存储,并与其他未加密的存储区信息一起存储。每次密钥轮替时,对象 ID 的高 32 位将单调递增。
卷中的对象数和使用的空间也提供了一个 侧信道(基本上是通过 statvfs 提供的所有信息)。解决此问题不在此设计的范围内(Fxfs 中并非新增)。
密钥管理
密钥封装和解封由单独的“crypt”服务负责,该服务将提供类似以下协议的服务:
/// Designates the purpose of a key.
type KeyPurpose = flexible enum {
/// The key will be used to encrypt metadata.
METADATA = 1;
/// The key will be used to encrypt data.
DATA = 2;
};
protocol Crypt {
/// Creates a new wrapped key. `owner`, effectively a nonce, identifies the
/// owner (an object ID) of the key and must be supplied to `UnwrapKey`.
/// `metadata` indicates that the key will be used to encrypt metadata which
/// might influence the choice of wrapping key used by the service. Returns
/// the wrapping key ID used to wrap this key along with the wrapped and
/// unwrapped key. Errors:
/// ZX_ERR_INVALID_ARGS: purpose is not recognised.
/// ZX_ERR_BAD_STATE: the crypt service has not been correctly initialized
/// with a wrapping key for the specified purpose.
CreateKey(struct {
owner uint64;
purpose KeyPurpose;
}) -> (struct {
wrapping_key_id uint64;
wrapped_key bytes:48;
unwrapped_key bytes:32;
}) error zx.status;
/// Unwraps keys that are wrapped by the key identified by `wrapping_key_id`.
/// `owner` must be the same as that passed to `CreateKey`. This can fail due
/// to permission reasons or if an incorrect key is supplied.
UnwrapKey(struct {
wrapping_key_id uint64;
owner uint64;
key bytes:48;
}) -> (struct {
unwrapped_key bytes:32;
}) error zx.status;
};
wrapping_key_id的含义取决于 crypt 服务的实现者。Fxfs 不会对其值赋予任何意义。预计每个加密的 Fxfs 卷都会有一个单独的连接,这样,如果需要,服务器就可以在不同的进程中托管。
目前,将支持 256 位密钥(与 Zxcrypt 一致)。
预计 crypt 服务将使用 AEAD 来封装密钥,这意味着封装的密钥大小可能为 48 字节。
purpose用于区分元数据使用的密钥和数据使用的密钥,旨在方便密钥轮替(见下文)。
crypt 服务的具体实现和密钥管理政策不在此设计的范围内。
磁盘格式
每个文件都将包含类似以下内容:
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum EncryptionKeys {
None,
AES256XTS(WrappedKeys),
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct WrappedKeys {
/// The keys (wrapped). To support key rolling and clones, there can be more
/// than one key. Each of the keys is given an identifier. The identifier is
/// unique to the object. AES256-XTS requires a 512 bit key, which is made
/// of two 256 bit keys, one for the data and one for the tweak. Both those
/// keys are derived from the single 256 bit key we have here.
pub keys: Vec<(/* wrapping_key_id= */ u64, /* id= */ u64, [u8; 48])>,
}
每个范围都将包含类似以下内容:
pub enum ExtentValue {
/// Indicates a deleted extent; that is, the logical range described by the
/// extent key is considered to be deleted.
None,
/// The location of the extent and other related information. `key_id`
/// identifies which of the object's keys should be used. `checksums` hold
/// post-encryption checksums.
Some { device_offset: u64, checksums: Checksums, key_id: u64 },
}
数据块将使用 AES-XTS-256(与 Zxcrypt 使用的相同)进行加密。文件中的逻辑偏移量将用于调整。
元数据
对象存储区维护日志结构合并 (LSM) 树来保存其元数据,这些元数据将以与存储区中包含的文件相同的方式进行加密。用于层文件的密钥将使用设置为“metadata”的用途创建。
提交事务时,突变会写入日志。这些突变将应用于 LSM 树的内存层。一段时间后,内存层会刷新到持久层。对象元数据树的任何突变都需要在写入日志时进行加密。为了支持这一点,Fxfs 使用了一个新的突变:
EncryptedObjectStore(Box<[u8]>),
流密码(不适合使用 AES-XTS-256 等块密码)Chacha20 将用于加密这些突变。此密码的密钥将被封装并与其他未加密的存储区数据一起存储。
在重放时,密钥可能不可用,在这种情况下,这些突变将以加密形式保存在内存中。如果需要刷新内存中的数据(以释放日志中的空间),这些加密的突变将写入根存储区中的一个对象。
当密钥可用时,可以解密并应用加密的突变(可能在内存中或存在于上述文件中)。如需解密突变,需要密码流中的偏移量;该偏移量与存储区的未加密信息一起存储。
以下是添加到 StoreInfo(存储区的未加密信息)的新字段:
// The (wrapped) key that encrypted mutations should use.
mutations_key: Option<Box<[u8]>>,
// Mutations for the store are encrypted using a stream cipher. To decrypt the
// mutations, we need to know the offset in the cipher stream to start it.
mutations_cipher_offset: u64,
// If we have to flush the store whilst we do not have the key, we need to
// write the encrypted mutations to an object. This is the object ID of that
// file if it exists.
encrypted_mutations_object_id: u64,
卷删除
为了支持删除卷,分配元数据将包含拥有元数据的存储区 ID 的对象 ID:
pub struct AllocatorKey {
pub device_range: Range<u64>,
}
pub enum AllocationRefCount {
// Tombstone variant indicating an extent is no longer allocated.
None,
// Used when we know there are no possible allocations below us in the stack.
// (e.g. on the first allocation of an extent.)
// This variant also tracks the owning ObjectStore for the extent.
Abs { count: u64, owner_object_id: u64 },
}
pub struct AllocatorValue {
/// Reference count for the allocated range.
/// (A zero reference count is treated as a 'tombstone', indicating that older
/// values in the LSM Tree for this range should be ignored).
pub refs: AllocationRefCount,
}
删除卷将涉及更改分配器,使其将属于已删除卷的所有记录视为可用。在进行主要压缩后,我们可以确定这些记录不再存在,然后就可以忘记该卷的存在。分配器会将其元数据的其余部分与已删除卷的列表相关信息一起保存。
内嵌数据
Fxfs 目前不支持内嵌数据,但如果支持,它将使用与加密元数据相同的密钥进行加密。随着新层文件的写入,这些密钥实际上会轮替。
安全擦除
安全擦除文件需要确保即使密钥(硬件中的密钥除外)随后被破解,文件仍然无法恢复。鉴于文件系统通常在闪存设备(采用垃圾回收)上运行,因此擦除所有出现的数据并非易事。唯一可行的解决方案是轮替封装密钥(不应存储在闪存上)。
安全擦除不是计划实现的功能,但应该可以通过以下过程实现:
开始使用新的封装密钥封装新的元数据密钥。
Fxfs 使用旧的元数据密钥重写所有对象。
旧的元数据封装密钥被销毁(通常直接或间接使用 TPM 功能 - 这不在此设计的范围内)。
在 Fxfs 中,通过执行主要压缩可以相对轻松地完成第 2 步。由于这是自然发生的事情,因此这是一个可以定期执行以及按需执行的过程。可以安排 Fxfs 保证每周执行一次主要压缩,但这取决于密钥是否可用。
此过程要求使用与数据不同的封装密钥来封装元数据密钥(否则会强制重写所有数据,这是禁止的),因此需要向 create_key 方法提供元数据参数。
请注意,该过程需要重写卷的所有元数据,因此不应频繁执行。
密钥轮替
上面概述了元数据封装密钥的密钥轮替,以实现安全擦除。使用以下过程应该可以轮替用于封装数据密钥的密钥:
开始使用新的封装密钥封装新密钥。为此类封装的密钥返回新的 wrapping_key_id。
要求 Fxfs 重新封装与给定 wrapping_key_id 匹配的所有密钥。此 API 留待后续设计:它不应需要更改磁盘格式。
按照安全擦除过程封装元数据密钥。由于密钥只会写入元数据文件,因此一旦旧的封装密钥被销毁,它应该确保数据封装密钥成功轮替。
请注意,第 2 步和第 3 步可以合并 - 应该可以同时执行主要压缩和重新封装密钥。
与安全擦除一样,密钥轮替支持最初不打算实现。
Fsck
没有密钥的 Fsck 将执行一组有限的检查。显然无法检查加密存储区的一致性,但可以检查范围和所有其他未加密存储区的一致性。
如果密钥可用,则可以执行完整的一致性检查,也可以仅检查单个卷的元数据。
多卷支持
fshost 将导出新目录:volumes。目录中的节点将表示由 fshost 导出的卷,并将支持新的卷协议(节点上将不支持任何其他协议,即它们将不支持 fuchsia.io 节点协议,因此无法克隆)。该协议将类似于:
type MountOptions = table {
// A handle to the crypt service. Unencrypted volumes will ignore this option.
crypt client_end:Crypt;
}
protocol Volume {
// Mounts the volume and starts serving the filesystem. An error will be
// returned if the volume is currently being served from a previous call
// to Mount.
Mount(resource struct {
export_root server_end:Directory;
options MountOptions;
}) -> () error zx.status;
}
如果提供的 crypt 服务不正确,装载将失败,但这应该是异常情况;用户应调用 Mount,并期望 Mount 成功;不应将其用作测试凭据的方法。
导出根目录看起来与现在文件系统公开的根目录一样。fs.Admin 服务将被公开,并且可以使用 Shutdown 方法锁定/关闭卷。如果关闭与卷的所有连接,卷也会被锁定。当卷被锁定时,我们将谨慎确保所有未封装的密钥都被丢弃。
枚举和移除将通过 volumes 目录上的 fuchsia.io Directory 协议完成。移除可能是异步的,也可能不是异步的,但在保证卷最终将被移除之前,不会返回成功。名称可以立即重复使用,但空间可能需要一段时间才能可供使用。
新卷无法使用 Directory 协议的 open 方法,因为我们需要提供 crypt 服务和其他选项。相反,我们将添加一个新协议:
type CreateVolumeOptions = table {
// Reserved for future use.
}
protocol Manager {
// Creates a new data volume.
CreateVolume(resource struct {
name string:MAX_FILENAME;
crypt client_end:Crypt;
export_root server_end: Directory;
options CreateVolumeOptions;
}) -> () error zx.status;;
}
最初,实现将假定 Fxfs 将管理所有这些卷,但将来应该可以提供一个与 Zxcrypt 支持的文件系统配合使用的组件。
卷上可能存在的卷的名称和确切集合将基于产品决策,不在此 RFC 的范围内。
实现
对多个卷和加密的支持将以典型方式实现。
目前没有计划支持密钥轮替、安全擦除和内嵌数据。
AES-XTS-256、ChaCha20 和 ff1 加密将由第三方 crate 提供。
需要进行安全审计。
性能
加密将对性能产生影响。现有文件系统基准将用于评估性能。实现后,可以消除 Zxcrypt,这应该可以抵消因实现而造成的任何损失。我们将调查当前性能的任何回归。
向后兼容性
这将是对 Fxfs 的重大更改,需要重新格式化。
安全注意事项
此 RFC 已由安全团队审核。
以下要求由安全性驱动:
每个卷都应使用不同的密钥进行加密。
应加密文件数据和元数据。
应该难以将对象 ID 用作侧信道。
实现将需要进行安全审核。
隐私注意事项
此 RFC 已由隐私团队审核。
主要的隐私注意事项是,以下数据将不会加密 :
- 卷中的文件数。
- 分配给卷的范围集(包括分配给卷的空间量)。
- 卷的名称。
所有其他数据和元数据都应加密。这应该足以满足包括指纹攻击在内的要求。
实现的安全性审核应验证设计是否满足这些要求。
测试
这将使用单元测试、集成测试和端到端测试的常用组合进行测试。Fxfs 将用于在 CQ 下运行的许多测试的数据分区,因此将通过这种方式获得曝光。
文档
在某个阶段,需要提供文档来帮助系统设计人员为给定产品选择不同的存储选项。 但是,该配置的进行方式尚未确定,不在此 RFC 的范围内。
此 RFC 中引入的 API 最初(很可能无限期)将位于树内,并且暂时将作为 FIDL 的一部分进行记录。
缺点、替代方案和未知事项
此 RFC 中概述的加密设计比 Zxcrypt 使用的基于分区的加密要复杂得多。
系统设计人员仍然可以使用基于分区的加密,但这会限制卷之间的空间共享:基于分区的方案如果需要灵活使用空间,则需要卷管理器(因此会因额外的间接寻址而导致性能下降),并且可能会出现碎片问题(在分区之间移动空间可能需要碎片整理)。在基于分区的方案中,支持安全擦除和密钥轮替也困难得多。
在先技术和参考文档
Zxcrypt 使用 AES-256 XTS 以与此 RFC 类似的方式加密块,但使用的调整有所不同。
此设计与 Android 12 的加密 要求基本兼容。一个显著的区别是,通过 Fxfs 日志的元数据需要使用流密码进行加密,而流密码未明确提及为可接受。