我们构建X.509证书链,让您无需亲自动手
在过去的八个月中,Trail of Bits与Python密码学权威机构合作,开发了cryptography-x509-verification——一个全新的纯Rust实现的X.509路径验证算法,该算法是TLS及其他加密认证协议的基础。我们的实现快速、符合标准且内存安全,为Python生态系统提供了现代替代方案,取代OpenSSL中易误用和存在漏洞的X.509 API,用于HTTPS证书验证等协议。这是一项基础性的安全改进,将惠及所有Python网络程序员,进而造福整个互联网。
我们的实现已作为Python API公开,并包含在Cryptography的42.0.0发布系列中,这意味着Python开发者现在就可以使用它!以下是一个示例用法,展示了其与作为根CA捆绑包的certifi的交互:
|
|
X.509路径验证
X.509和路径验证的范围太广,无法在一篇文章中合理总结。因此,我们将X.509极度简化为两个基本事实:
- X.509是一种证书格式:它将公钥及其元数据(用途、标识主题)与由私钥生成的签名绑定。证书的主题可以是域名或其他相关标识符。
- 验证X.509证书需要获取其签名的公钥,使用该公钥检查签名,并(最终)根据一组有效性规则(有时称为X.509配置文件)验证相关元数据。在公共Web环境中,有两个重要的配置文件:RFC 5280和CA/B论坛基线要求(“CABF BRs”)。
这两个事实使得X.509证书可链式化:通过找到包含适当公钥的父证书来验证X.509证书的签名;父证书又有自己的父证书。此链构建过程持续进行,直到遇到先验信任的证书,通常是因为主机操作系统本身声明的信任(维护一组预配置的受信任证书)。
链构建(也称为“路径验证”)是TLS认证保证的基石:它允许Web服务器(如x509-limbo.com)提供一个不受信任的“叶”证书以及零个或多个不受信任的父证书(称为中间证书),这些证书最终必须链接到连接客户端已知且信任的根证书。
作为可视化,以下是x509-limbo.com的有效证书链,箭头表示“由…签名”的关系:
|
|
在此场景中,x509-limbo.com向我们提供两个最初不受信任的证书:x509-limbo.com本身的叶证书,以及为叶证书签名的中间证书(Let’s Encrypt R3)。中间证书又由根证书(ISRG Root X1)签名,该根证书已被信任(由于存在于我们的操作系统或运行时信任存储中),从而使我们对完整链有信心,进而对TLS会话启动所需的叶公钥有信心。
可能出什么问题?
上述对X.509和路径验证的解释描绘了一幅田园诗般的画面:为了构建链,我们只需在每个步骤中迭代父候选,一旦到达信任根就成功终止,或在耗尽所有候选后失败。简单,对吧?
不幸的是,现实要混乱得多:
- 上述抽象(“一个证书,一个公钥”)是极度简化。实际上,单个公钥(对应单个“逻辑”颁发机构)可能有多个“物理”证书,用于交叉颁发目的。
- 由于信任集由主机操作系统或语言运行时定义,给定叶证书没有“唯一真实”链。实际上,大多数(叶,[中间])元组有多个候选解决方案,其中任何一个是有效链。
- 这就是第一个要点的“原因”:Web服务器无法保证任何特定客户端有任何特定的受信任根集,因此中间颁发者通常为单个公钥拥有多个证书,以最大化成功构建链的可能性。
- 并非所有证书都相同:证书(包括同一“逻辑”颁发机构的不同“物理”证书)可能包含阻止其他有效路径的约束:名称限制、总长度限制、用途限制等。换句话说,正确的路径构建实现必须在遇到消除当前候选链的约束后能够回溯。
- X.509配置文件本身可以对整个链及其组成成员施加约束:例如,CABF BRs禁止已知弱签名算法和公钥类型,许多路径验证库还允许用户将有效链构建限制在可配置的最大长度以下。
实际上,这些(非穷尽)复杂性意味着我们简单的递归线性扫描链构建实际上是一个带有静态和动态约束的深度优先图搜索。未能将其视为如此会有灾难性后果:
- 未能实现动态搜索通常导致过于保守的链构建,有时会导致互联网中断。OpenSSL 1.0.x在2020年无法构建“痛苦链”是最近的一个例子。
- 未能遵守内部约束和配置文件范围的证书要求可能导致过于宽松的链构建。CVE-2021-3450是最近的一个例子,导致某些OpenSSL 1.1.x配置接受使用非CA证书构建的链。
因此,构建正确且最大化的(在找到任何有效链的意义上)X.509路径验证器对于可用性和安全性都至关重要。
怪癖、惊喜和模糊性
尽管支撑Web PKI和其他互联网基础设施的关键部分,但X.509路径验证的独立实现相对较少:大多数平台和语言重用少数常见实现之一(OpenSSL及其分支、NSS、Go的crypto/x509、GnuTLS等)或主机操作系统的实现(Windows上的CryptoAPI,macOS上的Security)。这表现为一些反复出现的怪癖和模糊性:
- 缺乏实现多样性意味着错误和设计决策(如过于或不足保守的配置文件检查)泄漏到其他实现中:当仅在OpenSSL上测试的PKI部署无法对抗crypto/x509工作时,用户会抱怨,因此实现经常弯曲其规范遵从性以适应现实世界的证书。
- 规范通常要求令人惊讶的行为,但(几乎)没有客户端正确实现。例如,RFC 5280规定路径长度和名称约束不适用于自颁发的中间证书,但这在实践中被广泛忽略。
- 由于规范本身很少被解释,它们包含仍未解决的模糊性:将根视为“信任锚”与承载策略的证书,处理20字节长但DER编码为21字节的序列号等。
我们的实现需要处理这些怪癖家族。为了保持一致,我们依赖三个基本策略:
- 先测试,后实现:为了对自己的设计有信心,我们构建了x509-limbo并针对其他实现进行了预验证。这为我们自己的实现提供了覆盖基线,并在必要时为放松各种策略级检查提供了经验依据。
- 一切保持在Rust中:Rust的性能、强类型系统和安全属性意味着我们可以快速迭代设计,同时专注于算法正确性而不是内存安全。当然,PyCA Cryptography的X.509解析已经在Rust中完成,这无疑有帮助。
- 遵守Sleevi法则:我们的实现将路径构建和路径验证视为一个统一的步骤,没有“唯一”真实链,意味着在整个图被搜索之前不会放弃并向用户返回失败。
- 必要时妥协:如上所述,实现经常保持与OpenSSL的兼容性,即使这样做违反了RFC 5280和CABF BRs中定义的配置文件。这种情况多年来已显著改善(并且随着Web PKI上证书颁发周期的缩短,改进速度加快),但一些妥协仍然是必要的。
展望未来
我们的初始实现已准备好生产,大约有2,500行Rust代码,不包括相对较小的仅Python API表面或x509-limbo:
|
|
从这里开始,还有很多可以做的事情。我们的一些想法包括:
- 公开客户端证书路径验证的API:为了加快进度,我们专注于服务器验证的初始实现(验证证明特定DNS名称或IP地址的叶证书链接到信任根)。这忽略了客户端验证,其中连接的客户端端呈现自己的证书供服务器验证一组已知主体。客户端路径验证与服务器验证共享相同的基本链构建算法,但具有略微不同的理想公共API(因为客户端的身份需要与服务器已知的潜在任意数量的身份匹配)。
- 公开不同的X.509配置文件(和更多配置旋钮):当前API暴露的配置很少;Python API用户唯一可以更改的是证书主题、验证时间和最大链深度。未来,我们将研究公开额外的旋钮,包括允许用户使用RFC 5280证书配置文件和其他常见配置文件(如Microsoft的Authenticode配置文件)执行验证的状态片段。长期来看,这将帮助定制(如企业)PKI用例迁移到Cryptography的X.509 API,并减少对OpenSSL的依赖。
- Carcinize现有的C和C++ X.509用户:Rust的最大优势之一是其与C和C++的原生、零成本兼容性。鉴于C和C++的X.509和路径验证实现历史上是可利用内存损坏错误的重要来源,我们相信围绕cryptography-x509-verification的薄“原生”包装可能对主要C和C++代码库的安全产生巨大的积极影响。
- 传播x509-limbo的福音:x509-limbo是我们能够自信地交付X.509路径验证器的关键组成部分。我们以这样的方式编写它,应该使集成到其他路径验证实现中就像下载和使用单个JSON文件一样简单。我们期待帮助其他实现(如rustls-webpki)直接将其集成到自己的测试方案中!
如果任何这些想法让您感兴趣(或者您有自己的想法),请联系我们!开源是Trail of Bits使命的关键,我们很乐意听取如何帮助您和您的团队充分利用并进一步保护开源生态系统的意见。
致谢
这项工作需要多个独立方的协调。我们向以下团体和个人表示诚挚的感谢:
- 主权科技基金,其OSS安全愿景和资金使这项工作成为可能。
- PyCA Cryptography维护者(Paul Kehrer和Alex Gaynor),他们从一开始就确定了这项工作的范围,并在整个开发过程中提供了持续的反馈和审查。
- BetterTLS开发团队,他们审查并合并了使x509-limbo能够供应商化和重用其(广泛)测试套件的补丁。
如果您喜欢这篇文章,请分享: [Twitter] [LinkedIn] [GitHub] [Mastodon] [Hacker News]