Skip to content

Commit 7e36e66

Browse files
authored
📝 docs(release): refresh release verification docs (#3330)
1 parent 07dcf29 commit 7e36e66

5 files changed

Lines changed: 139 additions & 69 deletions

File tree

‎.github/THREAT_MODEL.md‎

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ flowchart LR
149149
main["main branch<br/>ruleset: pull request, checks"]
150150
pre["pre-release.yaml<br/>release commit and tag"]
151151
tag["Tag *.*.*<br/>tag ruleset"]
152-
build["release.yaml build<br/>no secrets"]
152+
build["release.yaml build<br/>no publishing credentials"]
153153
publish["release.yaml publish<br/>environment: release"]
154154
verify["release.yaml verify jobs<br/>no publishing credentials"]
155155
pypi[("PyPI<br/>sdist, wheel, attestations")]
@@ -181,12 +181,12 @@ flowchart LR
181181
class pypi,ghr,getv,boot output;
182182
```
183183

184-
| Boundary | Crossing | Controls |
185-
| -------- | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
186-
| TB1 | Caller-supplied values into activation scripts and `pyvenv.cfg` | [Per-shell quoting][src-activation], [line boundary collapsing][src-text], [property tests][tests-property] and [fuzzing][atheris] |
187-
| TB2 | Wheel bytes from an index through app-data into an environment | pip's TLS, SHA-256 comparison with PyPI's published digest, refusal when PyPI has none ([#3302][pr-3302]) |
188-
| TB3 | Built artifacts from GitHub Actions to PyPI, GitHub Releases, get-virtualenv and bootstrap.pypa.io | [Trusted publishing][pypi-tp], [attestations][gh-attestations], [post-publish verification][workflow-release] of each destination |
189-
| TB4 | Code from contributors and dependencies into a release | [Rulesets][gh-rulesets], [SHA-pinned actions][gh-sha-pinning], [embedded wheel hashes][src-embed-init], [wheel age gate][task-wheel-age], [pinned CI tool downloads][ci-tools] |
184+
| Boundary | Crossing | Controls |
185+
| -------- | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
186+
| TB1 | Caller-supplied values into activation scripts and `pyvenv.cfg` | [Per-shell quoting][src-activation], [line boundary collapsing][src-text], [property tests][tests-property] and [fuzzing][atheris] |
187+
| TB2 | Wheel bytes from an index through app-data into an environment | pip's TLS, SHA-256 comparison with PyPI's published digest, refusal when PyPI has none ([#3302][pr-3302]) |
188+
| TB3 | Built artifacts from GitHub Actions to PyPI, GitHub Releases, get-virtualenv and bootstrap.pypa.io | [Trusted publishing][pypi-tp], [attestations][gh-attestations], [immutable releases][gh-immutable], [post-publish verification][workflow-release] of each destination |
189+
| TB4 | Code from contributors and dependencies into a release | [Rulesets][gh-rulesets], [SHA-pinned actions][gh-sha-pinning], [embedded wheel hashes][src-embed-init], [wheel age gate][task-wheel-age], [pinned CI tool downloads][ci-tools], [zipapp lock][pylock-zipapp] |
190190

191191
## Threats and existing mitigations
192192

@@ -218,9 +218,9 @@ risk to lowest.
218218

219219
### Repudiation
220220

221-
| ID | Threat | Risk | Mitigation |
222-
| --- | --------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
223-
| R1 | A user cannot tell whether a published artifact came from the project's CI. | Low | [PyPI attestations][pypi-attestations] cover the sdist and wheel, and a [SLSA build provenance][slsa-provenance] attestation ships beside the zipapp. [GitHub attestations][gh-attestations] bind the CycloneDX SBOM and its [SPDX] rendering to both distributions ([#3299][pr-3299]) and the zipapp SBOM to the zipapp ([#3310][pr-3310]). The wheel carries its SBOM as [PEP 770][pep-770] describes. The release pins timestamps to [`SOURCE_DATE_EPOCH`][sde], so anyone can [rebuild the sdist][verify-rebuild] and compare it; wheels differ between build machines, see open items. [Verify a virtualenv release][verify] shows each check. We do not require signed commits. |
221+
| ID | Threat | Risk | Mitigation |
222+
| --- | --------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
223+
| R1 | A user cannot tell whether a published artifact came from the project's CI. | Low | [PyPI attestations][pypi-attestations] cover the sdist and wheel, and a [SLSA build provenance][slsa-provenance] attestation ships beside the zipapp. [GitHub attestations][gh-attestations] bind the CycloneDX SBOM and its [SPDX] rendering to both distributions ([#3299][pr-3299]) and the zipapp SBOM to the zipapp ([#3310][pr-3310]). The wheel carries its SBOM as [PEP 770][pep-770] describes. GitHub signs a release attestation over the assets of each immutable release. The release pins timestamps to [`SOURCE_DATE_EPOCH`][sde] and the zipapp's bundled wheels to a [PEP 751][pep-751] lock, and the SBOMs record the build tools, so anyone can [rebuild the sdist, wheel and zipapp][verify-rebuild] and compare the bytes ([#3306][pr-3306], [#3311][pr-3311]). [Verify a virtualenv release][verify] shows each check. We do not require signed commits. |
224224

225225
### Information disclosure
226226

@@ -262,7 +262,7 @@ close submissions with no human in the loop.
262262

263263
T3, T5 and T6 cover compromised dependencies and embedded wheels, and S2 covers registry typosquatting. Tools that
264264
install virtualenv unpinned take each new release on the day PyPI publishes it. We cannot slow that down for them, so we
265-
keep releases verifiable with attestations, an sdist and wheel that [rebuild byte for byte][ra-repro], SBOMs and
265+
keep releases verifiable with attestations, an sdist, wheel and zipapp that [rebuild byte for byte][ra-repro], SBOMs and
266266
[published advisories][advisories]. The [CRA section][security-cra] of SECURITY.md points integrators to the same
267267
material, and the project reports its practices through [OpenSSF Scorecard][scorecard-virtualenv] and the
268268
[OpenSSF Best Practices badge][bestpractices-virtualenv].
@@ -347,8 +347,9 @@ with no token permissions and grants each job the permissions it needs and no mo
347347
to contents. The repository's [default workflow token][gh-token-permissions] is read-only, and GitHub
348348
[rejects actions not pinned to a full commit SHA][gh-sha-pinning]. Checkouts do not persist credentials, except in
349349
[pre-release.yaml][workflow-pre-release], which pushes the release commit and tag with a release App token, and the
350-
upgrade publish job, which pushes with a deploy key. PyPI uploads use trusted publishing (S1). In the tool, the app-data
351-
seeder [marks the extracted wheel image read-only][src-symlink] when it links packages by symlink.
350+
upgrade publish job, which pushes the upgrade branch with the workflow token. PyPI uploads use trusted publishing (S1).
351+
In the tool, the app-data seeder [marks the extracted wheel image read-only][src-symlink] when it links packages by
352+
symlink.
352353

353354
virtualenv uses fail-safe defaults on the network and in the batch activator. If the TLS handshake with the PyPI
354355
metadata API fails, virtualenv does not retry without verification; the unverified fallback needs the
@@ -486,8 +487,10 @@ the workflows as I1 describes, and [scorecard.yaml][workflow-scorecard] runs [Op
486487
[pr-3306]: https://github.com/pypa/virtualenv/pull/3306
487488
[pr-3309]: https://github.com/pypa/virtualenv/pull/3309
488489
[pr-3310]: https://github.com/pypa/virtualenv/pull/3310
490+
[pr-3311]: https://github.com/pypa/virtualenv/pull/3311
489491
[precommit-ci]: https://pre-commit.ci/
490492
[precommit-config]: https://github.com/pypa/virtualenv/blob/main/.pre-commit-config.yaml
493+
[pylock-zipapp]: https://github.com/pypa/virtualenv/blob/main/pylock.zipapp.toml
491494
[pypa-bootstrap]: https://github.com/pypa/bootstrap
492495
[pypi-2fa]: https://blog.pypi.org/posts/2024-01-01-2fa-enforced/
493496
[pypi-attestations]: https://docs.pypi.org/attestations/
@@ -549,7 +552,7 @@ the workflows as I1 describes, and [scorecard.yaml][workflow-scorecard] runs [Op
549552
[usage-insecure]: https://virtualenv.pypa.io/en/latest/how-to/usage.html#allow-unverified-https-for-periodic-updates
550553
[usage-override-app-data]: https://virtualenv.pypa.io/en/latest/how-to/usage.html#override-app-data-location
551554
[verify]: https://virtualenv.pypa.io/en/latest/how-to/verify-release.html
552-
[verify-rebuild]: https://virtualenv.pypa.io/en/latest/how-to/verify-release.html#rebuild-the-sdist
555+
[verify-rebuild]: https://virtualenv.pypa.io/en/latest/how-to/verify-release.html#rebuild-the-release-files
553556
[workflow-check]: https://github.com/pypa/virtualenv/blob/main/.github/workflows/check.yaml
554557
[workflow-codeql]: https://github.com/pypa/virtualenv/blob/main/.github/workflows/codeql.yaml
555558
[workflow-pre-release]: https://github.com/pypa/virtualenv/blob/main/.github/workflows/pre-release.yaml

‎docs/changelog/3330.doc.rst‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Show how to check a release with ``gh release verify`` and ``gh release verify-asset``, and how to rebuild the zipapp
2+
byte for byte with the build tool versions its SBOMs list. Drop the claims that the zipapp build does not pin the
3+
distributions it bundles and that wheels differ between build machines - by :user:`gaborbernat`.

‎docs/explanation.rst‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -598,12 +598,18 @@ involved.
598598
zipapp and the SBOM attestations. Verifying a file recomputes its digest and checks it against a signed statement
599599
whose identity you name.
600600

601+
**Immutable releases**
602+
From 21.11.0 on, once the workflow publishes a GitHub release, GitHub refuses changes to its tag and assets and
603+
signs a release attestation over the tag's commit and each asset's SHA-256. A stolen maintainer token cannot swap
604+
the zipapp or an SBOM on a published release. The release attestation names no workflow, so pair it with the
605+
provenance check.
606+
601607
**Reproducible builds**
602608
Provenance tells you which workflow run built a file, but you still trust that run. The release pins every timestamp
603-
to ``SOURCE_DATE_EPOCH``, the commit time of the tag, so you can rebuild the sdist and the wheel from the tag
604-
yourself and compare the bytes. The wheel's SBOM records the Python version and build backend the release used,
605-
which a rebuild must match. It leaves out the machine that ran the build, which the provenance attestation already
606-
names.
609+
to ``SOURCE_DATE_EPOCH``, the commit time of the tag, so you can rebuild the sdist, the wheel and the zipapp from
610+
the tag yourself and compare the bytes. The SBOMs record the Python version and build tools the release used, which
611+
a rebuild must match, and a lock file pins the distributions the zipapp bundles. The SBOMs leave out the machine
612+
that ran the build, which the provenance attestation already names.
607613

608614
**The SBOMs**
609615
Dependency scanners find the packages a project declares, and virtualenv declares neither ``pip`` nor

0 commit comments

Comments
 (0)