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

Python 增强提案

PEP 759 – 外部 Wheel 托管

作者:
Barry Warsaw <barry at python.org>, Emma Harper Smith <emma at python.org>
PEP 代理人:
Donald Stufft <donald at python.org>
讨论至:
Discourse 帖子
状态:
已撤回
类型:
标准跟踪
主题:
打包
创建日期:
2024 年 10 月 1 日
发布历史:
2024 年 10 月 10 日, 2025 年 1 月 31 日
决议:
2025 年 1 月 31 日

目录

摘要

此 PEP 提出了一种机制,允许托管在 pypi.org 上的项目在 PyPI 以外的外部网站上安全地托管 wheel 构件。此 PEP 明确提议托管项目、包或其元数据。此功能已通过外部托管独立包索引可用。由于此 PEP 仅提供项目自定义特定已发布 wheel 构件下载 URL 的机制,因此像 pipuv 等常见安装工具已实现的依赖项解析无需更改。

此 PEP 定义了在此上下文中“安全”的含义,以及一种名为 .rim 文件的新包上传文件格式。它定义了 .rim 文件如何影响包的 Simple Repository API 在 HTML 和 JSON 格式下的元数据,以及传统 Wheel 如何轻松转换为 .rim 文件。

PEP 已撤销

此 PEP 已由作者于 2025 年 1 月 31 日撤销。我们对讨论串中的情绪的理解是,尽管此 PEP 试图解决的问题是有效的,但大多数人会倾向于不同的方法。具体来说,我们认为大多数用户更希望能够更好地控制指定多个索引、这些索引如何互操作以及这些索引的优先级和信任断言。例如,像 PEP 766 这样的解决方案可能提供了更好的前进方向。现有的临时措施(例如 PyPI 限制增加请求和 “wheel-stub” 方法)在间歇期内已经足够(即使不是理想的)。作者感谢所有为建设性讨论做出贡献的人,特别是那些公开和私下表示支持此 PEP 的人。

基本原理

Python 包索引,托管在 https://pypi.ac.cn,对上传构件文件大小(100 MiB)和总项目大小(10 GiB)施加了 默认限制。大多数项目在项目生命周期内,通过多年的上传,都可以舒适地保持在这些限制内。少数项目遇到了这些限制,并获得了文件大小和项目大小的例外,允许它们继续上传新版本,而无需采取更极端的措施,例如删除可能仍在使用中的文件(例如通过版本固定)。

一个相关的解决方案是 “wheel stub” 方法,它提供了 PyPI 和外部第三方包索引之间的间接链接,在那里可以避免此类限制。Wheel stubs 是 源发行版(也称为“sdists”),它使用 PEP 517 构建后端,该后端不将源代码转换为二进制 wheel,而是执行一些逻辑来计算现有、外部托管 wheel 的 URL 以下载和安装。这种方法有效,但它模糊了 PyPI、sdist 和外部托管 wheel 之间的联系,因为没有办法将此信息呈现给 pip 或其他此类工具。

历史背景

2013 年,PEP 438 提出了一种“向后兼容的两阶段过渡过程”,以修改 PyPI 上发布文件托管的多个方面。正如本 PEP 所述,PyPI 最初仅支持项目和发布注册,而不允许构件文件托管。因此,大多数项目都在其他地方托管了发布文件构件。后来添加了构件托管,但外部和 PyPI 托管文件的混合导致了广泛的可用性和潜在的安全问题。PEP 438 旨在提供多种设施来支持外部托管,同时推广 PyPI 优先托管。

PEP 438 很复杂,有三种不同的“托管模式”,简单 HTML 索引页中的 rel 元数据用于指示托管位置,以及影响 PyPI 和安装程序的两阶段过渡计划。PEP 438 最终在 2015 年被 PEP 470 撤销,后者承认 PEP 438 确实成功地...

吸引了更多人使用 PyPI 的存储库功能,这总之是一件好事,因为 PyPI 驱动的全球 CDN 为许多人提供了加速 […]

PEP 470 提倡使用显式的多个存储库,而不是外部托管,提供完整的包索引和构件托管,并通过安装程序工具支持,例如 pip install --extra-index-url 允许 pip essentially 将多个存储库视为 一个单一的全局存储库 进行包安装解析。由于多年来这一直是首选的标准,因此所有 Python 包安装工具都支持查询多个索引来进行依赖项解析。

多个索引的问题

那么,为什么此 PEP 会提议允许更有限形式的外部托管,以及此提案如何避免 PEP 470 中记录的问题?

整合多个索引所带来的一个众所周知的问题是 依赖项混淆攻击,Python可能特别容易受到此类攻击,因为 pip install 用于解析包依赖项和首选版本的算法。 uv 工具通过支持附加的 索引策略 选项来解决此问题,用户可以选择,例如,一个与 pip 兼容的策略,以及一个更有限的策略,以防止此类依赖项混淆攻击。

PEP 708 提供了关于依赖项混淆攻击的额外背景,并采取了不同的方法来防止它们。其核心是,PEP 708 允许存储库所有者指明项目跨越不同的存储库,这使得安装程序能够确定在跨多个存储库组合时如何处理全局包命名空间。PEP 708 已暂定接受,但需满足 PEP 708 中概述的几项必要条件,其中一些条件可能具有不确定的未来。正如 PEP 708 本身所说,这本身并不能解决依赖项混淆攻击,但它是提供足够信息给安装程序的途径之一,以帮助最大程度地减少这些攻击。

虽然完全独立的包索引仍然有有效的用例(例如,为 GPU 提供更丰富的平台支持,直到接受了一个完整的 变体提议),但此 PEP 采取了一种不同的、更简单的方法,并且不替换任何现有的、提议的或已批准的包索引协作规范。

此 PEP 还保留了 PyPI 的核心目的,并允许它继续成为所有 Python 包的传统、规范、中心化的索引。

解决 PyPI 的限制

此提案还解决了 PyPI 施加的大小限制问题,其中存在 100 MiB 的默认构件大小限制和 10 GiB 的默认总体项目大小限制。大多数包和构件可以轻松地在这些限制内,即使是包含多种平台二进制扩展模块的包。一小部分重要的包会经常超出这些限制,需要它们提交 PyPI 例外请求支持票。获得此类例外的解决并不一定困难,但这是一个特殊流程,需要一些时间来解决,而且授予此类例外的标准并未得到充分记录。

降低操作复杂性

设置和维护整个包索引可能是一个复杂的操作解决方案,既耗时又耗资源。如果此类索引的主要目的是仅仅为了避免文件大小限制,那么尤其如此。外部索引方法还对外部索引上项目的消费者施加了棘手的用户体验,要求他们理解 --external-index-url 等 CLI 选项的工作原理,以及这些标志的安全含义。对于大型 Wheel 包的生产者和消费者来说,只需设置和维护一个简单的 Web 服务器会更容易,该服务器能够提供单个文件,其 API 不比 HTTP GET 复杂。这种接口也很容易缓存或放在 CDN 后面。简单的 HTTP 服务器在安全审计方面也容易得多,更容易代理,通常运行、支持和维护所需的资源也少得多。即使是像 Amazon S3 这样的服务也可以用来托管外部 Wheel。

此 PEP 提出了一种偏向此类操作简单性的方法。

规范

定义了一种新型可上传文件,称为“RIM”(即 .rim)或“远程可安装元数据”文件。该名称令人联想到去掉轮胎的 Wheel 的图像,并强调 .rim 文件很容易从 .whl 文件派生。将 .whl 转换为 .rim 的过程在下面概述。文件名格式与 Wheel 文件命名格式 规范完全匹配,只是 RIM 文件使用 .rim 后缀。这意味着用于区分 .whl 文件的所有标签也区分了不同的 .rim 文件,因此可以在依赖项解析步骤中像今天的 .whl 文件一样使用。在这方面,.whl.rim 文件是可互换的。

一个 .rim 文件内容与 .whl 文件几乎相同,但 .rim 文件必须仅包含 Wheel 中的 .dist-info 目录。在 .rim zip 文件中不允许包含任何其他顶级文件或目录。 .dist-info 目录必须包含一个额外文件,除了允许.whl 文件的 .dist-info 目录中的文件之外:一个名为 EXTERNAL-HOSTING.json 的文件。

这是一个 JSON 文件,包含以下键

version
这是文件格式版本,对于此 PEP,必须1.0
owner
必须是此外部托管文件的 PyPI 组织所有者的名称,原因将在下文详细介绍
uri
这是一个单一 URL,命名了托管在外部网站上的物理 .whl 文件的位置。此 URL必须使用 https 方案。
size
这是一个整数值,描述了远程主机上物理 .whl 文件的大小(以字节为单位)。
hashes
这是一个字典,格式如PEP 694中所述,用于捕获物理 .whl 文件的PEP 694,具有与该 PEP 中提出的相同约束。由于这些哈希值一旦上传到 PyPI 就不可变,因此它们作为关键验证,以确保外部托管的 wheel 没有损坏或被泄露。

RIM 文件的影响

一个 .rim 文件的唯一影响是更改 HTML 和 JSON 接口中 simple repository API 中 Wheel 构件的下载 URL。在包版本的 HTML 页面中,href 属性必须uri 键的值,包括一个 #<hashname>=<hashvalue> 片段。此哈希片段必须签名 Wheel 文件格式.dist-info/RECORD 文件中描述的格式完全相同。哈希算法和编码的选择规则与此处相同。

同样,在JSON 响应中,指向下载文件的 url 键必须是 uri 键的值,并且 hashes 字典必须包含值,这些值从上面的 hashes 字典填充。

在所有其他方面,兼容的包索引应将 .rim 文件视为与 .whl 文件相同,但有一些其他小的例外情况如下所述。例如,.rim 文件可以像任何 .whl 文件一样被删除和撤销(PEP 592),具有完全相同的语义(即删除是永久性的)。当一个 .rim 被删除时,索引不得允许匹配的 .whl.rim 文件被(重新)上传。

可用性顺序

在上传相应的 .rim 文件到 PyPI 之前,外部托管的 Wheel必须可用,否则会引入发布竞争条件,尽管对于上传到 PEP 694 暂缓发布的 .rim 文件,此要求可能会放宽。

Wheel 可以覆盖 RIM

索引必须拒绝 .rim 文件,如果存在具有完全相同文件名标签的匹配 .whl 文件。但是,只要该 .rim 文件未被删除或撤销,索引可以接受一个 .whl 文件,如果存在匹配的 .rim 文件。这允许上传者用索引托管的 Wheel 文件替换外部托管的 Wheel 文件,但反之则禁止。由于默认是将 Wheel 托管在包含包元数据的同一个包索引上,因此不允许在上传后“降级”现有的 Wheel 文件。当一个 .whl 替换一个 .rim 时,索引必须使用其自己的托管文件服务提供包的下载 URL。上传覆盖性 .whl 文件时,包索引必须验证现有 .rim 文件中的哈希值,并且这些哈希值必须匹配,否则覆盖性上传必须被拒绝。

PyPI API 升级不必要

很可能更改是向后兼容的,以至于不需要增加 PyPI 存储库版本。由于 .rim 文件本质上只是对上传 API 的更改,包解析器和包安装程序可以继续使用它们一直支持的 API。

外部托管的弹性

导致 PEP 438 在 PEP 470 中被撤销的关键顾虑之一是,当外部索引消失时,用户可能会感到困惑。来自 PEP 470

这种困惑在于项目最终用户没有意识到项目是托管在 PyPI 上还是依赖于外部服务。这通常在外部服务停止而 PyPI 没有停止时显现出来。人们会看到 PyPI 工作正常,其他项目也工作正常,但这个特定的项目不行。他们通常不知道他们需要联系谁来解决这个问题,或者他们的补救步骤是什么。

虽然此 PEP 没有直接解决外部 Wheel 托管服务停止运行的问题,但采取了几项保护措施,以大大减轻 PyPI 管理员的潜在负担。

因此,此 PEP 提议:

  • 仅允许由组织账户拥有的包进行外部 Wheel 托管。外部托管是组织范围内的设置。
  • 组织账户不会自动获得外部托管 Wheel 的能力;此功能必须由 PyPI 管理员酌情明确启用。由于这不是一个常见的请求,我们不认为其开销会像PEP 541 解决方案、账户恢复请求,甚至文件/项目大小增加请求那样繁重。外部托管请求将以与这些请求相同的方式处理,即通过 PyPI GitHub 支持跟踪器
  • 请求外部 Wheel 托管的组织账户必须注册自己的支持联系 URI,无论是用于联系电子邮件地址的 mailto URI,还是组织支持跟踪器的 URL。对于不使用外部 Wheel 文件托管的组织,此类联系 URI 是可选的。

结合 EXTERNAL-HOSTING.json 文件中的 owner 键,这使得安装工具能够明确地将任何下载错误从 PyPI 支持管理员重定向到组织的(支持)管理员。

虽然存储和检索此组织支持 URL 的确切机制将单独定义,但举例来说,假设一个名为 foo 的包在 `https://foo.example.com <https://foo.example.com>`__ 上外部托管 Wheel 文件,并且该主机变得不可达。当安装工具尝试下载和安装 foo 包的 Wheel 时,下载步骤将失败。然后,安装工具将能够查询 PyPI,向最终用户提供有用的错误消息

  • 安装程序下载 .rim 文件,并读取 .rim zip 文件内的 EXTERNAL-HOSTING.json 文件中的 owner 键。
  • 安装程序查询 PyPI 以获取外部托管 Wheel 的组织所有者的支持 URI。
  • 然后将显示一条信息性错误消息,例如:
    无法下载外部托管的 Wheel 文件 foo-....whl。请联系 support@foo.example.com 获取帮助。请勿将此问题报告给 PyPI 管理员。

卸载 Wheel

通常很容易从现有的 .whl 文件生成 .rim 文件。这可以通过 PEP 518 构建后端附加命令行选项,或一个单独的工具来实现,该工具以 .whl 文件作为输入并创建相关的 .rim 文件。为了完成类比,将 .whl 转换为 .rim 的过程称为“卸载”。此类工具将执行的步骤是

  • 接受源 .whl 文件、包的组织所有者、.whl 将要托管的 URL 以及用于报告下载问题的支持 URI 作为输入。这些实际上可以包含在 pyproject.toml 文件中,但这超出了本 PEP 的范围。
  • 解压 .whl 并创建 .rim zip 存档。
  • .rim 文件中省略.dist-info 目录为根的 .whl 中的任何路径。
  • 计算源 .whl 文件的哈希值。
  • 将包含上述 JSON 键和值的 EXTERNAL-HOSTING.json 文件添加到 .rim 存档中。

工具的更改

理论上,安装程序工具不需要任何更改,因为当它们识别出要下载和安装的 Wheel 时,它们只需查询 PyPI 的 Simple API 返回的下载 URL。但实际上,像 pipuv 这样的工具可能对允许下载的托管服务器有限制,例如 PyPI 自带的 pythonhosted.org 域名。

在这种情况下,这些工具将需要放宽这些限制,但具体策略由安装程序工具自行决定。可以实现任何数量的方法,例如下载 .rim 文件并验证 EXTERNAL-HOSTING.json 元数据,或者简单地信任具有匹配校验和的 Wheel 的外部下载。它们还可以查询 PyPI 以获取项目的组织所有者和支持 URI,然后再信任下载。它们可以警告用户何时遇到外部托管的 Wheel 文件,和/或要求使用命令行选项来启用其他下载主机。任何这些验证策略都可以通过配置文件选择。

安装程序工具在外部托管的 Wheel 文件无法下载时(例如,因为主机不可达)可能还需要提供更好的错误消息。如上所述,这些工具可以从 PyPI 查询足够的元数据,以提供清晰且区别的错误消息,将用户指向包的外部托管支持电子邮件或问题跟踪器。

外部托管服务的约束

以下约束条件可确保可靠且兼容的外部 Wheel 托管服务

  • 外部 Wheel必须通过 HTTPS 提供服务,并使用由 Mozilla 的根证书存储签名的证书。这确保了与 pipuv 的兼容性。在撰写本文时,Python 3.10 或更高版本上的 pip 24.2 除了第三方 certifi Python 包提供的 Mozilla 存储外,还使用系统证书存储。 uv 使用 webpki-roots crate 提供的 Mozilla 存储,但不使用系统存储,除非提供了 --native-tls 标志 [1]PyPI 管理员可能会在将来修改此要求,但与流行安装程序的兼容性不会受到损害。
  • 外部 Wheel 主机使用内容分发网络(CDN),就像 PyPI 一样。
  • 外部 Wheel 主机必须承诺为其托管的所有 Wheel 提供稳定的 URL。
  • 外部托管的 Wheel不得从外部 Wheel 主机中删除,除非相应的 .rim 文件首先从 PyPI 中删除,并且不得删除已撤销版本的外部 Wheel。
  • 外部 Wheel 主机必须支持 HTTP Range 请求
  • 外部 Wheel 主机支持 HTTP/2 协议。

安全

本提案中描述的几个因素应能缓解外部托管 Wheel 的安全问题,例如:

  • Wheel 文件校验和必须包含在 .rim 文件中,并且一旦上传就无法更改。由于存储在 PyPI 上的校验和是不可变的且必需的,因此即使所有者组织失去了对其托管域的控制,也无法伪造外部 Wheel 文件。
  • 外部托管的 Wheel必须通过 HTTPS 提供服务。
  • 为了提供外部托管的 Wheel,组织必须获得 PyPI 管理员的批准。

当用户发现 PyPI 托管项目中的恶意软件或漏洞时,他们现在可以使用 PyPI 上的恶意软件报告工具,如本博客文章中所述。相同的流程可用于报告外部托管 Wheel 中的安全问题,并且应使用相同的补救流程。此外,由于启用了外部托管的组织必须提供支持联系 URI,因此在某些情况下可以使用该 URI 向托管组织报告安全问题。此类组织报告对于恶意软件可能没有意义,但对于报告外部托管 Wheel 中的安全漏洞来说,可能确实是一种非常有用的方式。

被拒绝的想法

已考虑并拒绝了几种想法。

  • 要求对外部托管的 Wheel 文件进行数字签名,无论是附加的还是除哈希值之外的。我们认为这是不必要的,因为校验和要求应该足以验证 PyPI 上 Wheel 的元数据是否与下载的 Wheel 完全匹配。密钥管理的额外复杂性会抵消此类数字签名可能带来的任何额外好处。
  • .rim 文件上传进行哈希验证。PyPI可以在接受上传之前验证上传的 .rim 文件中的哈希值是否与外部托管的 Wheel 匹配,但这需要下载外部 Wheel 并执行校验和,这也意味着在下载和验证此外部 .whl 文件之前,无法接受 .rim 文件的上传。这会增加 PyPI 的带宽并减慢上传查询速度,尽管 PEP 694 草稿上传可能潜在地缓解了这些担忧。尽管如此,收益可能不值得额外的复杂性。
  • 索引定期验证下载 URL。PyPI 可以尝试定期确保外部 Wheel 主机或外部 .whl 文件本身仍然可用,例如通过HTTP HEAD 请求。这可能是多余的,并且如果响应[2]中不包含文件的校验和,可能不会带来太多额外的好处。
  • 此 PEP 允许组织提供备用下载主机,这样在主主机出现故障时,次级主机就可以使用。我们认为基于 DNS 的复制是一种更好、更广为人知且可能更具弹性的技术。
  • .rim 文件替换。虽然允许 .whl 文件替换现有的 .rim 文件(只要 a) .rim 文件未被删除或撤销,b) 校验和匹配),但我们不允许用 .rim 文件替换 .whl 文件,也不允许 .rim 文件覆盖现有的 .rim 文件。后者可能是一种更改外部托管 .whl 的托管 URL 的技术;然而,我们认为这不是一个好主意。如上所述,“修复”外部主机 URL 有其他方法,我们不希望鼓励大规模重新上传现有的 .rim 文件。

脚注


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

最后修改:2025-01-31 18:51:56 GMT