diff --git a/src/pypi_attestations/_impl.py b/src/pypi_attestations/_impl.py index 4a9b127..61ee13a 100644 --- a/src/pypi_attestations/_impl.py +++ b/src/pypi_attestations/_impl.py @@ -10,6 +10,10 @@ from enum import Enum from typing import TYPE_CHECKING, Annotated, Any, Literal, NewType, Optional, Union, get_args +import packaging +import packaging.tags +import packaging.utils +import packaging.version import sigstore.errors from annotated_types import MinLen # noqa: TCH002 from cryptography import x509 @@ -291,14 +295,17 @@ def verify( # be an exact match for their distribution filename. # See: https://github.com/pypi/warehouse/issues/18128 # See: https://github.com/trailofbits/pypi-attestations/issues/123 - _check_dist_filename(subject.name) - subject_name = subject.name + parsed_subject_name = _check_dist_filename(subject.name) except ValueError as e: raise VerificationError(f"invalid subject: {str(e)}") - if subject_name != dist.name: + # NOTE: Cannot fail, since we validate the `Distribution` name + # on construction. + parsed_dist_name = _check_dist_filename(dist.name) + + if parsed_subject_name != parsed_dist_name: raise VerificationError( - f"subject does not match distribution name: {subject_name} != {dist.name}" + f"subject does not match distribution name: {subject.name} != {dist.name}" ) digest = subject.digest.root.get("sha256") @@ -392,7 +399,17 @@ def _der_decode_utf8string(der: bytes) -> str: return der_decode(der, UTF8String)[0].decode() # type: ignore[no-any-return] -def _check_dist_filename(dist: str) -> None: +_SdistName = tuple[packaging.utils.NormalizedName, packaging.version.Version] +_BdistName = tuple[ + packaging.utils.NormalizedName, + packaging.version.Version, + packaging.utils.BuildTag, + frozenset[packaging.tags.Tag], +] +_DistName = Union[_SdistName, _BdistName] + + +def _check_dist_filename(dist: str) -> _DistName: """Validate a distribution filename for well-formedness. This does **not** fully normalize the filename. For example, @@ -406,10 +423,10 @@ def _check_dist_filename(dist: str) -> None: # already rejects non-lowercase variants. if dist.endswith(".whl"): # `parse_wheel_filename` raises a supertype of ValueError on failure. - parse_wheel_filename(dist) + return parse_wheel_filename(dist) elif dist.endswith((".tar.gz", ".zip")): # `parse_sdist_filename` raises a supertype of ValueError on failure. - parse_sdist_filename(dist) + return parse_sdist_filename(dist) else: raise ValueError(f"unknown distribution format: {dist}") diff --git a/test/assets/spt3g-1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.publish.attestation b/test/assets/spt3g-1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.publish.attestation new file mode 100644 index 0000000..9590156 --- /dev/null +++ b/test/assets/spt3g-1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.publish.attestation @@ -0,0 +1 @@ +{"version":1,"verification_material":{"certificate":"MIIC1DCCAlqgAwIBAgIUI6ApkULorPzdGxrzzFiq5NISDzowCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjUwNjAzMDIxMTQ3WhcNMjUwNjAzMDIyMTQ3WjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEOgbM4XGw276qP0M8QHDbFEBuF/ZHDwsPG9Ufu90fwppINr5B5X72CfWEWQd+xQH2j/n0K3IbuNTX2+abrDyZP6OCAXkwggF1MA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUMkpwWIsig5OJQxqUNOg5yDhP6L0wHwYDVR0jBBgwFoAUcYYwphR8Ym/599b0BRp/X//rb6wwIwYDVR0RAQH/BBkwF4EVd2lsbGlhbUB5b3NzYXJpYW4ubmV0MCwGCisGAQQBg78wAQEEHmh0dHBzOi8vZ2l0aHViLmNvbS9sb2dpbi9vYXV0aDAuBgorBgEEAYO/MAEIBCAMHmh0dHBzOi8vZ2l0aHViLmNvbS9sb2dpbi9vYXV0aDCBigYKKwYBBAHWeQIEAgR8BHoAeAB2ACswvNxoiMni4dgmKV50H0g5MZYC8pwzy15DQP6yrIZ6AAABlzOPIW0AAAQDAEcwRQIgITGONvxxY9K6dDmLOk0MpkNDAksT79ZgGi97hrMwxhYCIQCpEAqFVWjZTxoVOMnrJdOfO32T9nODooxEuZO93NL4WzAKBggqhkjOPQQDAwNoADBlAjAnFj1zpQbOOybxCoCQLpGZXKib2DIKZ2PgWqZPcviHF3hnHyVPOlZsEzoZRmENn0ACMQCYVhunuSXKPwqcEDCizOrPrmuUxMV6QOA5JTfm2FTjCoByHHiJnNg6zaGtCdcH934=","transparency_entries":[{"logIndex":"43047182","logId":{"keyId":"0y8wo8MtY5wrdiIFohx7sHeI5oKDpK5vQhGHI6G+pJY="},"kindVersion":{"kind":"dsse","version":"0.0.1"},"integratedTime":"1748916708","inclusionPromise":{"signedEntryTimestamp":"MEUCIQCfM8ovwc04CEOHgX6fGf5Pzz82+vDTsHslOBVUGezYIAIgGn7dk6tO7J39kmf/qnlxmZPydLBJoncbLNp6E8fvgFM="},"inclusionProof":{"logIndex":"11364770","rootHash":"ENMXdOX33Y4lNWu52ZxECkS2DK3mEFZgkfS/kSRm5oA=","treeSize":"11364771","hashes":["AWheyozzascmgR49/VMzWgWt9UmWTNKIf2rc6PuTugA=","kumo1jNbrVlSyW2vbHZ0ZpHoWT2V1JvhDp24nCXPPC8=","JyRDG0Zq0G+9t/GT8bw4OZT1wp5JTuvBg15t12oQYXw=","xIvGcJZcwnG+yR9P1yHBaTTAyorVGqdeoa5jid5x97c=","V48BwQhJSweCIRk7yVu1rhWZ8oFfO9Qxs3SZbzVJ0kw=","58Noh/WgnaJutPtj9YL2QRs6Kp42XcPEibW2RlJltE8=","hiBsgkAwBBrSUXxyhhIXU+eEpNvBb20KnrGpN0zTnkU=","QIeOgI91ZwOrM9y+4z277AZ241pn7POReCZzUU/gM8k=","kPnAtj3bBHoLjYDGPJ3n/tPr3Zuy0BYmlTJOFsUAiHU=","4X3ZcZ5JupJMfhwYrQk+zbHb+9n5mEEw+A6nkd7pL0g=","3itMWeZPQXMyYki8lnPZVbzzneXD3w/xvhQsm7VP1ck=","OdoqbUqBYHhj2W1RLM8APkQOnM2K9gzGm1KPFmwIIeQ="],"checkpoint":{"envelope":"rekor.sigstage.dev - 8202293616175992157\n11364771\nENMXdOX33Y4lNWu52ZxECkS2DK3mEFZgkfS/kSRm5oA=\n\n— rekor.sigstage.dev 0y8wozBFAiAYfxAWPCarnkqNKYuOWM332aYBjoqbunGVZpRwpWbBrAIhAJSmUeuXeFaS1aWT57W7u0o5hQatvLGlYtjUYXFXZc7y\n"}},"canonicalizedBody":"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiZHNzZSIsInNwZWMiOnsiZW52ZWxvcGVIYXNoIjp7ImFsZ29yaXRobSI6InNoYTI1NiIsInZhbHVlIjoiZTMyZmQyNzE5ZTgyZTgyYzI3OWRjNGIwMThlMzdkZDk2OTY1MDFjY2E2YzM3ZDIwNjI2MjJlOGM3OTIzZDJmOCJ9LCJwYXlsb2FkSGFzaCI6eyJhbGdvcml0aG0iOiJzaGEyNTYiLCJ2YWx1ZSI6IjA5NTcwNjM4Njk0ZTY4MDJmNzlhOGY3NjJlMGJiYzcxYWQyOTNiZGE1MTQyNDg2NjQ5OTU4OGRiM2M1NDE5YTIifSwic2lnbmF0dXJlcyI6W3sic2lnbmF0dXJlIjoiTUVVQ0lRRDNuZUk1cmxsMjFPSFBhenp1TERHZklGWUVoOTA0WVdkdkhka2oxUlptb3dJZ0t6NVJSazdJMUNCNnhLSGM4b1QxVVh1TE91Mnc2ZzdINFFJN1ZCVGhKUFU9IiwidmVyaWZpZXIiOiJMUzB0TFMxQ1JVZEpUaUJEUlZKVVNVWkpRMEZVUlMwdExTMHRDazFKU1VNeFJFTkRRV3h4WjBGM1NVSkJaMGxWU1RaQmNHdFZURzl5VUhwa1IzaHllbnBHYVhFMVRrbFRSSHB2ZDBObldVbExiMXBKZW1vd1JVRjNUWGNLVG5wRlZrMUNUVWRCTVZWRlEyaE5UV015Ykc1ak0xSjJZMjFWZFZwSFZqSk5ValIzU0VGWlJGWlJVVVJGZUZaNllWZGtlbVJIT1hsYVV6RndZbTVTYkFwamJURnNXa2RzYUdSSFZYZElhR05PVFdwVmQwNXFRWHBOUkVsNFRWUlJNMWRvWTA1TmFsVjNUbXBCZWsxRVNYbE5WRkV6VjJwQlFVMUdhM2RGZDFsSUNrdHZXa2w2YWpCRFFWRlpTVXR2V2tsNmFqQkVRVkZqUkZGblFVVlBaMkpOTkZoSGR6STNObkZRTUUwNFVVaEVZa1pGUW5WR0wxcElSSGR6VUVjNVZXWUtkVGt3Wm5kd2NFbE9jalZDTlZnM01rTm1WMFZYVVdRcmVGRklNbW92YmpCTE0wbGlkVTVVV0RJcllXSnlSSGxhVURaUFEwRllhM2RuWjBZeFRVRTBSd3BCTVZWa1JIZEZRaTkzVVVWQmQwbElaMFJCVkVKblRsWklVMVZGUkVSQlMwSm5aM0pDWjBWR1FsRmpSRUY2UVdSQ1owNVdTRkUwUlVablVWVk5hM0IzQ2xkSmMybG5OVTlLVVhoeFZVNVBaelY1UkdoUU5rd3dkMGgzV1VSV1VqQnFRa0puZDBadlFWVmpXVmwzY0doU09GbHRMelU1T1dJd1FsSndMMWd2TDNJS1lqWjNkMGwzV1VSV1VqQlNRVkZJTDBKQ2EzZEdORVZXWkRKc2MySkhiR2hpVlVJMVlqTk9lbGxZU25CWlZ6UjFZbTFXTUUxRGQwZERhWE5IUVZGUlFncG5OemgzUVZGRlJVaHRhREJrU0VKNlQyazRkbG95YkRCaFNGWnBURzFPZG1KVE9YTmlNbVJ3WW1rNWRsbFlWakJoUkVGMVFtZHZja0puUlVWQldVOHZDazFCUlVsQ1EwRk5TRzFvTUdSSVFucFBhVGgyV2pKc01HRklWbWxNYlU1MllsTTVjMkl5WkhCaWFUbDJXVmhXTUdGRVEwSnBaMWxMUzNkWlFrSkJTRmNLWlZGSlJVRm5VamhDU0c5QlpVRkNNa0ZEYzNkMlRuaHZhVTF1YVRSa1oyMUxWalV3U0RCbk5VMWFXVU00Y0hkNmVURTFSRkZRTm5seVNWbzJRVUZCUWdwc2VrOVFTVmN3UVVGQlVVUkJSV04zVWxGSlowbFVSMDlPZG5oNFdUbExObVJFYlV4UGF6Qk5jR3RPUkVGcmMxUTNPVnBuUjJrNU4yaHlUWGQ0YUZsRENrbFJRM0JGUVhGR1ZsZHFXbFI0YjFaUFRXNXlTbVJQWms4ek1sUTViazlFYjI5NFJYVmFUemt6VGt3MFYzcEJTMEpuWjNGb2EycFBVRkZSUkVGM1RtOEtRVVJDYkVGcVFXNUdhakY2Y0ZGaVQwOTVZbmhEYjBOUlRIQkhXbGhMYVdJeVJFbExXakpRWjFkeFdsQmpkbWxJUmpOb2JraDVWbEJQYkZwelJYcHZXZ3BTYlVWT2JqQkJRMDFSUTFsV2FIVnVkVk5ZUzFCM2NXTkZSRU5wZWs5eVVISnRkVlY0VFZZMlVVOUJOVXBVWm0weVJsUnFRMjlDZVVoSWFVcHVUbWMyQ25waFIzUkRaR05JT1RNMFBRb3RMUzB0TFVWT1JDQkRSVkpVU1VaSlEwRlVSUzB0TFMwdENnPT0ifV19fQ=="}]},"envelope":{"statement":"eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoic3B0M2ctMS4wLWNwMzEwLWNwMzEwLW1hbnlsaW51eF8yXzE3X3g4Nl82NC5tYW55bGludXgyMDE0X3g4Nl82NC53aGwiLCJkaWdlc3QiOnsic2hhMjU2IjoiZDI3NzJmOWE1MTk5ZjA1ZWQxYmU4ZDlhYTc4Yjg3OWU1MTc3MmUzZWFkOWQ3M2ZlODA1NzI1N2IxYWVjN2NmOCJ9fV0sInByZWRpY2F0ZVR5cGUiOiJodHRwczovL2RvY3MucHlwaS5vcmcvYXR0ZXN0YXRpb25zL3B1Ymxpc2gvdjEiLCJwcmVkaWNhdGUiOm51bGx9","signature":"MEUCIQD3neI5rll21OHPazzuLDGfIFYEh904YWdvHdkj1RZmowIgKz5RRk7I1CB6xKHc8oT1UXuLOu2w6g7H4QI7VBThJPU="}} \ No newline at end of file diff --git a/test/test_impl.py b/test/test_impl.py index 5bd1e9f..19990d9 100644 --- a/test/test_impl.py +++ b/test/test_impl.py @@ -458,6 +458,32 @@ def test_certificate_claims(self) -> None: assert not results ^ set(attestation.certificate_claims.items()) + def test_verify_different_wheel_tag_order(self) -> None: + attestation_path = ( + _ASSETS + / "spt3g-1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.publish.attestation" # noqa: E501 + ) + + attestation = impl.Attestation.model_validate_json(attestation_path.read_bytes()) + + pol = policy.Identity( + identity="william@yossarian.net", issuer="https://github.com/login/oauth" + ) + + dist = impl.Distribution( + # Distribution intentionally has a different tag order. + name="spt3g-1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", + digest="d2772f9a5199f05ed1be8d9aa78b879e51772e3ead9d73fe8057257b1aec7cf8", + ) + + attestation.verify(pol, dist, staging=True, offline=True) + + # Distribution names are not string equivalent, but do compare + # as equal when parsed. + subject_name = attestation.statement["subject"][0]["name"] + assert impl._check_dist_filename(subject_name) == impl._check_dist_filename(dist.name) + assert subject_name != dist.name + def test_from_bundle_missing_signatures() -> None: bundle = Bundle.from_json(dist_bundle_path.read_bytes())