ASP.NET Core 中的子密钥派生和已验证加密

密钥环中的大多数密钥将包含某种形式的熵,并且包含说明“CBC 模式加密 + HMAC 验证”或“GCM 加密 + 验证”的算法信息。 在这些情况下,我们将嵌入的熵称为该密钥的主密钥材料(或 KM),并且执行密钥派生功能来派生将用于实际加密操作的密钥。

注意

密钥是抽象的,并且自定义实现的行为可能不会如下所示。 如果密钥提供自己的 IAuthenticatedEncryptor 实现,而不是使用我们的内置工厂之一,则此部分中所述的机制将不再适用。

其他经过身份验证的数据和子密钥派生

IAuthenticatedEncryptor 接口充当所有已验证加密操作的核心接口。 它的 Encrypt 方法采用两个缓冲区:plaintext 和 additionalAuthenticatedData (AAD)。 plaintext 内容流不更改对 IDataProtector.Protect 的调用,但 AAD 由系统生成并包含三个组成部分:

  1. 标识此版数据保护系统的 32 位 magic 标头 09 F0 C9 F0。

  2. 128 位密钥 ID。

  3. 由目标链构成的可变长度字符串,前者创建了正在执行此操作的 IDataProtector

因为 AAD 对于这三个组成部分的元组都是唯一的,我们可以使用它从 KM 派生新密钥,而不是在所有加密操作中使用 KM 本身。 每次调用 IAuthenticatedEncryptor.Encrypt 时,都会发生以下密钥派生过程:

( K_E, K_H ) = SP800_108_CTR_HMACSHA512(K_M, AAD, contextHeader || keyModifier)

此处,我们将使用以下参数在计数器模式下调用 NIST SP800-108 KDF(请参阅 NIST SP800-108,第 5.1 节):

  • 密钥派生密钥 (KDK) = K_M

  • PRF = HMACSHA512

  • label = additionalAuthenticatedData

  • context = contextHeader || keyModifier

上下文标头的长度可变,实质上充当我们为其派生 K_EK_H 的算法的指纹。 密钥修饰符是每次调用 Encrypt 时随机生成的 128 位字符串,它以极大的可能确保 KE 和 KH 对于此特定身份验证加密操作是唯一的,即使 KDF 的所有其他输入都是常量也是如此。

对于 CBC 模式加密 + HMAC 验证操作,| K_E | 是对称分组加密密钥的长度,| K_H | 是 HMAC 例程的摘要大小。 对于 GCM 加密 + 验证操作,| K_H | = 0

CBC 模式加密 + HMAC 验证

通过上述机制生成 K_E 后,我们将生成一个随机初始化向量,并运行对称分组加密算法来加密纯文本。 然后,初始化向量和已加密文本通过使用密钥 K_H 初始化的 HMAC 例程运行,以生成 MAC。 此过程和返回值如下图所示。

CBC-mode process and return

output:= keyModifier || iv || E_cbc (K_E,iv,data) || HMAC(K_H, iv || E_cbc (K_E,iv,data))

注意

IDataProtector.Protect 实现将在输出前面追加 magic 标头和密钥 ID,然后将其返回给调用方。 因为 magic 标头和密钥 ID 是 AAD 的隐含部分,并且因为密钥修饰符作为输入馈送给 KDF,这意味着最终返回的有效负载的每个字节都由 MAC 进行身份验证。

Galois/Counter Mode 加密 + 验证

通过上述机制生成 K_E 后,我们将生成一个随机的 96 位 nonce,并运行对称分组加密算法来加密纯文本并生成 128 位身份验证标记。

GCM-mode process and return

output := keyModifier || nonce || E_gcm (K_E,nonce,data) || authTag

注意

尽管 GCM 本身支持 AAD 的概念,但我们仍然只将 AAD 馈送给原始 KDF,并选择将空字符串传递给 GCM 作为其 AAD 参数。 其原因有两个。 首先,为了支持灵活性,我们绝不希望将 K_M 直接用作加密密钥。 此外,GCM 对其输入施加了非常严格的唯一性要求。 对具有相同(密钥、nonce)对的两个或更多个不同输入数据集调用 GCM 加密例程的概率不得超过 2^32。 如果固定 K_E,就不能执行超过 2^32 次加密操作,否则就会违反 2^32 的限制。 这个操作数量看起来可能很多,但高流量的 Web 服务器可以在短短几天内处理 40 亿个请求,这完全是在这些密钥的正常生存期内。 为了遵守 2^32 概率限制,我们继续使用 128 位密钥修饰符和 96 位 nonce,从根本上扩展了任何给定 K_M 的可用操作计数。 为简化设计,我们在 CBC 和 GCM 操作之间共享 KDF 代码路径,并且由于已经考虑在 KDF 中使用 AAD,因此无需将其转发到 GCM 例程。