Skip to content

Commit c8012e8

Browse files
authored
feat: add support for Google Cloud-based Trusted Publishers (#114)
1 parent 22cc304 commit c8012e8

File tree

6 files changed

+68
-6
lines changed

6 files changed

+68
-6
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- The `GooglePublisher` type has been added to support
13+
Google Cloud-based Trusted Publishers
14+
([#114](https://github.com/trailofbits/pypi-attestations/pull/114))
15+
1016
## [0.0.23]
1117

1218
### Added

README.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,11 @@ pypi-attestations verify attestation \
141141
```
142142
143143
### Verifying a PyPI package
144+
145+
> [!IMPORTANT]
146+
> This subcommand supports publish attestations from GitHub and GitLab.
147+
> It **does not currently support** Google Cloud-based publish attestations.
148+
144149
> [!NOTE]
145150
> The package to verify can be passed either as a path to a local file, a
146151
> `pypi:` prefixed filename (e.g: 'pypi:sampleproject-1.0.0-py3-none-any.whl'),
@@ -161,8 +166,8 @@ pypi-attestations verify pypi --repository https://github.com/sigstore/sigstore-
161166
~/Downloads/sigstore-3.6.1-py3-none-any.whl
162167
```
163168
164-
This command downloads the artifact and its provenance from PyPI. The artifact
165-
is then verified against the provenance, while also checking that the provenance's
169+
This command downloads the artifact and its provenance from PyPI. The artifact
170+
is then verified against the provenance, while also checking that the provenance's
166171
signing identity matches the repository specified by the user.
167172

168173
### Converting a Sigstore bundle into a PEP 740 Attestation

src/pypi_attestations/_cli.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,9 @@
3131
ConversionError,
3232
Distribution,
3333
GitHubPublisher,
34+
GitLabPublisher,
35+
GooglePublisher,
3436
Provenance,
35-
Publisher,
3637
)
3738

3839
if typing.TYPE_CHECKING: # pragma: no cover
@@ -382,7 +383,9 @@ def _get_provenance_from_pypi(dist: Distribution) -> Provenance:
382383
_die(f"Invalid provenance: {validation_error}")
383384

384385

385-
def _check_repository_identity(expected_repository_url: str, publisher: Publisher) -> None:
386+
def _check_repository_identity(
387+
expected_repository_url: str, publisher: GitHubPublisher | GitLabPublisher
388+
) -> None:
386389
"""Check that a repository url matches the given publisher's identity."""
387390
validator = (
388391
validators.Validator()
@@ -566,6 +569,8 @@ def _verify_pypi(args: argparse.Namespace) -> None:
566569
try:
567570
for attestation_bundle in provenance.attestation_bundles:
568571
publisher = attestation_bundle.publisher
572+
if isinstance(publisher, GooglePublisher): # pragma: no cover
573+
_die("This CLI doesn't support Google Cloud-based publisher verification")
569574
_check_repository_identity(expected_repository_url=args.repository, publisher=publisher)
570575
policy = publisher._as_policy() # noqa: SLF001
571576
for attestation in attestation_bundle.attestations:

src/pypi_attestations/_impl.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -648,7 +648,22 @@ def _as_policy(self) -> VerificationPolicy:
648648
return _GitLabTrustedPublisherPolicy(self.repository, self.workflow_filepath)
649649

650650

651-
_Publisher = Union[GitHubPublisher, GitLabPublisher]
651+
class GooglePublisher(_PublisherBase):
652+
"""A Google Cloud-based Trusted Publisher."""
653+
654+
kind: Literal["Google"] = "Google"
655+
656+
email: str
657+
"""
658+
The email address of the Google Cloud service account that performed
659+
the publishing action.
660+
"""
661+
662+
def _as_policy(self) -> VerificationPolicy:
663+
return policy.Identity(identity=self.email, issuer="https://accounts.google.com")
664+
665+
666+
_Publisher = Union[GitHubPublisher, GitLabPublisher, GooglePublisher]
652667
Publisher = Annotated[_Publisher, Field(discriminator="kind")]
653668

654669

test/assets/200170367.pem

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
See: https://search.sigstore.dev/?logIndex=200170367
2+
3+
-----BEGIN CERTIFICATE-----
4+
MIIC7DCCAnKgAwIBAgIUVJkn21utBSU3vjewVuPQb3e8Jz0wCgYIKoZIzj0EAwMw
5+
NzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRl
6+
cm1lZGlhdGUwHhcNMjUwNDIxMTUwMjI3WhcNMjUwNDIxMTUxMjI3WjAAMFkwEwYH
7+
KoZIzj0CAQYIKoZIzj0DAQcDQgAE16HRcqztt38BoUOwhhagqdU43mBPeR9sctF0
8+
jTQ00NUpjWqvPc8CMmKR85kpwFxS2WfPe7D0wIByY8ZfdgT/66OCAZEwggGNMA4G
9+
A1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUi5A/
10+
s39XjLixRjkQs8mHtSEpTFMwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4Y
11+
ZD8wQAYDVR0RAQH/BDYwNIEyOTE5NDM2MTU4MjM2LWNvbXB1dGVAZGV2ZWxvcGVy
12+
LmdzZXJ2aWNlYWNjb3VudC5jb20wKQYKKwYBBAGDvzABAQQbaHR0cHM6Ly9hY2Nv
13+
dW50cy5nb29nbGUuY29tMCsGCisGAQQBg78wAQgEHQwbaHR0cHM6Ly9hY2NvdW50
14+
cy5nb29nbGUuY29tMIGLBgorBgEEAdZ5AgQCBH0EewB5AHcA3T0wasbHETJjGR4c
15+
mWc3AqJKXrjePK3/h4pygC8p7o4AAAGWWN88EgAABAMASDBGAiEA9EUW3yTYEtEe
16+
Z0SMaYlHPZ2+LHrae1hb+9bCRmdMjgwCIQDSMxXrTejGcgOZqJT8jxCZT77yieMU
17+
16PO92ZrpQ5wrjAKBggqhkjOPQQDAwNoADBlAjEAxl/X0fmqgftikX/Lq+c++syG
18+
CCNf1zHB35VYPSqN+vZvLEzbASrJjx6fFMID8pF4AjBXeTTem553VCEM3Y9bMuM9
19+
eSen6by5XyGTWL0j7ro/YjmSC+xs9IHoSHQ6vYRQH00=
20+
-----END CERTIFICATE-----

test/test_impl.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -668,7 +668,7 @@ def test_encoding(self) -> None:
668668
assert "\\n" not in model.model_dump_json()
669669

670670

671-
class TestGitHubublisher:
671+
class TestGitHubPublisher:
672672
def test_verifies_cert_with_missing_ref(self) -> None:
673673
cert_path = _ASSETS / "no-source-repository-ref-extension.pem"
674674
cert = x509.load_pem_x509_certificate(cert_path.read_bytes())
@@ -717,3 +717,14 @@ def test_fails_cert_with_no_digest_or_ref(self) -> None:
717717
),
718718
):
719719
publisher._as_policy().verify(cert)
720+
721+
722+
class TestGooglePublisher:
723+
def test_verifies(self) -> None:
724+
cert_path = _ASSETS / "200170367.pem"
725+
cert = x509.load_pem_x509_certificate(cert_path.read_bytes())
726+
727+
publisher = impl.GooglePublisher(
728+
email="919436158236-compute@developer.gserviceaccount.com",
729+
)
730+
publisher._as_policy().verify(cert)

0 commit comments

Comments
 (0)