本文深入探讨了密钥派生函数(KDFs)在密码学应用中的重要性,并着重指出了KDFs使用中常见的误用情况,同时阐述了密钥派生的最佳实践,详细讨论了诸如HKDF的使用、盐值处理、以及如何组合多个密钥源等问题。此外,文章还介绍了如何安全地组合多个密钥,以应对量子计算带来的潜在威胁,并强调在选择KDF时应根据具体需求选择合适的工具,避免安全漏洞。
密钥派生在许多密码学应用中至关重要,包括密钥交换、密钥管理、安全通信以及构建强大的密码学原语。但它也很容易出错:虽然针对不同的密钥派生需求存在标准工具,但我们的审计经常发现不正确地使用这些工具,这可能会损害密钥的安全性。Flickr 的 API 签名伪造漏洞就是一个在密钥派生过程中误用哈希函数的著名例子。
这些误用表明可能对密钥派生函数 (KDF) 存在误解。本文涵盖了使用 KDF 的最佳实践,包括需要仔细处理密钥派生以实现所需安全属性的特定场景。在此过程中,我们提供了一些建议来回答常见问题,例如:
在深入探讨密钥派生的最佳实践之前,我们将回顾一些重要的概念,以帮助我们更好地理解它们。
密钥密码学原语(例如 AEAD)需要满足某些要求的密钥材料,以保证安全性。在大多数情况下,原语要求密钥是以一致的随机方式生成,或者在密码学上接近一致的随机。我们将区分四种类型的密钥材料:
图 1:各种密钥集合(由 AI 生成)
上面的最后一个类别与当前抗量子密码学的发展尤其相关。结合经典密钥交换和后量子密钥交换的混合密钥交换协议旨在防止先存储后解密攻击。
密钥派生是从某些初始密钥材料 (IKM) 生成可接受的用于密码学的密钥材料的过程。从密码学的角度来看,“可接受”通常意味着从所有可能的密钥集合中一致地随机选择,或者与真正的随机密钥无法区分。根据初始密钥材料的性质,主要有两个密钥派生任务。
这种分类方式深受广泛使用的 KDF 算法 HKDF 的影响;其他 KDF 设计不一定遵循相同的原则。然而,提取和扩展很好地反映在大多数 KDF 应用中。此外,我们将考虑与密钥材料的复杂来源(例如一组来源)相关的其他 KDF 任务。
提示:如果你喜欢 HKDF 的直观演示,请参阅下面的动画。
HKDF 旨在同时提供提取和扩展。HKDF 通常可以通过 API 供应用程序访问,例如HKDF(ikm, salt, info, key_len)
。但是,在底层,会发生以下情况:首先,提取过程从 IKM 和 salt 生成一个伪随机密钥 (PRK) prk = HKDF.Extract(ikm, salt) = HMAC(salt, ikm)
。然后,生成长度为 key_len 的子密钥:sub_key = feedback[HMAC](prk, info)
。在这里,feedback[HMAC]
是HMAC
的一个包装器,它通过重复调用HMAC
来生成所需长度的输出;换句话说,它实现了一个可变长度的伪随机函数。对于给定的密钥,feedback
将为每个新的info
输入返回所需长度的随机位字符串;固定的info
值将始终产生相同的输出。如果info
保持不变但长度可变,则较小的输出将是较长输出的前缀。
图 2:可视化 KDF 的提取和扩展阶段
关于提取 salt:HKDF 的提取阶段可以选择使用 salt。提取 salt 是一个随机的、非秘密的值,用于从密钥材料中提取足够的随机性。至关重要的是,salt 不能由攻击者控制,因为这可能会导致 KDF 的灾难性后果。Hugo Krawczyk 提供了一个理论示例,说明了攻击者控制的 salt 如何破坏 salt 和 IKM 之间的独立性,从而导致提取器构造较弱。然而,正如我们在下一节中讨论的那样,其后果也可能具有实际意义。许多应用程序(例如,经过身份验证的密钥交换除外)的一个典型痛点是对 salt 进行身份验证。因此,HKDF 标准 建议大多数应用程序使用常量,例如全零字节字符串。不使用 salt 的代价是对 HMAC 做出稍微更强的假设,尽管这些假设仍然是合理的。
开发人员在选择 KDF 时必须考虑几个问题,但对 KDF 的误解可能会导致引入安全问题的选择。下面,我们提供了一些误用示例以及最佳实践,以帮助避免不正确地使用 KDF。
借助前面提到的 KDF 抽象,子密钥生成更适合随机性扩展。给定一个伪随机密钥(可能在提取步骤之后获得),可以使用随机性扩展通过每个子密钥的唯一信息输入来获得子密钥。Salt 用于提取。此外,如上所述,攻击者控制的 salt 可能会对安全性造成损害。考虑一个按需生成用户密钥的密钥管理应用程序。一种实现方式可能决定使用用户名作为 salt 从主密钥派生密钥。除了可以自由选择用户名之外,用户还可以提供一个上下文字符串(例如,“file-encryption-key”),该字符串指示密钥的用途并确保不同的应用程序使用独立的密钥。核心功能如下面的代码片段所示:
## 对于每个子密钥
def generate_user_key(username, purpose, key_len):
ikm = fetch_master_key_from_kms()
sub_key = hkdf(ikm=ikm, salt=username, info=purpose, key_len=key_len)
图 3:密钥管理应用程序使用主密钥按需派生密钥
这种构造很糟糕:由于 salt 用作 HMAC 提取的“密钥”,因此首先通过 PAD-or-HASH 方案(密钥填充、密钥哈希)对其进行预处理,以处理可变长度的密钥。在这种实现中,如果你的用户名是b”A”*65
,并且我选择我的用户名是sha256(b”A”*65)
,那么我将获得你的所有密钥!
那么我们应该怎么做呢?首先要避免的是潜在的攻击者控制的 salt。在上面的例子中,应用程序可以在初始化时生成一个随机 salt,并根据需要从受信任的地方检索它。或者,应用程序也可以使用常量 salt,例如全零字节字符串,如 RFC 5869 建议的那样。值得注意的是,对于 HMAC,如果ikm
已经是一个一致的随机密钥,那么使用常量不需要更强的假设。最后,如果 IKM 最初是一个随机密钥,并且用户名被限制为我们对双 PRF 的讨论中描述的一组值,也可以避免这个问题。
应用程序必须确保为每个新子密钥使用唯一的info
值。最好在info
值中包含尽可能多的上下文信息,例如会话标识符或transcript哈希值。将上下文编码到info
中必须是单射的,例如,通过注意规范化问题。
我们经常遇到在info
参数中包含额外随机性以生成子密钥的实现。希望是使 HKDF 在某种程度上更具随机性。
## 对于每个子密钥
extra_randomness = random(32)
sub_key = hkdf(ikm=ikm, salt=salt, info=concat(info, extra_randomness), key_len=key_len)
图 4:使用额外的随机性来派生子密钥
虽然这没有坏处,但对随机性提取的初始任务也没有太大帮助。请注意,额外的随机性仅影响随机性扩展。考虑以下思想实验:如果 IKM 没有足够的熵,或者 HMAC 结果是一个非常糟糕的随机性提取器,那么额外的随机性将无助于创建一个适合在随机性扩展期间使用的密钥。用于随机性扩展的远非随机的密钥会偏离安全要求,因此不提供安全保证。从上面的讨论中,假设 HKDF 是安全的,如果ikm
具有足够的随机性,我们将为其提取一个随机密钥。然后,扩展将确保 sub_key 与相同长度的随机密钥无法区分。此外,HKDF 不需要信息材料是秘密的;它只需要对每个子密钥都是唯一的。
但是,应用程序可以使用额外的随机性来进一步保证 info 输入的唯一性。除非你对额外的随机性做一些有趣的事情,否则使用它不会变得更糟。
不。HKDF 仅包含几个 HMAC 调用。密码破解者可以相当有效地破解大量用于 KDF 的密码,这些 KDF 并非专门设计为速度慢且占用大量内存。最好使用缓慢的、内存硬算法,例如 Argon2,用于哈希和从密码派生密钥。此外,最好避免使用密码哈希作为密钥来加密数据。最好创建密钥层次结构(例如密钥加密密钥),使用密码哈希来加密随机生成的密钥,然后可以根据需要从中派生更多密钥。
哈希函数不应用于 KDF 中的通用目的。在密钥派生期间使用的信息由攻击者控制的情况下,使用哈希函数作为 KDF 可能会使应用程序暴露于长度扩展攻击。对于从秘密与用户提供的数据相结合生成随机性的应用程序来说,这些攻击是一个主要问题(例如在 Flickr 的 API 签名伪造漏洞中)。相反,最好使用专门为密钥派生设计的 HKDF 和其他 KDF。虽然在特定情况下将哈希作为 KDF 是可以接受的,但我们告诫不要这样做,除非用户可以合理地在形式上论证其应用程序的用法。如果你的应用程序确实遭受了一两次额外的压缩函数调用的困扰,如果你没有充分的理由使用现有的 KDF,请咨询专家。此建议也适用于其他临时构造,例如 YOLO 构造。
AEAD(以及大多数其他密钥对称算法)的安全协议需要适当长度的一致随机位字符串,以提供有意义的安全保证。DH 输出是高熵的,但通常不是一致的位字符串。因此,将它们用作密钥会偏离安全协议。某些实现可能允许毫无戒心的用户为给定的原语使用错误的密钥材料(例如,将 DH 输出馈送到 Chacha20 密码中)。这种用法违反了 AEAD 构造的要求。
密码学中的一项常见任务是组合原语的两个实例,以便整体构造与最强的构造一样强大。当然,这是一个与密钥派生高度相关的问题:我们可以从一组密钥材料派生出一个秘密,以便只要其中一个密钥材料是安全的,那么整体秘密就是安全的吗?混合密钥交换协议是目前这种技术的相关用例。这些协议结合了通过经典密钥交换原语和后量子密钥交换原语建立的密钥,以防止攻击者今天收集加密的通信,并希望一旦有能力的量子计算机可用就对其进行解密。此类协议包括 PQXDH、Apple 的 PQ3 和后量子 Noise。但是,密钥组合广泛用于与量子威胁无关的其他环境中,例如带有预共享密钥的 TLS1.3、双棘轮算法和 MLS。
那么,我们如何组合密钥呢?为简单起见,我们将以下讨论限制为两个密钥,k_1
和k_2
。用于此任务的经典工具是双 PRF。与 PRF 一样,双 PRF 接受一个密钥和一个输入,并且只要其中一个密钥或输入包含一致的密钥,它的行为就像 PRF 一样。在双 PRF 中,你可以切换密钥和输入值,而不会影响安全性。实际上,双 PRF 最常见的实例化是 HMAC。
但是,使用 HMAC 作为双 PRF 需要谨慎。标准化的 HMAC 允许使用可变长度的密钥,这些密钥通过 PAD-or-HASH 函数处理。PAD-or-HASH 不具有抗冲突性,并且为不受限制的 HMAC 密钥创建 HMAC 输出冲突是微不足道的。幸运的是,本文 确立了 HMAC 的双 PRF 安全性,并充分描述了预期双 PRF 安全性的密钥集。简而言之,安全地使用 HMAC 作为双 PRF 需要密钥参数(即,传递给 HMAC 的内容作为密钥)是一个固定长度的位字符串(即,所有密钥必须具有相同的长度)或可变长度的位字符串只要所有密钥的长度至少都是基础哈希函数的块长度。
双 PRF 结果仅在组合两个一致的随机位字符串时适用。虽然一些研究认为可以使用 HMAC 作为双 PRF,其中包含其他高熵输入,例如 Diffie-Hellman 共享密钥 (G^xy
),但更保守的用法是将初始提取步骤应用于每个需要它的密钥材料。一个例子是prk = HMAC( HKDF.Extract(G^xy, salt), random_kem_secret)
。虽然一些分析省去了初始提取步骤,但这些用法偏离了 HMAC 的现有安全分析,并且不能直接享受安全保证。
双 PRF 使用的另一个好做法是确保最终组合的密钥尽可能依赖于上下文。此处的上下文可以是 Diffie-Hellman 共享、完整通信transcript的(哈希)。一个好的解决方案是使用额外的扩展步骤,该步骤在扩展期间使用上下文作为info
输入。
最后,还存在其他组合方法,例如连接 KDF (CatKDF)。CatKDF 大致在密钥的连接上使用 KDF。在其中一个密钥可能由攻击者控制的情况下,CatKDF 的安全性超出了现有安全分析的范围。以上评论并不意味着实际的攻击,而是提高了对某些情况下有时需要超出已知范围的更强假设的认识。有关实践中双 PRF 使用的更多讨论,请参阅来自单向性和应用于 TLS 的实用(后量子)密钥组合器。
本博客文章考察了不同的 KDF 任务、执行这些任务的适当工具以及我们在实践中看到的一些典型误用。总而言之,我们邀请你在处理下一个 KDF 任务时也这样做。邀请如下:当你面临下一个 KDF 任务时,请退后一步,考虑更高级别的目标,以及更高级别的工具是否更适合。
例如,你是否需要 KDF,因为你已经建立了一些 Diffie-Hellman 共享密钥,并且必须创建一个“安全通道”?考虑使用现有的经过实战考验的身份验证密钥交换协议,例如 Noise、TLS 1.3 或 EDHOC。
你是否需要 KDF 来加密数据流的各种块,同时期望对块和整个流具有一定的安全保证?考虑改用流式 AEAD!
当然,总会有需要新颖解决方案的时候;在这种情况下,请确保你对提议的解决方案有合理的理由,然后与你最喜欢的密码学家交谈(或者来找我们)!
- 原文链接: blog.trailofbits.com/202...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!