PyPI has been moving faster on supply chain security than most package registries. Trusted Publishing landed in 2023 and eliminated the need for long-lived API tokens. PEP 740, which defines a format for attaching digitally signed attestations to PyPI packages, reached provisional acceptance in mid-2024 and is the basis for PyPI's integrated attestation support. The net result is that Python projects can now publish packages carrying SLSA Build L2 provenance without leaving the PyPI ecosystem.
This post walks through the publish workflow as it exists in October 2024, the verification story for consumers, and the places where the integration still has rough edges. We focus on GitHub Actions as the build environment because that is where the tooling is most mature; GitLab and other CI systems are catching up but lag by roughly a release cycle.
What is Trusted Publishing, and why does it matter?
Trusted Publishing is PyPI's name for its OpenID Connect-based publishing flow. Instead of a project generating a long-lived API token and storing it in CI secrets, the project registers its CI workflow as a trusted publisher. At publish time, the CI workflow receives an OIDC token from its identity provider (GitHub, in most cases), exchanges it with PyPI for a short-lived API token, and uses that token to upload.
This matters for SLSA because the same OIDC identity that authenticates the publish is the one that Fulcio can bind to a signing certificate. When PyPI sees a publish request, it can verify that the OIDC token's issuer, repository, workflow, and ref match the registered trusted publisher. When Fulcio sees the same OIDC token, it issues a certificate asserting the same claims. The two checks reinforce each other: a publish that reaches PyPI and an attestation that is signed for the same identity are verifiably from the same workflow run.
To enable Trusted Publishing, a project owner adds a publisher under "Publishing" in the project's PyPI settings, specifying the GitHub organisation, repository, workflow filename, and optionally the environment name. The CI workflow uses pypi-publish action (currently at pypa/gh-action-pypi-publish@v1.10) with id-token: write permission, and no secrets are required.
How does PEP 740 wire attestations into PyPI?
PEP 740 defines a new PyPI API field for each release file: an attestations array containing signed attestations about that specific file. Each attestation is a bundle conforming to the Sigstore bundle specification v0.3, which wraps a DSSE envelope containing an in-toto Statement. The predicate type can be any in-toto predicate, but PyPI currently validates only SLSA Provenance v1 and PyPI publish attestations.
The attestation is tied to the release file by the file's SHA256 digest, which appears in the in-toto Statement's subject array. At upload time, the publisher sends the file and its attestation together. PyPI validates the attestation against the file hash, verifies the Sigstore bundle's signature against the Fulcio certificate, confirms the certificate's OIDC identity matches the registered trusted publisher, and stores the attestation alongside the file.
The consumer-side API exposes attestations through the JSON API (https://pypi.org/pypi/PACKAGE/VERSION/json) and through the new Simple API extension defined in PEP 740 itself. A verifier can fetch attestations by file digest and then run any Sigstore-compatible verifier against them. There is no PyPI-specific verification tool; the attestations are plain Sigstore bundles.
What does the publish workflow actually look like?
The minimum GitHub Actions workflow for PEP 740 attestation and Trusted Publishing uses two actions. The pypa/gh-action-pypi-publish@v1.10 action handles the Trusted Publishing upload. The actions/attest-build-provenance@v1 action generates a SLSA v1 provenance attestation and, in its v1.4+ releases, emits it in the Sigstore bundle format that PEP 740 expects.
The flow is: build job produces the distributions in dist/. attest job calls actions/attest-build-provenance with subject-path: 'dist/*' and show-summary: true. publish job calls pypa/gh-action-pypi-publish with attestations: true, which uploads both the distributions and their corresponding PEP 740 attestations from the attestations directory.
The configuration detail worth spelling out is that attestations: true in the publish action tells it to discover attestations via the OCI referrer convention from the local filesystem. The attest-build-provenance action writes attestations to $RUNNER_TEMP/attestations by default, and the publish action knows where to look. If you override the output path of attest-build-provenance, you also need to tell the publish action via attestations-path.
The publish action in v1.10 correctly handles the case where a package has multiple distribution files (a wheel and a sdist) each with their own attestation. It uploads them atomically: if any attestation fails PyPI's validation, the whole publish is aborted.
How do consumers verify?
For pip-based consumers, PEP 740 attestations are currently metadata that can be queried but is not automatically verified during pip install. pip v24.2 exposes attestations through pip show --verbose and through the JSON API, but verification must be performed externally.
The practical verification tools are pypi-attestations (a CLI from the Python Packaging Authority) and slsa-verifier for the SLSA provenance predicate specifically. The pypi-attestations CLI takes a distribution file and an attestation, verifies the Sigstore bundle's signature, confirms the OIDC identity matches an expected value, and returns pass/fail. For a release of my-package version 1.2.3 published from github.com/myorg/my-package, a complete verification is:
pypi-attestations verify pypi my-package==1.2.3 --identity https://github.com/myorg/my-package/.github/workflows/release.yml@refs/tags/v1.2.3
This downloads the file and the attestation, verifies both, and matches the identity against the Fulcio certificate claims. slsa-verifier verify-artifact can do additional structural checks on the SLSA provenance predicate, confirming that buildDefinition.externalParameters.workflow matches the expected path.
At an organisational level, the right pattern is to run verification at the artifact ingestion point (the internal PyPI mirror or the dependency proxy) rather than at every pip install. This gives you one place to enforce policy and one place to fail closed.
What are the rough edges?
Three rough edges remain as of October 2024. First, attest-build-provenance@v1.4 emits provenance that claims SLSA Build L2, not L3. The reusable slsa-framework/slsa-github-generator workflows can produce L3 provenance for Python, but integrating them with Trusted Publishing requires more plumbing than the attest-build-provenance flow and is not what most projects are doing today. L2 is the realistic ceiling for out-of-the-box PEP 740 publishes.
Second, PyPI's validation of attestations is strict about subject digests. If the distribution file is rebuilt between attestation and publish (for example because a wheel is regenerated for a different Python version), the digests will not match and the publish will fail. The flow must attest and publish the exact same bytes; no intermediate rebuild is safe.
Third, the ecosystem tooling lags. conda does not yet integrate with PEP 740. Poetry does not ship PEP 740 attestations by default. uv added support in v0.4 but it is opt-in. A pure pypa/build + pypa/gh-action-pypi-publish flow is the only one that works out of the box.
How Safeguard Helps
Safeguard ingests PEP 740 attestations automatically when scanning Python projects, validating the Sigstore bundle, the OIDC identity, and the SLSA provenance structure without requiring your team to run pypi-attestations by hand. We track which of your PyPI dependencies carry attestations and which do not, and we flag regressions when a previously attested package ships an unsigned release. For internal PyPI mirrors we provide an ingestion hook that fails closed if a package's attestation does not match policy, letting you enforce provenance requirements at the dependency proxy instead of at every pip install.