Go 博客

crypto/tls 中的自动密码套件排序

Filippo Valsorda
2021 年 9 月 15 日

Go 标准库提供了 crypto/tls,这是一个健壮的传输层安全 (TLS) 实现,TLS 是互联网上最重要的安全协议,也是 HTTPS 的基础组成部分。在 Go 1.17 中,我们通过自动化密码套件的优先级顺序,使其配置更加容易、更安全、更高效。

密码套件的工作原理

密码套件的历史可以追溯到 TLS 的前身 Secure Socket Layer (SSL),它称其为“cipher kinds”。它们是那些看起来令人望而生畏的标识符,例如 TLS_RSA_WITH_AES_256_CBC_SHATLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,它们详细说明了在 TLS 连接中用于密钥交换、证书认证和记录加密的算法。

密码套件是在 TLS 握手期间进行协商的:客户端在其第一个消息 Client Hello 中发送它支持的密码套件列表,服务器从该列表中选择一个,并将其选择告知客户端。客户端按照自己的偏好顺序发送支持的密码套件列表,服务器可以根据其配置自由地从中进行选择。最常见的情况是,服务器会根据客户端偏好顺序或服务器偏好顺序,选择列表中第一个双方都支持的密码套件。

密码套件实际上只是众多协商参数中的一个——支持的曲线/群组和签名算法通过各自的扩展进行额外协商——但它们是最复杂、最出名的参数,也是多年来开发者和管理员被训练得对此有意见的唯一参数。

在 TLS 1.0–1.2 中,所有这些参数在一个复杂的相互依赖网络中交互:例如,支持的证书取决于支持的签名算法、支持的曲线和支持的密码套件。在 TLS 1.3 中,这一切都得到了极大的简化:密码套件仅指定对称加密算法,而支持的曲线/群组控制密钥交换,支持的签名算法应用于证书。

一项复杂的选择被推给了开发者

大多数 HTTPS 和 TLS 服务器将密码套件的选择和偏好顺序委托给服务器操作员或应用程序开发者。这是一个复杂的选择,由于多种原因需要最新的专业知识。

一些旧的密码套件包含不安全的组件,一些需要极其仔细和复杂的实现才能安全,还有一些只有在客户端应用某些缓解措施或甚至拥有特定硬件时才安全。除了单个组件的安全性外,不同的密码套件可以为整个连接提供截然不同的安全属性,因为不带 ECDHE 或 DHE 的密码套件不提供前向保密性——即连接不能用未来泄露的证书密钥进行追溯或被动解密。最后,支持的密码套件的选择会影响兼容性和性能,在不了解生态系统最新情况的情况下进行更改可能导致旧客户端无法连接、增加服务器消耗的资源或耗尽移动客户端的电量。

这种选择如此深奥和精妙,以至于有专门的工具来指导操作员,例如出色的 Mozilla SSL 配置生成器

我们是如何走到这一步的,为什么会这样?

首先,单个密码组件过去更容易出现故障。2011 年,当 BEAST 攻击破坏了 CBC 密码套件,使得只有客户端能够缓解攻击时,服务器转向优先使用不受影响的 RC4。2013 年,当 RC4 被发现已失效时,服务器又回到了 CBC。当 Lucky Thirteen 表明由于其逆向 MAC-then-encrypt 设计,实现 CBC 密码套件极其困难时……嗯,当时没有其他替代方案,所以实现不得不小心翼翼地绕过困难来实现 CBC,并且多年来一直在这项艰巨的任务上失败。可配置的密码套件和密码敏捷性曾经提供了一些保证,即当某个组件出现故障时,可以即时替换它。

现代密码学显著不同。协议仍然会不时出现故障,但很少是单个抽象组件的问题。自 2008 年 TLS 1.2 引入基于 AEAD 的密码套件以来,这些套件没有一个被攻破过。如今,密码敏捷性是一种负担:它引入了可能导致弱点或降级的复杂性,并且仅因性能和合规性原因而必需。

补丁更新过去也不同。今天我们承认,及时应用已披露漏洞的软件补丁是安全软件部署的基石,但十年前这并不是标准做法。更改配置被视为对易受攻击密码套件作出回应的更快捷选项,因此操作员通过配置完全负责它们。我们现在面临相反的问题:有些完全打了补丁并更新的服务器,但由于其配置多年未被触动,仍然行为异常、表现不佳或不安全。

最后,大家普遍认为服务器更新速度往往比客户端慢,因此在选择最佳密码套件方面不如客户端可靠。然而,服务器在密码套件选择上拥有最终决定权,因此默认做法是让服务器听从客户端的偏好顺序,而不是持有强烈的意见。这一点仍然部分正确:浏览器设法实现了自动更新,比普通服务器更新得更快。另一方面,许多旧设备现在已不再提供支持,并停留在旧的 TLS 客户端配置上,这使得更新的服务器通常比其某些客户端更能做出更好的选择。

无论我们如何走到这一步,要求应用程序开发者和服务器操作员成为密码套件选择细微差别的专家,并跟上最新发展以保持其配置最新,这是密码工程的失败。如果他们正在部署我们的安全补丁,那就应该足够了。

Mozilla SSL 配置生成器很棒,但它不应该存在。

情况是否有所改善?

过去几年情况的发展有好消息也有坏消息。坏消息是,排序正变得更加细微复杂,因为有些密码套件具有同等的安全属性。在这种集合中选择最佳选项取决于可用的硬件,并且难以在配置文件中表达。在其他系统中,最初简单的密码套件列表现在取决于更复杂的语法或额外的标志,例如SSL_OP_PRIORITIZE_CHACHA

好消息是,TLS 1.3 极大地简化了密码套件,并且它使用的套件与 TLS 1.0–1.2 是不相干的。所有 TLS 1.3 密码套件都是安全的,因此应用程序开发者和服务器操作员根本不必担心它们。实际上,一些 TLS 库(如 BoringSSL 和 Go 的 crypto/tls)根本不允许配置它们。

Go 的 crypto/tls 和密码套件

Go 确实允许在 TLS 1.0–1.2 中配置密码套件。应用程序始终可以通过 Config.CipherSuites 设置启用的密码套件和偏好顺序。除非设置了 Config.PreferServerCipherSuites,否则服务器默认优先使用客户端的偏好顺序。

当我们在 Go 1.12 中实现 TLS 1.3 时,我们没有让 TLS 1.3 密码套件可配置,因为它们与 TLS 1.0–1.2 的套件是互斥的,最重要的是它们都是安全的,所以没有必要将选择委托给应用程序。Config.PreferServerCipherSuites 仍然控制使用哪一方的偏好顺序,而本地的偏好取决于可用的硬件。

在 Go 1.14 中,我们公开了支持的密码套件,但明确选择以中立顺序(按其 ID 排序)返回它们,以便我们不会受限于以静态排序顺序来表示我们的优先级逻辑。

在 Go 1.16 中,当我们检测到客户端或服务器缺乏对 AES-GCM 的硬件支持时,我们开始主动在服务器端优先使用 ChaCha20Poly1305 密码套件而非 AES-GCM。这是因为没有专门的硬件支持(例如 AES-NI 和 CLMUL 指令集),AES-GCM 很难高效且安全地实现。

最近发布的 Go 1.17 接管了所有 Go 用户的密码套件偏好排序。虽然 Config.CipherSuites 仍然控制哪些 TLS 1.0–1.2 密码套件已启用,但它不用于排序,并且 Config.PreferServerCipherSuites 现在被忽略。相反,crypto/tls 根据可用的密码套件、本地硬件和推断的远程硬件能力做出所有排序决定。

当前的 TLS 1.0–1.2 排序逻辑遵循以下规则:

  1. ECDHE 优先于静态 RSA 密钥交换。

    密码套件最重要的属性是启用前向保密性。我们没有实现“经典”的有限域 Diffie-Hellman,因为它复杂、慢、弱,并且在 TLS 1.0–1.2 中存在微妙的缺陷,因此这意味着优先使用椭圆曲线 Diffie-Hellman 密钥交换而不是传统的静态 RSA 密钥交换。(后者只是使用证书的公钥加密连接的秘密,如果证书未来被泄露,就可以解密。)

  2. AEAD 模式优先于 CBC 进行加密。

    即使我们对 Lucky13 实施了部分对策(我 2015 年对 Go 标准库的第一次贡献!),CBC 套件要正确实现是一场噩梦,因此在其他更重要的事情同等的情况下,我们选择 AES-GCM 和 ChaCha20Poly1305。

  3. 3DES、CBC-SHA256 和 RC4 仅在没有其他可用选项时使用,并按该偏好顺序排列。

    3DES 有 64 位块,这使得它在流量足够大的情况下根本上容易受到生日攻击。3DES 列在InsecureCipherSuites下,但默认启用以确保兼容性。(控制偏好顺序的另一个好处是,我们可以默认启用安全性较低的密码套件,而不必担心应用程序或客户端会选择它们,除非作为最后手段。这是安全的,因为没有降级攻击依赖于较弱密码套件的可用性来攻击支持更好替代方案的对等方。)

    CBC 密码套件容易受到 Lucky13 式的侧信道攻击,我们只对 SHA-1 哈希部分实现了上面讨论的复杂对策,而不是 SHA-256。CBC-SHA1 套件具有兼容性价值,这证明了额外的复杂性是合理的,而 CBC-SHA256 则没有,因此默认禁用。

    RC4 具有可实际利用的偏差,这可能导致在没有侧信道的情况下恢复明文。情况不能比这更糟了,因此 RC4 默认禁用。

  4. ChaCha20Poly1305 优先于 AES-GCM 进行加密,除非双方都对后者有硬件支持。

    如前所述,没有硬件支持,AES-GCM 很难高效且安全地实现。如果我们检测到本地没有硬件支持,或者(在服务器端)客户端没有优先选择 AES-GCM,我们就选择 ChaCha20Poly1305。

  5. AES-128 优先于 AES-256 进行加密。

    AES-256 比 AES-128 有更大的密钥,这通常是好的,但它也执行更多轮次的核心加密函数,使其速度变慢。(AES-256 中的额外轮次与密钥大小的改变无关;它们是试图提供更大的抗密码分析裕量。)更大的密钥只在多用户和后量子设置中有用,而这与 TLS 无关,TLS 生成足够随机的 IV 并且没有后量子密钥交换支持。由于更大的密钥没有任何好处,我们优先选择 AES-128 以提高速度。

TLS 1.3 的排序逻辑只需要最后两条规则,因为 TLS 1.3 消除了前三条规则所防范的有问题的算法。

常见问题解答

如果某个密码套件被发现存在缺陷怎么办?就像任何其他漏洞一样,它将在所有受支持的 Go 版本的安全更新中得到修复。所有应用程序都需要准备好应用安全修复才能安全运行。从历史上看,存在缺陷的密码套件越来越少见。

为什么仍然允许配置启用的 TLS 1.0–1.2 密码套件?在选择启用哪些密码套件时,需要在基本安全性和旧版本兼容性之间进行有意义的权衡,这是我们无法自行决定的选择,否则将要么排除不可接受的生态系统一部分,要么降低现代用户的安全保障。

为什么不允许配置 TLS 1.3 密码套件?相反,TLS 1.3 不需要进行权衡,因为其所有密码套件都提供强大的安全性。这使得我们可以全部启用它们,并根据连接的具体情况选择最快的,而无需开发者参与。

要点总结

从 Go 1.17 开始,crypto/tls 接管了可用密码套件的选择顺序。通过定期更新 Go 版本,这比让可能过时的客户端选择顺序更安全,使我们能够优化性能,并减轻了 Go 开发者的显著复杂性。

这与我们总的哲学一致,即尽可能地由我们来做出加密决策,而不是委托给开发者,也符合我们的加密原则。希望其他 TLS 库也能采用类似的改变,让精密的密码套件配置成为过去。

下一篇文章:行为准则更新
上一篇文章:整理 Go Web 体验
博客索引