From 9e73105307cfba3e50affcb4ffbabe887c80ccbf Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Wed, 10 Jul 2024 12:06:36 -0400 Subject: [PATCH 1/6] _impl: enumerate known attestation types Signed-off-by: William Woodruff --- src/pypi_attestations/_impl.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/pypi_attestations/_impl.py b/src/pypi_attestations/_impl.py index 735494a..440e48d 100644 --- a/src/pypi_attestations/_impl.py +++ b/src/pypi_attestations/_impl.py @@ -6,6 +6,7 @@ from __future__ import annotations import base64 +from enum import StrEnum from typing import TYPE_CHECKING, Annotated, Any, Literal, NewType import sigstore.errors @@ -37,6 +38,13 @@ from sigstore.verify.policy import VerificationPolicy # pragma: no cover +class AttestationType(StrEnum): + """Attestation types known to PyPI.""" + + SLSA_PROVENANCE_V1 = "https://slsa.dev/provenance/v1" + PYPI_PUBLISH_V1 = "https://docs.pypi.org/attestations/publish/v1" + + class AttestationError(ValueError): """Base error for all APIs.""" @@ -119,7 +127,7 @@ def sign(cls, signer: Signer, dist: Path) -> Attestation: ) ] ) - .predicate_type("https://docs.pypi.org/attestations/publish/v1") + .predicate_type(AttestationType.SLSA_PROVENANCE_V1) .build() ) except DsseError as e: From e255778a862fb27b3f417c0aae25571da5d51337 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Wed, 10 Jul 2024 12:09:27 -0400 Subject: [PATCH 2/6] _impl: reject unknown attestations during verify Signed-off-by: William Woodruff --- src/pypi_attestations/_impl.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/pypi_attestations/_impl.py b/src/pypi_attestations/_impl.py index 440e48d..73cbe39 100644 --- a/src/pypi_attestations/_impl.py +++ b/src/pypi_attestations/_impl.py @@ -194,6 +194,11 @@ def verify( if digest is None or digest != expected_digest: raise VerificationError("subject does not match distribution digest") + try: + AttestationType(statement.predicate_type) + except ValueError: + raise VerificationError(f"unknown attestation type: {statement.predicate_type}") + return statement.predicate_type, statement.predicate def to_bundle(self) -> Bundle: From bf3af0f2ff106267e4cbeb24956eb180502b92fb Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Wed, 10 Jul 2024 12:20:55 -0400 Subject: [PATCH 3/6] fix online markers, add coverage Signed-off-by: William Woodruff --- test/test_impl.py | 45 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/test/test_impl.py b/test/test_impl.py index 5a8c832..4b2e08b 100644 --- a/test/test_impl.py +++ b/test/test_impl.py @@ -58,9 +58,7 @@ def test_sign_invalid_dist_filename(self, tmp_path: Path) -> None: ): impl.Attestation.sign(pretend.stub(), bad_dist) - def test_sign_raises_attestation_exception( - self, id_token: IdentityToken, tmp_path: Path - ) -> None: + def test_sign_raises_attestation_exception(self, tmp_path: Path) -> None: non_existing_file = tmp_path / "invalid-name.tar.gz" with pytest.raises(impl.AttestationError, match="No such file"): impl.Attestation.sign(pretend.stub(), non_existing_file) @@ -77,9 +75,7 @@ def test_sign_raises_attestation_exception( with pytest.raises(impl.AttestationError, match="Invalid sdist filename"): impl.Attestation.sign(pretend.stub(), bad_sdist_filename) - def test_wrong_predicate_raises_exception( - self, id_token: IdentityToken, monkeypatch: pytest.MonkeyPatch - ) -> None: + def test_wrong_predicate_raises_exception(self, monkeypatch: pytest.MonkeyPatch) -> None: def dummy_predicate(self_: _StatementBuilder, _: str) -> _StatementBuilder: # wrong type here to have a validation error self_._predicate_type = False @@ -89,6 +85,7 @@ def dummy_predicate(self_: _StatementBuilder, _: str) -> _StatementBuilder: with pytest.raises(impl.AttestationError, match="invalid statement"): impl.Attestation.sign(pretend.stub(), artifact_path) + @online def test_expired_certificate( self, id_token: IdentityToken, monkeypatch: pytest.MonkeyPatch ) -> None: @@ -102,6 +99,7 @@ def in_validity_period(_: IdentityToken) -> bool: with pytest.raises(impl.AttestationError): impl.Attestation.sign(signer, artifact_path) + @online def test_multiple_signatures( self, id_token: IdentityToken, monkeypatch: pytest.MonkeyPatch ) -> None: @@ -312,6 +310,41 @@ def test_verify_subject_invalid_name(self) -> None: with pytest.raises(impl.VerificationError, match="invalid subject: Invalid wheel filename"): attestation.verify(verifier, pol, artifact_path) + def test_verify_unknown_attestation_type(self) -> None: + statement = ( + _StatementBuilder() # noqa: SLF001 + .subjects( + [ + _Subject( + name="rfc8785-0.1.2-py3-none-any.whl", + digest=_DigestSet( + root={ + "sha256": "c4e92e9ecc828bef2aa7dba1de8ac983511f7532a0df11c770d39099a25cf201" + } + ), + ), + ] + ) + .predicate_type("foo") + .build() + ._inner.model_dump_json() + ) + + verifier = pretend.stub( + verify_dsse=pretend.call_recorder( + lambda bundle, policy: ( + "application/vnd.in-toto+json", + statement.encode(), + ) + ) + ) + pol = pretend.stub() + + attestation = impl.Attestation.model_validate_json(attestation_path.read_text()) + + with pytest.raises(impl.VerificationError, match="unknown attestation type: foo"): + attestation.verify(verifier, pol, artifact_path) + def test_from_bundle_missing_signatures() -> None: bundle = Bundle.from_json(bundle_path.read_bytes()) From e6e68548d7e739b3d3dd3658da15bc27bd988d0f Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Wed, 10 Jul 2024 12:26:37 -0400 Subject: [PATCH 4/6] pypi_attestations: avoid StrEnum Not available before 3.11. Signed-off-by: William Woodruff --- src/pypi_attestations/_impl.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pypi_attestations/_impl.py b/src/pypi_attestations/_impl.py index 73cbe39..016d215 100644 --- a/src/pypi_attestations/_impl.py +++ b/src/pypi_attestations/_impl.py @@ -6,7 +6,7 @@ from __future__ import annotations import base64 -from enum import StrEnum +from enum import Enum from typing import TYPE_CHECKING, Annotated, Any, Literal, NewType import sigstore.errors @@ -38,7 +38,7 @@ from sigstore.verify.policy import VerificationPolicy # pragma: no cover -class AttestationType(StrEnum): +class AttestationType(str, Enum): """Attestation types known to PyPI.""" SLSA_PROVENANCE_V1 = "https://slsa.dev/provenance/v1" From 7f7d1620cf2d13b4a2aa979f2a0376978aa1315e Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Wed, 10 Jul 2024 12:28:07 -0400 Subject: [PATCH 5/6] lintage Signed-off-by: William Woodruff --- test/test_impl.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/test_impl.py b/test/test_impl.py index 4b2e08b..ec181b5 100644 --- a/test/test_impl.py +++ b/test/test_impl.py @@ -319,7 +319,9 @@ def test_verify_unknown_attestation_type(self) -> None: name="rfc8785-0.1.2-py3-none-any.whl", digest=_DigestSet( root={ - "sha256": "c4e92e9ecc828bef2aa7dba1de8ac983511f7532a0df11c770d39099a25cf201" + "sha256": ( + "c4e92e9ecc828bef2aa7dba1de8ac983511f7532a0df11c770d39099a25cf201" + ), } ), ), From fe2cc436271ce96747faa4f1c20324afcf32543e Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Wed, 10 Jul 2024 12:30:26 -0400 Subject: [PATCH 6/6] _impl: fix predicate_type Signed-off-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 016d215..0db7e2d 100644 --- a/src/pypi_attestations/_impl.py +++ b/src/pypi_attestations/_impl.py @@ -127,7 +127,7 @@ def sign(cls, signer: Signer, dist: Path) -> Attestation: ) ] ) - .predicate_type(AttestationType.SLSA_PROVENANCE_V1) + .predicate_type(AttestationType.PYPI_PUBLISH_V1) .build() ) except DsseError as e: