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
31 changes: 24 additions & 7 deletions src/pypi_attestations/_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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,
Expand All @@ -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}")

Expand Down
Original file line number Diff line number Diff line change
@@ -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="}}
26 changes: 26 additions & 0 deletions test/test_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NB: This isn't the actual attestation from this package; I've re-signed the artifact with my own identity from the Sigstore staging instance for this test.

)

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