Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

PEP 458 – 使用签名存储库元数据保护 PyPI 下载

作者:
Trishank Karthik Kuppusamy <karthik at trishank.com>, Vladimir Diaz <vladimir.diaz at nyu.edu>, Marina Moore <mm9693 at nyu.edu>, Lukas Puehringer <lukas.puehringer at nyu.edu>, Joshua Lock <jlock at vmware.com>, Lois Anne DeLong <lad278 at nyu.edu>, Justin Cappos <jcappos at nyu.edu>
发起人:
Alyssa Coghlan <ncoghlan at gmail.com>
BDFL 委托
Donald Stufft <donald at stufft.io>
讨论至:
Discourse 帖子
状态:
已接受
类型:
标准跟踪
主题:
打包
创建日期:
2013年9月27日
发布历史:
2019年1月6日,2019年11月13日
决议:
Discourse 消息

目录

摘要

本 PEP 描述了 PyPI 基础设施所需的更改,以确保用户从 PyPI 获取有效软件包。这些更改应尽可能减少对生态系统其他部分的影响。本 PEP 侧重于 PyPI 和用户之间的通信,因此不需要软件包开发人员采取任何行动。开发人员将使用当前流程上传软件包,PyPI 将自动为这些软件包生成已签名的存储库元数据。

为了使安全机制有效,PyPI 消费者(如 pip)需要进行额外的工作来验证 PyPI 提供的签名和元数据。此验证对用户来说可以是透明的(除非失败),并提供自动安全机制。TUF 存储库中提供了如何使用 TUF 元数据的文档。但是,对 PyPI 消费者的更改并不是发布 PyPI 元数据的先决条件,可以根据各个项目的时间表和优先级进行。

提议的 TUF 集成

本 PEP 提出了如何将更新框架 [2] (TUF) 与 Python Package Index (PyPI [1]) 集成。TUF 旨在作为软件更新程序或软件包管理器的灵活安全附加组件。该框架的完整实现集成了最佳安全实践,例如分离角色职责、采用多方签名规则来签署软件包、将签名密钥离线保存以及撤销过期或泄露的签名密钥。因此,攻击者需要窃取多个独立存储的签名密钥,才能危及负责指定存储库可用文件的角色。或者,负责指示存储库最新快照的角色也可能被泄露。

本 PEP 中提议的初始集成将允许现代软件包管理器(如 pip [3])在 PyPI 镜像和 PyPI 自身的内容分发网络受到攻击时更加安全,并能更好地保护用户免受此类攻击。具体来说,本 PEP 描述了 PyPI 流程应如何调整以生成和合并 TUF 元数据(即最低安全模型)。此最低安全模型支持验证使用存储在 PyPI 上的密钥签名的 PyPI 分发。开发人员上传的分发由 PyPI 签名,开发人员无需采取任何操作(除了上传分发),并且可以立即下载。最低安全模型还通过自动化大部分签名过程来最大程度地减少 PyPI 管理职责。

本 PEP 中没有讨论支持由开发人员签名的项目分发(最高安全模型)。此可能的未来扩展在 PEP 480 中详细介绍。最高安全模型需要更多的 PyPI 管理工作(但客户端无需额外工作),还为开发人员/发布者提出了易于使用的密钥管理解决方案、关于如何与 PyPI 基础设施上潜在的未来构建农场接口的想法,以及端到端签名的可行性。

尽管本 PEP 提供了实施建议,但它并未规定软件包管理器(例如 pip)应如何调整以使用 TUF 元数据从 PyPI 安装或更新项目。对在客户端采用 TUF 感兴趣的软件包管理器可以查阅其 库文档,该文档正是为此目的而创建的。

非目标

本 PEP 不会从 PyPI 中删除任何现有功能。特别是,它不会取代对 OpenPGP 签名的现有支持。开发人员可以继续将分离的 OpenPGP 签名与分发一起上传。将来,PEP 480 可能会允许开发人员使用其 OpenPGP 密钥直接签署 TUF 元数据。

PEP 状态

由于实施此 PEP 需要大量工作,在2019年初,它被推迟,直到获得适当的资金来实施该 PEP。Python 软件基金会获得了这笔资金 [22],新的 PEP 共同作者重新启动了 PEP 讨论

动机

对软件存储库的攻击很常见,即使是在具有非常良好安全 实践 的组织中也是如此。由此导致的存储库泄露允许攻击者编辑存储在存储库上的所有文件,并使用存储在存储库上的任何密钥(在线密钥)对这些文件进行签名。在许多签名方案(如 TLS)中,这种访问允许攻击者替换存储库上的文件,并使其看起来像是来自 PyPI。如果没有办法撤销和替换受信任的私钥,从存储库泄露中恢复将非常具有挑战性。除了存储库泄露的危险之外,软件存储库还容易受到网络攻击者 (MITM) 拦截和更改文件的攻击。这些以及其他对软件存储库的攻击在此处 详细介绍

本 PEP,连同 PEP 480 中的后续提案,旨在保护 PyPI 用户免受 PyPI 软件包的完整性、一致性和新鲜度属性的损害,并通过减轻密钥风险和提供从 PyPI 或其签名密钥泄露中恢复的机制来增强泄露恢复能力。

2013年1月5日,Python 软件基金会 (PSF) 宣布 [4] Python 和 Jython 的 python.org 维基发生安全漏洞。结果,所有维基数据都被销毁。幸运的是,PyPI 基础设施未受此漏洞影响。然而,此事件提醒人们,PyPI 需要采取防御措施,在发生泄露时尽可能保护用户。软件存储库攻击一直都在发生 [5]。PSF 必须接受安全漏洞的可能性,并相应地准备 PyPI,因为它是一个被成千上万甚至数百万人使用的宝贵资源。

在维基攻击之前,PyPI 使用 MD5 散列告知软件包管理器(如 pip)分发文件在传输过程中是否损坏。然而,缺少 SSL 使得软件包管理器难以验证到 PyPI 的传输完整性。因此,在 pip 和 PyPI 之间发起中间人攻击并任意更改分发内容很容易。结果,用户可能会被诱骗安装恶意分发。在维基攻击之后,提出了几个步骤(其中一些已实施)以提供比以前高得多的安全级别。这些步骤包括要求使用 SSL 与 PyPI 通信 [6],限制项目名称 [7],以及从 MD5 迁移到 SHA-2 散列 [8]

尽管这些步骤是必要的,但它们不足以保护分发,因为攻击仍然可能通过其他途径发生。例如,公共镜像被信任诚实地镜像 PyPI,但一些镜像可能会出现异常行为,无论是意外还是恶意干预。软件包管理器(如 pip)应该使用 PyPI 的签名来验证从 公共镜像 下载的分发文件,但目前已知没有一个真正这样做 [10]。因此,明智的做法是增加更多安全措施来检测来自公共镜像或内容分发网络 [11] (CDNs) 的攻击。

尽管官方镜像已在 PyPI 上弃用,但软件包管理器仍存在各种其他攻击向量 [13]。这些攻击可能导致客户端系统崩溃,导致安装过时的分发,甚至允许攻击者执行任意代码。2013 年 9 月,Distutils 邮件列表中发布了一篇帖子,表明当时最新版本的 pip 容易受到此类攻击,以及 TUF 如何保护用户免受其害 [14]。具体来说,测试了 pip 在有无 TUF 的情况下如何应对这些攻击。测试的攻击包括重放和冻结、任意安装、慢速检索和无休止数据。该帖子还演示了 PyPI 被攻破时 pip 将如何响应。

为了为 PyPI 提供具有抗泄露能力的保护,本 PEP 提出了使用更新框架 [2] (TUF)。TUF 提供了针对软件更新系统中各种攻击的保护,同时还提供了从存储库泄露中恢复的机制。TUF 已被许多组织在生产环境中使用,包括在云原生计算基金会的 Notary 服务中使用,该服务为 Docker Registry 中的容器镜像签名提供了基础设施。TUF 规范已通过三项独立的安全 审计

本 PEP 的范围是保护用户免受 PyPI 镜像以及 PyPI 自身 TLS 终止和内容分发基础设施泄露的影响。PEP 480 中讨论了对 PyPI 自身泄露的保护。

威胁模型

威胁模型假定以下内容

  • 离线密钥安全可靠地存储。
  • 攻击者 不能 破坏 PyPI 在线存储的受信任密钥。
  • 攻击者可以响应客户端请求。

如果攻击者能够导致客户端安装(或保留已安装)的软件分发文件不是最新版本,则认为攻击者成功。如果攻击者阻止更新的安装,他们不希望客户端意识到有问题。

此威胁模型描述了最低安全模型。PEP 480 中描述的最高安全模型还假设攻击者可以泄露 PyPI 的在线密钥。

定义

本文件中的关键词“必须”、“不得”、“要求”、“应”、“不应”、“建议”、“可以”和“可选”应按照 RFC 2119 中的描述进行解释。

本 PEP 仅关注将 TUF 集成到 PyPI 中。但是,鼓励读者回顾 TUF 设计原则 [2],并且 熟悉 TUF 规范 [16]

本 PEP 中使用的以下术语在 Python 打包术语表 [17] 中定义: 项目发布分发

本 PEP 中使用的其他术语定义如下

  • 角色:TUF 指定一个 角色和多个其他角色, 角色直接或间接将职责委托给这些角色。术语 顶层 角色指 角色和由 角色直接指定的任何角色,即 时间戳快照目标 角色。每个角色都有一个元数据文件,该文件被信任提供。
  • 分发文件:一个版本化的存档文件,包含用于分发发布版的 Python 软件包、模块和其他资源文件。在本 PEP 中,术语 分发文件分发软件包 [17],或简称 分发软件包 可以互换使用。
  • 简单索引:包含指向分发文件内部链接的 HTML 页面。
  • 目标文件:通常,目标文件是 PyPI 上所有应通过 TUF 保证其完整性的文件。通常,这包括分发文件和 PyPI 元数据,例如简单索引。
  • 元数据:元数据是描述角色、其他元数据和目标文件的签名文件。如果未另行指定,元数据表示 TUF 特定的元数据。
  • 存储库:存储库是命名元数据和目标文件的来源。客户端请求存储在存储库上的元数据和目标文件。
  • 一致性快照:一组 TUF 元数据和目标文件,捕获了 PyPI 上所有项目在某个固定时间点的完整状态。
  • 开发人员:项目的拥有者或维护者,被允许更新 TUF 元数据以及项目的目标文件。
  • 在线密钥:必须存储在 PyPI 服务器基础设施上的私有加密密钥。这通常是为了允许使用密钥进行自动化签名。但是,攻击者一旦攻破 PyPI 基础设施,将能够读取这些密钥。
  • 离线密钥:必须独立于 PyPI 服务器基础设施存储的私有加密密钥。这可以防止使用该密钥进行自动化签名。攻破 PyPI 基础设施的攻击者无法立即读取这些密钥。
  • 门限签名方案:一个角色可以通过指定至少 n 个密钥中的 t 个密钥来签署其元数据来提高其对密钥泄露的弹性。泄露 t-1 个密钥不足以泄露该角色本身。声明一个角色需要 (t, n) 个密钥表示门限签名属性。

TUF 概述

在最高层面上,TUF 为应用程序提供了一种安全的方法来了解和获取文件的新版本。表面上看,这听起来很简单。更新应用程序的基本步骤是

  • 知道存在更新。
  • 下载最新版本更新文件的正确副本。

问题在于,只有在没有恶意活动的情况下,更新应用程序才简单。如果攻击者试图干扰这些看似简单的步骤,他们可以做很多事情。

假设一个软件更新器采用大多数系统的方法(至少是那些试图安全的系统)。它下载它想要的文件和该文件的加密签名。软件更新器已经知道它信任哪个密钥来生成签名。它检查签名是否正确,并且是由这个受信任的密钥生成的。不幸的是,软件更新器仍然面临许多风险,包括以下场景

  • 攻击者不断向软件更新器提供相同的更新文件,因此它从不意识到存在更新。
  • 攻击者向软件更新器提供一个它已经拥有的旧的、不安全的文件版本,因此它下载那个文件并盲目地使用它,认为它是新的。
  • 攻击者向软件更新器提供了一个文件的新版本,但不是最新的。该文件对软件更新器来说是新的,但它可能不安全,并可能被攻击者利用。
  • 攻击者破坏了用于签名这些文件的密钥,现在软件更新器下载了一个正确签名的恶意文件。

TUF 旨在通过向存储库添加签名元数据(描述存储库文件的文本文件)并在更新过程中引用元数据文件来解决这些攻击和其他攻击。存储库文件在移交给软件更新系统之前,会根据元数据中包含的信息进行验证。该框架还提供多重签名信任、加密密钥的显式和隐式撤销、元数据的责任分离以及最小化的密钥风险。有关 TUF 解决的存储库攻击和软件更新程序弱点的完整列表和概述,请参阅附录 A。

将 PyPI 与 TUF 集成

一个软件更新系统必须完成两项主要任务才能与 TUF 集成。首先,服务器端的存储库 必须 被修改以提供签名的 TUF 元数据。本 PEP 关注集成的前半部分,以及 PyPI 上支持使用 TUF 进行软件更新所需的更改。

其次,它必须将框架添加到更新系统的客户端。例如,TUF 可以 与 pip 软件包管理器集成。因此,未来的 pip 新版本 应该 默认使用 TUF 来下载和验证 PyPI 的分发,然后才安装它们。但是,可能会出现不可预见的问题,这可能会阻止用户通过 TUF 安装或更新分发(包括 pip 本身)。因此,pip 应该 提供一个选项,例如 --unsafely-disable-package-verification,以便在这些问题解决之前解决它们。请注意,建议的选项名称故意很长,因为必须帮助用户理解该操作是不安全的,通常不推荐。

我们假设 pip 将使用 TUF 仅验证从 PyPI 下载的分发。pip 可以 支持 TAP 4,以便使用 TUF 来验证从 其他地方 下载的分发。

PyPI 上需要哪些额外的存储库文件?

为了让像 pip 这样的软件包管理器能够使用 TUF 下载和验证分发,PyPI 必须添加一些额外的文件。这些额外的存储库文件称为 TUF 元数据,它们包含诸如哪些密钥可以被信任、文件的 加密哈希、签名、元数据版本号以及元数据应被视为过期后的日期等信息。

当软件包管理器想要检查更新时,它会要求 TUF 来完成这项工作。也就是说,软件包管理器永远不必处理这些额外的元数据或理解其底层原理。如果 TUF 报告有可用更新,软件包管理器就可以要求 TUF 从 PyPI 下载这些文件。TUF 会下载它们,并根据它也从存储库下载的 TUF 元数据进行检查。如果下载的目标文件是可信的,TUF 就会将它们移交给软件包管理器。

TUF 规范的 文档格式 部分提供了每种所需元数据类型及其预期内容的信息。下一节将介绍推荐用于 PyPI 的不同类型的元数据。

此外,所有目标文件都应在磁盘上至少提供两次。一次以其原始文件名提供,以实现向后兼容性;另一次以其 SHA-512 哈希包含在其文件名中提供。这是生成 一致性快照 所必需的。

根据所使用的文件系统,可以采用不同的数据去重机制来避免因目标文件的硬拷贝而增加存储空间。

PyPI 和 TUF 元数据

TUF 元数据提供客户端可用于做出更新决策的信息。例如, targets 元数据列出了 PyPI 上可用的目标文件,并包含每个目标文件所需的签名、加密哈希和文件大小。不同的元数据文件提供不同的信息,这些信息由不同的角色签名。 root 角色指示哪些元数据属于哪个角色。角色的概念允许 TUF 将职责委托给多个角色,从而最大限度地减少任何一个受损角色的影响。

TUF 需要四个顶层角色。它们是 roottimestampsnapshottargetsroot 角色指定顶层角色(包括其自身)的公共加密密钥。 timestamp 角色引用最新的 snapshot,并可以指示存储库何时有新的快照可用。 snapshot 角色指示所有 TUF 元数据文件(除了 timestamp)的最新版本。 targets 角色列出可用目标文件的文件路径及其加密哈希。文件路径必须相对于基本 URL 指定。这允许实际目标文件从任何位置提供,只要客户端可以访问基本 URL。每个顶层角色都将毫无例外地履行其职责。表 1 提供了 TUF 中角色的概述。

角色和职责
root 根角色是整个存储库的信任中心。根角色签署 root.json 元数据文件。此文件指示哪些密钥被授权用于每个顶层角色,包括根角色本身。必须指定“root”、“snapshot”、“timestamp”和“targets”角色,并且每个角色都有一组公钥。
targets 目标角色负责指示存储库中可用的目标文件。更准确地说,它分担提供更新内容信息的责任。目标角色签署 targets.json 元数据,并可以将存储库文件的信任委托给其他角色(委托角色)。
委托角色 如果顶层目标角色执行委托,则由此产生的委托角色可以提供自己的元数据文件。委托目标角色提供的元数据文件的格式与 targets.json 相同。与 targets.json 一样,属于委托角色的元数据文件的最新版本在快照角色的元数据中描述。
snapshot 快照角色负责确保客户端看到一致的存储库状态。它通过在 snapshot.json 中指示存储库上顶层目标和委托目标元数据文件的最新版本来提供存储库状态信息。root 和 timestamp 不在 snapshot.json 中列出,因为 timestamp 在 snapshot.json 创建后签署其新鲜度,而 root 拥有所有顶层密钥,需要提前信任任何顶层角色。
timestamp 时间戳角色负责提供有关可用更新及时性的信息。及时性信息通过频繁签署具有短过期时间的新 timestamp.json 文件来提供。此文件指示 snapshot.json 的最新版本。

表 1:TUF 角色的概述。

除非另有说明,本 PEP 建议使用 SHA-2 系列的 SHA2-512 函数对每个元数据或目标文件进行哈希。SHA-2 具有原生且经过充分测试的 Python 2 和 3 支持(允许在没有额外非 Python 依赖项的情况下验证这些哈希)。如果需要更强的安全保证,则可以同时使用 SHA2-256 和 SHA2-512,或同时使用 SHA2-256 和 SHA3-256。SHA2-256 和 SHA3-256 基于彼此非常不同的设计,提供了额外的保护以防止 碰撞攻击。但是,SHA-3 需要为 Python 2 安装额外的非 Python 依赖项。

签名元数据和存储库管理

顶层 root 角色签署顶层 timestampsnapshottargetsroot 角色的密钥。 timestamp 角色签署存储库元数据的每个新快照。 snapshot 角色签署 roottargets 和所有委托的 targets 角色。委托的 targets 角色 bins 进一步委托给 bin-n 角色,后者签署属于已注册 PyPI 项目的所有分发文件。

图 1 提供了 PyPI 中可用角色的概述,包括顶层角色和由 targets 委托的角色。该图还指示了用于签署每个角色的密钥类型,以及哪些角色被信任签署 PyPI 上可用的文件。接下来的两节将介绍签署存储库文件和每个角色使用的密钥类型的详细信息。

../_images/pep-0458-1.png

图 1:PyPI 上可用角色元数据概述。

变更最频繁的角色是 timestampsnapshot 和由 bins 委托的角色(即 bin-n)。当 roottargets 或委托元数据更新时, timestampsnapshot 元数据 必须 更新。然而,请注意, roottargets 元数据不太可能像委托元数据那样频繁更新。同样, bins 角色只会在添加、更新或删除 bin-n 角色时更新。因此, timestampsnapshotbin-n 元数据很可能会频繁更新(可能每分钟一次),因为委托元数据会频繁更新以支持项目的持续交付。持续交付是 PyPI 用于生成可以安全共存并独立于其他快照删除的快照的一组流程 [18]

每年,PyPI 管理员 签署 roottargets 角色密钥。自动化将持续签署所有项目的带时间戳快照。可用的存储库 元数据 API 可用于 管理 TUF 存储库

在标准操作中,当新的分发上传到 PyPI 时, bin-n 元数据将被更新和签名。但是,每次 PyPI 重新初始化时,还需要一个一次性在线初始化机制来为 PyPI 存储库中所有现有分发创建和签名 bin-n 元数据。

如何建立对 PyPI 根密钥的初始信任

像 pip 这样的软件包管理器 必须 在用户最初下载的安装文件中附带 root 元数据文件。这包括有关所有顶层角色(包括根密钥本身)信任的密钥信息。软件包管理器还必须捆绑一个 TUF 客户端库。TUF 客户端库可能下载的任何新版本的 root 元数据都将根据最初与软件包管理器捆绑的根密钥进行验证。如果根密钥被泄露,但仍有足够的密钥安全,则 PyPI 管理员 必须 推送新的 root 元数据以撤销对被泄露密钥的信任。如果根密钥的阈值被泄露,则 root 元数据 必须 带外更新。(但是,应选择根密钥的阈值,使此事件极不可能发生。)如果根密钥在软件包管理器的新版本之间被撤销或添加,则软件包管理器不一定需要立即更新,因为 TUF 更新过程会自动处理前一个 root 密钥的阈值签署新 root 密钥的情况(假设使用的 TUF 规范没有向后不兼容性)。因此,例如,如果软件包管理器最初附带版本 1 的 root 元数据,并且版本 1 中 root 密钥的阈值签署了版本 2 的 root 元数据,并且版本 2 中 root 密钥的阈值签署了版本 3 的 root 元数据,那么软件包管理器应该能够使用其 TUF 客户端库透明地将其 root 元数据的副本从版本 1 更新到版本 3。

因此,重申一下,任何随 CPython 提供的 pip 新版本(通过 ensurepip)都 必须 包含 root 元数据的最新良好副本和 TUF 客户端库。然后,软件包管理器内部的 TUF 客户端库加载 root 元数据并下载其余角色,包括在 root 元数据发生更改时对其进行更新。可查看 更新过程概述

最低安全模型

将 TUF 集成到 PyPI 中时需要考虑两种安全模型。本 PEP 中提出的模型是最低安全模型,它支持验证使用存储在 PyPI 上的私有加密密钥签名的 PyPI 分发。开发人员上传的分发由 PyPI 签名并可立即下载。本 PEP 的未来扩展(在 PEP 480 中讨论)提出了最高安全模型,并允许开发人员为其项目签名。开发人员的密钥不会在线存储:因此,项目在 PyPI 泄露时是安全的。

最低安全模型无需开发人员采取任何操作,并能抵御恶意 CDN [19] 和公共镜像。为了支持上传分发的持续交付,PyPI 使用在线密钥对项目进行签名。这种安全级别可以防止项目被镜像或 CDN 意外或故意篡改,因为两者都不会拥有签署项目所需的任何密钥。但是,它不能保护项目免受已攻破 PyPI 的攻击者,因为他们可以使用在线存储的密钥操纵 TUF 元数据。

本 PEP 提议 bin-n 角色使用在线密钥签署所有 PyPI 项目。这些 bin-n 角色 必须 都由上级 bins 角色委托,该角色使用离线密钥签名,并且反过来 必须 由顶层 targets 角色委托,该角色也使用离线密钥签名。这意味着,当软件包管理器(例如 pip,即使用 TUF)从 PyPI 上的项目下载分发文件时,它将咨询 targets 角色关于该分发文件的 TUF 元数据。如果最终没有通过 bins 委托给 bin-n 的角色指定分发文件,则认为它在 PyPI 上不存在。

请注意, targets 不直接委托给 bin-n,而是使用中介 bins 角色的原因是,这样可以轻松添加或删除其他委托,而不会影响 binsbin-n 的映射。这对于 PEP 480 的实现至关重要。

元数据有效期

roottargetsbins 角色的元数据应在一年内过期,因为这些元数据文件预计很少更改。

timestampsnapshotbin-n 元数据应在一天内过期,因为 CDN 或镜像应每天与 PyPI 同步。此外,这个宽裕的时间框架也考虑到了高度偏斜或漂移的客户端时钟。

元数据可扩展性

随着存储库上项目和分发数量的增长,TUF 元数据也需要相应增长。例如,考虑 bins 角色。2013 年 8 月,发现如果 bins 角色本身为大约 22 万个 PyPI 目标(即简单索引和分发)签名,则 bins 元数据的大小约为 42MB。本 PEP 不深入细节,但 TUF 具有一种所谓的 “哈希 bin 委托” 方案,将一个大型目标元数据文件拆分成许多小文件。这允许 TUF 客户端更新器智能地仅下载少量 TUF 元数据文件,以更新 bins 角色签名的任何项目。例如,将此方案应用于之前的存储库后,pip 下载 1.3KB 到 111KB 即可通过 TUF 安装或升级 PyPI 项目。

根据本文档更新以供实施时(2019 年 11 月 7 日)的调查结果(总结在表 2-3 中),PyPI bins 角色中的所有目标拆分,并将它们委托给 16,384 个 bin-n 角色(参见表 2 中的 C10)。每个 bin-n 角色将签署其 SHA2-512 哈希落入该 bin 的 PyPI 目标(参见图 1 和 一致性快照)。结果发现,此数量的 bin 将导致回头用户产生 5-9% 的元数据开销(相对于下载的分发文件的平均大小;参见表 3 中的 V13 和 V15),而首次安装 pip 的新用户将产生 69% 的开销(参见表 3 中的 V17)。

计算这些元数据开销百分比时使用的一些假设

  1. 我们忽略了 root、timestamp 和顶层 targets 元数据。
  2. pip 总是捆绑所有角色的最新良好元数据副本。
名称 描述
C1 SHA2-512 十六进制摘要中的字节数 128
C2 SHA2-512 公钥 ID 的字节数 64
C3 Ed25519 签名的字节数 128
C4 Ed25519 公钥的字节数 64
C5 目标相对文件路径的字节数 256
C6 编码目标文件大小的字节数 7
C7 编码版本号的字节数 6
C8 目标数(简单索引和分发) 2,273,539
C9 下载的分发平均字节数 2,184,393
C10 bin 数 16,384

C8 通过查询发布文件的数量计算得出。C9 通过取过去 31 天内 下载 的发布文件的平均大小(1,628,321 字节)和磁盘上发布文件的平均大小(2,740,465 字节)之间的平均值得出。Ee Durbin 于 2019 年 11 月 7 日提供了这些数字。

表 2:用于计算元数据开销的常量列表。

名称 描述 公式
V1 路径哈希前缀的长度 math.ceil(math.log(C10, 16)) 4
V2 路径哈希前缀总数 16**V1 65,536
V3 每个 bin 的平均目标数 math.ceil(C8/C10) 139
V4 每个 bin 的 SHA-512 哈希平均大小 V3*C1 17,792
V5 每个 bin 的目标路径平均大小 V3*C5 35,584
V6 每个 bin 的长度平均大小 V3*C6 973
V7 bin-n 元数据平均大小(字节) V4+V5+V6 54,349
V8 bins 中公钥 ID 的总大小 C10*C2 1,048,576
V9 bins 中路径哈希前缀的总大小 V1*V2 262,144
V10 bins 元数据估算大小(字节) V8+V9 1,310,720
V11 快照元数据估算大小(字节) C10*C7 98,304
V12 每个分发对回头用户(相同快照)的元数据开销估算大小 2*V7 108,698
V13 每个分发对回头用户(相同快照)的元数据开销估算百分比 round((V12/C9)*100) 5%
V14 每个分发对回头用户(不同快照)的元数据开销估算大小 V12+V11 207,002
V15 每个分发对回头用户(不同快照)的元数据开销估算百分比 round((V14/C9)*100) 9%
V16 每个分发对新用户的元数据开销估算大小 V14+V10 1,517,722
V17 每个分发对新用户的元数据开销估算百分比 round((V16/C9)*100) 69%

表 3:新用户和回头用户的元数据开销估算。

感兴趣的读者可以在 此处 找到元数据开销计算器的交互式版本

当回头用户的元数据开销超过 50% 时,bin 的数量 增加。目前,当目标数量从超过 200 万增加至少 10 倍到超过 2200 万时,这种情况 发生,届时回头用户和新用户的元数据开销将分别约为 50-54% 和 114%(假设 bin 数量保持不变)。如果 bin 数量增加,则所有用户的成本将有效地成为新用户的成本,因为他们的成本将主要由下载 bins 元数据中大量委托(偶尔发生)的成本所主导。如果新用户的成本被证明过高,主要是由于下载 bins 元数据的开销,那么在发生这种情况之前 重新审视此问题。

请注意,服务器上 bin 数量的更改对客户端是透明的。软件包管理器将需要下载一套全新的元数据,就像一个新用户一样,但此操作不需要任何显式代码逻辑或用户交互。

TUF 元数据可以通过二进制格式而非 JSON 文本格式表示,使其更紧凑。然而,数量足够多的项目和分发迟早会引入可伸缩性挑战,因此 bins 角色仍需要委托(如图 1 所示)来解决此问题。JSON 格式是一种开放且众所周知的数据交换标准,TUF 参考实现已支持该格式,因此本 PEP 推荐使用该数据格式。但是,由于委托数量庞大,所有元数据的压缩版本 也应 通过现有的 Warehouse HTTP 压缩机制提供给客户端。此外,JSON 元数据可以在发送给客户端之前进行压缩。TUF 参考实现目前不支持下载压缩的 JSON 元数据,但可以添加此功能以减小元数据大小。

PyPI 和密钥要求

本节将探讨 PyPI 上签署 TUF 角色所需的密钥类型。TUF 对数字签名算法的选择是无关紧要的。然而,本 PEP 建议所有数字签名都使用 Ed25519 算法 [15] 生成。Ed25519 具有原生且经过充分测试的 Python 支持(允许在没有额外非 Python 依赖项的情况下验证签名),使用小密钥,并受现代 HSM 和身份验证令牌硬件支持。

管理在线密钥

timestampsnapshot 和所有 bin-n 角色共享的在线密钥 可以 存储在 Python 基础设施上,无论是否加密。例如,密钥 可以 保留在自托管密钥管理服务(例如 Hashicorp Vault)或第三方服务(例如 AWS KMS、Google Cloud KMS 或 Azure Key Vault)上。

其中一些密钥管理服务允许将密钥存储在硬件安全模块(HSM)上(例如,Hashicorp Vault、AWS CloudHSM、Google Cloud HSM、Azure Key Vault)。这可以防止攻击者泄露在线私钥(尽管不能阻止其使用,但他们的行为现在可以进行加密审计)。然而,这需要修改参考 TUF 实现以支持 HSMs(WIP)。

无论此在线密钥存储在何处以及如何存储,其使用 仔细记录、监控和审计,理想情况下,以攻击者在攻破 PyPI 后无法立即关闭此记录、监控和审计的方式进行。

管理离线密钥

如前一节所述, roottargetsbins 角色密钥 必须 离线以实现最大安全性。这些密钥将离线,意味着它们的私钥 绝不能 存储在 PyPI 上,尽管其中一些 可以 在项目的私有基础设施中在线。

有一个离线密钥仪式,以生成、备份和存储这些密钥,方式是只有 Python 管理员在必要时(例如,轮换顶层 TUF 角色的密钥)才能读取私钥。因此,密钥 最好在没有侧信道 攻击 问题的物理位置生成,使用

  1. 一台可信的、 气隙隔离的 计算机,带有真随机数 生成器,并且在仪式后没有 数据 持久化
  2. 受信任的操作系统
  3. 一组受信任的第三方软件包(例如加密库的更新版本或 TUF 参考实现,如果受信任操作系统提供的版本不够新)

为了避免敏感数据(例如私钥)在仪式结束后持久化(除了备份介质上),离线密钥 使用强密码加密生成,优先级递减为:私有 HSM(例如 YubiHSM)、基于云的 HSM(例如上面列出的那些)、易失性内存(例如 RAM)或非易失性内存(例如 SSD 或 microSD)。如果密钥必须在非易失性内存上生成,则在安全备份密钥后,此内存 必须 不可恢复地销毁。

用于加密密钥的密码 存储在只有 Python 管理员才能访问的持久且值得信任的地方。

为了最大限度地减少仪式期间的 操作安全 错误, 编写脚本,在可信的密钥生成计算机上执行,以自动化仪式中繁琐的步骤,例如

  • 将生成新密钥和替换旧密钥所需的所有代码和数据(以前的 TUF 元数据和 root 密钥)导出到 人肉网络
  • 收紧防火墙,更新整个操作系统以修复安全漏洞,并对计算机进行气隙隔离
  • 所有 新的 TUF 元数据和密钥导出到加密备份介质。此备份提供了恢复 PyPI TUF 存储库所需数据的完整副本
  • 新的 TUF 元数据和在线密钥导出到加密备份介质。此备份提供了所有在线数据,可导入到 PyPI 基础设施中,并且在需要从以前存档状态恢复在线数据时很有用
  • 打印并保存新 TUF 元数据的加密哈希。此打印副本提供了额外的离线纸质备份,可在发生泄露时用作比较

注意, targetsbins 角色的一次性密钥 可以 在离线密钥仪式期间安全地生成、使用和删除。此外, root 密钥 可以 不在离线密钥仪式本身期间生成。相反,如上所述,t 个 n 个 Python 管理员的阈值 可以 在用于生成所有其他密钥的离线密钥仪式 之后 独立签署 root 元数据。

如何生成元数据?

项目开发人员希望他们上传到 PyPI 的分发能够立即下载。不幸的是,当许多读写者同时访问相同的元数据和目标文件时,会出现问题。也就是说,需要一种方法来确保元数据和目标文件的一致性,当多个开发人员同时更改这些文件时。PyPI 在没有 TUF 的情况下也存在一致性问题,但对于 必须 实时跟踪 PyPI 上可用文件的签名元数据来说,问题更为严重。

假设 PyPI 生成了一个 快照,它指示除了 timestamp 之外的所有元数据在版本 1 时的最新版本,并且客户端向 PyPI 请求此 快照。当客户端忙于下载此 快照 时,PyPI 会在例如版本 2 时对新快照进行时间戳。如果不确保元数据的一致性,客户端将发现其 快照 副本与 PyPI 上可用的内容不一致。结果将与攻击者注入的任意元数据无法区分。镜像尝试与 PyPI 同步时也会出现此问题。

一致性快照

为使 PyPI 上的 TUF 元数据与高度不稳定的目标文件保持一致, 使用一致性快照。每个一致性快照都捕获给定时间所有已知项目的状态,并 可以 安全地与任何其他快照共存,或独立删除,而不会影响任何其他快照。

为了保持一致性快照,所有 TUF 元数据在写入磁盘时 必须 在其文件名中包含版本号

VERSION_NUMBER.ROLENAME.json,
其中 VERSION_NUMBER 是一个递增的整数,ROLENAME 是顶级元数据角色之一—— rootsnapshottargets ——或委托的目标角色之一—— binsbin-n

唯一的例外是 timestamp 元数据文件,其版本在客户端执行更新时无法提前知道。 timestamp 元数据列出 snapshot 元数据的版本,而 snapshot 元数据又列出 targets 和委托 targets 元数据的版本,所有这些都作为给定一致性快照的一部分。

在正常使用中,版本号溢出不太可能发生。例如,一个 8 字节整数可以每毫秒递增一次,并持续近 3 亿年。如果攻击者任意增加版本号,存储库可以通过撤销被泄露的密钥并重置版本号来恢复,如 TUF 规范 中所述。

targets 或委托的 targets 元数据指的是实际的目标文件,包括上面指定的加密哈希。因此,要将目标文件标记为一致性快照的一部分,它在写入磁盘时 必须 在其文件名中包含其哈希

HASH.FILENAME
其中 HASH 是文件内容的哈希的 十六进制摘要,FILENAME 是原始文件名。

这意味着每个目标文件 可能 有多个副本,每个副本对应上面指定的每个加密哈希函数。

假设无限磁盘空间、严格递增的版本号且没有 哈希冲突,客户端可以在 PyPI 生成另一个快照时安全地从一个快照中读取。

使用 TUF 协议的客户端(如 pip) 必须 进行修改,以下载每个元数据和目标文件,除了 timestamp 元数据。这是通过在文件请求中包含文件版本(针对元数据)或文件加密哈希(针对目标文件)作为文件名来完成的。

通过这种简单而有效的方式,PyPI 能够捕获所有项目及相关元数据在给定时间的一致性快照。下一小节提供了此想法的实施细节。

注意:本 PEP 不禁止使用高级文件系统或工具来生成一致性快照。本 PEP 提出简单解决方案有两个重要原因。首先,该解决方案不强制 PyPI 使用任何特定的文件系统或工具。其次,通用的基于文件系统的方法允许镜像使用现有的文件传输工具(如 rsync)从 PyPI 高效传输一致性快照。

生成一致性快照

当新的分发文件上传到 PyPI 时,PyPI 必须 更新负责的 bin-n 元数据。请记住,所有目标文件都按其文件名哈希排序到 bin 中。PyPI 必须 还要更新 snapshot 以解释更新后的 bin-n 元数据,并更新 timestamp 以解释更新后的 snapshot 元数据。这些更新 由自动化 快照进程 处理。

文件上传 可以 并行处理,然而,一致性快照 必须 以严格的顺序生成。此外,只要分发文件是自包含的,就 可以 为每个上传的文件生成一致性快照。为此,上传过程将新的分发文件放入并发安全的 FIFO 队列中,快照过程一次从该队列中读取一个文件并执行以下任务

首先,它将新文件路径添加到相关的 bin-n 元数据中,增加其版本号,使用 bin-n 角色密钥进行签名,并将其写入 VERSION_NUMBER.bin-N.json

然后,它获取最新的 快照 元数据,更新其 bin-n 元数据版本号,增加自己的版本号,使用 快照 角色密钥进行签名,并将其写入 VERSION_NUMBER.snapshot.json

最后,快照进程获取最新的 时间戳 元数据,更新其 快照 元数据哈希和版本号,增加自己的版本号,设置新的过期时间,使用 时间戳 角色密钥进行签名,并将其写入 timestamp.json

更新一致性快照的 bin-n 元数据时,快照过程 还在相关的 bin-n 元数据中包含任何新的或更新的简单索引页面的哈希。请注意,简单索引页面可以在 API 调用时动态生成,因此保持其输出在一致性快照的有效期内稳定非常重要。

由于快照进程 必须 以严格的顺序生成一致性快照,因此它构成了一个瓶颈。幸运的是,签名操作足够快,每秒可以执行一千次或更多次。

此外,PyPI 可以 在生成相应的持续快照元数据之前向客户端提供分发文件。在这种情况下,客户端软件 通知用户完整的 TUF 保护尚不可用,但很快就会提供。

PyPI 使用 事务日志 记录上传过程和快照队列,以进行审计并在服务器故障后从错误中恢复。

清理旧元数据

为了避免因不断生成新的持续快照而导致磁盘空间耗尽,PyPI 定期删除旧的持续快照,即在过去某个合理时间(例如 1 小时)已过时的元数据和目标文件。

为了保留最新的、一致的快照,PyPI 可以使用“标记-清除”算法。也就是说,从最新的、一致的快照的根目录开始,即从 timestamp 经过 snapshot 再到 targets 和委托的 targets,直到目标文件,标记所有访问过的文件,并删除所有未标记的文件。最新的几个一致快照也可以以类似的方式保留。

删除一致快照将导致客户端对该一致快照中任何文件的请求仅看到 HTTP 404 响应。客户端应(SHOULD)随后使用最新的、一致的快照重试其请求(如前所述)。

请注意,即使经过版本控制,root 元数据也不属于任何一致快照。PyPI 不得(MUST NOT)删除旧版本的 root 元数据。这保证了客户端可以更新到最新的 root 角色密钥,无论其本地 root 元数据多么过时。

撤销对项目和分发的信任

项目或发行版有时需要被撤销。要撤销对项目或发行版的信任,bin-n 角色可以简单地删除相应的 targets 并重新签署 bin-n 元数据。此操作仅需要使用在线 bin-n 密钥执行操作。

密钥泄露分析

本 PEP 涵盖了最小安全模型、为支持持续交付发行版而应添加的 TUF 角色,以及如何为每个角色生成和签署元数据。其余部分讨论了 PyPI 应(SHOULD)如何审计仓库元数据,以及 PyPI 可用于检测和从 PyPI 泄露中恢复的方法。

表 4 总结了当一定数量的私有加密密钥(属于任何 PyPI 角色)被泄露时可能发生的一些攻击。最左列列出了已被泄露的角色(或角色组合),其右侧的列显示了被泄露的角色是否使客户端容易受到恶意更新、冻结攻击或元数据不一致攻击。请注意,如果 timestamp、snapshot 和 bin-n 角色存储在同一个在线位置,那么其中一个被泄露意味着它们都将被泄露。因此,该表将这些角色视为一个整体。PEP 480 中包含了考虑这些角色分别的表格版本。

角色泄露 恶意更新 冻结攻击 元数据不一致攻击
targets bins 否,timestamp 和 snapshot 需要协作
timestamp snapshot bin-n 是,受限于最早的 root、targets 或 bins 元数据过期时间
root

表 4:通过泄露某些角色密钥组合可能发生的攻击。2013 年 9 月,有人演示了 pip(当时的最新版本)如何容易受到这些攻击,以及 TUF 如何保护用户免受这些攻击 [14]

请注意,泄露 targetsbins 并不能立即允许攻击者提供恶意更新。攻击者还必须泄露 timestampsnapshot 角色,这两个角色都在线,因此更容易被泄露。这意味着,为了发起任何攻击,不仅必须能够充当中间人,还必须泄露 timestamp 密钥(或泄露 root 密钥并签署新的 timestamp 密钥)。要发起除冻结攻击之外的任何攻击,还必须泄露 snapshot 密钥。实际上,本 PEP 建议将 snapshottimestampbin-n 密钥一起存储,甚至对所有这些角色使用同一个密钥。因此,攻击者只需泄露这个单一服务器即可执行上述任何攻击。请注意,客户端仍然受到非签名基础设施(例如 CDN 或镜像)泄露的保护。此外,离线 root 密钥将允许仓库通过撤销在线密钥从攻击中恢复。

最大安全模型展示了 TUF 如何通过引入额外的角色进行端到端签名来缓解在线密钥泄露。关于如何生成开发者密钥和签署上传发行版的详细信息在PEP 480中提供。

发生密钥泄露时

密钥泄露意味着一定数量的密钥(属于 PyPI 上的元数据角色)以及 PyPI 基础设施已被泄露,并用于在 PyPI 上签署新的元数据。

如果一定数量的 timestampsnapshottargetsbinsbin-n 密钥已被泄露,则 PyPI 必须采取以下步骤:

  1. root 角色中撤销 timestampsnapshottargets 角色密钥。这通过用新发布的密钥替换被泄露的 timestampsnapshottargets 密钥来完成。
  2. 通过用新发布的密钥替换 bins 密钥,从 targets 角色中撤销 bins 密钥。签署新的 targets 角色元数据并丢弃新密钥(因为,如前所述,这增加了 targets 元数据的安全性)。
  3. 应(SHOULD)将所有 bin-n 角色的 targets 与最后一个已知良好的一致快照进行比较,在该快照中,timestampsnapshotbinsbin-n 密钥均未被已知泄露。在被泄露的一致快照中,与最后一个已知良好的一致快照不匹配的已添加、已更新或已删除的 targets 可以(MAY)恢复到其以前的版本。在确保所有 bin-n targets 的完整性后,应在 bins 元数据中更新其密钥。
  4. binsbin-n 元数据必须(MUST)增加其版本号,适当地延长过期时间,并更新签名。
  5. 必须(MUST)发布一个新的带时间戳的一致快照。

遵循这些步骤将预防性地保护所有这些角色,即使其中只有一个可能被泄露。

如果一定数量的 root 密钥已被泄露,则 PyPI 必须采取上述步骤,并且还要替换 root 角色中的所有 root 密钥。

还建议(RECOMMENDED)PyPI 充分记录安全公告中的泄露事件。当 pip-with-TUF 的用户因 timestampsnapshotroot 角色的密钥不再有效而无法安装或更新项目时,这些安全公告将最具信息量。然后他们可以访问 PyPI 网站查阅安全公告,这将有助于解释为什么他们不再能够安装或更新,然后采取相应的行动。当由于泄露而未撤销一定数量的 root 密钥时,新的 root 元数据可以安全地更新,因为一定数量的现有 root 密钥将用于签署新 root 元数据的完整性。TUF 客户端将能够使用一定数量的先前已知的 root 密钥来验证新 root 元数据的完整性。这将是常见情况。否则,在最坏情况下,即由于泄露而撤销了一定数量的 root 密钥时,终端用户可以选择使用带外机制更新新的 root 元数据。

审计快照

如果恶意方泄露了 PyPI,他们可以使用任何在线密钥签署任意文件。具有离线密钥的角色(即 roottargetsbins)仍然受到保护。为了从仓库泄露中安全恢复,应审计快照以确保文件仅恢复到受信任的版本。

当检测到仓库泄露时,必须验证三类信息的完整性:

  1. 如果仓库的在线密钥已被泄露,可以通过 targets 角色签署委托给新密钥的新元数据来撤销它们。
  2. 如果仓库上的角色元数据已更改,这将影响由在线密钥签署的元数据。自上一个周期以来创建的任何角色信息都应丢弃。因此,新项目的开发者将需要重新注册他们的项目。
  3. 如果目标文件本身可能已被篡改,可以使用存储在上次周期时存在的哈希信息来验证目标文件。

为了在发生泄露时安全地恢复快照,PyPI 应(SHOULD)维护少量自己的镜像,根据时间表复制 PyPI 快照。镜像协议可以立即用于此目的。镜像必须安全隔离,使其仅负责镜像 PyPI。镜像可以相互检查以检测意外或恶意的故障。

另一种方法是定期生成 snapshot 的加密哈希并发布到 Twitter。也许有用户会提供实际的元数据,仓库维护者可以验证元数据文件的加密哈希。或者,PyPI 可以定期存档自己的 snapshot 版本,而不是依赖外部提供的元数据。在这种情况下,PyPI 应(SHOULD)对仓库上的每个目标文件进行加密哈希,并将此数据存储在离线设备上。如果任何目标文件哈希发生变化,则表明发生了攻击。

至于提供不同版本的元数据,或将某个发行版冻结在特定版本的攻击,TUF 可以通过隐式密钥撤销和元数据不匹配检测等技术来处理这些攻击[2]

管理更新过程的未来更改

如果更新流程发生重大变化,PyPI 应在不中断现有客户端的情况下实施这些变化。有关如何操作的一般指导,请参阅 TAP 仓库中正在进行的讨论。

请注意,本 PEP 对 PyPI 的更改将向后兼容。本 PEP 未更改目标文件和简单索引的位置,因此任何现有 PyPI 客户端仍将能够使用这些文件执行更新。本 PEP 增加了客户端使用 TUF 元数据来提高更新过程安全性的能力。

哈希算法转换计划

如果用于哈希目标文件和元数据文件的算法变得脆弱,则应(SHOULD)将其替换为更强的哈希算法。

TUF 元数据格式允许并列列出来自不同哈希算法的摘要,以及算法标识符,以便客户端可以在算法之间无缝切换。

但是,一旦旧算法的支持被关闭,不支持新算法的客户端将只能通过禁用 TUF 验证来安装或更新软件包,包括客户端本身。为了让客户端在不暂时失去 TUF 安全保证的情况下进行转换,我们建议以下步骤。

  1. 在 Warehouse 中实施新算法。
  2. 重新生成现有、未过期的 TUF 元数据,以包含使用旧算法和新算法的哈希。所有新的元数据都应列出两种哈希算法。请注意,只有列出目标文件或其他元数据哈希摘要的 TUF 元数据需要更新,即 bin-nsnapshottimestamp。因此,只需要在线密钥来签署更新的元数据。
  3. 在高可见度渠道上宣布转换,例如Python Discourse 上的打包PyPI 变更邮件列表
  4. 让 pip 和 bandersnatch 等流行客户端有机会采用新的哈希算法。
  5. 让最终用户有机会更新客户端。
  6. 从 PyPI 维护者那里获得粗略共识以移除旧哈希算法。
  7. 移除 Warehouse 对旧算法的支持,只支持新算法。

附录 A:TUF 阻止的存储库攻击

  • 任意软件安装:攻击者可以在客户端系统上安装任何其想要的东西。也就是说,攻击者可以响应下载请求提供任意文件,并且这些文件不会被检测为非法文件。
  • 回滚攻击:攻击者向软件更新系统呈现比客户端已见的更旧的文件。这会导致客户端使用过时的文件。
  • 无限冻结攻击:攻击者继续向软件更新系统呈现客户端已见的相同文件。结果是客户端不知道有新文件可用。
  • 无尽数据攻击:攻击者以无尽的数据流响应文件下载请求,对客户端造成损害(例如,磁盘分区填满或内存耗尽)。
  • 缓慢检索攻击:攻击者以非常慢的数据流响应客户端,这实际上导致客户端永远无法继续更新过程。
  • 冗余依赖攻击:攻击者向客户端表明,为了安装他们想要的软件,他们还需要安装不相关的软件。这种不相关的软件可能来自受信任的来源,但可能存在攻击者可利用的已知漏洞。
  • 混搭攻击:攻击者向客户端呈现的仓库视图包含在仓库上从未同时存在的文件。例如,这可能导致安装过时的依赖项。
  • 错误软件安装:攻击者向客户端提供了一个受信任的文件,但该文件不是客户端想要的。
  • 恶意镜像阻止更新:控制一个仓库镜像的攻击者能够阻止用户从其他良好镜像获取更新。
  • 密钥泄露漏洞:能够泄露单个密钥或少于给定密钥阈值的攻击者可以泄露客户端。这包括依赖单个在线密钥(例如仅受 SSL 保护)或单个离线密钥,因为大多数软件更新系统都使用它们来签署文件。

参考资料

致谢

本材料基于美国国家科学基金会拨款号 CNS-1345049 和 CNS-0959138 支持的工作。本材料中表达的任何意见、发现、结论或建议均为作者的意见,不一定反映美国国家科学基金会的观点。

我们感谢 Alyssa Coghlan、Daniel Holth、Donald Stufft 以及整个 distutils-sig 社区在帮助我们思考如何可​​用且高效地将 TUF 与 PyPI 集成方面所做的贡献。

Roger Dingledine、Sebastian Hahn、Nick Mathewson、Martin Peck 和 Justin Samuel 帮助我们从 TUF 的前身 Tor 项目的 Thandy 设计 TUF。

我们感谢 Konstantin Andrianov、Geremy Condra、Zane Fisher、Justin Samuel、Tian Tian、Santiago Torres、John Ward 和 Yuyu Zheng 在开发 TUF 方面所做的努力。

Vladimir Diaz、Monzur Muhammad、Sai Teja Peddinti、Sumana Harihareswara、Ee Durbin 和 Dustin Ingram 帮助我们审阅了本 PEP。

Zane Fisher 帮助我们审阅和转录了本 PEP。


来源:https://github.com/python/peps/blob/main/peps/pep-0458.rst

最后修改:2025-02-01 08:59:27 GMT