Skip to content

内部结构和技术细节

笔记

此页面对受信任发布商的用户没有用处!

它主要面向 PyPI 开发人员和其他希望支持类似身份验证模型的软件包索引开发人员。

可信出版的运作方式

PyPI 的可信发布功能建立在 OpenID Connect(简称“OIDC”)之上。

OIDC 为服务(例如 GitHub Actions)提供了一种可验证自身身份的方法 :授权实体(例如 GitHub 用户或自动化工作流)可以向第三方服务提供OIDC 令牌。该第三方服务随后可以验证该令牌,并确定其是否被授权执行其他操作。

在可信发布(Trusted Publishing)的背景下,其机制如下:

  • GitHub 等OIDC 身份提供商(简称“提供商”)生成包含作用域声明的OIDC 令牌,这些声明传达适当的授权范围。

    • 例如,该repo声明可能绑定到值 octo-org/example,表示该令牌应该被授权访问资源,其中octo-org/example是有效的存储库。
  • 受信任的发布者是 PyPI 上的配置,它告诉 PyPI 要信任哪些OIDC 提供商,以及何时信任(即,要将哪一组特定的声明视为有效)。

    • 例如,GitHub Actions 的受信任发布者配置可能会指定repo: octo-org/example`with`workflow: release.yml和 `and` environment: pypi,表示所提供的 OIDC 令牌必须 包含这些声明才能被视为有效。

    • 在适用情况下,PyPI 还会检查防止 账户复活攻击的声明。例如,当 GitHub 作为 OIDC 身份提供商时,PyPI 会检查该repository_owner_id声明。

  • 令牌交换是 PyPI 将 OIDC 令牌转换为凭据(PyPI API 令牌)的方式,这些凭据可用于对软件包上传端点进行身份验证。

    • 代币交换归根结底是将提供的 OIDC 代币与 PyPI 上当前配置的每个受信任发布者进行匹配的过程:首先验证代币的签名(以确保它确实来自预期的提供商),然后将其声明与零个或多个已注册受信任发布者的项目进行匹配。

    如果 OIDC 令牌对应于一个或多个受信任的发布者,则会颁发一个有效期仅为 15 分钟的 PyPI API 令牌。此 API 令牌的作用域限定于所有具有匹配受信任发布者的项目,这意味着它可以用于上传到多个项目(如果已配置)。

如果一切顺利,成功的可信发布流程将生成一个有效期较短的 PyPI API 令牌,而无需任何用户交互,这反过来为 PyPI 打包者提供了安全性和人体工程学方面的优势:用户不再需要担心令牌的配置或撤销。

问答

Trusted Publishing 为什么采用“两阶段”代币交换?

如上所述,可信发布采用“令牌交换”机制,该机制分两个阶段进行:

  1. 上传客户端提供一个 OIDC 令牌,PyPI 会对其进行验证。如果验证成功,PyPI 会返回一个有效且作用域合适的 PyPI API 令牌。

  2. 上传客户端会获取到有效的 PyPI API 令牌并像往常一样使用它。

原则上,这比必要的要复杂得多:PyPI 可以直接获取 OIDC 令牌,并在 API 令牌处理期间将其视为特殊情况,从而跳过上传客户端和软件包索引之间的网络往返。

虽然概念上更简单,“单阶段”代币交换也存在自身的问题:

  1. 关注点隔离:从概念上讲,OIDC 令牌是外部颁发的令牌,具有外部关注点:它具有 PyPI 本身内部不存在的故障模式(例如,颁发身份提供程序未能正确签名)。

    将这些问题与 PyPI 的实际业务逻辑隔离开来,可以确保它们保持封装状态,并且不会对 PyPI 本身施加设计或安全限制(例如,强制 PyPI 在不适合的地方使用 OIDC 令牌)。

  2. 现有身份验证和授权逻辑的复杂性:PyPI 拥有大量预先存在的身份验证和授权代码。大多数现有的 API 令牌代码都直接适配了基于 Macaroons 的PyPI API 令牌格式。

    处理 OIDC 令牌(其底层是JSON Web Token)需要大量重复编写现有代码路径,这反过来又会增加测试面(以及漏洞风险)。通过将 OIDC 令牌替换为 PyPI 现有格式的 API 令牌,我们的实现无需任何重大更改即可重用现有(且经过充分测试)的代码路径。

  3. 自动秘密扫描和撤销挑战:PyPI 是GitHub 秘密扫描系统的合作伙伴,该系统允许 PyPI 自动撤销在公共存储库中意外泄露的 PyPI API 令牌。

该系统依赖于 PyPI 令牌的唯一前缀:它们都以 . 开头pypi-。如果没有这个前缀,GitHub 将无法高效地扫描公共仓库以查找令牌。

OIDC 令牌由独立提供商发行,这意味着 PyPI 无法为其添加pypi-前缀。此外,OIDC 令牌严格定义为JSON Web Tokens(JWT),这意味着它们主要由无结构的随机字符组成。这使得扫描它们变得困难。最后,即使是有效的 JWT 扫描器也需要将每个被泄露的 JWT 同时报告给其发行方(例如 GitHub 本身)和使用者(例如 PyPI),这会增加撤销过程的复杂性并导致更多故障。

将 OIDC 令牌交换为 PyPI API 令牌可以完全规避所有这些问题。

虽然 PyPI 记录了这些原因,但其他 OIDC 的“联合”消费者(如云提供商)采用类似的“两阶段”交换机制,很可能也是出于同样的一些原因。

为什么 PyPI 项目与发布者之间的关系是“多对多”?

如果你在 PyPI 上体验一下 Trusted Publishing,你会发现 PyPI 项目可以有多个发布者,而单个发布者也可以注册到多个项目。

这是 PyPI 项目与其受信任的发布者之间的“多对多”关系,就像“两阶段”交换一样,原则上似乎比必要的要复杂得多。

实际上,这种多对多的关系解决了 Python 打包社区常用的发布模式:

  1. 一个发布者,多个项目:多个相关的 PyPI 项目共享同一个源代码库并不罕见。此外,由于同步发布(例如,同时发布库包及其对应的 CLI 工具),多个相关的 PyPI 项目共享相同的发布工作流程也很常见。

Trusted Publishing 的设计满足了这种使用场景:维护者可以对所有软件包使用相同的release.yml工作流程,而无需按软件包进行拆分。

  1. 一个项目,多个发布者:PyPI 包含大量已构建的发行版(“wheel”),其中一些是“二进制 wheel”,其中包含处理器、操作系统或平台特定的二进制文件。

由于这些二进制文件是针对特定平台的,因此它们通常必须在不同的平台上构建,而且通常需要为每个平台配置专用的构建器。

在此基础上,通常的做法是让每个平台构建者也为该平台发布版本:Linux 构建者上传 Linux 特有的 wheel 文件,等等。

从可靠性和关注点隔离的角度来看,这可能不是最佳实践:最佳实践是将所有特定于平台的构建版本收集到一个最终的、与平台无关的发布步骤中,然后使用单个发布器。

然而,为了让用户能够使用可信发布者功能,而无需他们对构建进行任何重大且无关的更改,可信发布功能允许用户针对单个项目注册多个发布者。因此, 无需将项目重构为单个发布者,sampleproject 即可从多个发布者发布内容。release-linux.ymlrelease-macos.ymlrelease.yml

什么是账户复活攻击?PyPI 如何防范此类攻击?

一些 OIDC 提供商支持用户名更改,因此声明 repository_owner: octo-org可能不一定是指octo-org 用户最初在受信任发布者配置中授权的同一用户名。

如果仓库所有者更改用户名或删除帐户,恶意攻击者可能能够利用释放的用户名,以原先受信任的名称创建自己的仓库。这被称为帐户复活攻击。

为了解决基于 GitHub 的发布者遇到的问题,PyPI 会始终检查声明 repository_owner_id。该声明验证仓库所有者的 ID,与用户名不同,它是稳定且永久的。配置受信任发布者时,PyPI 会查找已配置用户名的 ID 并将其存储。在 API 令牌生成过程中,PyPI 会将声明repository_owner_id与存储的 ID 进行比对,如果不匹配则生成失败。通过此流程,即使原始 GitHub 用户更改用户名或删除帐户,也只有该用户仍然有权发布到其 PyPI 项目。

如何成为可信出版服务提供商?

如果您是托管计算服务的运营商或 CI 提供商,您可能希望 PyPI 作为受信任的发布者来支持您的平台或服务。

将新的可信发布者平台添加到 PyPI 有三个主要要求:

  1. OIDC身份提供商:可信发布依赖于使用OpenID Connect规范运行的身份提供商平台。其他形式的身份提供商不符合要求。

  2. OIDC 发现:您的 OIDC IdP必须支持OpenID Connect 发现,即提供一个https://{iss}/.well-known/openid-configuration至少包含以下内容的端点:

    • jwks_uri:指向身份提供商用于签名的 JSON Web 密钥 (JWK) 集的 URL;
    • claims_supported:PyPI 应该在身份提供商 (IdP) 颁发的 OIDC 凭证中看到的声明名称数组。

    (其中是所提供的 OIDC 代币中声明iss的价值)iss

    无法提供发现信息或在发现响应中提供这些字段的身份提供商不符合资格。

  3. 合理的 OIDC 声明集:您的 OIDC 声明必须能够充分标识一个可限定于 PyPI 项目或项目集的唯一工作负载。这些声明必须支持防止复活攻击,这意味着可重用或可修改的声明(例如仓库或项目名称)必须由一个不可变且有保证的唯一标识符(例如数字 ID)支持。此外,声明集必须支持一个可自定义的aud 声明,该声明可以设置为特定值pypi。不符合此声明标准的身份提供商将不具备资格。

  4. 可靠性和知名度:与新的可信发布者集成所需的工作量并非微不足道,但也绝非罕见。为了最大限度地利用 PyPI 的有限资源,我们计划仅支持在 PyPI 用户中拥有合理发布使用率的平台。此外,我们对受支持的身份提供商的整体可靠性和安全性有着很高的标准:实际上,这意味着自建或个人使用的身份提供商将不符合条件。

如果您认为您的平台已充分满足这些要求,我们鼓励您提交问题,请求为您的平台或服务申请可信发布商支持。