内部机制和技术细节
注意
此页面对可信发布者的用户没有用!
它主要供 PyPI 开发人员和其他软件包索引的开发人员使用,他们希望支持类似的认证模型。
可信发布工作原理
PyPI 的可信发布功能建立在OpenID Connect,简称“OIDC”之上。
OIDC 为服务(如 GitHub Actions)提供了一种可证明识别自身的方式:授权实体(如 GitHub 用户或自动化工作流程)可以向第三方服务呈现OIDC 令牌。然后,该第三方服务可以验证令牌并确定其是否被授权执行其他操作。
在可信发布的背景下,机制如下
-
OIDC 身份提供者,如 GitHub(简称“提供者”),生成包含有作用域的声明的 OIDC 令牌,这些声明传达相应的授权范围。
- 例如,
repo
声明可能绑定到值octo-org/example
,表示该令牌应被授权访问octo-org/example
是有效存储库的资源。
- 例如,
-
可信发布者是 PyPI 上的配置片段,告诉 PyPI信任哪些 OIDC 提供者,以及何时信任(即,哪些特定的声明集被认为有效)。
-
例如,GitHub Actions 的可信发布者配置可能指定
repo: octo-org/example
,其中workflow: release.yml
和environment: release
,表示提交的 OIDC 令牌必须包含这些确切的声明才能被视为有效。 -
在适用情况下,PyPI 还检查防止帐户复活攻击的声明。例如,使用 GitHub 作为 OIDC IdP,PyPI 检查
repository_owner_id
声明。
-
-
令牌交换是 PyPI 如何将 OIDC 令牌转换为可用于对软件包上传端点进行身份验证的凭据(PyPI API 令牌)。
- 令牌交换归结为提交的 OIDC 令牌与当前在 PyPI 上配置的每个可信发布者之间的匹配过程:首先验证令牌的签名(以确保它实际上来自预期的提供者),然后将其声明与一个或多个已注册可信发布者的项目进行匹配。
如果 OIDC 令牌对应于一个或多个可信发布者,则会颁发一个短时效(15 分钟)的 PyPI API 令牌。此 API 令牌的范围限定为具有匹配可信发布者的每个项目,这意味着它可用于上传到多个项目(如果配置如此)。
如果一切正常,成功的可信发布流程会导致生成一个短时效的 PyPI API 令牌无需任何用户交互,进而为 PyPI 打包人员提供安全性和符合人体工程学的好处:用户不再需要担心令牌的供应或撤销。
问答
为什么可信发布使用“两阶段”令牌交换?
如上所述,可信发布使用“令牌交换”机制,该机制分为两个阶段
-
上传客户端提交一个 OIDC 令牌,PyPI 会验证该令牌。如果有效,PyPI 会响应一个有效的、具有相应作用域的 PyPI API 令牌。
-
上传客户端获取其获得的有效 PyPI API 令牌并照常使用它。
原则上,这比必要的要复杂:PyPI 可以直接使用 OIDC 令牌,并在 API 令牌处理期间将其视为特殊情况,从而跳过上传客户端和软件包索引之间的网络往返。
虽然从概念上讲更简单,但“单阶段”令牌交换会带来自身的问题
-
关注点分离:从概念上讲,OIDC 令牌是外部颁发的令牌,具有外部关注点:它具有不属于 PyPI 本身的故障模式(例如,颁发身份提供者无法正确签署)。
将这些关注点与 PyPI 实际业务逻辑隔离可确保它们保持封装,并且不会对 PyPI 本身施加设计或安全约束(例如,强制 PyPI 在不适合的地方使用 OIDC 令牌)。
-
对现有身份验证和授权逻辑的复杂化:PyPI 有一个庞大的现有 AuthN 和 AuthZ 代码库。API 令牌的大部分现有代码直接适用于 PyPI API 令牌格式,该格式基于Macaroons。
处理 OIDC 令牌(它们是JSON Web Tokens)将需要大量复制现有代码路径,进而意味着测试(和漏洞)范围增加。通过将 OIDC 令牌交换为 PyPI 的现有格式的 API 令牌,我们的实现可以在无需任何重大更改的情况下重复使用我们现有的(并且经过充分测试的)代码路径。
-
自动秘密扫描和撤销挑战:PyPI 是GitHub 秘密扫描系统的合作伙伴,该系统允许 PyPI 自动撤销意外泄露到公共存储库中的 PyPI API 令牌。
此系统依赖于 PyPI 令牌具有唯一的开头:它们都以 pypi-
开头。如果没有该开头,GitHub 将无法有效地扫描公共存储库以查找令牌。
OIDC 令牌由独立提供者颁发,这意味着 PyPI 无法对它们强制执行 pypi-
开头。此外,OIDC 令牌严格定义为JSON Web Tokens,这意味着它们看起来大多是无结构的随机字符。这使得它们难以扫描。最后,即使是有效的 JWT 扫描器也需要将其发现的每个受损 JWT 报告给其颁发者(例如,GitHub 本身)以及其使用者(例如,PyPI),从而在撤销过程中引入复杂性和额外的故障模式。
将 OIDC 令牌交换为 PyPI API 令牌完全规避了所有这些问题。
虽然这些原因已在 PyPI 中记录,但它们很可能是其他“联合”OIDC 使用者(如云提供商)执行类似“两阶段”交换机制的一些相同原因。
为什么 PyPI 项目与发布者之间的关系是“多对多”?
如果您在 PyPI 上试用可信发布者,您会注意到 PyPI 项目可以拥有多个发布者,单个发布者可以注册到多个项目。
这是 PyPI 项目与其可信发布者之间的“多对多”关系,它与“两阶段”交换一样,原则上看起来比必要的要复杂。
在实践中,这种多对多关系解决了 Python 打包社区常用的发布模式
- 一个发布者,多个项目:多个相关 PyPI 项目共享单个源存储库并不罕见。此外,由于同步发布(例如,库软件包及其相应 CLI 工具的同步发布),多个相关 PyPI 项目共享相同的发布工作流程并不罕见。
可信发布的设计适应了这种情况:维护人员可以对所有软件包使用相同的 release.yml
工作流程,而不是按软件包将其拆分。
- 一个项目,多个发布者:PyPI 包含大量构建的分布式文件(“轮子”),其中一些是包含处理器、操作系统或平台特定二进制文件的“二进制轮子”。
由于这些二进制文件特定于各个平台,因此它们通常必须在不同的平台上构建,通常在每个平台的专用构建器配置上构建。
从那里,每个平台构建器通常也会执行该平台的发布:Linux 特定的轮子由 Linux 构建器上传,等等。
从可靠性和关注点分离的角度来看,这可能不是最佳实践:最佳实践是收集所有平台特定的构建,然后在最终的平台无关发布步骤中发布,该步骤可以是一个发布者。
但是,为了将可信发布者交付到用户手中,而无需要求他们对构建进行重大无关更改,可信发布功能允许用户将多个发布者注册到单个项目。因此,sampleproject
可以从 release-linux.yml
和 release-macos.yml
发布,而无需重构为单个 release.yml
。
什么是帐户复活攻击,PyPI 如何防范这些攻击?
某些 OIDC 提供者支持用户名更改,因此 repository_owner: octo-org
的声明可能不一定是指用户最初在可信发布者配置中授权的同一个 octo-org
。
如果存储库所有者更改了用户名或删除了其帐户,恶意攻击者可能会获取释放的用户名并在原始可信名称下创建他们自己的存储库。这被称为帐户复活攻击。
为了解决针对基于 GitHub 的发布者的此问题,PyPI 始终检查 repository_owner_id
声明。此声明证明了存储库所有者的 ID,该 ID 与用户名不同,它是稳定且永久的。配置可信发布者时,PyPI 会查找配置的用户名 ID 并将其存储起来。在 API 令牌铸造期间,PyPI 会将 repository_owner_id
声明与存储的 ID 进行比较,如果不匹配,则会失败。通过此过程,即使用户更改了用户名或删除了其帐户,也只有原始 GitHub 用户仍然被授权发布到其 PyPI 项目。