From 79eea19c52b2ce4e732ab9820a57ca6e5191f151 Mon Sep 17 00:00:00 2001 From: Facundo Tuesca Date: Tue, 19 Nov 2024 22:19:58 +0100 Subject: [PATCH 1/2] _impl: fix GitLabPublisher workflow path check Signed-off-by: Facundo Tuesca --- CHANGELOG.md | 10 ++ src/pypi_attestations/_impl.py | 106 +++++++++++++----- test/assets/gitlab_oidc_project-0.0.3.tar.gz | Bin 0 -> 5679 bytes ...c_project-0.0.3.tar.gz.publish.attestation | 1 + test/test_impl.py | 48 +++++--- 5 files changed, 123 insertions(+), 42 deletions(-) create mode 100644 test/assets/gitlab_oidc_project-0.0.3.tar.gz create mode 100644 test/assets/gitlab_oidc_project-0.0.3.tar.gz.publish.attestation diff --git a/CHANGELOG.md b/CHANGELOG.md index cff2c39..1f94234 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- The `GitLabPublisher` policy now takes the workflow file path in order to + verify attestations, rathen than assuming it will always be `gitlab-ci.yml` + ([#71](https://github.com/trailofbits/pypi-attestations/pull/71)). +- The `GitLabPublisher` now longer expects claims being passed during construction, + rather the `ref` and `sha` claims are extracted from the certificate's extensions, + similar to `GitHubPublisher`'s behavior + ([#71](https://github.com/trailofbits/pypi-attestations/pull/71)). + ## [0.0.16] ### Added diff --git a/src/pypi_attestations/_impl.py b/src/pypi_attestations/_impl.py index da34820..087afa4 100644 --- a/src/pypi_attestations/_impl.py +++ b/src/pypi_attestations/_impl.py @@ -342,6 +342,11 @@ class Envelope(BaseModel): """ +def _der_decode_utf8string(der: bytes) -> str: + """Decode a DER-encoded UTF8String.""" + return der_decode(der, UTF8String)[0].decode() # type: ignore[no-any-return] + + def _ultranormalize_dist_filename(dist: str) -> str: """Return an "ultranormalized" form of the given distribution filename. @@ -424,11 +429,6 @@ def __init__(self, repository: str, workflow: str) -> None: ] ) - @classmethod - def _der_decode_utf8string(cls, der: bytes) -> str: - """Decode a DER-encoded UTF8String.""" - return der_decode(der, UTF8String)[0].decode() # type: ignore[no-any-return] - def verify(self, cert: Certificate) -> None: """Verify the certificate against the Trusted Publisher identity.""" self._subpolicy.verify(cert) @@ -452,18 +452,18 @@ def verify(self, cert: Certificate) -> None: # where OWNER/REPO and WORKFLOW are controlled by the TP identity, # and REF is controlled by the certificate's own claims. build_config_uri = cert.extensions.get_extension_for_oid(policy._OIDC_BUILD_CONFIG_URI_OID) # noqa: SLF001 - raw_build_config_uri = self._der_decode_utf8string(build_config_uri.value.public_bytes()) + raw_build_config_uri = _der_decode_utf8string(build_config_uri.value.public_bytes()) # (2) Extract the source repo digest and ref. source_repo_digest = cert.extensions.get_extension_for_oid( policy._OIDC_SOURCE_REPOSITORY_DIGEST_OID # noqa: SLF001 ) - sha = self._der_decode_utf8string(source_repo_digest.value.public_bytes()) + sha = _der_decode_utf8string(source_repo_digest.value.public_bytes()) source_repo_ref = cert.extensions.get_extension_for_oid( policy._OIDC_SOURCE_REPOSITORY_REF_OID # noqa: SLF001 ) - ref = self._der_decode_utf8string(source_repo_ref.value.public_bytes()) + ref = _der_decode_utf8string(source_repo_ref.value.public_bytes()) # (3)-(4): Build the expected URIs and compare them for suffix in [sha, ref]: @@ -507,6 +507,71 @@ def _as_policy(self) -> VerificationPolicy: return _GitHubTrustedPublisherPolicy(self.repository, self.workflow) +class _GitLabTrustedPublisherPolicy: + """A custom sigstore-python policy for verifying against a GitLab-based Trusted Publisher.""" + + def __init__(self, repository: str, workflow_filepath: str) -> None: + self._repository = repository + self._workflow_filepath = workflow_filepath + # This policy must also satisfy some baseline underlying policies: + # the issuer must be GitHub Actions, and the repo must be the one + # we expect. + self._subpolicy = policy.AllOf( + [ + policy.OIDCIssuerV2("https://gitlab.com"), + policy.OIDCSourceRepositoryURI(f"https://gitlab.com/{self._repository}"), + ] + ) + + def verify(self, cert: Certificate) -> None: + """Verify the certificate against the Trusted Publisher identity.""" + self._subpolicy.verify(cert) + + # This process has a few annoying steps, since a Trusted Publisher + # isn't aware of the commit or ref it runs on, while Sigstore's + # leaf certificate claims (like GitLab CI/CD's OIDC claims) only + # ever encode the workflow filename (which we need to check) next + # to the ref/sha (which we can't check). + # + # To get around this, we: + # (1) extract the `Build Config URI` extension; + # (2) extract the `Source Repository Digest` and + # `Source Repository Ref` extensions; + # (3) build the *expected* URI with the user-controlled + # Trusted Publisher identity *with* (2) + # (4) compare (1) with (3) + + # (1) Extract the build config URI, which looks like this: + # https://gitlab.com/NAMESPACE/PROJECT//WORKFLOW_FILEPATH@REF + # where NAMESPACE/PROJECT and WORKFLOW_FILEPATH are controlled by the TP identity, + # and REF is controlled by the certificate's own claims. + build_config_uri = cert.extensions.get_extension_for_oid(policy._OIDC_BUILD_CONFIG_URI_OID) # noqa: SLF001 + raw_build_config_uri = _der_decode_utf8string(build_config_uri.value.public_bytes()) + + # (2) Extract the source repo digest and ref. + source_repo_digest = cert.extensions.get_extension_for_oid( + policy._OIDC_SOURCE_REPOSITORY_DIGEST_OID # noqa: SLF001 + ) + sha = _der_decode_utf8string(source_repo_digest.value.public_bytes()) + + source_repo_ref = cert.extensions.get_extension_for_oid( + policy._OIDC_SOURCE_REPOSITORY_REF_OID # noqa: SLF001 + ) + ref = _der_decode_utf8string(source_repo_ref.value.public_bytes()) + + # (3)-(4): Build the expected URIs and compare them + for suffix in [sha, ref]: + expected = f"https://gitlab.com/{self._repository}//{self._workflow_filepath}@{suffix}" + if raw_build_config_uri == expected: + return + + # If none of the expected URIs matched, the policy fails. + raise sigstore.errors.VerificationError( + f"Certificate's Build Config URI ({build_config_uri}) does not match expected " + f"Trusted Publisher ({self._workflow_filepath} @ {self._repository})" + ) + + class GitLabPublisher(_PublisherBase): """A GitLab-based Trusted Publisher.""" @@ -519,30 +584,19 @@ class GitLabPublisher(_PublisherBase): `bar` owned by group `foo` and subgroup `baz`. """ + workflow_filepath: str + """ + The path for the CI/CD configuration file. This is usually ".gitlab-ci.yml", + but can be customized. + """ + environment: Optional[str] = None """ The optional environment that the publishing action was performed from. """ def _as_policy(self) -> VerificationPolicy: - policies: list[VerificationPolicy] = [ - policy.OIDCIssuerV2("https://gitlab.com"), - policy.OIDCSourceRepositoryURI(f"https://gitlab.com/{self.repository}"), - ] - - if not self.claims: - raise VerificationError("refusing to build a policy without claims") - - if ref := self.claims.get("ref"): - policies.append( - policy.OIDCBuildConfigURI( - f"https://gitlab.com/{self.repository}//.gitlab-ci.yml@{ref}" - ) - ) - else: - raise VerificationError("refusing to build a policy without a ref claim") - - return policy.AllOf(policies) + return _GitLabTrustedPublisherPolicy(self.repository, self.workflow_filepath) _Publisher = Union[GitHubPublisher, GitLabPublisher] diff --git a/test/assets/gitlab_oidc_project-0.0.3.tar.gz b/test/assets/gitlab_oidc_project-0.0.3.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..fdcb6fb8b5bdf0b8fdc52262f7a7dc0a55706159 GIT binary patch literal 5679 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 VecGpe+UKAD`5%WXyfOfI0046{?JEEP literal 0 HcmV?d00001 diff --git a/test/assets/gitlab_oidc_project-0.0.3.tar.gz.publish.attestation b/test/assets/gitlab_oidc_project-0.0.3.tar.gz.publish.attestation new file mode 100644 index 0000000..e6b5a62 --- /dev/null +++ b/test/assets/gitlab_oidc_project-0.0.3.tar.gz.publish.attestation @@ -0,0 +1 @@ +{"version":1,"verification_material":{"certificate":"MIIF1DCCBVugAwIBAgIUHl3MO/hxGtgk/PZ8bUcn9V1/Ql4wCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjQxMTE5MTczNjU5WhcNMjQxMTE5MTc0NjU5WjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEPDlszbJKMqT8XFwsOTP0BFSjBRQr8BG00z416LHErJLwqt+xFdFxWgEX0wbqzY/zarEHAStTP56YmrEvRJTdQ6OCBHowggR2MA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUAEYvjlQjVNj1tQNLDw1v+jjRT3MwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4YZD8wXwYDVR0RAQH/BFUwU4ZRaHR0cHM6Ly9naXRsYWIuY29tL2ZhY3V0dWVzY2EvZ2l0bGFiLW9pZGMtcHJvamVjdC8vLmdpdGxhYi1jaS55bWxAcmVmcy9oZWFkcy9tYWluMCAGCisGAQQBg78wAQEEEmh0dHBzOi8vZ2l0bGFiLmNvbTAiBgorBgEEAYO/MAEIBBQMEmh0dHBzOi8vZ2l0bGFiLmNvbTBhBgorBgEEAYO/MAEJBFMMUWh0dHBzOi8vZ2l0bGFiLmNvbS9mYWN1dHVlc2NhL2dpdGxhYi1vaWRjLXByb2plY3QvLy5naXRsYWItY2kueW1sQHJlZnMvaGVhZHMvbWFpbjA4BgorBgEEAYO/MAEKBCoMKDcyZjdjNjNiNzVlYjU1ZWE4MDg2NDk2MmYwZTY0NWM5MzQxNGRhMzQwHQYKKwYBBAGDvzABCwQPDA1naXRsYWItaG9zdGVkMEEGCisGAQQBg78wAQwEMwwxaHR0cHM6Ly9naXRsYWIuY29tL2ZhY3V0dWVzY2EvZ2l0bGFiLW9pZGMtcHJvamVjdDA4BgorBgEEAYO/MAENBCoMKDcyZjdjNjNiNzVlYjU1ZWE4MDg2NDk2MmYwZTY0NWM5MzQxNGRhMzQwHwYKKwYBBAGDvzABDgQRDA9yZWZzL2hlYWRzL21haW4wGAYKKwYBBAGDvzABDwQKDAg1NTIzNTY2NDAtBgorBgEEAYO/MAEQBB8MHWh0dHBzOi8vZ2l0bGFiLmNvbS9mYWN1dHVlc2NhMBgGCisGAQQBg78wAREECgwIMTI4ODU4MDEwYQYKKwYBBAGDvzABEgRTDFFodHRwczovL2dpdGxhYi5jb20vZmFjdXR1ZXNjYS9naXRsYWItb2lkYy1wcm9qZWN0Ly8uZ2l0bGFiLWNpLnltbEByZWZzL2hlYWRzL21haW4wOAYKKwYBBAGDvzABEwQqDCg3MmY3YzYzYjc1ZWI1NWVhODA4NjQ5NjJmMGU2NDVjOTM0MTRkYTM0MBQGCisGAQQBg78wARQEBgwEcHVzaDBTBgorBgEEAYO/MAEVBEUMQ2h0dHBzOi8vZ2l0bGFiLmNvbS9mYWN1dHVlc2NhL2dpdGxhYi1vaWRjLXByb2plY3QvLS9qb2JzLzg0MTU3NTQ5NDkwFwYKKwYBBAGDvzABFgQJDAdwcml2YXRlMIGJBgorBgEEAdZ5AgQCBHsEeQB3AHUA3T0wasbHETJjGR4cmWc3AqJKXrjePK3/h4pygC8p7o4AAAGTRX+9KQAABAMARjBEAiAmmQwhQX2nP9vsK12F+RQxv0x+iTRuB7uF3BDmQ0r1YwIgPVcLb14rgXdmj1JXamu1qCYNvlWbJeaJDpJE/3JtiEEwCgYIKoZIzj0EAwMDZwAwZAIwKvO/AAywpJGaBdJ2SZ2nJYcCi36MeID4BpBq5mN7shyCUzjiB6EVqhYxZaERGNOgAjAOgMyByDiuIa4CqUCXa32E639XEULbNfMAnMdEumsFC6RJ+aEldYS7Rq4tZ+GDjAo=","transparency_entries":[{"logIndex":"149945774","logId":{"keyId":"wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0="},"kindVersion":{"kind":"dsse","version":"0.0.1"},"integratedTime":"1732037820","inclusionPromise":{"signedEntryTimestamp":"MEYCIQDjqijkErZKeE5/se4xQqFQ5kP675QB4CtWuWPjH2MBNAIhAICIsptracjrLUF0kvySUhAiWRkbfCdpC0V5ZYx8Bzhg"},"inclusionProof":{"logIndex":"28041512","rootHash":"XLhUYuDAJiP6uPyhU+vxECyT4VAbtpWYAVdp/r/0IJQ=","treeSize":"28041513","hashes":["ou7HYOC/wjAzYv/5XVk7Dj7rwfJxmoXLKanK5kVbXB8=","uS1ExhqHy/LavONWCe+3ZkJtTZsogfnEHW/N2QS8APo=","0MOwvYwuDyGm8t9NOhLozfWwxieRm1cinUfykuBwUZs=","yByu3shb/r+bv1TUvtb0o+GWnleoOVD3w5ypLetYvFA=","ybI3cJVONWy02Pl1TrgP7ah1Sx5rY4F4mKKfpxjeWoY=","FdHKii91opeRT8Tzx7nKjZO/OA9UgHPPVOicwvp939o=","DpDOpGhoNhkb7mHanMmlbtfD7xzzhh5AU5MwyWOGLDI=","yHaIr00Cr8UwHagETV3qXuwei/B6CgyHrzcxTpFYwpA=","HdjiYX8LA9CwwDDzSy7LwCMkXLwAYQdvYIbxEn7wwOs=","E2rLOYPJFKiizYiyu07QLqkMVTVL7i2ZgXiQywdI9KQ=","4lUF0YOu9XkIDXKXA0wMSzd6VeDY3TZAgmoOeWmS2+Y=","gf+9m552B3PnkWnO0o4KdVvjcT3WVHLrCbf1DoVYKFw="],"checkpoint":{"envelope":"rekor.sigstore.dev - 1193050959916656506\n28041513\nXLhUYuDAJiP6uPyhU+vxECyT4VAbtpWYAVdp/r/0IJQ=\n\n— rekor.sigstore.dev wNI9ajBEAiB+O4uYsXgc7d0dkLvz/Suu0QszPI4cBCf3SwGYfFyjnwIgA1J8mDSavgv1sadUfe410TumS/LJlvx65+QNJ7VBvoc=\n"}},"canonicalizedBody":"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiZHNzZSIsInNwZWMiOnsiZW52ZWxvcGVIYXNoIjp7ImFsZ29yaXRobSI6InNoYTI1NiIsInZhbHVlIjoiMzU0ODZhM2Y1MjRjM2IxNTk1YTYzNjBiMzQ4NDAwM2FjNDYyODgyM2U0ZTIxYTU3NGE5ZDg4YTY3NjAwNDY5MCJ9LCJwYXlsb2FkSGFzaCI6eyJhbGdvcml0aG0iOiJzaGEyNTYiLCJ2YWx1ZSI6IjMyMTFiNDYyNmFhNjU1YWQ0NDZmMzQxN2UxM2UwODcxMThjYTc5YmViZWQyMTc5NWZiOWNmMzJjOGQyODg0NGEifSwic2lnbmF0dXJlcyI6W3sic2lnbmF0dXJlIjoiTUVRQ0lDT29HT0lPTU1RQVNUdVFJbnVEdWltR3p6NGhReUhJWHlmNHR0c1hoY0lzQWlCK0JFSnNLbjJZMGNuTzdKZnUrYWpHU1dzenVGQytBenFZM1psZERwUXZWZz09IiwidmVyaWZpZXIiOiJMUzB0TFMxQ1JVZEpUaUJEUlZKVVNVWkpRMEZVUlMwdExTMHRDazFKU1VZeFJFTkRRbFoxWjBGM1NVSkJaMGxWU0d3elRVOHZhSGhIZEdkckwxQmFPR0pWWTI0NVZqRXZVV3cwZDBObldVbExiMXBKZW1vd1JVRjNUWGNLVG5wRlZrMUNUVWRCTVZWRlEyaE5UV015Ykc1ak0xSjJZMjFWZFZwSFZqSk5ValIzU0VGWlJGWlJVVVJGZUZaNllWZGtlbVJIT1hsYVV6RndZbTVTYkFwamJURnNXa2RzYUdSSFZYZElhR05PVFdwUmVFMVVSVFZOVkdONlRtcFZOVmRvWTA1TmFsRjRUVlJGTlUxVVl6Qk9hbFUxVjJwQlFVMUdhM2RGZDFsSUNrdHZXa2w2YWpCRFFWRlpTVXR2V2tsNmFqQkVRVkZqUkZGblFVVlFSR3h6ZW1KS1MwMXhWRGhZUm5kelQxUlFNRUpHVTJwQ1VsRnlPRUpITURCNk5ERUtOa3hJUlhKS1RIZHhkQ3Q0Um1SR2VGZG5SVmd3ZDJKeGVsa3ZlbUZ5UlVoQlUzUlVVRFUyV1cxeVJYWlNTbFJrVVRaUFEwSkliM2RuWjFJeVRVRTBSd3BCTVZWa1JIZEZRaTkzVVVWQmQwbElaMFJCVkVKblRsWklVMVZGUkVSQlMwSm5aM0pDWjBWR1FsRmpSRUY2UVdSQ1owNVdTRkUwUlVablVWVkJSVmwyQ21wc1VXcFdUbW94ZEZGT1RFUjNNWFlyYW1wU1ZETk5kMGgzV1VSV1VqQnFRa0puZDBadlFWVXpPVkJ3ZWpGWmEwVmFZalZ4VG1wd1MwWlhhWGhwTkZrS1drUTRkMWgzV1VSV1VqQlNRVkZJTDBKR1ZYZFZORnBTWVVoU01HTklUVFpNZVRsdVlWaFNjMWxYU1hWWk1qbDBUREphYUZrelZqQmtWMVo2V1RKRmRncGFNbXd3WWtkR2FVeFhPWEJhUjAxMFkwaEtkbUZ0Vm1wa1F6aDJURzFrY0dSSGVHaFphVEZxWVZNMU5XSlhlRUZqYlZadFkzazViMXBYUm10amVUbDBDbGxYYkhWTlEwRkhRMmx6UjBGUlVVSm5OemgzUVZGRlJVVnRhREJrU0VKNlQyazRkbG95YkRCaVIwWnBURzFPZG1KVVFXbENaMjl5UW1kRlJVRlpUeThLVFVGRlNVSkNVVTFGYldnd1pFaENlazlwT0haYU1td3dZa2RHYVV4dFRuWmlWRUpvUW1kdmNrSm5SVVZCV1U4dlRVRkZTa0pHVFUxVlYyZ3daRWhDZWdwUGFUaDJXakpzTUdKSFJtbE1iVTUyWWxNNWJWbFhUakZrU0Zac1l6Sk9hRXd5WkhCa1IzaG9XV2t4ZG1GWFVtcE1XRUo1WWpKd2JGa3pVWFpNZVRWdUNtRllVbk5aVjBsMFdUSnJkV1ZYTVhOUlNFcHNXbTVOZG1GSFZtaGFTRTEyWWxkR2NHSnFRVFJDWjI5eVFtZEZSVUZaVHk5TlFVVkxRa052VFV0RVkza0tXbXBrYWs1cVRtbE9lbFpzV1dwVk1WcFhSVFJOUkdjeVRrUnJNazF0V1hkYVZGa3dUbGROTlUxNlVYaE9SMUpvVFhwUmQwaFJXVXRMZDFsQ1FrRkhSQXAyZWtGQ1EzZFJVRVJCTVc1aFdGSnpXVmRKZEdGSE9YcGtSMVpyVFVWRlIwTnBjMGRCVVZGQ1p6YzRkMEZSZDBWTmQzZDRZVWhTTUdOSVRUWk1lVGx1Q21GWVVuTlpWMGwxV1RJNWRFd3lXbWhaTTFZd1pGZFdlbGt5UlhaYU1td3dZa2RHYVV4WE9YQmFSMDEwWTBoS2RtRnRWbXBrUkVFMFFtZHZja0puUlVVS1FWbFBMMDFCUlU1Q1EyOU5TMFJqZVZwcVpHcE9hazVwVG5wV2JGbHFWVEZhVjBVMFRVUm5NazVFYXpKTmJWbDNXbFJaTUU1WFRUVk5lbEY0VGtkU2FBcE5lbEYzU0hkWlMwdDNXVUpDUVVkRWRucEJRa1JuVVZKRVFUbDVXbGRhZWt3eWFHeFpWMUo2VERJeGFHRlhOSGRIUVZsTFMzZFpRa0pCUjBSMmVrRkNDa1IzVVV0RVFXY3hUbFJKZWs1VVdUSk9SRUYwUW1kdmNrSm5SVVZCV1U4dlRVRkZVVUpDT0UxSVYyZ3daRWhDZWs5cE9IWmFNbXd3WWtkR2FVeHRUbllLWWxNNWJWbFhUakZrU0Zac1l6Sk9hRTFDWjBkRGFYTkhRVkZSUW1jM09IZEJVa1ZGUTJkM1NVMVVTVFJQUkZVMFRVUkZkMWxSV1V0TGQxbENRa0ZIUkFwMmVrRkNSV2RTVkVSR1JtOWtTRkozWTNwdmRrd3laSEJrUjNob1dXazFhbUl5TUhaYWJVWnFaRmhTTVZwWVRtcFpVemx1WVZoU2MxbFhTWFJpTW14ckNsbDVNWGRqYlRseFdsZE9NRXg1T0hWYU1td3dZa2RHYVV4WFRuQk1ibXgwWWtWQ2VWcFhXbnBNTW1oc1dWZFNla3d5TVdoaFZ6UjNUMEZaUzB0M1dVSUtRa0ZIUkhaNlFVSkZkMUZ4UkVObk0wMXRXVE5aZWxsNldXcGpNVnBYU1RGT1YxWm9UMFJCTkU1cVVUVk9ha3B0VFVkVk1rNUVWbXBQVkUwd1RWUlNhd3BaVkUwd1RVSlJSME5wYzBkQlVWRkNaemM0ZDBGU1VVVkNaM2RGWTBoV2VtRkVRbFJDWjI5eVFtZEZSVUZaVHk5TlFVVldRa1ZWVFZFeWFEQmtTRUo2Q2s5cE9IWmFNbXd3WWtkR2FVeHRUblppVXpsdFdWZE9NV1JJVm14ak1rNW9UREprY0dSSGVHaFphVEYyWVZkU2FreFlRbmxpTW5Cc1dUTlJka3hUT1hFS1lqSktla3g2WnpCTlZGVXpUbFJSTlU1RWEzZEdkMWxMUzNkWlFrSkJSMFIyZWtGQ1JtZFJTa1JCWkhkamJXd3lXVmhTYkUxSlIwcENaMjl5UW1kRlJRcEJaRm8xUVdkUlEwSkljMFZsVVVJelFVaFZRVE5VTUhkaGMySklSVlJLYWtkU05HTnRWMk16UVhGS1MxaHlhbVZRU3pNdmFEUndlV2RET0hBM2J6UkJDa0ZCUjFSU1dDczVTMUZCUVVKQlRVRlNha0pGUVdsQmJXMVJkMmhSV0RKdVVEbDJjMHN4TWtZclVsRjRkakI0SzJsVVVuVkNOM1ZHTTBKRWJWRXdjakVLV1hkSloxQldZMHhpTVRSeVoxaGtiV294U2xoaGJYVXhjVU5aVG5ac1YySktaV0ZLUkhCS1JTOHpTblJwUlVWM1EyZFpTVXR2V2tsNmFqQkZRWGROUkFwYWQwRjNXa0ZKZDB0MlR5OUJRWGwzY0VwSFlVSmtTakpUV2pKdVNsbGpRMmt6TmsxbFNVUTBRbkJDY1RWdFRqZHphSGxEVlhwcWFVSTJSVlp4YUZsNENscGhSVkpIVGs5blFXcEJUMmROZVVKNVJHbDFTV0UwUTNGVlExaGhNekpGTmpNNVdFVlZUR0pPWmsxQmJrMWtSWFZ0YzBaRE5sSktLMkZGYkdSWlV6Y0tVbkUwZEZvclIwUnFRVzg5Q2kwdExTMHRSVTVFSUVORlVsUkpSa2xEUVZSRkxTMHRMUzBLIn1dfX0="}]},"envelope":{"statement":"eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoiZ2l0bGFiX29pZGNfcHJvamVjdC0wLjAuMy50YXIuZ3oiLCJkaWdlc3QiOnsic2hhMjU2IjoiYzFjYTliMGQ4NWRmMTYwNjQ1MTA5ODIzMzAxODUzNDQ5N2JmNTg0MzYyZTEwZTRhOGMyMWRmYWVhOTJjMDJhOCJ9fV0sInByZWRpY2F0ZVR5cGUiOiJodHRwczovL2RvY3MucHlwaS5vcmcvYXR0ZXN0YXRpb25zL3B1Ymxpc2gvdjEiLCJwcmVkaWNhdGUiOm51bGx9","signature":"MEQCICOoGOIOMMQASTuQInuDuimGzz4hQyHIXyf4ttsXhcIsAiB+BEJsKn2Y0cnO7Jfu+ajGSWszuFC+AzqY3ZldDpQvVg=="}} \ No newline at end of file diff --git a/test/test_impl.py b/test/test_impl.py index b9dafc2..e501983 100644 --- a/test/test_impl.py +++ b/test/test_impl.py @@ -35,6 +35,10 @@ gh_signed_dist = impl.Distribution.from_file(gh_signed_dist_path) gh_signed_dist_bundle_path = _ASSETS / "pypi_attestation_models-0.0.4a2.tar.gz.sigstore" +gl_signed_dist_path = _ASSETS / "gitlab_oidc_project-0.0.3.tar.gz" +gl_signed_dist = impl.Distribution.from_file(gl_signed_dist_path) +gl_attestation_path = _ASSETS / "gitlab_oidc_project-0.0.3.tar.gz.publish.attestation" + class TestDistribution: def test_from_file_nonexistent(self, tmp_path: Path) -> None: @@ -147,6 +151,17 @@ def test_verify_from_github_publisher(self, claims: Optional[dict]) -> None: assert predicate_type == "https://docs.pypi.org/attestations/publish/v1" assert predicate == {} + def test_verify_from_gitlab_publisher(self) -> None: + publisher = impl.GitLabPublisher( + repository="facutuesca/gitlab-oidc-project", + workflow_filepath=".gitlab-ci.yml", + ) + + attestation = impl.Attestation.model_validate_json(gl_attestation_path.read_text()) + predicate_type, predicate = attestation.verify(publisher, gl_signed_dist) + assert predicate_type == "https://docs.pypi.org/attestations/publish/v1" + assert predicate is None + def test_verify_from_github_publisher_wrong(self) -> None: publisher = impl.GitHubPublisher( repository="trailofbits/pypi-attestation-models", @@ -159,6 +174,16 @@ def test_verify_from_github_publisher_wrong(self) -> None: with pytest.raises(impl.VerificationError, match=r"Build Config URI .+ does not match"): attestation.verify(publisher, gh_signed_dist) + def test_verify_from_gitlab_publisher_wrong(self) -> None: + publisher = impl.GitLabPublisher( + repository="facutuesca/gitlab-oidc-project", + workflow_filepath="wrong.yml", + ) + + attestation = impl.Attestation.model_validate_json(gl_attestation_path.read_text()) + with pytest.raises(impl.VerificationError, match=r"Build Config URI .+ does not match"): + attestation.verify(publisher, gl_signed_dist) + def test_verify(self) -> None: # Our checked-in asset has this identity. pol = policy.Identity( @@ -541,10 +566,16 @@ def test_discriminator(self) -> None: assert gh.workflow == "publish.yml" assert TypeAdapter(impl.Publisher).validate_json(json.dumps(gh_raw)) == gh - gl_raw = {"kind": "GitLab", "repository": "foo/bar/baz", "environment": "publish"} + gl_raw = { + "kind": "GitLab", + "repository": "foo/bar/baz", + "workflow_filepath": "dir/release.yml", + "environment": "publish", + } gl: impl.Publisher = TypeAdapter(impl.Publisher).validate_python(gl_raw) assert isinstance(gl, impl.GitLabPublisher) assert gl.repository == "foo/bar/baz" + assert gl.workflow_filepath == "dir/release.yml" assert gl.environment == "publish" assert TypeAdapter(impl.Publisher).validate_json(json.dumps(gl_raw)) == gl @@ -573,21 +604,6 @@ def test_claims(self) -> None: } -class TestGitLabPublisher: - def test_as_policy(self) -> None: - publisher = impl.GitLabPublisher(repository="fake/fake", claims={"ref": "refs/heads/main"}) - pol: policy.AllOf = publisher._as_policy() # type: ignore[assignment] - - assert len(pol._children) == 3 - - @pytest.mark.parametrize("claims", [None, {}, {"something": "unrelated"}, {"ref": None}]) - def test_as_policy_invalid(self, claims: Optional[dict]) -> None: - publisher = impl.GitLabPublisher(repository="fake/fake", claims=claims) - - with pytest.raises(impl.VerificationError, match="refusing to build a policy"): - publisher._as_policy() - - class TestProvenance: def test_version(self) -> None: attestation = impl.Attestation.model_validate_json(dist_attestation_path.read_bytes()) From 86333975881a68623548e1e1cdaa490cf54335da Mon Sep 17 00:00:00 2001 From: Facundo Tuesca Date: Tue, 19 Nov 2024 22:30:03 +0100 Subject: [PATCH 2/2] Update src/pypi_attestations/_impl.py Co-authored-by: William Woodruff --- src/pypi_attestations/_impl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pypi_attestations/_impl.py b/src/pypi_attestations/_impl.py index 087afa4..a6089ae 100644 --- a/src/pypi_attestations/_impl.py +++ b/src/pypi_attestations/_impl.py @@ -514,7 +514,7 @@ def __init__(self, repository: str, workflow_filepath: str) -> None: self._repository = repository self._workflow_filepath = workflow_filepath # This policy must also satisfy some baseline underlying policies: - # the issuer must be GitHub Actions, and the repo must be the one + # the issuer must be GitLab, and the repo must be the one # we expect. self._subpolicy = policy.AllOf( [