From fc925561f6c29bd14957dcc572c2a0c31563b942 Mon Sep 17 00:00:00 2001 From: Facundo Tuesca Date: Fri, 3 May 2024 18:40:10 +0200 Subject: [PATCH 1/5] Add high-level APIs to sign and verify Python artifacts --- README.md | 36 +++ pyproject.toml | 4 +- src/pypi_attestation_models/_impl.py | 18 ++ ...rfc8785-0.1.2-py3-none-any.whl.attestation | 45 ++-- .../rfc8785-0.1.2-py3-none-any.whl.sigstore | 49 ++-- test/test_impl.py | 225 ++++++++++-------- 6 files changed, 232 insertions(+), 145 deletions(-) diff --git a/README.md b/README.md index f73b868..3096327 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,42 @@ python -m pip install pypi-attestation-models See the full API documentation [here]. + +### Signing and verification +Use these APIs to create a PEP 740-compliant `Attestation` object by signing a Python artifact +(i.e: sdist or wheel files), and to verify an `Attestation` object against a Python artifact. + +```python +from pathlib import Path + +from pypi_attestation_models import Attestation, AttestationPayload +from sigstore.oidc import Issuer +from sigstore.sign import SigningContext +from sigstore.verify import Verifier, policy + +artifact_path = Path("test_package-0.0.1-py3-none-any.whl") + +# Sign a Python artifact +issuer = Issuer.production() +identity_token = issuer.identity_token() +signing_ctx = SigningContext.production() +with signing_ctx.signer(identity_token, cache=True) as signer: + attestation = AttestationPayload.from_dist(artifact_path).sign(signer) + +print(attestation.model_dump_json()) + +# Verify an attestation against a Python artifact +attestation_path = Path("test_package-0.0.1-py3-none-any.whl.attestation") +attestation = Attestation.model_validate_json(attestation_path.read_bytes()) +verifier = Verifier.production() +policy = policy.Identity(identity="example@gmail.com", issuer="https://accounts.google.com") +attestation.verify(verifier, policy, attestation_path) + +``` + +### Low-level model conversions +These conversions assume that any Sigstore Bundle used as an input was created +by signing an `AttestationPayload` object. ```python from pathlib import Path from pypi_attestation_models import pypi_to_sigstore, sigstore_to_pypi, Attestation diff --git a/pyproject.toml b/pyproject.toml index ecca645..b54cad9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,11 +73,11 @@ target-version = "py39" [tool.ruff.lint] select = ["ALL"] -# ANN102 is deprecated +# ANN101 and ANN102 are deprecated # D203 and D213 are incompatible with D211 and D212 respectively. # COM812 and ISC001 can cause conflicts when using ruff as a formatter. # See https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules. -ignore = ["ANN102", "D203", "D213", "COM812", "ISC001"] +ignore = ["ANN101", "ANN102", "D203", "D213", "COM812", "ISC001"] [tool.ruff.lint.per-file-ignores] diff --git a/src/pypi_attestation_models/_impl.py b/src/pypi_attestation_models/_impl.py index 5219daf..90802db 100644 --- a/src/pypi_attestation_models/_impl.py +++ b/src/pypi_attestation_models/_impl.py @@ -22,6 +22,10 @@ if TYPE_CHECKING: from pathlib import Path # pragma: no cover + from sigstore.sign import Signer # pragma: no cover + from sigstore.verify import Verifier # pragma: no cover + from sigstore.verify.policy import VerificationPolicy # pragma: no cover + class ConversionError(ValueError): """The base error for all errors during conversion.""" @@ -72,6 +76,15 @@ class Attestation(BaseModel): is the raw bytes of the signing operation over the attestation payload. """ + def verify(self, verifier: Verifier, policy: VerificationPolicy, dist: Path) -> None: + """Verify against an existing Python artifact. + + On failure, raises `sigstore.errors.InvalidAttestationError`. + """ + payload_to_verify = AttestationPayload.from_dist(dist) + bundle = pypi_to_sigstore(self) + verifier.verify_artifact(bytes(payload_to_verify), bundle, policy) + class AttestationPayload(BaseModel): """Attestation Payload object as defined in PEP 740.""" @@ -94,6 +107,11 @@ def from_dist(cls, dist: Path) -> AttestationPayload: digest=sha256(dist.read_bytes()).hexdigest(), ) + def sign(self, signer: Signer) -> Attestation: + """Create a PEP 740 attestation by signing this payload.""" + sigstore_bundle = signer.sign_artifact(bytes(self)) + return sigstore_to_pypi(sigstore_bundle) + def __bytes__(self: AttestationPayload) -> bytes: """Convert to bytes using a canonicalized JSON representation (from RFC8785).""" return rfc8785.dumps(self.model_dump()) diff --git a/test/assets/rfc8785-0.1.2-py3-none-any.whl.attestation b/test/assets/rfc8785-0.1.2-py3-none-any.whl.attestation index bf2affa..883ce1e 100644 --- a/test/assets/rfc8785-0.1.2-py3-none-any.whl.attestation +++ b/test/assets/rfc8785-0.1.2-py3-none-any.whl.attestation @@ -1,10 +1,10 @@ { "version": 1, "verification_material": { - "certificate": "MIIC1zCCAl2gAwIBAgIUZk9ToGFUJexy+/rxwIF8BB+3C5YwCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjQwNDI5MTY1MTMzWhcNMjQwNDI5MTcwMTMzWjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEM5H5f7A4HutVTfKFimTd2UbTzgUOY7rph9GKqgsZ7ChAp8FGJbrrgn6o+nprUEFKqEaIi+fWQJvR+RJkoQcWMKOCAXwwggF4MA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUnrc9nJ2dxJd1a5sCFj/P+y3MuhQwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4YZD8wLAYDVR0RAQH/BCIwIIEeZmFjdW5kby50dWVzY2FAdHJhaWxvZmJpdHMuY29tMCkGCisGAQQBg78wAQEEG2h0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbTArBgorBgEEAYO/MAEIBB0MG2h0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbTCBigYKKwYBBAHWeQIEAgR8BHoAeAB2AN09MGrGxxEyYxkeHJlnNwKiSl643jyt/4eKcoAvKe6OAAABjyrE0sQAAAQDAEcwRQIhAORhP1HaCcD4QK8+8VcNL46W0AAk6cIDUAH3SV4stJUVAiBafyw+FTpgvoTU+2U7QCyjlQZ5J2dPpVqv9Up3vV2GTDAKBggqhkjOPQQDAwNoADBlAjEAzoA4cMHxHCEXA80ahwJUSz/1kYotTXRNzeWU69SyaZE7Po+vZ5/ANfKvbCv9s19rAjABbw/INkA4dGKWEDNtSjnloZuH5N9aPBOV425+iKCe2bmf9cYVlFvCbGmHiEg/r5k=", + "certificate": "MIIC1jCCAlygAwIBAgIUTDMXIHMGbNF+Sm0qoQoj35hY3zkwCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjQwNTAzMTQwNTU0WhcNMjQwNTAzMTQxNTU0WjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEqv2vvFAD66IdGSg/+JbB/nYfook1FqpmM773o9MdVZktl1LkUWAU8SzaZhSso/7qVriyH/S8km0HqTVMzuZ+SaOCAXswggF3MA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQU5TQvo55q5OXkVFmYDsR93Neffq0wHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4YZD8wLAYDVR0RAQH/BCIwIIEeZmFjdW5kby50dWVzY2FAdHJhaWxvZmJpdHMuY29tMCkGCisGAQQBg78wAQEEG2h0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbTArBgorBgEEAYO/MAEIBB0MG2h0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbTCBiQYKKwYBBAHWeQIEAgR7BHkAdwB1AN09MGrGxxEyYxkeHJlnNwKiSl643jyt/4eKcoAvKe6OAAABjz7Gm3oAAAQDAEYwRAIgGkFpx5q3NzmgBIPywysdADDRRRPM/xsa8Fkfva+chKECIAu5HgO2eKsdc9pohmgn/modVcJ1Q5Muou9d4l1c5fXyMAoGCCqGSM49BAMDA2gAMGUCMHschAnWt88W4cu35dEv0MJ72s3BZsudUQzZ8dtg0xBlF3uwdDoprNfbA2tM5piDlAIxAMBg5Dqx0BV/9Rp/QMLhb+aGqZm7n7E5GkXJpHA8TySZamdyYuRlEiPf4cj7x/ruyw==", "transparency_entries": [ { - "logIndex": "89569370", + "logIndex": "90818200", "logId": { "keyId": "wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0=" }, @@ -12,38 +12,37 @@ "kind": "hashedrekord", "version": "0.0.1" }, - "integratedTime": "1714409493", + "integratedTime": "1714745154", "inclusionPromise": { - "signedEntryTimestamp": "MEUCIQC29r0j8BCu5Zye9dvFaVBmCFzHIDBDDWH+LqcZ8aTAyAIgas85UjhG4jrVwqBr/U/nQ86vwOWQnHKjGnd/bdnQafQ=" + "signedEntryTimestamp": "MEUCIQCyiwcUzPS8I8WKRuLGelfRdlmx/AtM6TwMmyvK8utURwIgDKtqe3gaZe8bbVupw3HJm4nzvpYAZHaHmv/cCbJRYlo=" }, "inclusionProof": { - "logIndex": "85405939", - "rootHash": "CdYMKt8P8arrU1iilSLHbPDwMzhWgUbA6xvb1DSZktc=", - "treeSize": "85405941", + "logIndex": "86654769", + "rootHash": "rpJ6l1p+pM8A+nMvMT80o0+qrCozIEfbG6qa0psYB0U=", + "treeSize": "86654770", "hashes": [ - "btr1R1ZOzi0Kqk+vdfHrDcC1zasNMvXaNehse4NeMu8=", - "8IgEN8pEU3WrVRCsbbCkFHeamV5xoyN1OByng98lPow=", - "i6x3rMfR1HZCafUIGTyYgvtfwjF9zUe9Q5MPKNdWhS8=", - "kE6NsBxRpnT9Q/DgLDqQELBaor5pThUMmHIRuKapA7c=", - "Y55DSeWN5DUnIuvK5RRsaF4b35EtjKasFpV2n2LfrA0=", - "UeVeMpyn/bEywYJvThS5PltcrELznbc/OFTARixCNUQ=", - "MaMJLiuvSigqcgOZ6BulADPyhWaoYE1C+sGj/twciwM=", - "YrLY+ujALEUYcIeyq2ri+QBsl7Sxh+frMg7GlVcIvZ0=", - "27GLifvQI1aATtmSwJQGmbKXDoBpa0R5Q8fQUuX3/kQ=", - "xhLV7tE4kPldhHSKGpGuBkcxIUSpEvNntVSzM+5raW4=", - "btR5C6cRDz4AcGce5sOqhKIlEYTQ29AJLmv7L3kPyDI=", - "TuI0yJQvmNIs3J9pfzPMu2aYibKQ0MUpGgkcmjsS30g=", - "TxcrLS66touilnjU30hIZ8JkbH6bfnBbD6pQ5OoIpXs=", + "9X9+Ewnt1Jv3SVUsUbtRTb8QezxTGW71hfmYyj4urho=", + "XMz5WH5iwOITJcBdVsAUj+h1uxgH8EPXezgGg4lNP5M=", + "LLawJ9A3X4PEGMwFRwVbwhUxkHMJj9wFemKagvJlDFw=", + "7PXuijLdBg+hW8HdjY+IyuFgLXvlTKOKqHRFTMJB4Ko=", + "er0Z148pal4gYGSVfQ/rvzPKTc1EIDd7pYQjHd7RtKk=", + "ZmLzbyYsyxNDvtbH/T00VHzMFhW8WBS9lgPH2rlNFMY=", + "DH+I5x/ZvX3i2Ysc2divZ/6MK1e3ppSmNtUTg+CQwEw=", + "nrCOZ+D41yjNI2zgMlBLN0LHOn5OP4sJYffOuoPgSJg=", + "vQtr96qBr+8s4Up0YK2ibgbVOYyLK4e8zsjHOyMnOM4=", + "4GdrZvm4dTLD8P8KPOQx8Op0UOT7XbRS1WG43uXseJU=", + "fp4aY4dEhrMBmS4ex9s/lm8UPsk42eWg77zd+uJn0F4=", + "2aaLz8XcvX3I3Ihft0W701fVKICZLYtBIxRbPmdkcZ0=", "sjohk/3DQIfXTgf/5XpwtdF7yNbrf8YykOMHr1CyBYQ=", "98enzMaC+x5oCMvIZQA5z8vu2apDMCFvE/935NfuPw8=" ], "checkpoint": { - "envelope": "rekor.sigstore.dev - 2605736670972794746\n85405941\nCdYMKt8P8arrU1iilSLHbPDwMzhWgUbA6xvb1DSZktc=\n\n— rekor.sigstore.dev wNI9ajBGAiEAkEVEjIsNbuvKywuzjow1tyD1IkIpHu/bCVK73fSpBzECIQD5ctzIS0Rp3cC3PF0ZcFEP8ObC2KJqojg4hfKjFxxy9A==\n" + "envelope": "rekor.sigstore.dev - 2605736670972794746\n86654770\nrpJ6l1p+pM8A+nMvMT80o0+qrCozIEfbG6qa0psYB0U=\n\n— rekor.sigstore.dev wNI9ajBFAiBXlxCjY0PMu4XUVJa/auC+EwEJws9xXfEbiYRM5uIjpgIhAKRduPCMlSRhNCQeGUifB2nAkKJDGlOJa75mkYYrrVMK\n" } }, - "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiJjNGU5MmU5ZWNjODI4YmVmMmFhN2RiYTFkZThhYzk4MzUxMWY3NTMyYTBkZjExYzc3MGQzOTA5OWEyNWNmMjAxIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FUUNJSGlXY2JRY2szTzE2K3dGR3pyR09sYWVWRkpodkNwT1EwajZJd0ZtUnp0YUFpQWJWL3NOSWg0OFJQSHdIdm9IZklDcFA3Y29seGpjbUx6WEhsU2pleGEvUVE9PSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTXhla05EUVd3eVowRjNTVUpCWjBsVldtczVWRzlIUmxWS1pYaDVLeTl5ZUhkSlJqaENRaXN6UXpWWmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcFJkMDVFU1RWTlZGa3hUVlJOZWxkb1kwNU5hbEYzVGtSSk5VMVVZM2ROVkUxNlYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZOTlVnMVpqZEJORWgxZEZaVVprdEdhVzFVWkRKVllsUjZaMVZQV1RkeWNHZzVSMHNLY1dkeldqZERhRUZ3T0VaSFNtSnljbWR1Tm04cmJuQnlWVVZHUzNGRllVbHBLMlpYVVVwMlVpdFNTbXR2VVdOWFRVdFBRMEZZZDNkblowWTBUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZ1Y21NNUNtNUtNbVI0U21ReFlUVnpRMFpxTDFBcmVUTk5kV2hSZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDB4QldVUldVakJTUVZGSUwwSkRTWGRKU1VWbFdtMUdhbVJYTld0aWVUVXdaRmRXZWxreVJrRmtTRXBvWVZkNGRscHRTbkJrU0UxMVdUSTVkQXBOUTJ0SFEybHpSMEZSVVVKbk56aDNRVkZGUlVjeWFEQmtTRUo2VDJrNGRsbFhUbXBpTTFaMVpFaE5kVm95T1haYU1uaHNURzFPZG1KVVFYSkNaMjl5Q2tKblJVVkJXVTh2VFVGRlNVSkNNRTFITW1nd1pFaENlazlwT0haWlYwNXFZak5XZFdSSVRYVmFNamwyV2pKNGJFeHRUblppVkVOQ2FXZFpTMHQzV1VJS1FrRklWMlZSU1VWQloxSTRRa2h2UVdWQlFqSkJUakE1VFVkeVIzaDRSWGxaZUd0bFNFcHNiazUzUzJsVGJEWTBNMnA1ZEM4MFpVdGpiMEYyUzJVMlR3cEJRVUZDYW5seVJUQnpVVUZCUVZGRVFVVmpkMUpSU1doQlQxSm9VREZJWVVOalJEUlJTemdyT0ZaalRrdzBObGN3UVVGck5tTkpSRlZCU0ROVFZqUnpDblJLVlZaQmFVSmhabmwzSzBaVWNHZDJiMVJWS3pKVk4xRkRlV3BzVVZvMVNqSmtVSEJXY1hZNVZYQXpkbFl5UjFSRVFVdENaMmR4YUd0cVQxQlJVVVFLUVhkT2IwRkVRbXhCYWtWQmVtOUJOR05OU0hoSVEwVllRVGd3WVdoM1NsVlRlaTh4YTFsdmRGUllVazU2WlZkVk5qbFRlV0ZhUlRkUWJ5dDJXalV2UVFwT1prdDJZa04yT1hNeE9YSkJha0ZDWW5jdlNVNXJRVFJrUjB0WFJVUk9kRk5xYm14dlduVklOVTQ1WVZCQ1QxWTBNalVyYVV0RFpUSmliV1k1WTFsV0NteEdka05pUjIxSWFVVm5MM0kxYXowS0xTMHRMUzFGVGtRZ1EwVlNWRWxHU1VOQlZFVXRMUzB0TFFvPSJ9fX19" + "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiJmMTQ2ZmY4NWMxMGZjMTg4ODM0MDVjMWQ5Mzc0NjIzZWI3YzI5ZjRlYTRiYTYyYzZmNWUyYzZmMTc5M2ZiZDEwIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FVUNJUUMvd1ZYcTlBeWExQW1JZlgzZmVoSUZTbkN1Q0tzNGhWTks1eGJ3ckVtV1d3SWdHMzhtcmtsOHhmNG1SYWhmYmNIckVTdFZYYjg3enFySVFoT1BUOVJTcFdFPSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTXhha05EUVd4NVowRjNTVUpCWjBsVlZFUk5XRWxJVFVkaVRrWXJVMjB3Y1c5UmIyb3pOV2haTTNwcmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcFJkMDVVUVhwTlZGRjNUbFJWTUZkb1kwNU5hbEYzVGxSQmVrMVVVWGhPVkZVd1YycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZ4ZGpKMmRrWkJSRFkyU1dSSFUyY3ZLMHBpUWk5dVdXWnZiMnN4Um5Gd2JVMDNOek1LYnpsTlpGWmFhM1JzTVV4clZWZEJWVGhUZW1GYWFGTnpieTgzY1ZaeWFYbElMMU00YTIwd1NIRlVWazE2ZFZvclUyRlBRMEZZYzNkblowWXpUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlUxVkZGMkNtODFOWEUxVDFoclZrWnRXVVJ6VWprelRtVm1abkV3ZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDB4QldVUldVakJTUVZGSUwwSkRTWGRKU1VWbFdtMUdhbVJYTld0aWVUVXdaRmRXZWxreVJrRmtTRXBvWVZkNGRscHRTbkJrU0UxMVdUSTVkQXBOUTJ0SFEybHpSMEZSVVVKbk56aDNRVkZGUlVjeWFEQmtTRUo2VDJrNGRsbFhUbXBpTTFaMVpFaE5kVm95T1haYU1uaHNURzFPZG1KVVFYSkNaMjl5Q2tKblJVVkJXVTh2VFVGRlNVSkNNRTFITW1nd1pFaENlazlwT0haWlYwNXFZak5XZFdSSVRYVmFNamwyV2pKNGJFeHRUblppVkVOQ2FWRlpTMHQzV1VJS1FrRklWMlZSU1VWQloxSTNRa2hyUVdSM1FqRkJUakE1VFVkeVIzaDRSWGxaZUd0bFNFcHNiazUzUzJsVGJEWTBNMnA1ZEM4MFpVdGpiMEYyUzJVMlR3cEJRVUZDYW5vM1IyMHpiMEZCUVZGRVFVVlpkMUpCU1dkSGEwWndlRFZ4TTA1NmJXZENTVkI1ZDNselpFRkVSRkpTVWxCTkwzaHpZVGhHYTJaMllTdGpDbWhMUlVOSlFYVTFTR2RQTW1WTGMyUmpPWEJ2YUcxbmJpOXRiMlJXWTBveFVUVk5kVzkxT1dRMGJERmpOV1pZZVUxQmIwZERRM0ZIVTAwME9VSkJUVVFLUVRKblFVMUhWVU5OU0hOamFFRnVWM1E0T0ZjMFkzVXpOV1JGZGpCTlNqY3ljek5DV25OMVpGVlJlbG80WkhSbk1IaENiRVl6ZFhka1JHOXdjazVtWWdwQk1uUk5OWEJwUkd4QlNYaEJUVUpuTlVSeGVEQkNWaTg1VW5BdlVVMU1hR0lyWVVkeFdtMDNiamRGTlVkcldFcHdTRUU0VkhsVFdtRnRaSGxaZFZKc0NrVnBVR1kwWTJvM2VDOXlkWGwzUFQwS0xTMHRMUzFGVGtRZ1EwVlNWRWxHU1VOQlZFVXRMUzB0TFFvPSJ9fX19" } ] }, - "message_signature": "MEQCIHiWcbQck3O16+wFGzrGOlaeVFJhvCpOQ0j6IwFmRztaAiAbV/sNIh48RPHwHvoHfICpP7colxjcmLzXHlSjexa/QQ==" + "message_signature": "MEUCIQC/wVXq9Aya1AmIfX3fehIFSnCuCKs4hVNK5xbwrEmWWwIgG38mrkl8xf4mRahfbcHrEStVXb87zqrIQhOPT9RSpWE=" } \ No newline at end of file diff --git a/test/assets/rfc8785-0.1.2-py3-none-any.whl.sigstore b/test/assets/rfc8785-0.1.2-py3-none-any.whl.sigstore index 290da36..9072423 100644 --- a/test/assets/rfc8785-0.1.2-py3-none-any.whl.sigstore +++ b/test/assets/rfc8785-0.1.2-py3-none-any.whl.sigstore @@ -2,11 +2,11 @@ "mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json", "verificationMaterial": { "certificate": { - "rawBytes": "MIIC1zCCAl2gAwIBAgIUZk9ToGFUJexy+/rxwIF8BB+3C5YwCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjQwNDI5MTY1MTMzWhcNMjQwNDI5MTcwMTMzWjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEM5H5f7A4HutVTfKFimTd2UbTzgUOY7rph9GKqgsZ7ChAp8FGJbrrgn6o+nprUEFKqEaIi+fWQJvR+RJkoQcWMKOCAXwwggF4MA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUnrc9nJ2dxJd1a5sCFj/P+y3MuhQwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4YZD8wLAYDVR0RAQH/BCIwIIEeZmFjdW5kby50dWVzY2FAdHJhaWxvZmJpdHMuY29tMCkGCisGAQQBg78wAQEEG2h0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbTArBgorBgEEAYO/MAEIBB0MG2h0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbTCBigYKKwYBBAHWeQIEAgR8BHoAeAB2AN09MGrGxxEyYxkeHJlnNwKiSl643jyt/4eKcoAvKe6OAAABjyrE0sQAAAQDAEcwRQIhAORhP1HaCcD4QK8+8VcNL46W0AAk6cIDUAH3SV4stJUVAiBafyw+FTpgvoTU+2U7QCyjlQZ5J2dPpVqv9Up3vV2GTDAKBggqhkjOPQQDAwNoADBlAjEAzoA4cMHxHCEXA80ahwJUSz/1kYotTXRNzeWU69SyaZE7Po+vZ5/ANfKvbCv9s19rAjABbw/INkA4dGKWEDNtSjnloZuH5N9aPBOV425+iKCe2bmf9cYVlFvCbGmHiEg/r5k=" + "rawBytes": "MIIC1jCCAlygAwIBAgIUTDMXIHMGbNF+Sm0qoQoj35hY3zkwCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjQwNTAzMTQwNTU0WhcNMjQwNTAzMTQxNTU0WjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEqv2vvFAD66IdGSg/+JbB/nYfook1FqpmM773o9MdVZktl1LkUWAU8SzaZhSso/7qVriyH/S8km0HqTVMzuZ+SaOCAXswggF3MA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQU5TQvo55q5OXkVFmYDsR93Neffq0wHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4YZD8wLAYDVR0RAQH/BCIwIIEeZmFjdW5kby50dWVzY2FAdHJhaWxvZmJpdHMuY29tMCkGCisGAQQBg78wAQEEG2h0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbTArBgorBgEEAYO/MAEIBB0MG2h0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbTCBiQYKKwYBBAHWeQIEAgR7BHkAdwB1AN09MGrGxxEyYxkeHJlnNwKiSl643jyt/4eKcoAvKe6OAAABjz7Gm3oAAAQDAEYwRAIgGkFpx5q3NzmgBIPywysdADDRRRPM/xsa8Fkfva+chKECIAu5HgO2eKsdc9pohmgn/modVcJ1Q5Muou9d4l1c5fXyMAoGCCqGSM49BAMDA2gAMGUCMHschAnWt88W4cu35dEv0MJ72s3BZsudUQzZ8dtg0xBlF3uwdDoprNfbA2tM5piDlAIxAMBg5Dqx0BV/9Rp/QMLhb+aGqZm7n7E5GkXJpHA8TySZamdyYuRlEiPf4cj7x/ruyw==" }, "tlogEntries": [ { - "logIndex": "89569370", + "logIndex": "90818200", "logId": { "keyId": "wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0=" }, @@ -14,44 +14,43 @@ "kind": "hashedrekord", "version": "0.0.1" }, - "integratedTime": "1714409493", + "integratedTime": "1714745154", "inclusionPromise": { - "signedEntryTimestamp": "MEUCIQC29r0j8BCu5Zye9dvFaVBmCFzHIDBDDWH+LqcZ8aTAyAIgas85UjhG4jrVwqBr/U/nQ86vwOWQnHKjGnd/bdnQafQ=" + "signedEntryTimestamp": "MEUCIQCyiwcUzPS8I8WKRuLGelfRdlmx/AtM6TwMmyvK8utURwIgDKtqe3gaZe8bbVupw3HJm4nzvpYAZHaHmv/cCbJRYlo=" }, "inclusionProof": { - "logIndex": "85405939", - "rootHash": "CdYMKt8P8arrU1iilSLHbPDwMzhWgUbA6xvb1DSZktc=", - "treeSize": "85405941", + "logIndex": "86654769", + "rootHash": "rpJ6l1p+pM8A+nMvMT80o0+qrCozIEfbG6qa0psYB0U=", + "treeSize": "86654770", "hashes": [ - "btr1R1ZOzi0Kqk+vdfHrDcC1zasNMvXaNehse4NeMu8=", - "8IgEN8pEU3WrVRCsbbCkFHeamV5xoyN1OByng98lPow=", - "i6x3rMfR1HZCafUIGTyYgvtfwjF9zUe9Q5MPKNdWhS8=", - "kE6NsBxRpnT9Q/DgLDqQELBaor5pThUMmHIRuKapA7c=", - "Y55DSeWN5DUnIuvK5RRsaF4b35EtjKasFpV2n2LfrA0=", - "UeVeMpyn/bEywYJvThS5PltcrELznbc/OFTARixCNUQ=", - "MaMJLiuvSigqcgOZ6BulADPyhWaoYE1C+sGj/twciwM=", - "YrLY+ujALEUYcIeyq2ri+QBsl7Sxh+frMg7GlVcIvZ0=", - "27GLifvQI1aATtmSwJQGmbKXDoBpa0R5Q8fQUuX3/kQ=", - "xhLV7tE4kPldhHSKGpGuBkcxIUSpEvNntVSzM+5raW4=", - "btR5C6cRDz4AcGce5sOqhKIlEYTQ29AJLmv7L3kPyDI=", - "TuI0yJQvmNIs3J9pfzPMu2aYibKQ0MUpGgkcmjsS30g=", - "TxcrLS66touilnjU30hIZ8JkbH6bfnBbD6pQ5OoIpXs=", + "9X9+Ewnt1Jv3SVUsUbtRTb8QezxTGW71hfmYyj4urho=", + "XMz5WH5iwOITJcBdVsAUj+h1uxgH8EPXezgGg4lNP5M=", + "LLawJ9A3X4PEGMwFRwVbwhUxkHMJj9wFemKagvJlDFw=", + "7PXuijLdBg+hW8HdjY+IyuFgLXvlTKOKqHRFTMJB4Ko=", + "er0Z148pal4gYGSVfQ/rvzPKTc1EIDd7pYQjHd7RtKk=", + "ZmLzbyYsyxNDvtbH/T00VHzMFhW8WBS9lgPH2rlNFMY=", + "DH+I5x/ZvX3i2Ysc2divZ/6MK1e3ppSmNtUTg+CQwEw=", + "nrCOZ+D41yjNI2zgMlBLN0LHOn5OP4sJYffOuoPgSJg=", + "vQtr96qBr+8s4Up0YK2ibgbVOYyLK4e8zsjHOyMnOM4=", + "4GdrZvm4dTLD8P8KPOQx8Op0UOT7XbRS1WG43uXseJU=", + "fp4aY4dEhrMBmS4ex9s/lm8UPsk42eWg77zd+uJn0F4=", + "2aaLz8XcvX3I3Ihft0W701fVKICZLYtBIxRbPmdkcZ0=", "sjohk/3DQIfXTgf/5XpwtdF7yNbrf8YykOMHr1CyBYQ=", "98enzMaC+x5oCMvIZQA5z8vu2apDMCFvE/935NfuPw8=" ], "checkpoint": { - "envelope": "rekor.sigstore.dev - 2605736670972794746\n85405941\nCdYMKt8P8arrU1iilSLHbPDwMzhWgUbA6xvb1DSZktc=\n\n\u2014 rekor.sigstore.dev wNI9ajBGAiEAkEVEjIsNbuvKywuzjow1tyD1IkIpHu/bCVK73fSpBzECIQD5ctzIS0Rp3cC3PF0ZcFEP8ObC2KJqojg4hfKjFxxy9A==\n" + "envelope": "rekor.sigstore.dev - 2605736670972794746\n86654770\nrpJ6l1p+pM8A+nMvMT80o0+qrCozIEfbG6qa0psYB0U=\n\n\u2014 rekor.sigstore.dev wNI9ajBFAiBXlxCjY0PMu4XUVJa/auC+EwEJws9xXfEbiYRM5uIjpgIhAKRduPCMlSRhNCQeGUifB2nAkKJDGlOJa75mkYYrrVMK\n" } }, - "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiJjNGU5MmU5ZWNjODI4YmVmMmFhN2RiYTFkZThhYzk4MzUxMWY3NTMyYTBkZjExYzc3MGQzOTA5OWEyNWNmMjAxIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FUUNJSGlXY2JRY2szTzE2K3dGR3pyR09sYWVWRkpodkNwT1EwajZJd0ZtUnp0YUFpQWJWL3NOSWg0OFJQSHdIdm9IZklDcFA3Y29seGpjbUx6WEhsU2pleGEvUVE9PSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTXhla05EUVd3eVowRjNTVUpCWjBsVldtczVWRzlIUmxWS1pYaDVLeTl5ZUhkSlJqaENRaXN6UXpWWmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcFJkMDVFU1RWTlZGa3hUVlJOZWxkb1kwNU5hbEYzVGtSSk5VMVVZM2ROVkUxNlYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZOTlVnMVpqZEJORWgxZEZaVVprdEdhVzFVWkRKVllsUjZaMVZQV1RkeWNHZzVSMHNLY1dkeldqZERhRUZ3T0VaSFNtSnljbWR1Tm04cmJuQnlWVVZHUzNGRllVbHBLMlpYVVVwMlVpdFNTbXR2VVdOWFRVdFBRMEZZZDNkblowWTBUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZ1Y21NNUNtNUtNbVI0U21ReFlUVnpRMFpxTDFBcmVUTk5kV2hSZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDB4QldVUldVakJTUVZGSUwwSkRTWGRKU1VWbFdtMUdhbVJYTld0aWVUVXdaRmRXZWxreVJrRmtTRXBvWVZkNGRscHRTbkJrU0UxMVdUSTVkQXBOUTJ0SFEybHpSMEZSVVVKbk56aDNRVkZGUlVjeWFEQmtTRUo2VDJrNGRsbFhUbXBpTTFaMVpFaE5kVm95T1haYU1uaHNURzFPZG1KVVFYSkNaMjl5Q2tKblJVVkJXVTh2VFVGRlNVSkNNRTFITW1nd1pFaENlazlwT0haWlYwNXFZak5XZFdSSVRYVmFNamwyV2pKNGJFeHRUblppVkVOQ2FXZFpTMHQzV1VJS1FrRklWMlZSU1VWQloxSTRRa2h2UVdWQlFqSkJUakE1VFVkeVIzaDRSWGxaZUd0bFNFcHNiazUzUzJsVGJEWTBNMnA1ZEM4MFpVdGpiMEYyUzJVMlR3cEJRVUZDYW5seVJUQnpVVUZCUVZGRVFVVmpkMUpSU1doQlQxSm9VREZJWVVOalJEUlJTemdyT0ZaalRrdzBObGN3UVVGck5tTkpSRlZCU0ROVFZqUnpDblJLVlZaQmFVSmhabmwzSzBaVWNHZDJiMVJWS3pKVk4xRkRlV3BzVVZvMVNqSmtVSEJXY1hZNVZYQXpkbFl5UjFSRVFVdENaMmR4YUd0cVQxQlJVVVFLUVhkT2IwRkVRbXhCYWtWQmVtOUJOR05OU0hoSVEwVllRVGd3WVdoM1NsVlRlaTh4YTFsdmRGUllVazU2WlZkVk5qbFRlV0ZhUlRkUWJ5dDJXalV2UVFwT1prdDJZa04yT1hNeE9YSkJha0ZDWW5jdlNVNXJRVFJrUjB0WFJVUk9kRk5xYm14dlduVklOVTQ1WVZCQ1QxWTBNalVyYVV0RFpUSmliV1k1WTFsV0NteEdka05pUjIxSWFVVm5MM0kxYXowS0xTMHRMUzFGVGtRZ1EwVlNWRWxHU1VOQlZFVXRMUzB0TFFvPSJ9fX19" + "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiJmMTQ2ZmY4NWMxMGZjMTg4ODM0MDVjMWQ5Mzc0NjIzZWI3YzI5ZjRlYTRiYTYyYzZmNWUyYzZmMTc5M2ZiZDEwIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FVUNJUUMvd1ZYcTlBeWExQW1JZlgzZmVoSUZTbkN1Q0tzNGhWTks1eGJ3ckVtV1d3SWdHMzhtcmtsOHhmNG1SYWhmYmNIckVTdFZYYjg3enFySVFoT1BUOVJTcFdFPSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTXhha05EUVd4NVowRjNTVUpCWjBsVlZFUk5XRWxJVFVkaVRrWXJVMjB3Y1c5UmIyb3pOV2haTTNwcmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcFJkMDVVUVhwTlZGRjNUbFJWTUZkb1kwNU5hbEYzVGxSQmVrMVVVWGhPVkZVd1YycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZ4ZGpKMmRrWkJSRFkyU1dSSFUyY3ZLMHBpUWk5dVdXWnZiMnN4Um5Gd2JVMDNOek1LYnpsTlpGWmFhM1JzTVV4clZWZEJWVGhUZW1GYWFGTnpieTgzY1ZaeWFYbElMMU00YTIwd1NIRlVWazE2ZFZvclUyRlBRMEZZYzNkblowWXpUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlUxVkZGMkNtODFOWEUxVDFoclZrWnRXVVJ6VWprelRtVm1abkV3ZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDB4QldVUldVakJTUVZGSUwwSkRTWGRKU1VWbFdtMUdhbVJYTld0aWVUVXdaRmRXZWxreVJrRmtTRXBvWVZkNGRscHRTbkJrU0UxMVdUSTVkQXBOUTJ0SFEybHpSMEZSVVVKbk56aDNRVkZGUlVjeWFEQmtTRUo2VDJrNGRsbFhUbXBpTTFaMVpFaE5kVm95T1haYU1uaHNURzFPZG1KVVFYSkNaMjl5Q2tKblJVVkJXVTh2VFVGRlNVSkNNRTFITW1nd1pFaENlazlwT0haWlYwNXFZak5XZFdSSVRYVmFNamwyV2pKNGJFeHRUblppVkVOQ2FWRlpTMHQzV1VJS1FrRklWMlZSU1VWQloxSTNRa2hyUVdSM1FqRkJUakE1VFVkeVIzaDRSWGxaZUd0bFNFcHNiazUzUzJsVGJEWTBNMnA1ZEM4MFpVdGpiMEYyUzJVMlR3cEJRVUZDYW5vM1IyMHpiMEZCUVZGRVFVVlpkMUpCU1dkSGEwWndlRFZ4TTA1NmJXZENTVkI1ZDNselpFRkVSRkpTVWxCTkwzaHpZVGhHYTJaMllTdGpDbWhMUlVOSlFYVTFTR2RQTW1WTGMyUmpPWEJ2YUcxbmJpOXRiMlJXWTBveFVUVk5kVzkxT1dRMGJERmpOV1pZZVUxQmIwZERRM0ZIVTAwME9VSkJUVVFLUVRKblFVMUhWVU5OU0hOamFFRnVWM1E0T0ZjMFkzVXpOV1JGZGpCTlNqY3ljek5DV25OMVpGVlJlbG80WkhSbk1IaENiRVl6ZFhka1JHOXdjazVtWWdwQk1uUk5OWEJwUkd4QlNYaEJUVUpuTlVSeGVEQkNWaTg1VW5BdlVVMU1hR0lyWVVkeFdtMDNiamRGTlVkcldFcHdTRUU0VkhsVFdtRnRaSGxaZFZKc0NrVnBVR1kwWTJvM2VDOXlkWGwzUFQwS0xTMHRMUzFGVGtRZ1EwVlNWRWxHU1VOQlZFVXRMUzB0TFFvPSJ9fX19" } ] }, "messageSignature": { "messageDigest": { "algorithm": "SHA2_256", - "digest": "xOkunsyCi+8qp9uh3orJg1EfdTKg3xHHcNOQmaJc8gE=" + "digest": "8Ub/hcEPwYiDQFwdk3RiPrfCn06kumLG9eLG8Xk/vRA=" }, - "signature": "MEQCIHiWcbQck3O16+wFGzrGOlaeVFJhvCpOQ0j6IwFmRztaAiAbV/sNIh48RPHwHvoHfICpP7colxjcmLzXHlSjexa/QQ==" + "signature": "MEUCIQC/wVXq9Aya1AmIfX3fehIFSnCuCKs4hVNK5xbwrEmWWwIgG38mrkl8xf4mRahfbcHrEStVXb87zqrIQhOPT9RSpWE=" } -} +} \ No newline at end of file diff --git a/test/test_impl.py b/test/test_impl.py index 7080d8e..68e4613 100644 --- a/test/test_impl.py +++ b/test/test_impl.py @@ -4,6 +4,7 @@ import json from pathlib import Path +import pretend import pypi_attestation_models._impl as impl import pytest from sigstore.models import Bundle @@ -14,100 +15,134 @@ attestation_path = Path(__file__).parent / "assets" / "rfc8785-0.1.2-py3-none-any.whl.attestation" -def test_sigstore_to_pypi() -> None: - # Load an existing Sigstore bundle, convert it to a PyPI attestation, - # and check that the result is what we expect. - with bundle_path.open("rb") as f: - sigstore_bundle = Bundle.from_json(f.read()) - attestation = impl.sigstore_to_pypi(sigstore_bundle) - with attestation_path.open("rb") as expected_file: - assert json.loads(attestation.model_dump_json()) == json.load(expected_file) - - -def test_pypi_to_sigstore() -> None: - # Load an existing PyPI attestation, convert it to a Sigstore bundle, - # and check that the result matches the original Sigstore bundle used - # to generate the attestation - with attestation_path.open("rb") as f: - attestation = impl.Attestation.model_validate_json(f.read()) - bundle = impl.pypi_to_sigstore(attestation) - with bundle_path.open("rb") as original_bundle_file: - original_bundle = Bundle.from_json(original_bundle_file.read()) - - # Sigstore Bundle -> PyPI attestation is a lossy operation, so when we go backwards - # the resulting Bundle will have fewer fields than the original Bundle. - assert bundle._inner.media_type == original_bundle._inner.media_type # noqa: SLF001 - assert bundle._inner.verification_material == original_bundle._inner.verification_material # noqa: SLF001 - assert ( - bundle._inner.message_signature.signature # noqa: SLF001 - == original_bundle._inner.message_signature.signature # noqa: SLF001 - ) - assert bundle.log_entry == original_bundle.log_entry - assert bundle.signing_certificate == original_bundle.signing_certificate - - -def test_pypi_to_sigstore_invalid_certificate_base64() -> None: - with attestation_path.open("rb") as f: - attestation = impl.Attestation.model_validate_json(f.read()) - attestation.verification_material.certificate = "invalid base64 @@@@ string" - with pytest.raises(impl.InvalidAttestationError): - impl.pypi_to_sigstore(attestation) - - -def test_pypi_to_sigstore_invalid_certificate() -> None: - with attestation_path.open("rb") as f: - attestation = impl.Attestation.model_validate_json(f.read()) - new_cert = attestation.verification_material.certificate.replace("M", "x") - attestation.verification_material.certificate = new_cert - with pytest.raises(impl.InvalidAttestationError): - impl.pypi_to_sigstore(attestation) - - -def test_pypi_to_sigstore_invalid_log_entry() -> None: - with attestation_path.open("rb") as f: - attestation = impl.Attestation.model_validate_json(f.read()) - new_log_entry = attestation.verification_material.transparency_entries[0] - del new_log_entry["inclusionProof"] - attestation.verification_material.transparency_entries = [new_log_entry] - with pytest.raises(impl.InvalidAttestationError): - impl.pypi_to_sigstore(attestation) - - -def test_verification_roundtrip() -> None: - # Load an existing Sigstore bundle, check that verification passes, - # convert it to a PyPI attestation and then back again to a Sigstore - # bundle, and check that verification still passes. - with bundle_path.open("rb") as f: - sigstore_bundle = Bundle.from_json(f.read()) - - verifier = Verifier.production() - with artifact_path.open("rb") as f: - verifier.verify_artifact( - f.read(), - sigstore_bundle, - policy.Identity( - identity="facundo.tuesca@trailofbits.com", issuer="https://accounts.google.com" - ), +class TestSigningAndVerifying: + def test_payload_sign(self) -> None: + # Call sign on a new AttestationPayload, but mock the underlying call to + # sigstore.Signer.sign() to check that it's called with the correct argument. + sigstore_bundle = Bundle.from_json(bundle_path.read_bytes()) + payload = impl.AttestationPayload.from_dist(artifact_path) + signer = pretend.stub(sign_artifact=pretend.call_recorder(lambda _input: sigstore_bundle)) + payload.sign(signer) + + # Sigstore sign operation should have been called with the AttestationPayload + # corresponding to the Python artifact + assert signer.sign_artifact.calls == [pretend.call(bytes(payload))] + + def test_attestation_verify(self) -> None: + # Call verify on an existing attestation, but mock the underlying call to + # sigstore.Verifier.verify() to check that it's called with the correct arguments. + attestation = impl.Attestation.model_validate_json(attestation_path.read_bytes()) + verifier = pretend.stub( + verify_artifact=pretend.call_recorder(lambda _input, _bundle, _policy: None) ) - - attestation = impl.sigstore_to_pypi(sigstore_bundle) - roundtrip_bundle = impl.pypi_to_sigstore(attestation) - with artifact_path.open("rb") as f: - verifier.verify_artifact( - f.read(), - roundtrip_bundle, - policy.Identity( - identity="facundo.tuesca@trailofbits.com", issuer="https://accounts.google.com" - ), + policy_stub = pretend.stub() + attestation.verify(verifier, policy_stub, artifact_path) + + # This is the input `verify_artifact` should have been called with + # (the bytes of the `AttestationPayload`, not the artifact itself) + expected_payload = bytes(impl.AttestationPayload.from_dist(artifact_path)) + + # This is the bundle `verify_artifact` should have been caled with + # (the Sigstore Bundle corresponding to the input Attestation) + expected_bundle = impl.pypi_to_sigstore(attestation) + + assert len(verifier.verify_artifact.calls) == 1 + verify_args = verifier.verify_artifact.calls[0].args + assert verify_args[0] == expected_payload + assert verify_args[1].to_json() == expected_bundle.to_json() + assert verify_args[2] == policy_stub + + +class TestModelConversions: + def test_sigstore_to_pypi(self) -> None: + # Load an existing Sigstore bundle, convert it to a PyPI attestation, + # and check that the result is what we expect. + with bundle_path.open("rb") as f: + sigstore_bundle = Bundle.from_json(f.read()) + attestation = impl.sigstore_to_pypi(sigstore_bundle) + with attestation_path.open("rb") as expected_file: + assert json.loads(attestation.model_dump_json()) == json.load(expected_file) + + def test_pypi_to_sigstore(self) -> None: + # Load an existing PyPI attestation, convert it to a Sigstore bundle, + # and check that the result matches the original Sigstore bundle used + # to generate the attestation + with attestation_path.open("rb") as f: + attestation = impl.Attestation.model_validate_json(f.read()) + bundle = impl.pypi_to_sigstore(attestation) + with bundle_path.open("rb") as original_bundle_file: + original_bundle = Bundle.from_json(original_bundle_file.read()) + + # Sigstore Bundle -> PyPI attestation is a lossy operation, so when we go backwards + # the resulting Bundle will have fewer fields than the original Bundle. + assert bundle._inner.media_type == original_bundle._inner.media_type # noqa: SLF001 + assert bundle._inner.verification_material == original_bundle._inner.verification_material # noqa: SLF001 + assert ( + bundle._inner.message_signature.signature # noqa: SLF001 + == original_bundle._inner.message_signature.signature # noqa: SLF001 ) - - -def test_attestation_payload() -> None: - payload = impl.AttestationPayload.from_dist(artifact_path) - - assert payload.digest == hashlib.sha256(artifact_path.read_bytes()).hexdigest() - assert payload.distribution == artifact_path.name - - expected = f'{{"digest":"{payload.digest}","distribution":"{payload.distribution}"}}' - - assert bytes(payload) == bytes(expected, "utf-8") + assert bundle.log_entry == original_bundle.log_entry + assert bundle.signing_certificate == original_bundle.signing_certificate + + def test_pypi_to_sigstore_invalid_certificate_base64(self) -> None: + with attestation_path.open("rb") as f: + attestation = impl.Attestation.model_validate_json(f.read()) + attestation.verification_material.certificate = "invalid base64 @@@@ string" + with pytest.raises(impl.InvalidAttestationError): + impl.pypi_to_sigstore(attestation) + + def test_pypi_to_sigstore_invalid_certificate(self) -> None: + with attestation_path.open("rb") as f: + attestation = impl.Attestation.model_validate_json(f.read()) + new_cert = attestation.verification_material.certificate.replace("M", "x") + attestation.verification_material.certificate = new_cert + with pytest.raises(impl.InvalidAttestationError): + impl.pypi_to_sigstore(attestation) + + def test_pypi_to_sigstore_invalid_log_entry(self) -> None: + with attestation_path.open("rb") as f: + attestation = impl.Attestation.model_validate_json(f.read()) + new_log_entry = attestation.verification_material.transparency_entries[0] + del new_log_entry["inclusionProof"] + attestation.verification_material.transparency_entries = [new_log_entry] + with pytest.raises(impl.InvalidAttestationError): + impl.pypi_to_sigstore(attestation) + + def test_verification_roundtrip(self) -> None: + # Load an existing Sigstore bundle, check that verification passes, + # convert it to a PyPI attestation and then back again to a Sigstore + # bundle, and check that verification still passes. + with bundle_path.open("rb") as f: + sigstore_bundle = Bundle.from_json(f.read()) + + verifier = Verifier.production() + with artifact_path.open("rb") as f: + payload = impl.AttestationPayload.from_dist(artifact_path) + verifier.verify_artifact( + bytes(payload), + sigstore_bundle, + policy.Identity( + identity="facundo.tuesca@trailofbits.com", issuer="https://accounts.google.com" + ), + ) + + attestation = impl.sigstore_to_pypi(sigstore_bundle) + roundtrip_bundle = impl.pypi_to_sigstore(attestation) + with artifact_path.open("rb") as f: + verifier.verify_artifact( + bytes(payload), + roundtrip_bundle, + policy.Identity( + identity="facundo.tuesca@trailofbits.com", issuer="https://accounts.google.com" + ), + ) + + def test_attestation_payload(self) -> None: + payload = impl.AttestationPayload.from_dist(artifact_path) + + assert payload.digest == hashlib.sha256(artifact_path.read_bytes()).hexdigest() + assert payload.distribution == artifact_path.name + + expected = f'{{"digest":"{payload.digest}","distribution":"{payload.distribution}"}}' + + assert bytes(payload) == bytes(expected, "utf-8") From a487f47952494a5204984c3e464d0945f760706e Mon Sep 17 00:00:00 2001 From: Facundo Tuesca Date: Fri, 3 May 2024 18:51:58 +0200 Subject: [PATCH 2/5] Add a verification test that doesn't mock the sigstore verifier --- test/test_impl.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/test_impl.py b/test/test_impl.py index 68e4613..a0c528d 100644 --- a/test/test_impl.py +++ b/test/test_impl.py @@ -7,6 +7,7 @@ import pretend import pypi_attestation_models._impl as impl import pytest +import sigstore.errors from sigstore.models import Bundle from sigstore.verify import Verifier, policy @@ -52,6 +53,20 @@ def test_attestation_verify(self) -> None: assert verify_args[1].to_json() == expected_bundle.to_json() assert verify_args[2] == policy_stub + def test_actual_verify(self) -> None: + # Test the actual online verification using a pre-generated attestation + verifier = Verifier.production() + identity_policy = policy.Identity( + identity="facundo.tuesca@trailofbits.com", issuer="https://accounts.google.com" + ) + + attestation = impl.Attestation.model_validate_json(attestation_path.read_bytes()) + attestation.verify(verifier, identity_policy, artifact_path) + + with pytest.raises(sigstore.errors.VerificationError): + # Pass a file that is not the Python artifact this attestation is for + attestation.verify(verifier, identity_policy, bundle_path) + class TestModelConversions: def test_sigstore_to_pypi(self) -> None: From 7fd87f5b886902a44c2f08ac99191adedf0995cf Mon Sep 17 00:00:00 2001 From: Facundo Tuesca Date: Tue, 7 May 2024 18:24:33 +0200 Subject: [PATCH 3/5] Update sigstore dependency --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b54cad9..f2bc1ca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ classifiers = [ "Programming Language :: Python :: 3", "License :: OSI Approved :: Apache Software License", ] -dependencies = ["cryptography", "pydantic", "sigstore==3.0.0rc1"] +dependencies = ["cryptography", "pydantic", "sigstore==3.0.0rc2"] requires-python = ">=3.9" [project.optional-dependencies] From 8d646bf24b41e8c70fe64431ae03cdd3977ef50f Mon Sep 17 00:00:00 2001 From: Facundo Tuesca Date: Tue, 7 May 2024 18:29:22 +0200 Subject: [PATCH 4/5] Fix type error --- src/pypi_attestation_models/_impl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pypi_attestation_models/_impl.py b/src/pypi_attestation_models/_impl.py index 90802db..f51793b 100644 --- a/src/pypi_attestation_models/_impl.py +++ b/src/pypi_attestation_models/_impl.py @@ -128,7 +128,7 @@ def sigstore_to_pypi(sigstore_bundle: Bundle) -> Attestation: version=1, verification_material=VerificationMaterial( certificate=b64encode(certificate).decode("ascii"), - transparency_entries=[sigstore_bundle.log_entry._to_dict_rekor()], # noqa: SLF001 + transparency_entries=[TransparencyLogEntry(sigstore_bundle.log_entry._to_dict_rekor())], # noqa: SLF001 ), message_signature=b64encode(signature).decode("ascii"), ) From d95bd5d4148699fd0c6ca9412d0c3d83fd32ba4f Mon Sep 17 00:00:00 2001 From: Facundo Tuesca Date: Wed, 8 May 2024 13:53:42 +0200 Subject: [PATCH 5/5] Raise own exception class for verification errors --- src/pypi_attestation_models/__init__.py | 2 ++ src/pypi_attestation_models/_impl.py | 18 ++++++++++++++++-- test/test_impl.py | 3 +-- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/pypi_attestation_models/__init__.py b/src/pypi_attestation_models/__init__.py index e539012..877b480 100644 --- a/src/pypi_attestation_models/__init__.py +++ b/src/pypi_attestation_models/__init__.py @@ -8,6 +8,7 @@ ConversionError, InvalidAttestationError, TransparencyLogEntry, + VerificationError, VerificationMaterial, pypi_to_sigstore, sigstore_to_pypi, @@ -19,6 +20,7 @@ "ConversionError", "InvalidAttestationError", "TransparencyLogEntry", + "VerificationError", "VerificationMaterial", "pypi_to_sigstore", "sigstore_to_pypi", diff --git a/src/pypi_attestation_models/_impl.py b/src/pypi_attestation_models/_impl.py index f51793b..b0b5316 100644 --- a/src/pypi_attestation_models/_impl.py +++ b/src/pypi_attestation_models/_impl.py @@ -39,6 +39,14 @@ def __init__(self: InvalidAttestationError, msg: str) -> None: super().__init__(f"Could not convert input Attestation: {msg}") +class VerificationError(ValueError): + """The PyPI Attestation failed verification.""" + + def __init__(self: VerificationError, msg: str) -> None: + """Initialize an `VerificationError`.""" + super().__init__(f"Verification failed: {msg}") + + TransparencyLogEntry = NewType("TransparencyLogEntry", dict[str, Any]) @@ -79,11 +87,17 @@ class Attestation(BaseModel): def verify(self, verifier: Verifier, policy: VerificationPolicy, dist: Path) -> None: """Verify against an existing Python artifact. - On failure, raises `sigstore.errors.InvalidAttestationError`. + On failure, raises: + - `InvalidAttestationError` if the attestation could not be converted to + a Sigstore Bundle. + - `VerificationError` if the attestation could not be verified. """ payload_to_verify = AttestationPayload.from_dist(dist) bundle = pypi_to_sigstore(self) - verifier.verify_artifact(bytes(payload_to_verify), bundle, policy) + try: + verifier.verify_artifact(bytes(payload_to_verify), bundle, policy) + except sigstore.errors.VerificationError as err: + raise VerificationError(str(err)) from err class AttestationPayload(BaseModel): diff --git a/test/test_impl.py b/test/test_impl.py index a0c528d..f69d639 100644 --- a/test/test_impl.py +++ b/test/test_impl.py @@ -7,7 +7,6 @@ import pretend import pypi_attestation_models._impl as impl import pytest -import sigstore.errors from sigstore.models import Bundle from sigstore.verify import Verifier, policy @@ -63,7 +62,7 @@ def test_actual_verify(self) -> None: attestation = impl.Attestation.model_validate_json(attestation_path.read_bytes()) attestation.verify(verifier, identity_policy, artifact_path) - with pytest.raises(sigstore.errors.VerificationError): + with pytest.raises(impl.VerificationError): # Pass a file that is not the Python artifact this attestation is for attestation.verify(verifier, identity_policy, bundle_path)