Skip to content

Commit

Permalink
Fix GitLabPublisher workflow path check (#71)
Browse files Browse the repository at this point in the history
Co-authored-by: William Woodruff <william@trailofbits.com>
  • Loading branch information
facutuesca and woodruffw authored Nov 19, 2024
1 parent e0ad4cf commit d684a65
Show file tree
Hide file tree
Showing 5 changed files with 123 additions and 42 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
106 changes: 80 additions & 26 deletions src/pypi_attestations/_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand All @@ -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]:
Expand Down Expand Up @@ -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 GitLab, 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."""

Expand All @@ -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]
Expand Down
Binary file added test/assets/gitlab_oidc_project-0.0.3.tar.gz
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -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=="}}
48 changes: 32 additions & 16 deletions test/test_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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",
Expand All @@ -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(
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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())
Expand Down

0 comments on commit d684a65

Please sign in to comment.