From ac18d11a7ca1bd28cd467f2157d4e8d0cc9605f1 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Mon, 21 Apr 2025 14:46:28 -0400 Subject: [PATCH 1/3] feat: add support for Google Cloud-based Trusted Publishers Signed-off-by: William Woodruff --- src/pypi_attestations/_impl.py | 17 ++++++++++++++++- test/assets/200170367.pem | 20 ++++++++++++++++++++ test/test_impl.py | 13 ++++++++++++- 3 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 test/assets/200170367.pem diff --git a/src/pypi_attestations/_impl.py b/src/pypi_attestations/_impl.py index 0fd0f53..958bebf 100644 --- a/src/pypi_attestations/_impl.py +++ b/src/pypi_attestations/_impl.py @@ -648,7 +648,22 @@ def _as_policy(self) -> VerificationPolicy: return _GitLabTrustedPublisherPolicy(self.repository, self.workflow_filepath) -_Publisher = Union[GitHubPublisher, GitLabPublisher] +class GooglePublisher(_PublisherBase): + """A Google Cloud-based Trusted Publisher.""" + + kind: Literal["Google"] = "Google" + + email: str + """ + The email address of the Google Cloud service account that performed + the publishing action. + """ + + def _as_policy(self) -> VerificationPolicy: + return policy.Identity(identity=self.email, issuer="https://accounts.google.com") + + +_Publisher = Union[GitHubPublisher, GitLabPublisher, GooglePublisher] Publisher = Annotated[_Publisher, Field(discriminator="kind")] diff --git a/test/assets/200170367.pem b/test/assets/200170367.pem new file mode 100644 index 0000000..1d33916 --- /dev/null +++ b/test/assets/200170367.pem @@ -0,0 +1,20 @@ +See: https://search.sigstore.dev/?logIndex=200170367 + +-----BEGIN CERTIFICATE----- +MIIC7DCCAnKgAwIBAgIUVJkn21utBSU3vjewVuPQb3e8Jz0wCgYIKoZIzj0EAwMw +NzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRl +cm1lZGlhdGUwHhcNMjUwNDIxMTUwMjI3WhcNMjUwNDIxMTUxMjI3WjAAMFkwEwYH +KoZIzj0CAQYIKoZIzj0DAQcDQgAE16HRcqztt38BoUOwhhagqdU43mBPeR9sctF0 +jTQ00NUpjWqvPc8CMmKR85kpwFxS2WfPe7D0wIByY8ZfdgT/66OCAZEwggGNMA4G +A1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUi5A/ +s39XjLixRjkQs8mHtSEpTFMwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4Y +ZD8wQAYDVR0RAQH/BDYwNIEyOTE5NDM2MTU4MjM2LWNvbXB1dGVAZGV2ZWxvcGVy +LmdzZXJ2aWNlYWNjb3VudC5jb20wKQYKKwYBBAGDvzABAQQbaHR0cHM6Ly9hY2Nv +dW50cy5nb29nbGUuY29tMCsGCisGAQQBg78wAQgEHQwbaHR0cHM6Ly9hY2NvdW50 +cy5nb29nbGUuY29tMIGLBgorBgEEAdZ5AgQCBH0EewB5AHcA3T0wasbHETJjGR4c +mWc3AqJKXrjePK3/h4pygC8p7o4AAAGWWN88EgAABAMASDBGAiEA9EUW3yTYEtEe +Z0SMaYlHPZ2+LHrae1hb+9bCRmdMjgwCIQDSMxXrTejGcgOZqJT8jxCZT77yieMU +16PO92ZrpQ5wrjAKBggqhkjOPQQDAwNoADBlAjEAxl/X0fmqgftikX/Lq+c++syG +CCNf1zHB35VYPSqN+vZvLEzbASrJjx6fFMID8pF4AjBXeTTem553VCEM3Y9bMuM9 +eSen6by5XyGTWL0j7ro/YjmSC+xs9IHoSHQ6vYRQH00= +-----END CERTIFICATE----- diff --git a/test/test_impl.py b/test/test_impl.py index 3dc2787..b19941d 100644 --- a/test/test_impl.py +++ b/test/test_impl.py @@ -668,7 +668,7 @@ def test_encoding(self) -> None: assert "\\n" not in model.model_dump_json() -class TestGitHubublisher: +class TestGitHubPublisher: def test_verifies_cert_with_missing_ref(self) -> None: cert_path = _ASSETS / "no-source-repository-ref-extension.pem" 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: ), ): publisher._as_policy().verify(cert) + + +class TestGooglePublisher: + def test_verifies(self) -> None: + cert_path = _ASSETS / "200170367.pem" + cert = x509.load_pem_x509_certificate(cert_path.read_bytes()) + + publisher = impl.GooglePublisher( + email="919436158236-compute@developer.gserviceaccount.com", + ) + publisher._as_policy().verify(cert) From 3919e510e06717ac44e74bbe06080c1ed954f1f2 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Mon, 21 Apr 2025 15:00:41 -0400 Subject: [PATCH 2/3] fix lint Signed-off-by: William Woodruff --- CHANGELOG.md | 6 ++++++ README.md | 9 +++++++-- src/pypi_attestations/_cli.py | 9 +++++++-- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68e08c7..54981fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- The `GooglePublisher` type has been added to support + Google Cloud-based Trusted Publishers + ([#114](https://github.com/trailofbits/pypi-attestations/pull/114)) + ## [0.0.23] ### Added diff --git a/README.md b/README.md index 05e803c..44f2b78 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,11 @@ pypi-attestations verify attestation \ ``` ### Verifying a PyPI package + +> [!IMPORTANT] +> This subcommand supports publish attestations from GitHub and GitLab. +> It **does not currently support** Google Cloud-based publish attestations. + > [!NOTE] > The package to verify can be passed either as a path to a local file, a > `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- ~/Downloads/sigstore-3.6.1-py3-none-any.whl ``` -This command downloads the artifact and its provenance from PyPI. The artifact -is then verified against the provenance, while also checking that the provenance's +This command downloads the artifact and its provenance from PyPI. The artifact +is then verified against the provenance, while also checking that the provenance's signing identity matches the repository specified by the user. ### Converting a Sigstore bundle into a PEP 740 Attestation diff --git a/src/pypi_attestations/_cli.py b/src/pypi_attestations/_cli.py index 96805cb..dde12f6 100644 --- a/src/pypi_attestations/_cli.py +++ b/src/pypi_attestations/_cli.py @@ -31,8 +31,9 @@ ConversionError, Distribution, GitHubPublisher, + GitLabPublisher, + GooglePublisher, Provenance, - Publisher, ) if typing.TYPE_CHECKING: # pragma: no cover @@ -382,7 +383,9 @@ def _get_provenance_from_pypi(dist: Distribution) -> Provenance: _die(f"Invalid provenance: {validation_error}") -def _check_repository_identity(expected_repository_url: str, publisher: Publisher) -> None: +def _check_repository_identity( + expected_repository_url: str, publisher: GitHubPublisher | GitLabPublisher +) -> None: """Check that a repository url matches the given publisher's identity.""" validator = ( validators.Validator() @@ -566,6 +569,8 @@ def _verify_pypi(args: argparse.Namespace) -> None: try: for attestation_bundle in provenance.attestation_bundles: publisher = attestation_bundle.publisher + if isinstance(publisher, GooglePublisher): + _die("This CLI doesn't support Google Cloud-based publisher verification") _check_repository_identity(expected_repository_url=args.repository, publisher=publisher) policy = publisher._as_policy() # noqa: SLF001 for attestation in attestation_bundle.attestations: From 94bb70dd9952d7359bc6fb0085bd732264ea1b21 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Mon, 21 Apr 2025 15:09:16 -0400 Subject: [PATCH 3/3] nocover Signed-off-by: William Woodruff --- src/pypi_attestations/_cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pypi_attestations/_cli.py b/src/pypi_attestations/_cli.py index dde12f6..52effea 100644 --- a/src/pypi_attestations/_cli.py +++ b/src/pypi_attestations/_cli.py @@ -569,7 +569,7 @@ def _verify_pypi(args: argparse.Namespace) -> None: try: for attestation_bundle in provenance.attestation_bundles: publisher = attestation_bundle.publisher - if isinstance(publisher, GooglePublisher): + if isinstance(publisher, GooglePublisher): # pragma: no cover _die("This CLI doesn't support Google Cloud-based publisher verification") _check_repository_identity(expected_repository_url=args.repository, publisher=publisher) policy = publisher._as_policy() # noqa: SLF001