Skip to content
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand All @@ -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
Expand Down
9 changes: 7 additions & 2 deletions src/pypi_attestations/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@
ConversionError,
Distribution,
GitHubPublisher,
GitLabPublisher,
GooglePublisher,
Provenance,
Publisher,
)

if typing.TYPE_CHECKING: # pragma: no cover
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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): # 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
for attestation in attestation_bundle.attestations:
Expand Down
17 changes: 16 additions & 1 deletion src/pypi_attestations/_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")]


Expand Down
20 changes: 20 additions & 0 deletions test/assets/200170367.pem
Original file line number Diff line number Diff line change
@@ -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-----
13 changes: 12 additions & 1 deletion test/test_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down Expand Up @@ -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)