Skip to content

Support attestations from Google Cloud publishers #18013

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Apr 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions docs/user/attestations/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ attestations per file: one for each of the allowed predicates. Uploads with more
than two attestations per file, or with attestations with repeated predicates will
be rejected.

Currently, PyPI allows for attestations to be signed by the following Trusted
Publisher identities:

* [GitHub Actions]
* [GitLab CI/CD]
* [Google Cloud]

[in-toto Attestation Framework]: https://github.com/in-toto/attestation/blob/main/spec/README.md

[PEP 740]: https://peps.python.org/pep-0740/
Expand All @@ -49,4 +56,9 @@ be rejected.

[SLSA Provenance]: https://slsa.dev/spec/v1.0/provenance

[GitHub Actions]: /trusted-publishers/using-a-publisher/#github-actions

[GitLab CI/CD]: /trusted-publishers/using-a-publisher/#gitlab-cicd

[Google Cloud]: /trusted-publishers/using-a-publisher/#google-cloud

20 changes: 20 additions & 0 deletions docs/user/attestations/producing-attestations.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,25 @@ Before uploading attestations to the index, please:
generated attestations from it.
- The publish job now calls `twine` passing the `--attestations` flag, to enable attestation upload.

=== "Google Cloud"

[`pypi-attestations`][pypi-attestations] is a convenience library and CLI
for generating and interacting with attestation objects. You can use
either interface to produce attestations.

For example, to generate attestations for all distributions in `dist/`:

```bash
python -m pip install pypi-attestations
python -m pypi_attestations sign dist/*
```

If the above is run within a Google Cloud service with a [workload identity]
(such as Cloud Build, Compute Engine, etc.), it will use the [ambient
identity] of the service that invoked it.

See [pypi-attestations' documentation] for usage as a Python library.


[Trusted Publishing]: /trusted-publishers/

Expand All @@ -228,3 +247,4 @@ Before uploading attestations to the index, please:

[GitLab Trusted Publishing]: /trusted-publishers/using-a-publisher/#gitlab-cicd
[Linux Foundation Immutable Record notice]: https://lfprojects.org/policies/hosted-project-tools-immutable-records/
[workload identity]: https://cloud.google.com/iam/docs/workload-identity-federation
2 changes: 1 addition & 1 deletion docs/user/attestations/publish/v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ that project to verify the following:
(such as a locally-held API token).
2. That a *specific* Trusted Publisher identity was used to publish to the
project, such as a particular GitHub Actions workflow, GitLab identity,
etc.
Google Cloud service account, etc.

Put together, these allow users to assert a higher degree of confidence in
the integrity (but not necessarily trustworthiness) of projects published to PyPI,
Expand Down
2 changes: 1 addition & 1 deletion requirements/main.in
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ redis>=2.8.0,<6.0.0
rfc3986
sentry-sdk
setuptools
pypi-attestations==0.0.23
pypi-attestations==0.0.25
sqlalchemy[asyncio]>=2.0,<3.0
stdlib-list
stripe
Expand Down
6 changes: 3 additions & 3 deletions requirements/main.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1816,9 +1816,9 @@ pyparsing==3.2.3 \
--hash=sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf \
--hash=sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be
# via linehaul
pypi-attestations==0.0.23 \
--hash=sha256:2eb89bf121d983ad58dcee70a550982bb2221b0c447b191a9291c9841c7f1ed6 \
--hash=sha256:f8530f4d0aa2aab335130b9ba1cfbadc06b118c73a3836fa74d00b94c4678163
pypi-attestations==0.0.25 \
--hash=sha256:5afd0fb151445b9ad154b5a41a7097b010d881e994173022033e913eab36a974 \
--hash=sha256:9f62b34c35b481e5931979f3f222340001041127bd82945b36e34b198075d331
# via -r requirements/main.in
pyqrcode==1.2.1 \
--hash=sha256:1b2812775fa6ff5c527977c4cd2ccb07051ca7d0bc0aecf937a43864abe5eff6 \
Expand Down
7 changes: 3 additions & 4 deletions tests/unit/attestations/test_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def test_interface_matches(self):

@pytest.mark.parametrize(
"publisher_factory",
[GitHubPublisherFactory, GitLabPublisherFactory],
[GitHubPublisherFactory, GitLabPublisherFactory, GooglePublisherFactory],
)
def test_build_provenance(self, db_request, dummy_attestation, publisher_factory):
db_request.oidc_publisher = publisher_factory.create()
Expand Down Expand Up @@ -94,7 +94,6 @@ def test_parse_attestations_fails_no_publisher(self, db_request):
@pytest.mark.parametrize(
"publisher_factory",
[
GooglePublisherFactory,
ActiveStatePublisherFactory,
],
)
Expand Down Expand Up @@ -317,7 +316,7 @@ def test_parse_attestations_succeeds(

@pytest.mark.parametrize(
"publisher_factory",
[GitHubPublisherFactory, GitLabPublisherFactory],
[GitHubPublisherFactory, GitLabPublisherFactory, GooglePublisherFactory],
)
def test_build_provenance_succeeds(
self, metrics, db_request, publisher_factory, dummy_attestation
Expand Down Expand Up @@ -347,7 +346,7 @@ def test_build_provenance_succeeds(

@pytest.mark.parametrize(
"publisher_factory",
[GitHubPublisherFactory, GitLabPublisherFactory],
[GitHubPublisherFactory, GitLabPublisherFactory, GooglePublisherFactory],
)
def test_extract_attestations_from_request_empty_list(db_request, publisher_factory):
db_request.oidc_publisher = publisher_factory.create()
Expand Down
5 changes: 5 additions & 0 deletions tests/unit/oidc/models/test_google.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,11 @@ def test_exists(self, db_request, exists_in_db):

assert publisher.exists(db_request.db) == exists_in_db

def test_google_publisher_attestation_identity(self):
publisher = google.GooglePublisher(email="[email protected]")
identity = publisher.attestation_identity
assert identity.email == publisher.email


class TestPendingGooglePublisher:
@pytest.mark.parametrize("sub", ["fakesubject", None])
Expand Down
5 changes: 5 additions & 0 deletions warehouse/oidc/models/google.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

from typing import Any

from pypi_attestations import GooglePublisher as GoogleIdentity, Publisher
from sqlalchemy import ForeignKey, String, UniqueConstraint, and_, exists
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Query, mapped_column
Expand Down Expand Up @@ -93,6 +94,10 @@ def publisher_base_url(self):
def publisher_url(self, claims=None):
return None

@property
def attestation_identity(self) -> Publisher | None:
return GoogleIdentity(email=self.email)

def stored_claims(self, claims=None):
return {}

Expand Down