Tink 使用密钥集来实现密钥轮替。从形式上讲,密钥集是一个非空密钥列表1,其中一个密钥被指定为主密钥(例如,用于对新明文进行签名和加密的密钥)。此外,密钥集中的密钥会获得一个唯一 ID 2 和密钥状态,从而可以停用密钥,而无需将其从密钥集中移除。
密钥集是用户访问密钥的主要方式(通过类 KeysetHandle)。这可确保每个用户都有代码来同时处理多个密钥。对于大多数密码学用户来说,处理多个密钥是必需的:需要能够更改密钥(例如,旧密钥可能会泄露),并且几乎不可能实现原子性的“切换到下一个密钥”,以应用于代码运行的机器和所有密文(全局范围内),并且在瞬间完成。因此,用户需要编写在从一个密钥更改为下一个密钥时有效的代码。
示例:AEAD
考虑使用 AEAD 密钥集,其中包含多个用于 AEAD 原语的密钥。如前所述,每个键都会唯一指定两个函数: \(\mathrm{Enc}\) 和 \(\mathrm{Dec}\)。密钥集现在还指定了两个新函数: \(\mathrm{Enc}\) 和 \(\mathrm{Dec}\) - \(\mathrm{Enc}\) 只是等于密钥集主密钥的函数 \(\mathrm{Enc}\) ,而函数 \(\mathrm{Dec}\) 尝试使用所有密钥进行解密,并按某种顺序遍历这些密钥(如需了解 Tink 如何提高此函数的性能,请参阅下文)。
值得注意的是,密钥集是完整密钥:它们是对所用函数 \(\mathrm{Enc}\) 和\(\mathrm{Dec}\) 的完整说明。这意味着,用户可以编写一个以 KeysetHandle 作为输入的类,以表达该类需要对象的完整说明 \(\mathrm{Enc}\) 和 \(\mathrm{Dec}\) 才能正常运行的想法。这使得用户能够编写 API 来传达以下信息:若要使用此类,您需要向我提供加密原语的说明。
密钥轮替
假设某位 Tink 用户编写了一个程序,该程序首先从 KMS 获取密钥集,然后从该密钥集创建 AEAD 对象,最后使用该对象加密和解密密文。
此类用户会自动为密钥轮替做好准备;如果他们当前选择的算法不再符合标准,系统会自动切换算法。
不过,在实现此类密钥轮替时,必须格外小心:首先,KMS 应向密钥集添加新密钥(但尚未将其设置为主密钥)。然后,需要将新的密钥集推广到所有二进制文件中,以便使用此密钥集的每个二进制文件都拥有密钥集中的最新密钥。只有这样,才能将新密钥设为主密钥,并再次将生成的密钥集分发给使用该密钥集的所有二进制文件。
密文中的密钥标识符
我们再来看一下 AEAD 密钥集的示例。如果以简单的方式进行,解密密文需要 Tink 尝试使用密钥集中的所有密钥进行解密,因为无法知道使用哪个密钥加密了密钥集。这可能会导致较大的性能开销。
因此,Tink 允许在密文前添加一个从 ID 派生的 5 字节字符串。根据上述“完整密钥”的理念,此前缀是密钥的一部分,并且使用此密钥派生的所有密文都应具有此前缀。用户创建密钥时,可以选择密钥是否应使用此类前缀,或者是否应使用不含此类前缀的密文格式。
当密钥位于密钥集内时,Tink 会根据密钥在密钥集中的 ID 计算此标记。ID 在密钥集内是唯一的2,这意味着标记是唯一的。因此,如果仅使用带标记的密钥,与使用单个密钥解密相比,性能不会下降:Tink 在解密时只需要尝试其中一个密钥。
不过,由于标记是密钥的一部分,这也意味着密钥只能在密钥集中,前提是它具有一个特定的 ID。这在以不同语言描述关键对象的实现时会产生一些影响。
需要 ID 但没有输出前缀的键
某些键必须具有特定 ID,但不会在其输出中添加任何前缀。例如,具有 NO_PREFIX_WITH_PREHASH_ID 变体的签名密钥(以输出前缀类型 WITH_ID_REQUIREMENT 存储)会生成不带前缀的签名。当您将此类密钥与 Prehash 原语搭配使用时,Tink 会将密钥 ID 写入 prehash 值,以便远程签名者知道要使用哪个密钥进行签名。
与使用前缀的密钥一样,此类密钥只能位于相应 ID 下的密钥集内。预哈希值中的密钥 ID 只是纯元数据,与输出前缀一样:签名不会绑定它,验证者也永远不会看到它。如需了解字节级布局,请参阅 Tink 线缆格式。