Skip to content

Commit 9464aa3

Browse files
authored
bugfix: compare distribution names by parsed forms (#127)
* bugfix: compare distribution names by parsed forms This fixes an error introduced with #124 -- that PR correctly dropped our "ultranormalization" of distribution names, but it didn't account for the fact that we were comparing distribution names as strings, rather than as parsed forms. See pypa/gh-action-pypi-publish#365 for more context. Signed-off-by: William Woodruff <[email protected]> * ignore long line Signed-off-by: William Woodruff <[email protected]> --------- Signed-off-by: William Woodruff <[email protected]>
1 parent 1ba4cb1 commit 9464aa3

File tree

3 files changed

+51
-7
lines changed

3 files changed

+51
-7
lines changed

src/pypi_attestations/_impl.py

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
from enum import Enum
1111
from typing import TYPE_CHECKING, Annotated, Any, Literal, NewType, Optional, Union, get_args
1212

13+
import packaging
14+
import packaging.tags
15+
import packaging.utils
16+
import packaging.version
1317
import sigstore.errors
1418
from annotated_types import MinLen # noqa: TCH002
1519
from cryptography import x509
@@ -291,14 +295,17 @@ def verify(
291295
# be an exact match for their distribution filename.
292296
# See: https://github.com/pypi/warehouse/issues/18128
293297
# See: https://github.com/trailofbits/pypi-attestations/issues/123
294-
_check_dist_filename(subject.name)
295-
subject_name = subject.name
298+
parsed_subject_name = _check_dist_filename(subject.name)
296299
except ValueError as e:
297300
raise VerificationError(f"invalid subject: {str(e)}")
298301

299-
if subject_name != dist.name:
302+
# NOTE: Cannot fail, since we validate the `Distribution` name
303+
# on construction.
304+
parsed_dist_name = _check_dist_filename(dist.name)
305+
306+
if parsed_subject_name != parsed_dist_name:
300307
raise VerificationError(
301-
f"subject does not match distribution name: {subject_name} != {dist.name}"
308+
f"subject does not match distribution name: {subject.name} != {dist.name}"
302309
)
303310

304311
digest = subject.digest.root.get("sha256")
@@ -392,7 +399,17 @@ def _der_decode_utf8string(der: bytes) -> str:
392399
return der_decode(der, UTF8String)[0].decode() # type: ignore[no-any-return]
393400

394401

395-
def _check_dist_filename(dist: str) -> None:
402+
_SdistName = tuple[packaging.utils.NormalizedName, packaging.version.Version]
403+
_BdistName = tuple[
404+
packaging.utils.NormalizedName,
405+
packaging.version.Version,
406+
packaging.utils.BuildTag,
407+
frozenset[packaging.tags.Tag],
408+
]
409+
_DistName = Union[_SdistName, _BdistName]
410+
411+
412+
def _check_dist_filename(dist: str) -> _DistName:
396413
"""Validate a distribution filename for well-formedness.
397414
398415
This does **not** fully normalize the filename. For example,
@@ -406,10 +423,10 @@ def _check_dist_filename(dist: str) -> None:
406423
# already rejects non-lowercase variants.
407424
if dist.endswith(".whl"):
408425
# `parse_wheel_filename` raises a supertype of ValueError on failure.
409-
parse_wheel_filename(dist)
426+
return parse_wheel_filename(dist)
410427
elif dist.endswith((".tar.gz", ".zip")):
411428
# `parse_sdist_filename` raises a supertype of ValueError on failure.
412-
parse_sdist_filename(dist)
429+
return parse_sdist_filename(dist)
413430
else:
414431
raise ValueError(f"unknown distribution format: {dist}")
415432

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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="}}

test/test_impl.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,32 @@ def test_certificate_claims(self) -> None:
458458

459459
assert not results ^ set(attestation.certificate_claims.items())
460460

461+
def test_verify_different_wheel_tag_order(self) -> None:
462+
attestation_path = (
463+
_ASSETS
464+
/ "spt3g-1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.publish.attestation" # noqa: E501
465+
)
466+
467+
attestation = impl.Attestation.model_validate_json(attestation_path.read_bytes())
468+
469+
pol = policy.Identity(
470+
identity="[email protected]", issuer="https://github.com/login/oauth"
471+
)
472+
473+
dist = impl.Distribution(
474+
# Distribution intentionally has a different tag order.
475+
name="spt3g-1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",
476+
digest="d2772f9a5199f05ed1be8d9aa78b879e51772e3ead9d73fe8057257b1aec7cf8",
477+
)
478+
479+
attestation.verify(pol, dist, staging=True, offline=True)
480+
481+
# Distribution names are not string equivalent, but do compare
482+
# as equal when parsed.
483+
subject_name = attestation.statement["subject"][0]["name"]
484+
assert impl._check_dist_filename(subject_name) == impl._check_dist_filename(dist.name)
485+
assert subject_name != dist.name
486+
461487

462488
def test_from_bundle_missing_signatures() -> None:
463489
bundle = Bundle.from_json(dist_bundle_path.read_bytes())

0 commit comments

Comments
 (0)