软件工件的供应链层 (SLSA) 是一个用于生成和验证软件工件来源的工具框架。在 Python 生态系统中,有两种主要类型的软件工件:轮子(wheels)和源代码分发(distributions)。
我们如何使用 SLSA 框架来生成和验证 Python 工件的来源呢?
本篇文章是由我翻译、并根据我的实践和理解而形成的。英文原文在这里:https://sethmlarson.dev/python-and-slsa
内容
注意:本文主要介绍托管在 GitHub 上的 Python 项目。SLSA 框架可通过 GitHub Actions 来实现开箱即用,只需最少的配置即可完成。
如果你对 Python 打包的术语或流程感到好奇,Python 打包用户指南 是了解更多信息的最佳场所。
下面是维护人员和用户的端到端工作流程,从构建 distributions、生成出处证明、验证出处、发布到 PyPI,以及在验证其出处后安装 wheel。让我们一起完成每一步!
构建纯净的Python包
纯 Python 包通常只有两个工件:源代码 distribution 和纯 Python wheel。纯 Python 包可以使用名为 build 的包从源代码构建。
下面是 GitHub Actions job 定义,它构建纯 Python Wheel Package 和源代码 distribution,并为每个工件创建 SHA-256 哈希值:
jobs: |
这里将 build 完的 wheel package 上传到 GitHub Artifacts 存起来,用作后续在 “上传到PyPI” job 中使用。另外还将 dist
下的所有文件的哈希值存储在 hashes
用作后续 job provenance
的输入。
注意: SLSA 使用
sha265sum
的输出作为出处证明中subject-base64
字段的输入。sha256sum
的输出是一个或多个对散列 + 名称。
生成出处证明
现在我们已经构建了 sdist 和 wheel,我们可以从文件哈希生成来源证明。
因为我们需要将 Build 阶段的的输出作为这里生成出处的输入,因此这里使用了 needs 选项来作为 job provenance
执行的前提条件。可以看到上面生成的哈希值在这里被 subject-base64
所使用。
jobs: |
你会注意到,这并没有像 GitHub 工作流那样定义任何单独的步骤。相反 SLSA builders 使用可重用工作流功能来证明给定的 builders 行为不能被用户或其他进程修改。
出处证明文件是 JSON lines,以 .intoto.jsonl
结尾。*.intoto.jsonl
文件可以包含多个工件的证明,也可以在同一文件中包含多个出处证明。该 .jsonl
格式意味着该文件是一个 “JSON lines” 文件,即每行一个 JSON 文档。
注意:这里有一点令人困惑的是 GitHub job 中的
id-token
需要write
权限才能读取 GitHub OIDC 令牌。read
不允许你读取 OIDC…🤷。有关id-token
权限的更多信息,请参阅 GitHub 文档。
上传到PyPI
我们使用官方 pypa/gh-action-pypi-publish GitHub Action 将 wheel 包上传到 PyPI。
请注意,这个 publish
job 需要在 build
和 provenance
都完成后开始执行,这意味着我们可以假设 provenance
job 已经为我们起草了 GitHub Release(因为 upload-assets: true
的设置),并且我们可以假设该 job 已成功。如果不先创建来 provenance 文件,我们不想将这些 wheel 包上传到 PyPI,因此我们最后上传到 PyPI。
publish: |
请注意,该 publish
job 需要在开始之前完成 build
和作业。provenance
这意味着我们可以假设出处作业已经为我们创建了 GitHub 发布草案(感谢该 upload-assets: true
设置),并且我们可以假设该作业已成功。如果不先创建来源文件,我们不想将这些发行版上传到 PyPI,因此我们最后上传到 PyPI。
验证Python包的来源
让我们使用一个真正的 Python 项目来验证它的出处。以 urllib3 项目为例,它在 GitHub Releases 发布了版本中包含出处证明,这里演示的是使用它的最新版本 2.1.0
。
首先我们需要下载 slsa-verifier 用来验证出处。下载完该 slsa-verifier
工具后,让我们从 PyPI 获取 urllib3 wheel 包,而不使用 pip download. 我们使用该 --only-binary
选项强制 pip 下载 wheel。
python3 -m pip download --only-binary=:all: urllib3 |
下载软件包后,我们需要从 GitHub 版本下载出处证明。我们需要使用与包版本相同的 GitHub Release 来确保获得正确的出处证明,因此 tag 也是 2.1.0。
curl --location -O https://github.com/urllib3/urllib3/releases/download/2.1.0/multiple.intoto.jsonl |
该出处文件的名称为 multiple.intoto.jsonl
,这是一个包含多个工件证明的出处证明的标准名称。对于 Python 项目来说几乎总是如此,因为几乎总是有一个源代码 distribution 和至少一个 wheel。
此时,我们当前的工作目录中应该有两个文件:wheel 和出处证明,ls
浏览一下确保已经准备好了:
ls |
从这里我们可以使用 slsa-verifier
来验证出处。我们可以验证最重要的事情,即哪个 GitHub 仓库实际构建了 wheel,以及其他信息,例如 git 标签、分支和建造者 ID:
源存储库 (--source-uri
)
建造者 ID (--builder-id
)
Git 分支 (--source-branch
)
git 标签 (--source-tag
)
因此,如果我们想验证轮子的源存储库,我们可以使用 --source-uri
。当然还可以验证 --builder-id
,--source-branch
和 --source-tag
。具体我的测试目前 --builder-id
,--source-branch
似乎有问题。
# 这里仅验证 GitHub 仓库 |
成功了!🥳 我们已经验证了这个 wheel 的出处,所以现在我们可以放心的安装它,因为我们知道它是按照我们的预期构建的:
python3 -m pip install urllib3-2.1.0-py3-none-any.whl |
二进制Python包
Python wheel 并不全是纯 Python!Python 的最大优势之一是作为 C、C++、Fortran、Rust、Go(等)的粘合语言,与其他一些编程语言相比,Python 的打包和起源故事变得更加复杂。
需要为多个平台、架构构建二进制 wheel,如果项目无法使用稳定的 ABI ,则需要为每个新的 Python 版本编译新的 wheel。要了解在这种情况下项目需要多少个 wheel,你可以查看 MarkupSafe,它在 v2.1.3 中提供了近 60 个wheel。当新的 Python 版本发布时,至少还会有 10 个版本来覆盖 Python 3.12 的所有平台。
不幸的是,这意味着我们需要在初始版本发布后的某个时间创建新的工件,而 SLSA 中没有一个简单的办法。这个问题有两种解决方案:
- 为包创建一个新版本,而不仅仅是新 wheels。
- 构建新的 wheels 并创建新的出处证明。
创建新版本
最简单的方法是在将 cibuildwheel
GitHub Action 更新到最新版本后为包创建一个新版本以支持所有新的 Wheel 目标。这将创建一个新的来源证明、sdist 和 wheel。然而,如果项目的源代码没有任何改变,这感觉像是一个不幸的结果。
与新轮子相比,新版本会引起更多的流失:
- 维护人员的额外工作
- 存储和计算资源(PyTorch 的每个版本大约有 6GB 的 wheels)
- 依赖者需要更新锁定文件(依赖机器人疲劳,有人吗?)
- 下游重新打包和分发
发布后wheels的出处
那么,如果我们不想仅仅为了新轮子的出处而创建新版本,该怎么办呢?
Markupsafe 的解决方案是添加一个手动 workflow_dispatch 触发器来运行典型的 wheel 构建工作流程,但配置为仅为给定的 Python 版本(即 cp312
CPython 3.12)构建新的 wheels。然后这些 wheels 会像平常一样通过 SLSA 进行证明并上传到 PyPI,但新的出处证明文件会添加到现有的 GitHub 发布工件中。
这是有效的,因为 PyPI 上的所有工件都有出处证明。有一些缺点:
- 新的出处证明位于 GitHub Release 的单独工件中,而不是现有的出处证明中。这意味着想要验证新车轮来源的用户可能需要一些手动发现和步骤来下载正确的来源证明文件。
- 通常新的架构或平台需要新版本的
cibuildwheel
。由于需要更新源代码才能升级正在cibuildwheel
使用的版本,因此过去版本的 git 标签将与构建新轮子所需的确切 git 提交不匹配。这意味着出处证明不会包含有关 git 标签的信息,因此使用该--source-tag
选项进行验证将无法按预期进行。
上述第一点的另一个潜在解决方案是创建一个全新的来源证明,其中包含新旧工件的哈希值。
文中用到的项目
以下这些是本文使用的所有项目和工具:
- SLSA GitHub Builder
- slsa-framework/slsa-verifier
- pypa/gha-action-pypi-publish
- pypa/build
- pypa/cibuildwheel
以下项目是用来展示如何在 GitHub Actions 中针对 Python 项目配置 SLSA:
转载本站文章请注明作者和出处,请勿用于任何商业用途。欢迎关注公众号「DevOps攻城狮」