内联加密¶
背景¶
内联加密硬件逻辑上位于内存和磁盘之间,可以在数据进出磁盘时进行加密/解密。对于每个 I/O 请求,软件可以精确控制内联加密硬件如何加密/解密数据,包括密钥、算法、数据单元大小(加密/解密的粒度)和数据单元编号(确定初始化向量的值)。
一些内联加密硬件直接在底层 I/O 请求中接受所有加密参数,包括原始密钥。但是,大多数内联加密硬件都有固定数量的“密钥槽”,并要求先将密钥、算法和数据单元大小编程到密钥槽中。每个底层 I/O 请求随后只包含一个密钥槽索引和数据单元编号。
请注意,内联加密硬件与通过内核加密 API 支持的传统加密加速器有很大不同。传统加密加速器对内存区域进行操作,而内联加密硬件对 I/O 请求进行操作。因此,内联加密硬件需要由块层管理,而不是内核加密 API。
内联加密硬件也与“自加密驱动器”非常不同,例如那些基于 TCG Opal 或 ATA 安全标准的驱动器。自加密驱动器不提供对加密的精细控制,并且无法验证生成的密文的正确性。内联加密硬件提供对加密的精细控制,包括为每个扇区选择密钥和初始化向量,并且可以进行正确性测试。
目标¶
我们希望在内核中支持内联加密。为了方便测试,我们也希望在没有实际内联加密硬件时支持回退到内核加密 API。我们还希望内联加密能够与设备映射器和回环等分层设备一起使用(即,我们希望能够使用底层设备(如果存在)的内联加密硬件,否则回退到加密 API 加密/解密)。
约束和注意事项¶
我们需要一种方法,让上层(例如文件系统)指定用于加密/解密 bio 的加密上下文,而设备驱动程序(例如 UFSHCD)需要在处理请求时能够使用该加密上下文。加密上下文还会对 bio 合并引入约束;块层需要了解这些约束。
不同的内联加密硬件具有不同的支持算法、支持的数据单元大小、最大数据单元编号等。我们将这些属性称为“加密能力”。我们需要一种方法,让设备驱动程序以通用方式向上层公布加密能力。
内联加密硬件通常(但不总是)要求在使用密钥之前将密钥编程到密钥槽中。由于编程密钥槽可能很慢,并且可能没有很多密钥槽,因此我们不应该为每个 I/O 请求都编程密钥,而是应该跟踪哪些密钥在密钥槽中,并在可能的情况下重复使用已编程的密钥槽。
上层通常为加密密钥定义特定的生命周期,例如当加密目录被锁定或加密映射被拆除时。在这些时候,密钥会从内存中擦除。我们必须提供一种方法,让上层也可以从它们所在的任何密钥槽中驱逐密钥。
在可能的情况下,设备映射器设备必须能够传递其底层设备的内联加密支持。但是,设备映射器设备本身拥有密钥槽是没有意义的。
基本设计¶
我们引入 struct blk_crypto_key
来表示内联加密密钥及其使用方式。这包括密钥的实际字节、密钥的大小、密钥将使用的算法和数据单元大小,以及表示密钥将使用的最大数据单元编号所需的字节数。
我们引入 struct bio_crypt_ctx
来表示加密上下文。它包含一个数据单元编号和一个指向 blk_crypto_key 的指针。我们向 struct bio
和 struct request
添加指向 bio_crypt_ctx 的指针;这允许块层的用户(例如文件系统)在创建 bio 时提供加密上下文,并将其传递到堆栈中,供块层和设备驱动程序处理。请注意,加密上下文没有明确说明是加密还是解密,因为这是从 bio 的方向隐式得出的;WRITE 表示加密,READ 表示解密。
我们还引入了 struct blk_crypto_profile
,其中包含特定内联加密设备的所有通用内联加密相关状态。blk_crypto_profile 作为内联加密硬件驱动程序公布其加密能力并向上层提供某些功能(例如,编程和驱逐密钥的功能)的方式。每个想要支持内联加密的设备驱动程序都将构建一个 blk_crypto_profile,然后将其与磁盘的 request_queue 相关联。
blk_crypto_profile 还在适用时管理硬件的密钥槽。这发生在块层中,以便块层的用户可以只指定加密上下文,而无需了解密钥槽,设备驱动程序也不需要关心密钥槽管理的大部分细节。
具体来说,对于每个密钥槽,块层(通过 blk_crypto_profile)会跟踪该密钥槽包含哪个 blk_crypto_key(如果有),以及有多少个正在进行的 I/O 请求正在使用它。当块层为具有加密上下文的 bio 创建一个 struct request
时,它会尽可能抓取一个已经包含密钥的密钥槽。否则,它会等待一个空闲密钥槽(一个未被任何 I/O 使用的密钥槽),然后使用设备驱动程序提供的函数将密钥编程到最近最少使用的空闲密钥槽中。在这两种情况下,生成的密钥槽都存储在请求的 crypt_keyslot
字段中,设备驱动程序可以在其中访问该密钥槽,并在请求完成后释放该密钥槽。
struct request
还包含一个指向原始 bio_crypt_ctx 的指针。请求可以从多个 bio 构建,块层在尝试合并 bio 和请求时必须考虑加密上下文。要合并两个 bio/请求,它们必须具有兼容的加密上下文:要么都未加密,要么都使用相同的密钥和连续的数据单元编号加密。仅保留请求中第一个 bio 的加密上下文,因为已经验证其余 bio 与第一个 bio 兼容合并。
为了使内联加密能够与基于 request_queue 的分层设备一起工作,当克隆请求时,其加密上下文也会被克隆。当提交克隆的请求时,它将像往常一样被处理;这包括在需要时从克隆的目标设备获取密钥槽。
blk-crypto-fallback¶
希望上层(例如文件系统)的内联加密支持可以在没有实际内联加密硬件的情况下进行测试,同样也希望块层的密钥槽管理逻辑可以进行测试。同样,希望允许上层始终使用内联加密,而不是必须以多种方式实现加密。
因此,我们还引入了 blk-crypto-fallback,它是使用内核加密 API 实现的内联加密。blk-crypto-fallback 构建在块层中,因此它可以在任何块设备上工作,而无需任何特殊设置。本质上,当将具有加密上下文的 bio 提交给不支持该加密上下文的 block_device 时,块层将使用 blk-crypto-fallback 处理 bio 的加密/解密。
对于加密,数据不能就地加密,因为调用者通常依赖于数据未被修改。相反,blk-crypto-fallback 会分配 bounce 页面,用这些 bounce 页面填充一个新的 bio,将数据加密到这些 bounce 页面中,并提交该“bounce” bio。当 bounce bio 完成时,blk-crypto-fallback 会完成原始 bio。如果原始 bio 太大,则可能需要多个 bounce bio;请参阅代码了解详细信息。
对于解密,blk-crypto-fallback 使用其自己的回调包装 bio 的完成回调 (bi_complete
) 和私有数据 (bi_private
),取消设置 bio 的加密上下文,然后提交 bio。如果读取成功完成,则 blk-crypto-fallback 恢复 bio 的原始完成回调和私有数据,然后使用内核加密 API 就地解密 bio 的数据。解密是从一个工作队列发生的,因为它可能会休眠。之后,blk-crypto-fallback 完成 bio。
在这两种情况下,blk-crypto-fallback 提交的 bio 都不再具有加密上下文。因此,下层只看到标准的未加密 I/O。
blk-crypto-fallback 还定义了自己的 blk_crypto_profile,并且有自己的“密钥槽”;其密钥槽包含 struct crypto_skcipher
对象。这样做的原因有两个。首先,它允许在没有实际内联加密硬件的情况下测试密钥槽管理逻辑。其次,与实际内联加密硬件类似,加密 API 不直接在请求中接受密钥,而是要求提前设置密钥,并且设置密钥的开销可能很高;此外,由于它会获取锁,因此根本无法在 I/O 路径上分配 crypto_skcipher。因此,密钥槽的概念对于 blk-crypto-fallback 仍然有意义。
请注意,无论使用真正的内联加密硬件还是 blk-crypto-fallback,写入磁盘的密文(以及因此产生的数据的磁盘格式)都将是相同的(假设内联加密硬件的实现和内核加密 API 中使用的算法实现都符合规范并正常工作)。
blk-crypto-fallback 是可选的,由 CONFIG_BLK_INLINE_ENCRYPTION_FALLBACK
内核配置选项控制。
呈现给块层用户的 API¶
blk_crypto_config_supported()
允许用户预先检查使用特定加密设置的内联加密是否能在特定的 block_device 上工作——通过硬件或通过 blk-crypto-fallback。此函数接收一个 struct blk_crypto_config
,它类似于 blk_crypto_key,但省略了密钥的实际字节,而仅包含算法、数据单元大小等。如果禁用 blk-crypto-fallback,此函数会很有用。
blk_crypto_init_key()
允许用户初始化一个 blk_crypto_key。
用户必须在开始在 block_device 上实际使用 blk_crypto_key 之前调用 blk_crypto_start_using_key()
(即使之前调用过 blk_crypto_config_supported()
)。这是为了初始化可能需要的 blk-crypto-fallback。这不能从数据路径调用,因为它可能需要分配资源,这在这种情况下可能会导致死锁。
接下来,要将加密上下文附加到 bio,用户应调用 bio_crypt_set_ctx()
。此函数分配一个 bio_crypt_ctx 并将其附加到 bio,给定 blk_crypto_key 和将用于加密/解密的数据单元编号。用户无需担心以后释放 bio_crypt_ctx,因为它会在 bio 被释放或重置时自动发生。
最后,当完成在 block_device 上使用 blk_crypto_key 进行内联加密时,用户必须调用 blk_crypto_evict_key()
。这可确保密钥从它可能被编程到的所有密钥槽中清除,并从它可能链接到的任何内核数据结构中取消链接。
总而言之,对于块层的用户,blk_crypto_key 的生命周期如下:
blk_crypto_config_supported()
(可选)blk_crypto_init_key()
blk_crypto_start_using_key()
bio_crypt_set_ctx()
(可能多次)blk_crypto_evict_key()
(在所有 I/O 完成后)清零 blk_crypto_key(没有专用函数)
如果 blk_crypto_key 用于多个 block_devices,则必须在每个 block_device 上调用 blk_crypto_config_supported()
(如果使用)、blk_crypto_start_using_key()
和 blk_crypto_evict_key()
。
呈现给设备驱动程序的 API¶
想要支持内联加密的设备驱动程序必须在其设备的 request_queue 中设置一个 blk_crypto_profile。为此,它首先必须调用 blk_crypto_profile_init()
(或其资源管理的变体 devm_blk_crypto_profile_init()
),提供密钥槽的数量。
接下来,它必须通过设置 blk_crypto_profile 中的字段来声明其加密功能,例如 modes_supported
和 max_dun_bytes_supported
。
然后,它必须在 blk_crypto_profile 的 ll_ops
字段中设置函数指针,以告知上层如何控制内联加密硬件,例如如何编程和清除密钥槽。大多数驱动程序需要实现 keyslot_program
和 keyslot_evict
。有关详细信息,请参阅 struct blk_crypto_ll_ops
的注释。
一旦驱动程序使用 request_queue 注册了 blk_crypto_profile,驱动程序通过该队列接收到的 I/O 请求可能具有加密上下文。所有加密上下文都将与 blk_crypto_profile 中声明的加密功能兼容,因此驱动程序无需担心处理不支持的请求。此外,如果在 blk_crypto_profile 中声明了非零数量的密钥槽,则所有具有加密上下文的 I/O 请求也将具有一个已使用相应密钥编程的密钥槽。
如果驱动程序实现了运行时挂起,并且其 blk_crypto_ll_ops 在设备运行时挂起时不起作用,则驱动程序还必须将 blk_crypto_profile 的 dev
字段设置为指向 struct device
,该设备将在调用任何底层操作之前恢复。
如果出现内联加密硬件丢失其密钥槽内容的情况(例如,设备重置),驱动程序必须处理重新编程密钥槽。为此,驱动程序可以调用 blk_crypto_reprogram_all_keys()
。
最后,如果驱动程序使用 blk_crypto_profile_init()
而不是 devm_blk_crypto_profile_init()
,那么当不再需要加密配置文件时,它负责调用 blk_crypto_profile_destroy()
。
分层设备¶
基于请求队列的分层设备(如 dm-rq)如果希望支持内联加密,则需要为其 request_queue 创建自己的 blk_crypto_profile,并公开他们选择的任何功能。当分层设备想要将该请求的克隆传递给另一个 request_queue 时,blk-crypto 将根据需要初始化并准备克隆。
内联加密和 blk 完整性之间的交互¶
在本文档发布之时,还没有真正的硬件支持这两个功能。但是,这些功能确实会相互影响,并且要使它们一起正常工作并非完全容易。特别是,当 WRITE bio 想要在支持这两个功能的设备上使用内联加密时,bio 将指定一个加密上下文,然后计算其完整性信息(使用明文数据,因为加密将在写入数据时发生),并将数据和完整性信息发送到设备。显然,必须先验证完整性信息,然后再加密数据。加密数据后,设备不得存储随明文数据一起接收到的完整性信息,因为这可能会泄露有关明文数据的信息。因此,它必须从密文数据重新生成完整性信息,并将其存储在磁盘上。存储明文数据的完整性信息的另一个问题是,它会根据是否存在硬件内联加密支持或使用内核加密 API 回退来更改磁盘上的格式(因为如果使用回退,设备将收到密文的完整性信息,而不是明文的完整性信息)。
由于目前还没有任何真正的硬件,因此假设硬件实现可能无法正确地将这两个功能一起实现,并且暂时禁止这种组合似乎是谨慎的。每当设备支持完整性时,内核都会假装设备不支持硬件内联加密(通过将设备 request_queue 中的 blk_crypto_profile 设置为 NULL)。当启用加密 API 回退时,这意味着所有具有加密上下文的 bio 都将使用回退,并且 IO 将照常完成。当禁用回退时,具有加密上下文的 bio 将失败。