PEP 480 – 在 PyPI 遭受入侵后幸存:软件包的端到端签名
- 作者:
- Trishank Karthik Kuppusamy <karthik at trishank.com>, Vladimir Diaz <vladimir.diaz at nyu.edu>, Justin Cappos <jcappos at nyu.edu>, Marina Moore <mm9693 at nyu.edu>
- BDFL 委托:
- Donald Stufft <donald at stufft.io>
- 讨论至:
- Discourse 帖子
- 状态:
- 草案
- 类型:
- 标准跟踪
- 主题:
- 打包
- 要求:
- 458
- 创建日期:
- 2014年10月8日
摘要
本 PEP 提议对 PEP 458 进行扩展,以增加对端到端签名和最高安全模型的支持。端到端签名允许 PyPI 和开发者都对客户端下载的分发进行签名。PEP 458 提出的最低安全模型支持分发的持续交付(因为它们由在线密钥签名),但该模型在 PyPI 受到入侵时无法保护分发。在最低安全模型中,攻击者如果入侵了存储在 PyPI 基础设施上的签名密钥,可能会为恶意分发进行签名。本 PEP 中描述的最高安全模型保留了 PEP 458 的优点(例如,上传到 PyPI 的分发立即可用),但额外确保如果 PyPI 受到入侵,最终用户不会面临安装伪造软件的风险。
本 PEP 要求对 PyPI 基础设施进行一些更改,并建议希望参与端到端签名的开发者进行一些更改。这些更改包括更新 PEP 458 的元数据布局以包含对开发者密钥的委托,增加一个在 PyPI 注册开发者密钥的流程,以及为利用端到端签名的开发者更改上传工作流程。所有这些更改将在本 PEP 的后续部分详细描述。希望利用端到端签名的软件包管理器除了消费 PEP 458 中描述的元数据所需的额外工作外,不需要做任何额外工作。
本 PEP 讨论了对 PEP 458 所做的更改,但排除了其信息性元素,主要侧重于最高安全模型。例如,本文不涉及 The Update Framework 的概述或 PEP 458 中的基本机制。对 PEP 458 的更改包括对快照过程、密钥泄露分析、审计快照以及在 PyPI 泄露时应采取的步骤的修改。PyPI 可能建议的签名和密钥管理过程进行了讨论,但未严格定义。如何实现发布过程以管理密钥和元数据留给签名工具的实现者。也就是说,本 PEP 描绘了开发者为了支持分发的端到端验证而必须上传的元数据中预期的密码密钥类型和签名格式。
PEP 状态
社区在2014年至2018年间讨论了本 PEP。由于实现本 PEP 所需的工作量巨大,讨论被推迟,直到 PEP 458 中的前置步骤获得批准。截至2020年年中,PEP 458 已获得批准并正在实施中,PEP 作者旨在获得批准,以便为实施获得适当的资金。
基本原理
PEP 458 提出了 PyPI 应如何与 The Update Framework (TUF) [2] 集成。它解释了如何使 pip 等现代软件包管理器更安全,以及如果 PyPI 在服务器端进行修改以包含 TUF 元数据,可以防止哪些类型的攻击。软件包管理器可以引用 PyPI 上可用的 TUF 元数据,以更安全地下载分发。
PEP 458 还描述了 PyPI 存储库的元数据布局,并采用了最低安全模型,该模型支持项目的持续交付,并使用在线加密密钥对开发者上传的分发进行签名。尽管最低安全模型可以抵御大多数针对软件更新程序的攻击 [5] [6],例如混搭和冗余依赖项攻击,但它可以改进以支持端到端签名,并在 PyPI 受到入侵时禁止伪造分发。
PEP 480 在 PEP 458 的基础上,增加了对开发者签名的支持,并减少了对在线密钥的依赖以防止恶意分发。PEP 458 和最低安全模型的主要优势是自动化和简化的发布过程:开发者可以上传分发,然后 PyPI 会对他们的分发进行签名。大部分发布过程由在线角色自动处理,这种方法需要在 PyPI 基础设施上存储加密签名密钥。不幸的是,存储在线的加密密钥容易被盗。本 PEP 中提出的最高安全模型允许开发者对其提供给 PyPI 用户的分发进行签名,并且如果存储在 PyPI 基础设施上的在线密钥遭到泄露,不会使最终用户面临下载恶意分发的风险。
威胁模型
威胁模型假设如下
- 离线密钥安全可靠地存储。
- 攻击者可以泄露 PyPI 存储在线的至少一个受信任密钥,并且可能一次或在一段时间内进行。
- 攻击者可以响应客户端请求。
- 攻击者可以控制客户端不想安装的任何数量项目的开发者密钥。
如果攻击者能够导致客户端安装(或保持安装)除了客户端正在更新软件的最新版本之外的任何东西,则攻击者被认为是成功的。当攻击者阻止更新安装时,攻击者的目标是让客户端不会意识到任何问题。
定义
本文件中的关键词“必须”、“不得”、“要求”、“应”、“不应”、“推荐”、“可”、“可选”应按照 RFC 2119 的描述进行解释。
本 PEP 侧重于将 TUF 与 PyPI 集成;但是,鼓励读者阅读 TUF 的设计原则 [2]。还建议读者熟悉 TUF 规范 [3] 以及 PEP 458(本 PEP 是对其的扩展)。
本 PEP 中使用的以下术语在 Python 软件包词汇表 [4] 中定义:项目、发布、分发。
本 PEP 中使用的术语定义如下
- 分发文件:一个版本化的存档文件,包含 Python 软件包、模块和其他用于分发版本的资源文件。在本 PEP 中,术语分发文件、分发软件包 [4],或简单地说分发或软件包可以互换使用。
- 简单索引:包含指向分发文件内部链接的 HTML 页面。
- 目标文件:通常,目标文件是 PyPI 上所有应通过 TUF 保证完整性的文件。这通常包括分发文件和 PyPI 元数据,例如简单索引。
- 角色:TUF 中的角色包含一方被授权执行的一系列行动,包括他们可能签名的元数据以及他们负责的软件包。PyPI 中有一个根角色。有多个角色,它们的职责由根角色直接或间接委托。术语“顶级角色”指根角色和由根角色委托的任何角色。每个角色都有一个它被信任提供的元数据文件。
- 元数据:元数据是描述角色、其他元数据和目标文件的文件。
- 存储库:存储库是由命名的元数据和目标文件组成的资源。客户端请求存储在存储库上的元数据和目标文件。
- 一致快照:一组 TUF 元数据和目标文件,捕获 PyPI 上所有项目在某个固定时间点的完整状态。
- 开发者:项目的拥有者或维护者,被允许更新 TUF 元数据,以及给定项目的分发元数据和文件。
- 在线密钥:必须存储在 PyPI 服务器基础设施上的私有加密密钥。这通常允许使用该密钥进行自动化签名。攻击者如果入侵 PyPI 基础设施,将能够立即读取这些密钥。
- 离线密钥:必须独立于 PyPI 服务器基础设施存储的私有加密密钥。这可以防止使用该密钥进行自动化签名。攻击者如果入侵 PyPI 基础设施,将无法立即读取这些密钥。
- 阈值签名方案:一个角色可以通过指定至少 t 个密钥中的 n 个密钥来签名其元数据,从而提高其对密钥泄露的弹性。泄露 t-1 个密钥不足以泄露角色本身。说一个角色需要 (t, n) 个密钥表示阈值签名属性。
最高安全模型
最高安全模型允许开发者对其项目进行签名并将签名的元数据上传到 PyPI。在本 PEP 提出的模型中,如果 PyPI 基础设施遭到入侵,攻击者将无法在不访问项目开发者密钥的情况下提供恶意版本的已声明项目。图 1 描绘了对最低安全模型元数据布局所做的更改,即现在支持开发者角色,并且存在三个新的委托角色:最近声明 (recently-claimed)、已声明 (claimed) 和未声明 (unclaimed)。最低安全模型中的 bins 角色已更名为 unclaimed,并且可以包含任何尚未添加到 claimed 中的项目。unclaimed 角色功能与以前一样(即,如 PEP 458 中所述,添加到此角色的项目由 PyPI 使用在线密钥签名)。开发者提供的离线密钥确保了最高安全模型相对于最低模型的强度。尽管最低安全模型支持项目的持续交付,但所有项目都由在线密钥签名。也就是说,攻击者能够在最低安全模型中破坏软件包,但在最高模型中不能,除非同时泄露开发者的密钥。
图 1:最高安全模型中的元数据布局概述。最高安全模型支持持续交付和可生存的密钥泄露。
首次由开发者签名并上传到 PyPI 的项目将添加到“最近声明”角色。 “最近声明”角色使用在线密钥,因此首次上传的项目可立即供客户端使用。一段时间后,PyPI 管理员可以定期(例如,每月)将“最近声明”中列出的项目移动到“已声明”角色以获得最高安全性。 “已声明”角色使用离线密钥,因此如果 PyPI 受到入侵,添加到此角色的项目不容易被伪造。
“最近声明”角色与“未声明”角色分离是为了可用性和效率,而非安全性。如果新的项目委托被附加到“未声明”元数据中,“未声明”将在项目获取密钥时每次都需要重新下载。通过分离新项目,检索的数据量减少了。从可用性角度来看,这也使管理员更容易看到哪些项目现在已被声明。当将密钥从“最近声明”移动到“已声明”时,需要此信息,这将在“生成一致快照”部分更详细地讨论。
端到端签名
端到端签名允许 PyPI 和开发者都对客户端下载的元数据进行签名。PyPI 受信任将上传的项目提供给客户端(PyPI 为此过程的这部分元数据签名),开发者则对他们上传到 PyPI 的分发进行签名。
为了向项目委托信任,开发者需要向 PyPI 提交至少一个公钥。开发者可以为同一个项目提交多个公钥(例如,每个项目维护者一个密钥)。PyPI 会获取项目的所有公钥,并将它们添加到 PyPI 签名的父元数据中。建立初始信任后,开发者需要使用至少一个公钥对应的私钥对他们上传到 PyPI 的分发进行签名。开发者上传到 PyPI 的签名 TUF 元数据包含分发文件大小和哈希等信息,软件包管理器使用这些信息来验证下载的分发。
端到端签名的实际影响是委托项目信任所需的额外管理工作,以及开发者必须与分发一起上传到 PyPI 的签名元数据。具体来说,PyPI 预计会定期使用离线密钥对元数据进行签名,通过将项目添加到“已声明”元数据文件并对其进行签名。相比之下,在最低安全模型中,项目只使用在线密钥进行签名。端到端签名确实需要手动干预才能委托信任(即,使用离线密钥对元数据进行签名),但这是一次性成本,此后项目将获得更强大的 PyPI 泄露保护。
元数据签名、密钥管理和签名分发
本节讨论 PyPI 可能推荐给签名工具实现者的工具、签名方案和签名方法。开发者应使用这些工具来签名和上传分发到 PyPI。总结下面小节中讨论的推荐工具和方案,开发者可以以某种自动化方式生成加密密钥并签名元数据(使用 Ed25519 签名方案),其中元数据包含验证分发真实性所需的信息。然后开发者将元数据上传到 PyPI,软件包管理器(例如支持 TUF 元数据的 pip)可以下载。整个过程对下载 PyPI 分发的最终用户(使用支持 TUF 的软件包管理器)是透明的。
前三个小节(加密签名方案、加密密钥文件和密钥管理)涵盖了开发者发布过程的加密组件。也就是说,PyPI 支持的密钥类型、密钥的存储方式以及密钥的生成方式。接下来的两个小节讨论了应该修改以支持 TUF 元数据的 PyPI 模块。例如,Twine 和 Distutils 是两个应该修改的项目。最后,最后一个小节介绍了为签名工具推荐的自动化密钥管理和签名解决方案。
TUF 的设计在加密密钥类型、签名和签名方法方面具有灵活性。以下部分讨论的工具、修改和方法是签名工具实现者的建议。
密码签名方案:Ed25519
CPython 附带的软件包管理器 (pip) 必须在非 CPython 解释器上工作,并且不能有需要编译的依赖项(即,PyPI+TUF 集成不能要求编译 C 扩展才能验证加密签名)。签名验证必须在 Python 中完成,而纯 Python 中验证 RSA [8] 签名可能因速度问题而不切实际。因此,PyPI 可以使用 Ed25519 签名方案。
Ed25519 [9] 是一种使用小型加密签名和密钥的公钥签名系统。Ed25519 签名方案的纯 Python 实现可用。即使在 Python 中执行,Ed25519 签名的验证速度也很快。
密码密钥文件
实现方案可以使用 AES-256-CTR-Mode 加密密钥文件,并使用 PBKDF2-HMAC-SHA256 加强密码(默认为 100K 次迭代,但开发者可以覆盖此设置)。当前 TUF 的 Python 实现可以使用任何加密库(未来将增加对 PyCA 加密的支持),可以覆盖 PBKDF2 的默认迭代次数,并根据需要调整 KDF。
密钥管理:miniLock
需要一个易于使用的密钥管理解决方案。一种解决方案是从密码派生私钥,这样开发者就不必在多台计算机上管理加密密钥文件。miniLock 是如何实现这一点的示例。开发者可以将加密密钥视为辅助密码。miniLock 也非常适合像 Ed25519 这样的签名方案,后者只需要非常小的密钥。
第三方上传工具:Twine
第三方工具,例如 Twine,可以进行修改(如果它们希望支持包含 TUF 元数据的分发),以对开发者项目进行签名并上传到 PyPI。Twine 是一个与 PyPI 交互的工具,它使用 TLS 上传分发,并防止对用户名和密码的 MITM 攻击。
构建后端
构建后端可以进行修改,以签署元数据并将已签署的分发上传到 PyPI。
自动化签名解决方案
为开发者推荐一个易于使用的密钥管理解决方案。一种方法是从用户密码生成加密私钥,类似于 miniLock。尽管开发者签名可以保持可选,但这种方法可能不够充分,因为每个分发都可能有大量的未签名依赖项。如果这些依赖项中的任何一个未签名,它就会抵消项目通过签名其自身分发所获得的任何好处(即,攻击者只需要破坏一个未签名的依赖项即可攻击最终用户)。要求开发者手动签名分发和管理密钥预计会使密钥签名成为一个未使用的功能。
为签名工具推荐一种默认的、由 PyPI 协调的密钥管理和软件包签名解决方案,该方案对开发者来说是透明的,并且不需要密钥托管(与 PyPI 共享加密的私钥)。此外,签名工具应避免在每个开发者的多台机器之间共享私钥。这意味着密钥管理解决方案应支持每个项目的多个密钥。
以下概述了一个新的开发者可以将分发上传到 PyPI 时可遵循的自动化签名解决方案
- 注册一个 PyPI 项目。
- 输入辅助密码(独立于 PyPI 用户账户密码)。
- 可选:从第二台机器向开发者的 PyPI 用户账户添加一个新身份(在密码提示后)。
- 上传项目。
- 可选:与项目关联的其他维护者可以登录并输入辅助密码以将他们的身份添加到项目中。
步骤 1 是开发者 注册 PyPI 项目 的正常程序。
步骤 2 生成一个加密密钥文件(私钥),将 Ed25519 公钥上传到 PyPI,并对为分发生成的 TUF 元数据进行签名。
在步骤 3 中,通过简单输入密码,从第二台机器选择性地添加新身份,也会生成一个加密的私钥文件并上传一个 Ed25519 公钥到 PyPI。可以创建独立的身份,以允许开发者在多台机器上签署发布。现有已验证的身份(其公钥包含在项目元数据中或已上传到 PyPI)为新身份签名。默认情况下,项目元数据的签名阈值为“1”,其他已验证的身份可以创建新的发布以满足阈值。
步骤 4 将分发文件和 TUF 元数据上传到 PyPI。“快照过程”部分详细讨论了开发者将分发上传到 PyPI 所遵循的程序。
步骤 5 允许其他维护者以类似于步骤 2 的方式生成加密密钥文件。这些密钥应上传到 PyPI 并添加到 TUF 元数据中。此密钥可用于上传项目的未来版本。
在默认情况下,加密文件和签名的生成对开发者来说是透明的:开发者无需知道软件包是自动签名的。但是,签名工具应该具有灵活性;开发者可能希望自己生成密钥并自行处理密钥管理。在这种情况下,开发者只需将他们的公钥上传到 PyPI。
存储库和开发者 TUF 工具目前支持所有前面提到的建议,除了自动化签名解决方案,该解决方案应添加到 Distlib、Twine 和其他第三方签名工具中。自动化签名解决方案调用可用的存储库工具函数来签名元数据并生成加密密钥文件。
快照过程
快照过程相当简单,应自动化。快照过程必须在内存中保存最新的根、目标和委托角色的工作集。每隔一分钟左右,快照过程将对这个最新的工作集进行签名。(请记住,项目上传以并发安全的方式不断通知快照过程最新的委托元数据。快照过程实际上将对最新工作集的副本进行签名,而内存中的最新工作集将通过项目事务过程不断传递的信息进行更新。)快照过程必须生成并签名新的时间戳元数据,该元数据将担保上一步生成的元数据(根、目标和委托角色)。最后,快照过程必须向客户端提供代表最新快照的新时间戳和快照元数据。
一个“已声明”或“最近声明”的项目需要在其向 PyPI 提交的事务中,不仅上传目标(一个简单的索引以及分发),还要上传 TUF 元数据。项目可以通过上传一个包含两个目录的 ZIP 文件来实现:/metadata/(包含委托的目标元数据文件)和 /targets/(包含目标,例如项目简单索引和由委托的目标元数据签名的分发)。
每当项目向 PyPI 上传元数据或目标文件时,PyPI 应至少检查项目 TUF 元数据的以下属性:
- 该项目已向 PyPI 注册的开发者密钥必须有达到阈值数量的密钥已签署代表该项目“根”的委托目标元数据文件(例如 metadata/targets/ project.txt)。
- 委托目标元数据文件的签名必须有效。
- 委托目标元数据文件不得过期。
- 委托目标元数据必须与目标一致。
- 委托人不得委托未经其他委托人委托给自己的目标。
- 受托人不得为未经委托人委托给自己的目标进行签名。
如果 PyPI 选择检查项目 TUF 元数据,则 PyPI 可以选择拒绝发布任何不符合这些要求的元数据或目标文件集。
PyPI 必须强制实施访问控制,确保每个项目只能写入其负责的 TUF 元数据。它必须通过确保项目上传过程写入正确的元数据以及这些元数据中的正确位置来实现。例如,一个未声明项目的项目上传过程必须写入正确委托的未声明元数据中的正确目标路径,用于该项目的目标。
在极少数情况下,PyPI 可能希望以向后不兼容的方式扩展项目的 TUF 元数据格式。请注意,PyPI 将无法自动代表项目重写现有 TUF 元数据,以将元数据升级到新的向后不兼容格式,因为这会使开发者密钥签名的元数据失效。相反,软件包管理器应编写为识别和处理多个不兼容版本的 TUF 元数据,以便为已声明和最近声明的项目提供合理的时间将其元数据迁移到更新但向后不兼容的格式。处理此版本更改的一种机制在 TAP 14 中描述。
如果 PyPI 最终耗尽磁盘空间来生成新的、一致的快照,那么 PyPI 可以使用类似“标记清除”算法来删除足够过时的、一致的快照。也就是说,只删除不再使用的过时元数据,如“时间戳”和“快照”。具体来说,为了保留最新的、一致的快照,PyPI 会从最新快照的根(“时间戳”)开始遍历对象,标记所有已访问的对象,并删除所有未标记的对象。最近的几个一致快照也可以类似地保留。删除一个一致快照将导致客户端在请求已删除快照的任何目标时,除了 HTTP 404 响应之外,看不到任何内容。客户端应然后(像以前一样)使用最新的、一致的快照重试他们的请求。
所有支持 TUF 元数据的软件包管理器都必须进行修改,以便下载每个元数据和目标文件(时间戳元数据除外),通过在文件请求中包含文件中加密哈希值作为文件名的一部分。遵循下一小节中推荐的文件名约定,对 filename.ext 文件的请求将转换为对 digest.filename 文件的等效请求。
最后,PyPI 应该使用事务日志来记录项目事务过程和队列,以便在服务器故障后更容易从错误中恢复。
生成一致的快照
PyPI 负责更新(根据项目情况)已声明、最近声明或未声明的元数据以及相关的委托元数据。每个项目都必须在一个事务中上传其元数据和目标集。上传的文件集称为“项目事务”。PyPI 如何验证项目事务中的文件将在后面的章节中讨论。本节的重点是 PyPI 如何响应项目事务。
每个元数据和目标文件都必须在其文件名中包含其 BLAKE2b-256 哈希值的 十六进制摘要,PyPI 可以在文件上传后将其添加到文件名前。对于本 PEP,建议 PyPI 采用一种简单的命名约定形式:digest.filename,其中 filename 是原始文件名,不包含哈希值的副本,digest 是哈希值的十六进制摘要。
当一个未声明的项目上传一个新的事务时,一个项目事务过程必须添加所有新的目标文件和相关的未声明的委托元数据。项目上传过程必须将新的未声明委托元数据通知快照过程。
当一个“最近声明”的项目上传一个新事务时,项目上传过程必须添加所有新的目标文件和项目的委托目标元数据。如果项目是新的,那么项目上传过程还必须添加新的“最近声明”元数据,其中包含项目的公钥(必须是事务的一部分)。“最近声明”项目的阈值由上传过程设置为“1”。最后,项目上传过程必须将新的“最近声明”元数据以及项目当前的委托目标元数据集通知快照过程。
已声明项目的上传过程略有不同,因为 PyPI 管理员会定期(可能每两周到一个月进行一次手动操作)将项目从“最近声明”角色移到“已声明”角色。(将项目从“最近声明”移到“已声明”是一个手动过程,因为 PyPI 管理员必须使用离线密钥对已声明项目的分发进行签名。)然后,项目上传过程必须添加新的“最近声明”和“已声明”元数据以反映此迁移。与“最近声明”项目的情况一样,项目上传过程必须始终添加已声明项目的所有新的目标文件和委托目标元数据。最后,项目上传过程必须将新的“最近声明”或“已声明”元数据以及项目当前的委托目标元数据集通知一致快照过程。
项目上传过程应自动化,除非 PyPI 管理员将项目从“最近声明”角色移到“已声明”角色。项目上传过程也必须以原子方式应用:要么添加所有元数据和目标文件,要么都不添加。项目事务过程和快照过程应并发工作。最后,项目上传过程应在内存中保留最新的“已声明”、“最近声明”和“未声明”元数据,以便在新的一致快照中正确更新它们。
队列可以按照出现的顺序并发处理,前提是遵守以下规则
- 任何一对项目上传过程都不能并发处理同一个项目。
- 任何一对项目上传过程都不能并发处理属于同一委托“未声明”角色的“未声明”项目。
- 任何一对项目上传过程都不能并发处理新的最近声明项目。
- 任何一对项目上传过程都不能并发处理新的已声明项目。
- 当一个项目上传过程正在处理一个新的已声明项目时,另一个项目上传过程不能处理一个新的最近声明项目,反之亦然。
必须遵守这些规则,以确保元数据不会被不一致地读取或写入。
审计快照
如果恶意方攻击 PyPI,他们可以使用任何在线密钥对任意文件进行签名。拥有离线密钥的角色(即,root 和 targets)仍然受到保护。为了安全地从存储库攻击中恢复,应审计快照,以确保文件只恢复到受信任的版本。
当检测到存储库被攻破时,必须验证三类信息的完整性
- 如果存储库的在线密钥已泄露,可以通过让“目标”角色签署新的元数据,并委托给新的密钥来撤销它们。
- 如果存储库上的角色元数据已更改,这将影响由在线密钥签名的元数据。自泄露以来创建的任何角色信息都应丢弃。因此,新项目的开发者将需要重新注册他们的项目。
- 如果软件包本身可能已被篡改,可以使用存储的哈希信息进行验证,这些哈希信息对应于在泄露之前存在于受信任元数据中的软件包。此外,由“已声明”角色中的开发者签名的新分发可以安全保留。然而,由“最近声明”或“未声明”角色中的开发者签名的任何分发都应丢弃。
为了在发生泄露时安全地恢复快照,PyPI 应该维护少量自己的镜像,并按照某种计划复制 PyPI 快照。镜像协议可以立即用于此目的。镜像必须安全隔离,使其仅负责镜像 PyPI。可以互相检查镜像,以检测意外或恶意故障。
另一种方法是定期生成每个“快照”的加密哈希并发布到推特。例如,收到推文后,用户会提供实际的元数据,然后存储库维护者能够验证元数据的加密哈希。或者,PyPI 可以定期归档自己的“快照”版本,而不是依赖外部提供的元数据。在这种情况下,PyPI 应该获取存储库上每个软件包的加密哈希,并将这些数据存储在离线设备上。如果任何软件包哈希已更改,则表明发生了攻击。
提供不同版本元数据或将软件包版本冻结在特定版本的攻击,可以通过 TUF 使用隐式密钥撤销和元数据不匹配检测等技术来处理 [2]。
密钥泄露分析
本 PEP 涵盖了最高安全模型、应添加以支持分发持续交付的 TUF 角色、如何生成和签名每个角色的元数据,以及如何支持由开发者签名的分发。其余部分讨论了 PyPI 应如何审计存储库元数据,以及 PyPI 可以用来检测和从 PyPI 泄露中恢复的方法。
表 1 总结了当达到阈值数量的私有加密密钥(属于任何 PyPI 角色)被泄露时可能发生的几种攻击。最左侧的列列出了已泄露的角色(或角色组合),右侧的列显示已泄露的角色是否使客户端容易受到恶意更新、冻结攻击或元数据不一致攻击。
| 角色泄露 | 恶意更新 | 冻结攻击 | 元数据不一致攻击 |
|---|---|---|---|
| 时间戳 | 否 快照和目标或任何委托角色需要合作 | 是 受限于最早的根、目标或 bin 元数据过期时间 | 否 快照需要合作 |
| 快照 | 否 时间戳和目标或任何委托角色需要合作 | 否 时间戳需要合作 | 否 时间戳需要合作 |
| 时间戳 和 快照 | 否 目标或任何委托角色需要合作 | 是 受限于最早的根、目标或 bin 元数据过期时间 | 是 受限于最早的根、目标或 bin 元数据过期时间 |
| 目标 或 已声明 或 最近声明 或 未声明 或 项目 | 否 时间戳和快照需要合作 | 不适用 需要时间戳和快照 | 不适用 需要时间戳和快照 |
| (时间戳 和 快照) 和 项目 | 是 | 是 受限于最早的根、目标或 bin 元数据过期时间 | 是 受限于最早的根、目标或 bin 元数据过期时间 |
| (时间戳 和 快照) 和 (最近声明 或 未声明) | 是 但仅限于未被“已声明”委托的项目 | 是 受限于最早的根、目标、已声明、最近声明、项目或未声明元数据过期时间 | 是 受限于最早的根、目标、已声明、最近声明、项目或未声明元数据过期时间 |
| (时间戳 和 快照) 和 (目标 或 已声明) | 是 | 是 受限于最早的根、目标、已声明、最近声明、项目或未声明元数据过期时间 | 是 受限于最早的根、目标、已声明、最近声明、项目或未声明元数据过期时间 |
| 根 | 是 | 是 | 是 |
表 1:通过泄露某些角色密钥组合可能发生的攻击。在 2013 年 9 月,展示了当时最新版本的 pip 如何容易受到这些攻击,以及 TUF 如何保护用户免受这些攻击 [7]。由离线密钥签名的角色以粗体显示。
请注意,泄露 targets 或任何委托角色(项目目标元数据除外)并不能立即允许攻击者提供恶意更新。攻击者还必须泄露 timestamp 和 snapshot 角色(两者都是在线的,因此更容易被泄露)。这意味着为了发起任何攻击,不仅要能够充当中间人,还要泄露 timestamp 密钥(或泄露 root 密钥并签署新的 timestamp 密钥)。为了发起除冻结攻击之外的任何攻击,还必须泄露 snapshot 密钥。最后,PyPI 基础设施的泄露可能会对 recently-claimed 项目引入恶意更新,因为这些角色的密钥是在线的。
发生密钥泄露时
密钥泄露意味着属于开发者或 PyPI 上的角色以及 PyPI 基础设施的密钥达到阈值数量被泄露,并用于在 PyPI 上签署新的元数据。
如果项目的开发者密钥达到阈值数量被泄露,项目必须采取以下步骤
- 项目元数据和目标必须恢复到项目已知未被泄露的最后一个已知良好一致快照。这可以通过开发者使用新密钥重新打包和重新签名所有目标来完成。
- 项目的元数据必须增加版本号,适当地延长有效期,并更新签名。
而 PyPI 必须采取以下步骤
- 从“最近声明”或“已声明”角色中撤销被泄露的开发者密钥。这通过用新颁发的开发者密钥替换被泄露的开发者密钥来完成。
- 必须发布新的带时间戳的一致快照。
如果“时间戳”、“快照”、“最近声明”或“未声明”密钥达到阈值数量被泄露,则 PyPI 必须采取以下步骤
- 从根角色撤销“时间戳”、“快照”和“目标”角色密钥。这通过用新颁发的密钥替换被泄露的“时间戳”、“快照”和“目标”密钥来完成。
- 通过替换“最近声明”和“未声明”密钥为新颁发的密钥,从“目标”角色中撤销这些密钥。签名新的目标角色元数据并丢弃新密钥(因为,如前所述,这增加了目标元数据的安全性)。
- 清除“最近声明”角色中的所有目标或委托,并删除所有相关的委托目标元数据。最近注册的项目应再次向 PyPI 注册其开发者密钥。
- “最近声明”和“未声明”角色的所有目标都应与已知没有时间戳、快照、最近声明或未声明密钥被泄露的最后一个已知良好一致快照进行比较。在被泄露的一致快照中,与最后一个已知良好一致快照不匹配的已添加、已更新或已删除的目标应恢复到其以前的版本。在确保所有未声明目标的完整性后,必须重新生成未声明元数据。
- “最近声明”和“未声明”元数据必须增加版本号,适当延长有效期,并更新签名。
- 必须发布新的带时间戳的一致快照。
即使只有其中一个角色被泄露,这也会预先保护所有这些角色。
如果“目标”或“已声明”密钥达到阈值数量被泄露,那么在没有“时间戳”和“快照”密钥的情况下,攻击者能做的事情很少。在这种情况下,PyPI 必须简单地通过在“根”和“目标”角色中分别用新密钥替换被泄露的“目标”或“已声明”密钥来撤销它们。
如果“时间戳”、“快照”和“已声明”密钥达到阈值数量被泄露,则 PyPI 除采取“时间戳”或“快照”密钥被泄露时采取的步骤外,还必须采取以下步骤
- 从目标角色撤销已声明角色密钥,并用新颁发的密钥替换它们。
- 已声明角色的所有项目目标都应与已知没有“时间戳”、“快照”或“已声明”密钥被泄露的最后一个已知良好一致快照进行比较。在被泄露的一致快照中,与最后一个已知良好一致快照不匹配的已添加、已更新或已删除的目标可以恢复到其以前的版本。在确保所有已声明项目目标的完整性后,必须重新生成已声明元数据。
- 已声明元数据必须增加版本号,适当延长有效期,并更新签名。
遵循这些步骤将预先保护所有这些角色,即使其中只有一个可能被泄露。
如果根密钥达到阈值数量被泄露,则 PyPI 必须采取与目标角色被泄露时相同的步骤。所有根密钥也必须被替换。
还建议 PyPI 通过安全公告充分记录泄露事件。当使用 pip-with-TUF 的用户因“时间戳”、“快照”或“根”角色的密钥不再有效而无法安装或更新项目时,这些安全公告将最具信息量。用户可以访问 PyPI 网站查阅安全公告,这些公告将有助于解释用户为何无法再安装或更新,然后采取相应的行动。当因泄露而未撤销达到阈值数量的“根”密钥时,新的“根”元数据可以安全更新,因为达到阈值数量的现有“根”密钥将用于签署新“根”元数据的完整性。TUF 客户端将能够通过达到阈值数量的先前已知“根”密钥来验证新“根”元数据的完整性。这将是常见情况。在最坏情况下,即因泄露而撤销了达到阈值数量的“根”密钥时,最终用户可以选择通过带外机制更新新的“根”元数据。
附录 A:PyPI 构建农场和端到端签名
PyPI 管理员打算支持一个中央构建农场。PyPI 构建农场将在 PyPI 基础设施和支持的平台上,为开发者上传的每个分发自动生成一个 Wheel。软件包管理器可能会通过下载这些 PyPI Wheel(比源分发安装快得多)来安装项目,而不是开发者签名的源分发。在实施最高安全模型之前,应调查具有端到端签名的中央构建农场的含义。
中央构建农场和端到端签名的一个问题是,开发者不太可能在 Wheel 分发在 PyPI 基础设施上生成后对其进行签名。然而,从开发者签名的源分发生成 Wheel 仍然有益,前提是构建 Wheel 是一个确定性过程。如果确定性构建不可行,开发者可以将这些 Wheel 的信任委托给一个用在线密钥对 Wheel 进行签名的 PyPI 角色。
参考资料
致谢
本材料基于国家科学基金会资助的 CNS-1345049 和 CNS-0959138 项目。本材料中表达的任何观点、发现、结论或建议均为作者的观点,不一定反映国家科学基金会的观点。
我们感谢 Alyssa Coghlan、Daniel Holth、Donald Stufft、Sumana Harihareswara 以及 distutils-sig 社区的全体成员,感谢他们帮助我们思考如何以可用且高效的方式将 TUF 与 PyPI 集成。
Roger Dingledine, Sebastian Hahn, Nick Mathewson, Martin Peck 和 Justin Samuel 帮助我们从 Tor 项目的前身 Thandy 设计了 TUF。
我们感谢 Konstantin Andrianov、Geremy Condra、Zane Fisher、Justin Samuel、Tian Tian、Santiago Torres、John Ward 和 Yuyu Zheng 为开发 TUF 所做的努力。
版权
本文档已置于公共领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0480.rst