Go 博客

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

Filippo Valsorda
2021年9月15日

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

密码套件的工作原理

密码套件可以追溯到 TLS 的前身安全套接字层 (SSL),它 称它们为“密码种类”。它们是诸如 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 中,我们开始在服务器上主动 优先使用 ChaCha20Poly1305 密码套件而不是 AES-GCM,当我们检测到客户端或服务器缺少 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 体验
博客索引