Skip to content

Files

Latest commit

9fd7533 · Jan 10, 2022

History

History
97 lines (55 loc) · 15.2 KB

File metadata and controls

97 lines (55 loc) · 15.2 KB

三、密钥导出

前面的 HMAC 讨论触及了秘密密钥的概念,它决定了密钥加密原语的输出。秘密密钥应该理想地包含足够的,以使暴力攻击不切实际。这通常需要至少 256 位的媒体访问控制密钥和至少 128 位的加密密钥。生成此类密钥的理想方法是使用真随机数生成器(很少可用),或者使用密码性强的伪随机数生成器(例如,基于 CSP、)。然而,在一些非常常见的场景中,这种随机生成的密钥要么不切实际,要么不充分(并且可能导致可用性或安全性缺陷)。密钥导出函数(KDFs) 在这些场景中经常被用来从单个秘密值中导出一个或多个强密钥。

一些密码算法和协议可能有已知为的密钥,因此应该避免;KDFs 可以帮助避免此类系统中的弱密钥。KDFs 的另一个常见用途是从用户提供的密钥(我们通常称之为密码或密码短语)中提取熵,并通过故意缓慢的计算过程导出实际的密钥,以阻止暴力密码猜测攻击。KDFs 也经常被用作深度防御安全策略的一部分。我们不会讨论 KDFs 的弱密钥避免使用,因为在这本书里你不会遇到任何已知弱密钥的加密算法,但是我们接下来会讨论另外两个常见的 KDF 场景。

KDF 通常被设计成一个两部分的提取然后扩展的过程。“提取”部分从输入键中提取尽可能多的熵,并生成固定长度的中间键。这个中间密钥被认为是从输入密钥中收集的熵的短而强的、均匀分布的加密表示。过程的第二个“扩展”部分获取中间密钥,并根据每个 KDF 计划实现的特定目标将其扩展为一个或多个最终密钥。

pbkd2

基于密码的密钥导出功能第 2 版 ( PBKDF2 ,由 RFC-2898 涵盖)是专为用户提供的密码加密而设计的特定 KDF。像 PBKDF2 这样基于密码的 KDF 通常采用三种防御机制来生成强派生密钥:

  • 所有可用的密码熵浓缩成一个统一的中间密钥(“提取”过程)。
  • 盐析“提取”过程,防止针对多个密钥的暴力攻击的可扩展性。 *** 通过一个缓慢的计算过程扩展中间键,这个过程的成本很高,但速度却很快。**

**第三项的缓慢计算将影响合法用户和暴力攻击者,但影响的比例不同。合法用户的密码检查可能会在每次检查时都经历如此缓慢的计算,这可能足够快,不会影响可用性,而攻击者每次猜测新密码时都必须进行这种计算。

同样谨慎的是,假设一个足智多谋的、受驱动的攻击者将能够访问比合法安装更好的硬件(可能是定制的并针对暴力强制进行了优化)。现代云计算服务也可以很容易地用来启动大型、强大的服务器群,这些服务器群专门用于暴力猜测,可以用大量的商品硬件实现与更少、更强大的定制硬件资源相同的结果。

如果密码一开始就包含足够的熵,第三种机制就没有必要了。不幸的是,高熵密码通常与可用性不一致,因为人类很难快速可靠地想出它们。它们也很难记忆,并且在没有错别字的情况下打字和重新打字既困难又耗时。

三种基于密码的 KDF 算法值得考虑: PBKDF2bcryptscrypt

表 3:pbkdf 2、bcrypt 和 scrypt 的比较

PBKDF2 断续器 隐窝
成熟度 One thousand nine hundred and ninety-nine One thousand nine hundred and ninety-nine Two thousand and nine
可变的中央处理器硬度
可变记忆硬度
内存需求 微小的 小的 可变的
IETF 规范 是(草稿)
奇怪的假设/限制
可变输出长度 否(192 位) 否(256 位)
NIST 批准
MS 支持。NET 实现 是(分段)

表 3 是这三个基于密码的 KDF 的快速、非全面的比较,基于从. NET 安全工程的角度来看应该相关的标准。最后两点使 PBKDF2 成为首选,因为您既不想信任不受支持的实现,也不想推出自己的实现。

那个。NET 提供的 PBKDF2 的实现在 Rfc2898DeriveBytes 类中,它实现了抽象的 DeriveBytes 基础。你可能还会遇到 PasswordDeriveBytes ,这是旧的 PBKDF1 算法的实现——不要使用它。PBKDF1 算法比较弱,微软对它的实现破开机。

关于Rfc2898DeriveBytes一个立即变得明显的问题是,它只在 HMAC-SHA1 实现了 PBKDF2 算法(微软紧密耦合设计的另一个例子)。因为 SHA-1 被硬编码到了 T2 实现中,所以它甚至不可能黑进任何其他 HMAC 哈希原语来操作。一个合理的方法是简单地接受所给的并使用Rfc2898DeriveBytes。在有限的情况下,这是一个合理的选择。另一种方法是通过尽可能重用微软的实现,并且只移除硬编码的 HMAC-SHA1 依赖项,来构建一个新的、灵活的 PBKDF2 实现。你可以在魔族密码库中找到这样的PBKDF2类实现。已经对照已发表的 PBKDF2-HMAC-SHA1、PBKDF2-HMAC-SHA256 和 PBKDF-HMAC-SHA512 测试载体进行了验证。

被迫重新实现 PBKDF2 的一个附带好处是选择更好的默认值。Rfc2898DeriveBytes默认为 1000 次迭代,这在 1999 年被认为是足够的,但是对于当前的暴力技术来说太低了。PBKDF2 默认为 10,000 次迭代,但是我们鼓励您覆盖它,并使用最大可能的值,而不影响您场景中的可用性。

HKDF 和 SP800_108

有很多场景你的源密钥素材(SKM) 已经有足够的熵(SKM 是 CSP 生成的 16 字节随机密钥,已经有 128 位熵)。这些高熵 SKM 场景不需要熵提取,但它们可能仍然受益于熵凝聚,其中固定长度或可变长度的高熵 SKM 被凝聚成固定长度的均匀分布表示。即使 SKM 中有大量的熵,这种凝聚也是有用的,因为熵可能不会均匀地分布在所有 SKM 比特中(一些 SKM 部分比其他部分捕获更多的熵)。

腌制步骤仍然相关,但原因不同。猜测不再是一个问题(由于高熵 SKM),但潜在的 SKM 重用仍然是,所以一个已知的识别符通常用于扩展步骤中的额外熵。当需要从单个主密钥生成多个派生密钥时,扩展步骤是相关的,并且均匀分布的固定长度的压缩可能不足以简单地将其分割成多个派生密钥长度。为什么你一开始就需要多个派生键?为什么从高熵 SKM 导出的一个密钥(你很幸运拥有)是不够的?

七年多来。NET 2.0 到。NET 4.0),微软认为这是完全足够的,基于他们的 ASP.NETmachineKey实现。微软对各种组件使用相同的强machineKey主密钥,每个组件使用相同的主密钥进行加密。

2010 年 9 月,在其中一个组件的实现中发现了严重的安全漏洞,使得攻击者能够发现该组件的加密密钥。受影响的 ASP.NET 部分不是一个关键部分,其本身也没有很高的可开发性或损害潜力。但是,因为所有其他组件都使用了相同的密钥,所以攻击者实际上获得了王国的密钥。这个 ASP.NET 漏洞对 ASP.NET 4.0 和所有早期版本的安全设计是一个严重的打击——严重到足以保证在 ASP.NET 4.5 和新的 ASP.NET 加密栈(将在后面的章节中介绍)中进行重大的安全检查,该栈现在基于从单个高熵machineKey主密钥派生的多个密钥。

致力于 ASP.NET 4.5 的微软 ASP.NET 安全团队意识到了这一点。NET 没有为高熵 SKM 设计的 KDF 实现,它有下面的可以说。网络开发和工具博客:

“在这一点上,我们不得不接受这样一个事实,即我们必须在自己的层中实现 KDF,而不是调用任何现有的 API。……我们最终选择了NIST sp 800-108【PDF 链接】,因为它旨在从现有的高熵密钥而不是低熵密码中导出新密钥,这种行为更符合我们的预期用途。”

微软的 SP800-108 实现是在一个新的SP800_108类中,该类与大约 18 个其他紧密耦合的新安全类一起,在他们的 System.Web.dll 中被标记为private internal。似乎我们也不得不接受这样一个事实,那就是我们必须实现我们自己的 KDF,因为微软不分享它的新玩具。

面对这样的辞职,我们可以思考一下 SP800-108 是否是最好的通用高熵 KDF 来实现。SP800-108 是不错的选择,也有 NIST 推荐。微软不得不迅速做出反应,他们选择的 SP800-108 将算法风险转嫁给了 NIST。SP800-108 实际上定义了三种 KDF 算法:计数器模式算法、反馈模式算法和双流水线迭代算法。微软的SP800_108类实现了计数器模式算法,因为它简单、快速,并且允许 O(1)派生任何键子部分。然而,还有另一种 KDF 算法,总部位于 HMAC 的 KDF (HKDF) ,它可以说是比 SP800-108 算法更好的通用多用途 KDF。HKDF 被证明是安全的,规范的( RFC-5869 ),并且包括不同的熵提取和密钥导出步骤。SP800_108 计数器 KDF 更适合性能至关重要且不需要熵提取步骤的场景,而 HKDF 则是更好、更安全的通用 KDF。火海加密库提供了 HKDFSP 800_108_Ctr 实现。

PBKDF2 与 HKDF 组合

同时使用 PBKDF2 和 HKDF 来派生多个密钥通常是有意义的。PBKDF2 擅长从低熵 SKM 导出单个主密钥,比如用户提供的任何东西。然而,PBKDF2 不适合产生超出其 HMAC 包装的散列函数长度的输出,因为每一个额外的 PBKDF2 旋转都要经历同样缓慢的计算过程。例如 PBKDF2-SHA256 的单旋转输出长度为 32 字节(256 位)。如果你向 PBKDF2-SHA256 要求 64 字节的输出(例如,如果你需要两个 256 位的派生键),或者即使你向它要求 32+1=33 字节,PBKDF2-SHA256 的运行时间也会是它的两倍,因为它会做两次旋转而不是一次。

您已经选择了 PBKDF2 迭代计数尽可能高,而不影响系统的可用性。最大迭代次数仅用于一次旋转——进行多次旋转肯定会影响可用性。您可以为 PBKDF2 切换到一个更长的散列函数(使用 SHA-512 基本上最大化 512 位),但是您将避免问题而不是解决问题(它也不会扩展到超过 2–4 个键,这取决于它们的用途)。****

**更好的方法是使用 PBKDF2 来导出主密钥,然后使用用 PBKDF2 导出的主密钥加密的 HKDF(或 SP800_108_Ctr)来有效地生成任意数量的附加主密钥导出密钥。

密码是额外的非秘密熵源,用于防止由于重复使用 SKM 而导致的漏洞,无论是通过同一来源,还是跨碰巧使用同一 SKM 的多个来源。当你不能保证 SKM 不被重复使用时(无论是确定性的还是统计性的),应该一直使用盐。请注意,SKM 的质量或熵并不重要,只有其潜在的重用才重要;你应该使用盐,即使你有一个高质量的 SKM,有再利用的潜力。

对盐的唯一要求是独一无二。如果盐不是独一无二的,那么当 SKM 被重复使用时,它就不能再作为一种独特的东西,你就失败了。唯一性要求可以确定性地实现,例如,通过使用自动递增的计数器,您可以保证永远不会对所有skm 重复。唯一性也可以通过使用 128 位 CSP 生成的随机值以统计方式或无状态方式实现。

**GUIDs 是盐的完美候选,因为它们旨在提供独特性,而不是不可预测性。确定性实现有一个不可取的要求,即必须保持状态,这增加了复杂性和出错的机会。我们建议您在需要 salt 时使用 GUID,原因有二:(1) GUID 满足所有 salt 要求,并且中的新 GUID 生成。NET Framework 是不可能搞砸的(我们谅你也不敢);(2)使用盐的 GUIDs 清楚地传达了意图:唯一性,而不是不可预测性。当我们说 GUID 时,我们指的是代表 GUID 的 16 字节数组,而不是 GUID 字符串形式。

Salt 经常与初始化向量混淆并被误用;我们将在“对称加密”一章中讨论差异。

客户端 KDF

采用 PBKDF2 和类似的基于密码的 KDF 方案从低熵密码中导出密钥的一个明显后果是,以 CPU 或内存利用率衡量,它们的计算成本很高。这一成本可能很快成为服务器端吞吐量(以每秒请求数衡量)的限制因素。

一个诱人的想法是将 PBKDF2 计算从服务器端转移到客户端。让我们仔细研究一下为什么这个想法会失败。

与服务器端基础架构相比,典型的客户端设备往往具有较差的 CPU 和内存资源。客户端软件通常甚至不能充分利用可用的客户端资源。许多现代浏览器即使在 64 位环境下运行,也无法充分利用可用内存,因此陷入了 32 位模式。即使浏览器在 64 位模式下运行,其计算能力也通常受到单线程 JavaScript 引擎的限制,无法充分利用可用的 CPU。移动客户端设备通常主要由电池供电,并且其资源能力甚至进一步受限。还有客户端中央处理器节流,以保持和延长电池寿命。

客户端资源劣势和不可预测性的关键含义是,与服务器端执行相比,可接受可用性的客户端 PBKDF2 轮数通常要低得多。PBKDF 回合数量的任何大幅减少都将削弱 PBKDF2 的安全性,并规避其预期目的。

另一个问题是 PBKDF2 是加盐的(如果不是,那么你没有正确使用它)。Salt 值在服务器端可用(这使得服务器端 PBKDF2 变得简单),但在客户端不可用。向客户端发送 salt 值不仅会增加复杂性,还会削弱安全性。虽然盐的价值不一定要保密,但也不应该愉快地透露给任何提出要求的人。对手可能会要求管理员帐户的 salt 值,并将其与常见密码列表相结合,以生成潜在的凭据并快速发起在线攻击。盲目披露盐的价值显然不符合我们的最佳利益。

按键分离

键分离原则规定,对于不同的算法和不同的操作模式,您应该始终使用不同的键。一些违反这一原则的行为甚至会导致在不知道密钥的情况下恢复明文。幸运的是,通过使用主密钥 HKDF 或 SP800_108_Ctr 以及特定用途的识别器来生成所需数量的不同密钥,很容易遵守密钥分离原则。在对称加密场景中,不同的加密和解密目的以及不同的操作(例如,加密和身份验证)应该使用独立的密钥。在非对称加密场景中,加密和签名应该使用独立的密钥对。******