Zxcrypt

概览

zxcrypt 是一种块设备过滤器驱动程序,能够以透明方式对块设备中写入的数据进行加密,并对从块设备读取的数据进行解密。zxcrypt 设备使用的底层块设备几乎可以是任何块设备,包括原始磁盘、ramdisk、GPT 分区、FVM 分区,甚至是其他 zxcrypt 设备。唯一的限制是块大小应与页面对齐。绑定后,zxcrypt 设备将在设备树中发布另一个块设备,消费者可以正常与之交互。

用法

zxcrypt 包含驱动程序。libzxcrypt.so 提供用于管理 zxcrypt 设备的四个函数。每个键都接受一个或多个 zxcrypt_key_t 键,这些键将密钥数据、长度和槽位(如果有多个键)相关联。

  • zxcrypt_format 函数采用开放块设备,并写入必要的加密元数据使其成为 zxcrypt 设备。提供的 zxcrypt 密钥不会直接保护设备上的数据,而是用于保护数据密钥材料。
zx_status_t zxcrypt_format(int fd, const zxcrypt_key_t* key);
  • zxcrypt_bind 函数指示驱动程序读取加密的元数据并提取数据密钥材料,以在透明地转换 I/O 数据时使用。
zx_status_t zxcrypt_bind(int fd, const zxcrypt_key_t *key);
  • zxcrypt_rekey 函数使用旧密钥先读取加密的元数据,然后使用新密钥将其写回。
zx_status_t zxcrypt_rekey(int fd, const zxcrypt_key_t* old_key, const zxcrypt_key_t* new_key);
  • zxcrypt_shred 函数首先会验证调用方是否可以使用提供的密钥来读取加密的元数据,从而访问数据。如果成功,则销毁包含数据密钥材料的加密元数据。这样可以防止用户日后再访问这些数据。
zx_status_t zxcrypt_shred(int fd, const zxcrypt_key_t* key);

技术详情

DDKTL 驱动程序

zxcrypt 编写为 DDKTL 设备驱动程序。src/lib/ddktl 是一个 C++ 框架,用于在 Fuchsia 中编写驱动程序。它允许作者使用模板化 Mixin 自动提供 src/lib/ddk 函数指针和回调。

有两小段功能无法用 DDKTL 和 C++ 编写:

  • 驱动程序绑定逻辑,使用 DDK 的 binding.h 的 C 预处理器宏编写。
  • ulib/sync 的完成例程用于同步 I/O,与 C++ 原子不兼容。

工作器线程

设备会启动在设备运行期间运行的工作器线程,并为所有 I/O 请求创建流水线。每种类型都有其操作的 I/O 类型、将等待的传入请求 I/O 队列,以及数据加密。收到请求后,如果运算码与要查找的运算码匹配,它会先使用加密算法转换请求中的数据,然后再进行传递。

整个流水线如下所示:

DdkIotxnQueue -+
                \       Worker 1:        Underlying      Worker 2:        Original
    BlockRead ---+--->  Encrypter   --->   Block   --->  Decrypter  ---> Completion
                /     Acts on writes       Device      Acts on reads      Callback
   BlockWrite -+

“加密器”工作器会在每个 I/O 写入请求中的数据发送到底层块设备之前对其进行加密,而“解密器”工作器将在来自底层块设备的每个 I/O 读取响应中解密数据。密码必须具有至少 16 个字节的密钥长度,在语义上是安全的 (IND-CCA2),并将块偏移量纳入“调整”。目前使用的是 AES256-XTS

戒指和 Txns

为了确保数据的加密和解密对原始 I/O 请求者透明,工作器必须在转换数据时复制数据。通过流水线发送的 I/O 请求实际上不是原始请求,而是封装了原始请求的“影子”请求。

由于需要影子请求,因此 VMO 中的页面会按顺序分配影子请求。当工作器需要转换数据时,它会将来自封装的原始写入请求的数据加密到影子请求中,或者将影子请求中的数据解密为原始封装读取请求。一旦可以将原始请求交回给原始请求者,影子请求就会取消分配,其页面也会被停用。这样可以确保使用的内存不会超过待处理的 I/O 请求所需的内存。

超级块格式

用于加密和解密数据的密钥材料称为“数据密钥”,存储在设备的预留部分,称为 superblock。此超级块的存在至关重要;如果没有它,就无法在设备上重新创建数据键并恢复数据。因此,超级块被复制到设备上的多个位置以实现冗余。这些位置对 zxcrypt 块设备使用方不可见。每当 zxcrypt 驱动程序从某个位置成功读取并验证超级块时,它会将其复制到所有其他超级块位置,以帮助“自动修复”任何损坏的超级块位置。

超级块格式如下所示,每个字段依次描述:

+----------------+----------------+----+-----...-----+----...----+------...------+
| Type GUID      | Instance GUID  |Vers| Sealed Key  | Reserved  | HMAC          |
| 16 bytes       | 16 bytes       | 4B | Key size    |    ...    | Digest length |
+----------------+----------------+----+-----...-----+----...----+------...------+
  • 类型 GUID:将相应设备标识为 zxcrypt 设备。与 GPT 兼容。
  • Instance GUID:基于设备的标识符,用作 KDF 盐(如下所述)。
  • 版本:用于指明要使用的加密算法。
  • 密封密钥:数据密钥,由下文所述的封装密钥加密。
  • 预留:用于将超级块与块边界对齐的未使用的数据。
  • HMAC:到目前为止的超级块的加密摘要(包括“预留”字段)。

封装密钥、封装 IV 和 HMAC 密钥均衍生自 KDF。此 KDF 是 RFC 5869 HKDF,它结合了所提供的密钥、实例 GUID 的“盐”以及“每次使用”标签(例如“wrap”或“hmac”)。KDF 不会尝试执行任何速率限制。KDF 可以降低重复使用密钥的风险,因为新的随机实例盐会导致新的派生密钥。HMAC 可防止意外或恶意修改被检测出来,而不会泄露有关 zxcrypt 密钥的任何实用信息。

_注意:KDF 不会执行任何键拉伸。系统会假定攻击者可以移除设备并自行尝试进行密钥派生,从而绕过 HMAC 检查以及任何可能的速率限制。为了防止出现这种情况,zxcrypt 使用方应在派生其 zxcrypt 密钥时包含速率适当的设备密钥,例如来自 TPM 的设备密钥。

后续工作

可以、应该或必须做的进一步的工作有很多:

  • 表面隐藏绑定失败

    目前,即使设备无法初始化,zxcrypt_bind 也可能表示成功。在绑定逻辑运行时,zxcrypt 不会将设备同步添加到设备树。它必须执行 I/O 操作,并且不能阻止对 device_bind 的调用返回,因此它会生成一个初始化程序线程并在完成后添加设备。

    从 2017 年 10 月开始,这是 DDK 开发的一个活跃领域,该政策将更改为要求在退回设备之前添加设备,并可能在后续发出一个额外的发布调用调用。因此,可能需要让调用方同步对 zxcrypt_bind 的调用,直到设备准备就绪或明确绑定失败。

  • 使用 AEAD,而非 AES-XTS

    大家普遍认为 AEAD 会在解密数据之前验证其数据的完整性,从而提供卓越的加密保护。这是可取的,但需要额外的每个块的开销。这意味着使用方将需要使用非页面对齐块(在内嵌开销被移除后),或者 zxcrypt 需要存储外行开销并处理非原子化写入失败

  • 支持多个密钥

    为便于密钥托管和/或恢复,可以直接将超级块格式修改为具有一系列加密包。考虑到这一点,libzxcrypt API 接受可变数量的密钥,但目前支持的唯一长度是 1,唯一有效槽位是 0。

  • 调整工作器数量

    目前有一个加密器和一个解密器。这些线程旨在处理任意数量的线程,因此可能需要调整性能来找到最佳的工作器数量,以平衡 I/O 带宽和调度器抖动

  • 移除内部检查

    目前,zxcrypt 代码会检查内部边界的许多错误条件,如果不满足这些条件,则返回信息性错误。对于性能,那些仅由程序员错误(而不是来自请求者或底层设备的数据)出现的可被转换为在发布模式下跳过的“调试”断言。