ASP.NET Core 中的上下文标头

背景信息和理论

在数据保护系统中,“密钥”是指可提供经过身份验证的加密服务的对象。 每个密钥由唯一 ID (GUID) 进行标识,带有算法信息和熵材料。 它旨在使每个密钥具有唯一熵,但系统无法实施这一点,并且我们还需要考虑到这种开发人员,他们可能会修改密钥环中现有密钥的算法信息来手动更改密钥环。 为了在这些情况下满足系统要求,数据保护系统有加密敏捷性的概念,这使得可跨多个加密算法使用一个熵值。

支持加密灵活性的大多数系统通过包含有效负载中有关算法的一些标识信息来实现这一点。 算法的 OID 通常很适合用于此操作。 但是,我们遇到了一个问题,那就是有很多方法来指定同一算法:AES (CNG) 以及托管的 Aes、AesManagedAesCryptoServiceProvider、AesCng 和 RijndaelManaged(给定特定参数)类实际上都是相同的,我们需要维护所有这些内容到正确的 OID 的映射。 如果开发人员想要提供自定义算法(或者甚至提供 AES 的另一个实现!),他们必须将其 OID 告知我们。 这个额外的注册步骤使系统配置特别麻烦。

回忆一下,我们当时认为我们从错误的方向处理了这个问题。 OID 指出了算法是什么,但我们实际上并不关心这一点。 如果我们需要在两种不同的算法中安全地使用单个熵值,那么我们不需要知道实际上采用哪些算法。 我们实际上关心的是它们的行为方式。 任何适当对称分组加密算法也是强伪随机排列 (PRP):修复输入(密钥、链模式、IV、纯文本),并且在给定相同输入的情况下,密文输出将极大可能与任何其他对称分组加密算法不同。 同样,任何适当带密钥的哈希函数也是强伪随机函数 (PRF),并且在提供固定输入集的情况下,其输出将与任何其他带密钥的哈希函数极大不同。

我们使用强 PRP 和 PRF 这一概念来构建上下文标头。 该上下文标头实质上是任何给定操作中使用的算法上的稳定指纹,它提供数据保护系统所需的加密灵活性。 此标头可重现,稍后用作子密钥派生过程的一部分。 有两种不同的方法来生成上下文标头,具体取决于基础算法的操作模式。

CBC 模式加密 + HMAC 身份验证

上下文标头由以下部分组成:

  • [16 位] 值 00 00,是一个表示“CBC 加密 + HMAC 身份验证”的标记。

  • [32 位] 对称分组加密算法的密钥长度(以字节为单位,大端)。

  • [32 位] 对称分组加密算法的块大大小(以字节为单位,大端)。

  • [32 位] HMAC 算法的密钥长度(以字节为单位,大端)。 (当前,密钥大小始终与摘要大小匹配。)

  • [32 位] HMAC 算法的摘要大小(以字节为单位,大端)。

  • EncCBC(K_E, IV, ""),这是给定空字符串输入和 IV 是全零向量的情况下,对称分组加密算法的输出。 K_E 的构造如下所述。

  • MAC(K_H, ""),这是给定空字符串输入的情况下 HMAC 算法的输出。 K_H 的构造如下所述。

理想情况下,我们可以为 K_EK_H 传递全零向量。 但是,我们希望避免这样的情况:基础算法在执行任何操作(尤其是 DES 和 3DES)之前检查是否存在弱密钥,这避免了使用简单或可重复的模式(例如全零向量)。

相反,我们在计数器模式下使用 NIST SP800-108 KDF(请参阅 NIST SP800-108 第 5.1 部分),将长度为零的密钥、标签、上下文和 HMACSHA512 作为基础 PRF。 我们会派生 | K_E | + | K_H | 字节的输出,然后将结果分解为 K_EK_H 本身。 从数学上来说,此内容表示如下。

( K_E || K_H ) = SP800_108_CTR(prf = HMACSHA512, key = "", label = "", context = "")

示例:AES-192-CBC + HMACSHA256

例如,假设对称分组加密算法是 AES-192-CBC,验证算法是 HMACSHA256。 系统将按照以下步骤生成上下文标头。

首先,设置 ( K_E || K_H ) = SP800_108_CTR(prf = HMACSHA512, key = "", label = "", context = ""),其中在每个指定算法中,| K_E | = 192 bits| K_H | = 256 bits。 这导致在下例中,K_E = 5BB6..21DDK_H = A04A..00A9

5B B6 C9 83 13 78 22 1D 8E 10 73 CA CF 65 8E B0
61 62 42 71 CB 83 21 DD A0 4A 05 00 5B AB C0 A2
49 6F A5 61 E3 E2 49 87 AA 63 55 CD 74 0A DA C4
B7 92 3D BF 59 90 00 A9

然后,在给定 Enc_CBC (K_E, IV, "")IV = 0* 的情况下,计算 AES-192-CBC 的 K_E,如上所述。

result := F474B1872B3B53E4721DE19C0841DB6F

接下来,在给定 K_H 的情况下,计算 HMACSHA256 的 MAC(K_H, ""),如上所述。

result := D4791184B996092EE1202F36E8608FA8FBD98ABDFF5402F264B1D7211536220C

这会生成下面的完整上下文标头:

00 00 00 00 00 18 00 00 00 10 00 00 00 20 00 00
00 20 F4 74 B1 87 2B 3B 53 E4 72 1D E1 9C 08 41
DB 6F D4 79 11 84 B9 96 09 2E E1 20 2F 36 E8 60
8F A8 FB D9 8A BD FF 54 02 F2 64 B1 D7 21 15 36
22 0C

该上下文标头是经过身份验证的加密算法对(AES-192-CBC 加密 + HMACSHA256 验证)的指纹。 如上所述,由以下部分组成:

  • 标记 (00 00)

  • 分组加密密钥长度 (00 00 00 18)

  • 分组加密块大小 (00 00 00 10)

  • HMAC 密钥长度 (00 00 00 20)

  • HMAC 摘要大小 (00 00 00 20)

  • 分组加密 PRP 输出 (F4 74 - DB 6F)

  • HMAC PRF 输出 (D4 79 - end)

注意

无论算法实现是 Windows CNG 提供的,还是由托管的 SymmetricAlgorithm 和 KeyedHashAlgorithm 类型提供的,都按相同方式生成 CBC 模式加密 + HMAC 身份验证上下文标头。 这使得在不同操作系统上运行的应用程序能够可靠地生成相同的上下文标头,即使不同操作系统的算法实现不同也是这样。 (实际上,KeyedHashAlgorithm 不必是适当的 HMAC。它可以是任何键控哈希算法类型。)

示例:3DES-192-CBC + HMACSHA1

首先,设置 ( K_E || K_H ) = SP800_108_CTR(prf = HMACSHA512, key = "", label = "", context = ""),其中在每个指定算法中,| K_E | = 192 bits| K_H | = 160 bits。 这导致在下例中,K_E = A219..E2BBK_H = DC4A..B464

A2 19 60 2F 83 A9 13 EA B0 61 3A 39 B8 A6 7E 22
61 D9 F8 6C 10 51 E2 BB DC 4A 00 D7 03 A2 48 3E
D1 F7 5A 34 EB 28 3E D7 D4 67 B4 64

然后,在给定 Enc_CBC (K_E, IV, "")IV = 0* 的情况下,计算 3DES-192-CBC 的 K_E,如上所述。

result := ABB100F81E53E10E

接下来,在给定 K_H 的情况下,计算 HMACSHA1 的 MAC(K_H, ""),如上所述。

result := 76EB189B35CF03461DDF877CD9F4B1B4D63A7555

这会生成完整上下文标头,该标头是经过身份验证的加密算法对(3DES-192-CBC 加密 + HMACSHA1 验证)的指纹,如下所示:

00 00 00 00 00 18 00 00 00 08 00 00 00 14 00 00
00 14 AB B1 00 F8 1E 53 E1 0E 76 EB 18 9B 35 CF
03 46 1D DF 87 7C D9 F4 B1 B4 D6 3A 75 55

组成部分如下划分:

  • 标记 (00 00)

  • 分组加密密钥长度 (00 00 00 18)

  • 分组加密块大小 (00 00 00 08)

  • HMAC 密钥长度 (00 00 00 14)

  • HMAC 摘要大小 (00 00 00 14)

  • 分组加密 PRP 输出 (AB B1 - E1 0E)

  • HMAC PRF 输出 (76 EB - end)

Galois/计数器模式加密 + 身份验证

上下文标头由以下部分组成:

  • [16 位] 值 00 01,是一个表示“GCM 加密 + 身份验证”的标记。

  • [32 位] 对称分组加密算法的密钥长度(以字节为单位,大端)。

  • [32 位] 在经过身份验证的加密操作期间使用的 nonce 大小(以字节为单位,大端)。 (对于我们的系统,这固定在 nonce 大小 = 96 位。)

  • [32 位] 对称分组加密算法的块大大小(以字节为单位,大端)。 (对于 GCM,这固定在块大小 = 128 位。)

  • [32 位] 由经过身份验证的加密函数生成的身份验证标记大小(以字节为单位,大端)。 (对于我们的系统,这固定在标记大小 = 128 位。)

  • [128 位] Enc_GCM (K_E, nonce, "") 的标记,这是给定空字符串输入和 nonce 是 96 位全零向量的情况下,对称分组加密算法的输出。

K_E 是使用与 CBC 加密 + HMAC 身份验证方案中相同的机制派生的。 但是,由于此处没有任何 K_H 发挥作用,我们实质上具有 | K_H | = 0,并且算法折叠成以下形式。

K_E = SP800_108_CTR(prf = HMACSHA512, key = "", label = "", context = "")

示例:AES-256-GCM

首先,设置 K_E = SP800_108_CTR(prf = HMACSHA512, key = "", label = "", context = ""),其中 | K_E | = 256 bits

K_E := 22BC6F1B171C08C4AE2F27444AF8FC8B3087A90006CAEA91FDCFB47C1B8733B8

接下来,在给定 nonce = 096K_E 的情况下,计算 AES-256-GCM 的 Enc_GCM (K_E, nonce, "") 的身份验证标记,如上所示。

result := E7DCCE66DF855A323A6BB7BD7A59BE45

这会生成下面的完整上下文标头:

00 01 00 00 00 20 00 00 00 0C 00 00 00 10 00 00
00 10 E7 DC CE 66 DF 85 5A 32 3A 6B B7 BD 7A 59
BE 45

组成部分如下划分:

  • 标记 (00 01)

  • 分组加密密钥长度 (00 00 00 20)

  • nonce 大小 (00 00 00 0C)

  • 分组加密块大小 (00 00 00 10)

  • 身份验证标记大小 (00 00 00 10)

  • 从运行分组加密得到的身份验证标记 (E7 DC - end)