diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..99d80f9 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,20 @@ +version: 2 + +updates: + - package-ecosystem: pip + directory: / + groups: + python: + patterns: + - "*" + schedule: + interval: daily + + - package-ecosystem: github-actions + directory: / + groups: + actions: + patterns: + - "*" + schedule: + interval: daily diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..209530d --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,45 @@ +name: Deploy Documentation + +on: + push: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-python@v5 + with: + python-version-file: pyproject.toml + cache: "pip" + cache-dependency-path: pyproject.toml + + - name: setup + run: | + make dev INSTALL_EXTRA=doc + + - name: build docs + run: | + make doc + + - name: upload docs artifact + uses: actions/upload-pages-artifact@v1 + with: + path: ./html/ + + deploy: + needs: build + runs-on: ubuntu-latest + permissions: + # NOTE: Needed to push to the repository. + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - id: deployment + uses: actions/deploy-pages@v1 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..da9720d --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,22 @@ +name: Lint + +on: + push: + branches: + - main + pull_request: + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-python@v5 + with: + python-version-file: pyproject.toml + cache: "pip" + cache-dependency-path: pyproject.toml + + - name: lint + run: make lint INSTALL_EXTRA=lint diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..1b2ba50 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,42 @@ +on: + release: + types: + - published + +name: release + +permissions: + # Used to sign the release's artifacts with sigstore-python. + # Used to publish to PyPI with Trusted Publishing. + id-token: write + + # Used to attach signing artifacts to the published release. + contents: write + +jobs: + pypi: + name: upload release to PyPI + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-python@v5 + with: + python-version-file: pyproject.toml + cache: "pip" + cache-dependency-path: pyproject.toml + + - name: deps + run: python -m pip install -U setuptools build wheel + + - name: build + run: python -m build + + - name: publish + uses: pypa/gh-action-pypi-publish@release/v1 + + - name: sign + uses: sigstore/gh-action-sigstore-python@v1.2.1 + with: + inputs: ./dist/*.tar.gz ./dist/*.whl + release-signing-artifacts: true diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..c4ca955 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,29 @@ +name: Unit tests + +on: + push: + branches: + - main + pull_request: + +jobs: + test: + strategy: + matrix: + python: + - "3.8" + - "3.9" + - "3.10" + - "3.11" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python }} + cache: "pip" + cache-dependency-path: pyproject.toml + + - name: test + run: make test INSTALL_EXTRA=test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b90ba49 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +env/ +pip-wheel-metadata/ +*.egg-info/ +__pycache__/ +.coverage* +.idea +html/ +dist/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f433b1a --- /dev/null +++ b/LICENSE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..26cfc62 --- /dev/null +++ b/Makefile @@ -0,0 +1,87 @@ +SHELL := /bin/bash + +PY_IMPORT = pypi_attestation_models + +ALL_PY_SRCS := $(shell find src -name '*.py') \ + $(shell find test -name '*.py') + +# Optionally overriden by the user, if they're using a virtual environment manager. +VENV ?= env + +# On Windows, venv scripts/shims are under `Scripts` instead of `bin`. +VENV_BIN := $(VENV)/bin +ifeq ($(OS),Windows_NT) + VENV_BIN := $(VENV)/Scripts +endif + +# Optionally overridden by the user in the `release` target. +BUMP_ARGS := + +# Optionally overridden by the user in the `test` target. +TESTS := + +# Optionally overridden by the user/CI, to limit the installation to a specific +# subset of development dependencies. +INSTALL_EXTRA := dev + +# If the user selects a specific test pattern to run, set `pytest` to fail fast +# and only run tests that match the pattern. +# Otherwise, run all tests and enable coverage assertions, since we expect +# complete test coverage. +ifneq ($(TESTS),) + TEST_ARGS := -x -k $(TESTS) + COV_ARGS := +else + TEST_ARGS := + COV_ARGS := --fail-under 100 +endif + +.PHONY: all +all: + @echo "Run my targets individually!" + +.PHONY: dev +dev: $(VENV)/pyvenv.cfg + +$(VENV)/pyvenv.cfg: pyproject.toml + # Create our Python 3 virtual environment + python3 -m venv env + # NOTE(ekilmer): interrogate v1.5.0 needs setuptools when using Python 3.12+. + # This should be fixed when the next release is made + $(VENV_BIN)/python -m pip install --upgrade pip setuptools + $(VENV_BIN)/python -m pip install -e .[$(INSTALL_EXTRA)] + +.PHONY: lint +lint: $(VENV)/pyvenv.cfg + . $(VENV_BIN)/activate && \ + ruff format --check $(ALL_PY_SRCS) && \ + ruff check $(ALL_PY_SRCS) && \ + mypy + . $(VENV_BIN)/activate && \ + interrogate -c pyproject.toml . + +.PHONY: reformat +reformat: + . $(VENV_BIN)/activate && \ + ruff check --fix $(ALL_PY_SRCS) && \ + ruff format $(ALL_PY_SRCS) + +.PHONY: test tests +test tests: $(VENV)/pyvenv.cfg + . $(VENV_BIN)/activate && \ + pytest --cov=$(PY_IMPORT) $(T) $(TEST_ARGS) && \ + python -m coverage report -m $(COV_ARGS) + +.PHONY: doc +doc: $(VENV)/pyvenv.cfg + . $(VENV_BIN)/activate && \ + pdoc -o html $(PY_IMPORT) + +.PHONY: package +package: $(VENV)/pyvenv.cfg + . $(VENV_BIN)/activate && \ + python3 -m build + +.PHONY: edit +edit: + $(EDITOR) $(ALL_PY_SRCS) diff --git a/README.md b/README.md new file mode 100644 index 0000000..6c32c57 --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ +# PyPI Attestation Models + + +[![CI](https://github.com/trailofbits/pypi-attestation-models/actions/workflows/tests.yml/badge.svg)](https://github.com/trailofbits/pypi-attestation-models/actions/workflows/tests.yml) +[![PyPI version](https://badge.fury.io/py/pypi-attestation-models.svg)](https://pypi.org/project/pypi-attestation-models) +[![Packaging status](https://repology.org/badge/tiny-repos/python:pypi-attestation-models.svg)](https://repology.org/project/python:pypi-attestation-models/versions) + + +A library to convert between Sigstore Bundles and PEP-740 Attestation objects + +## Installation + +```bash +python -m pip install pypi-attestation-models +``` + +## Usage + +See the full API documentation [here]. + +```python +from pathlib import Path +from pypi_attestation_models import sigstore_to_pypi +from sigstore_protobuf_specs.dev.sigstore.bundle.v1 import Bundle + +# Sigstore Bundle -> PEP 740 Attestation object +bundle_path = Path("test_package-0.0.1-py3-none-any.whl.sigstore") +with bundle_path.open("rb") as f: + sigstore_bundle = Bundle().from_json(f.read()) +attestation_object = sigstore_to_pypi(sigstore_bundle) +print(attestation_object.to_json()) + + +# PEP 740 Attestation object -> Sigstore Bundle +attestation_path = Path("attestation.json") +with attestation_path.open("rb") as f: + attestation = impl.Attestation.from_dict(json.load(f)) +bundle = impl.pypi_to_sigstore(attestation) +print(bundle.to_json()) +``` + +[here]: https://trailofbits.github.io/pypi-attestation-models diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..7fad43a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,89 @@ +[build-system] +requires = ["flit_core >=3.5,<4"] +build-backend = "flit_core.buildapi" + +[project] +name = "pypi-attestation-models" +dynamic = ["version"] +description = "A library to convert between Sigstore Bundles and PEP-740 Attestation objects" +readme = "README.md" +license = { file = "LICENSE" } +authors = [ + { name = "Trail of Bits", email = "opensource@trailofbits.com" }, +] +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: Apache Software License", +] +dependencies = ["sigstore-protobuf-specs"] +requires-python = ">=3.9" + +[project.optional-dependencies] +doc = ["pdoc"] +test = ["pytest", "pytest-cov", "pretend", "coverage[toml]"] +lint = [ + # NOTE: ruff is under active development, so we pin conservatively here + # and let Dependabot periodically perform this update. + "ruff ~= 0.2", + "mypy >= 1.0", + "types-html5lib", + "types-requests", + "types-toml", + "interrogate", +] +dev = ["pypi-attestation-models[doc,test,lint]", "twine", "wheel", "build"] + + + +[project.urls] +Homepage = "https://pypi.org/project/pypi-attestation-models" +Documentation = "https://trailofbits.github.io/pypi-attestation-models/" +Issues = "https://github.com/trailofbits/pypi-attestation-models/issues" +Source = "https://github.com/trailofbits/pypi-attestation-models" + +[tool.flit.module] +name = "pypi_attestation_models" + +[tool.coverage.run] +# don't attempt code coverage for the CLI entrypoints +omit = ["src/pypi_attestation_models/_cli.py"] + +[tool.mypy] +mypy_path = "src" +packages = "pypi_attestation_models" +allow_redefinition = true +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_untyped_defs = true +ignore_missing_imports = true +no_implicit_optional = true +show_error_codes = true +sqlite_cache = true +strict_equality = true +warn_no_return = true +warn_redundant_casts = true +warn_return_any = true +warn_unreachable = true +warn_unused_configs = true +warn_unused_ignores = true + +[tool.ruff] +line-length = 100 +target-version = "py39" + +[tool.ruff.lint] +select = ["ALL"] + +[tool.ruff.lint.per-file-ignores] + +"test/**/*.py" = [ + "D", # no docstrings in tests + "S101", # asserts are expected in tests +] + +[tool.interrogate] +# don't enforce documentation coverage for packaging, testing, the virtual +# environment, or the CLI (which is documented separately). +exclude = ["env", "test", "src/pypi_attestation_models/_cli.py"] +ignore-semiprivate = true +fail-under = 100 diff --git a/src/pypi_attestation_models/__init__.py b/src/pypi_attestation_models/__init__.py new file mode 100644 index 0000000..f1816b2 --- /dev/null +++ b/src/pypi_attestation_models/__init__.py @@ -0,0 +1,23 @@ +"""The `pypi-attestation-models` APIs.""" + +__version__ = "0.0.1" + +from ._impl import ( + Attestation, + ConversionError, + InvalidAttestationError, + InvalidBundleError, + VerificationMaterial, + pypi_to_sigstore, + sigstore_to_pypi, +) + +__all__ = [ + "Attestation", + "ConversionError", + "InvalidAttestationError", + "InvalidBundleError", + "VerificationMaterial", + "pypi_to_sigstore", + "sigstore_to_pypi", +] diff --git a/src/pypi_attestation_models/_impl.py b/src/pypi_attestation_models/_impl.py new file mode 100644 index 0000000..d57df42 --- /dev/null +++ b/src/pypi_attestation_models/_impl.py @@ -0,0 +1,171 @@ +"""Internal implementation module for `pypi-attestation-models`. + +This module is NOT a public API, and is not considered stable. +""" + +from __future__ import annotations + +import binascii +import json +from base64 import b64decode, b64encode +from dataclasses import asdict, dataclass +from typing import Any, Literal + +import sigstore_protobuf_specs.dev.sigstore.bundle.v1 as sigstore +from sigstore_protobuf_specs.dev.sigstore.common.v1 import MessageSignature, X509Certificate +from sigstore_protobuf_specs.dev.sigstore.rekor.v1 import TransparencyLogEntry + +_NO_CERTIFICATES_ERROR_MESSAGE = "No certificates found in Sigstore Bundle" + + +class ConversionError(ValueError): + """The base error for all errors during conversion.""" + + +class InvalidBundleError(ConversionError): + """The Sigstore Bundle given as input is not valid.""" + + def __init__(self: InvalidBundleError, msg: str) -> None: + """Initialize an `InvalidBundleError`.""" + super().__init__(f"Could not convert input Bundle: {msg}") + + +class InvalidAttestationError(ConversionError): + """The PyPI Attestation given as input is not valid.""" + + def __init__(self: InvalidAttestationError, msg: str) -> None: + """Initialize an `InvalidAttestationError`.""" + super().__init__(f"Could not convert input Attestation: {msg}") + + +@dataclass +class VerificationMaterial: + """Cryptographic materials used to verify attestation objects.""" + + certificate: str + """ + The signing certificate, as `base64(DER(cert))`. + """ + + transparency_entries: list[dict[str, Any]] + """ + One or more transparency log entries for this attestation's signature + and certificate. + """ + + @staticmethod + def from_dict(dict_input: dict[str, Any]) -> VerificationMaterial: + """Create a VerificationMaterial object from a dict.""" + return VerificationMaterial( + certificate=dict_input["certificate"], + transparency_entries=dict_input["transparency_entries"], + ) + + +@dataclass +class Attestation: + """Attestation object as defined in PEP 740.""" + + version: Literal[1] + """ + The attestation format's version, which is always 1. + """ + + verification_material: VerificationMaterial + """ + Cryptographic materials used to verify `message_signature`. + """ + + message_signature: str + """ + The attestation's signature, as `base64(raw-sig)`, where `raw-sig` + is the raw bytes of the signing operation. + """ + + def to_json(self: Attestation) -> str: + """Serialize the attestation object into JSON.""" + return json.dumps(asdict(self)) + + @staticmethod + def from_dict(dict_input: dict[str, Any]) -> Attestation: + """Create an Attestation object from a dict.""" + return Attestation( + version=dict_input["version"], + verification_material=VerificationMaterial.from_dict( + dict_input["verification_material"], + ), + message_signature=dict_input["message_signature"], + ) + + +@dataclass +class Provenance: + """Provenance object as defined in PEP 740.""" + + version: Literal[1] + """ + The provenance object's version, which is always 1. + """ + + publisher: object | None + """ + An optional open-ended JSON object, specific to the kind of Trusted + Publisher used to publish the file, if one was used. + """ + + attestations: list[Attestation] + """ + One or more attestation objects. + """ + + +def sigstore_to_pypi(sigstore_bundle: sigstore.Bundle) -> Attestation: + """Convert a Sigstore Bundle into a PyPI attestation object, as defined in PEP 740.""" + certificate = sigstore_bundle.verification_material.certificate.raw_bytes + if certificate == b"": + # If there's no single certificate, we check for a leaf certificate in the + # x509_certificate_chain.certificates` field. + certificates = sigstore_bundle.verification_material.x509_certificate_chain.certificates + if not certificates: + raise InvalidBundleError(_NO_CERTIFICATES_ERROR_MESSAGE) + # According to the spec, the first member of the sequence MUST be the leaf certificate + # conveying the signing key + certificate = certificates[0].raw_bytes + + certificate = b64encode(certificate).decode("ascii") + tlog_entries = [t.to_dict() for t in sigstore_bundle.verification_material.tlog_entries] + verification_material = VerificationMaterial( + certificate=certificate, + transparency_entries=tlog_entries, + ) + + return Attestation( + version=1, + verification_material=verification_material, + message_signature=b64encode(sigstore_bundle.message_signature.signature).decode("ascii"), + ) + + +def pypi_to_sigstore(pypi_attestation: Attestation) -> sigstore.Bundle: + """Convert a PyPI attestation object as defined in PEP 740 into a Sigstore Bundle.""" + try: + certificate_bytes = b64decode(pypi_attestation.verification_material.certificate) + signature_bytes = b64decode(pypi_attestation.message_signature) + except binascii.Error as err: + raise InvalidAttestationError(str(err)) from err + + certificate = X509Certificate(raw_bytes=certificate_bytes) + tlog_entries = [ + TransparencyLogEntry().from_dict(x) + for x in pypi_attestation.verification_material.transparency_entries + ] + + verification_material = sigstore.VerificationMaterial( + certificate=certificate, + tlog_entries=tlog_entries, + ) + return sigstore.Bundle( + media_type="application/vnd.dev.sigstore.bundle+json;version=0.3", + verification_material=verification_material, + message_signature=MessageSignature(signature=signature_bytes), + ) diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/assets/rfc8785-0.0.2-py3-none-any.whl.json b/test/assets/rfc8785-0.0.2-py3-none-any.whl.json new file mode 100644 index 0000000..2d61d51 --- /dev/null +++ b/test/assets/rfc8785-0.0.2-py3-none-any.whl.json @@ -0,0 +1,48 @@ +{ + "version": 1, + "verification_material": { + "certificate": "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", + "transparency_entries": [ + { + "logIndex": "76153733", + "logId": { + "keyId": "wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0=" + }, + "kindVersion": { + "kind": "hashedrekord", + "version": "0.0.1" + }, + "integratedTime": "1709764175", + "inclusionPromise": { + "signedEntryTimestamp": "MEUCIBf3PdlV0ZcrEbGAEgk4MqZ/2XrXVaoNgNY7SPtb4UAjAiEAtaNeC7d3W2nVXv3k/bO9kVn8tGm/VLqNmq6HmXip7gk=" + }, + "inclusionProof": { + "logIndex": "71990302", + "rootHash": "H4vwFjFMXB1wMptG/HzWZxTAgVMQeRLyxYVAjj0V77Q=", + "treeSize": "71990304", + "hashes": [ + "XXmMcYxafHY92ufCKlDRFQwaNYJ3DKDjVCuLrI3RgS4=", + "MAl1oWo7cvENduy3CJT4vKGWWm2THPYS857XyrIIGnQ=", + "oL5/l+vGeEBm3EMXuGB6bKzrDQgob9s16l2LqzsmZg0=", + "17Uy+jw+z+pkXH5rjqeDC2vmP9CRuCa6Ci7AZw99HjU=", + "yg2mFeuGCgCZW2AYrQZWlRs8VYsz1xInyl3sFzTOC84=", + "yTX4XLfADd7KZrB1B1BOaHpH122CDK+GzwDXlay8j+k=", + "mV2QNtrV+WDkXBNPjE3Im6+kLyPsZoMfQguW8X1KBKo=", + "hdXMInhF1t95XTq7cglzKp5fw6gL4Z/NKAOpfM8X0hE=", + "kpmAb9wGPYqvRAzw4czyQLynD593JMN/wxSv0qD66JE=", + "AcSXgK1w5baiJJ/RBVtApFQMMIZJtwE1Q6UASzPH+zs=", + "CayiUjRB6Htq5omYI+/lGCgRJmGU1DDkMvBLZiP1r4w=", + "XE7+Pykrktsdsy1ru6V4IsFAOKTJosu3KUa0//TCa0w=", + "7Z18YLBAvejEV4nJHIKoks/xlijnhR005qTW2w4QtHg=", + "98enzMaC+x5oCMvIZQA5z8vu2apDMCFvE/935NfuPw8=" + ], + "checkpoint": { + "envelope": "rekor.sigstore.dev - 2605736670972794746\n71990304\nH4vwFjFMXB1wMptG/HzWZxTAgVMQeRLyxYVAjj0V77Q=\n\n— rekor.sigstore.dev wNI9ajBFAiBXGe9HyQ/f0gV0XkOPNYPiKxN43AbeZdMD0SppBabIowIhAL+jT6Z/wWrGrtn7qdldgVH/jlRAweoFIhTnKvvLNysu\n" + } + }, + "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiIwYTE4NjY2NjM0MGI1N2MxYzhmMzY1NDI5YTA3ZjU1MjYxYTFhNjBiMjk1N2Y2M2IyOTQ4MjBjNGUzYzBiNWU0In19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FVUNJSElLV3pTd3dJNjJHcUhkb3Zjc1dpVzBLYk5hSFMwYjQyTEJzeHlSVk5EckFpRUErdEFLbkcrdk9mbXpGNVNmeEJrRlduaDdvR2xsNWhPRWl6YVdvK1dYdHZnPSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVZDZha05EUW14VFowRjNTVUpCWjBsVlQzaFNZbXd6VUVSU2VVWnNWbFZwU0ZwVFZHcElWRFZ3ZGpnNGQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcFJkMDE2UVRKTmFrbDVUMVJOTVZkb1kwNU5hbEYzVFhwQk1rMXFTWHBQVkUweFYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVUzTURsWVdGZHRSazlGZEhKWlMzcHFSRkJxWjIwMVRVdFRZV05QVVdGRVJXbFhkVWtLVVVwSU5IVjROV3h1U1Rndk1XTTFWVlIyVkVOMGRtZzNVR2RNVW05aVFXWmpiamhUWmtGdmJtbHJNa3RCUlhoak1HRlBRMEpZVFhkbloxWjJUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZWZVdaUENtRm1RMUY1UVV0cFYzQlRlVkZuUzFvMGFEUktURGRaZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDFwbldVUldVakJTUVZGSUwwSkdkM2RYYjFwWllVaFNNR05JVFRaTWVUbHVZVmhTYjJSWFNYVlpNamwwVEROU2VWbFhiSE5pTWxwcFlWaFNlZ3BNTTBwdFdYcG5NMDlFVlhWalNHdDJURzFrY0dSSGFERlphVGt6WWpOS2NscHRlSFprTTAxMlkyMVdjMXBYUm5wYVV6VTFZbGQ0UVdOdFZtMWplVGt3Q2xsWFpIcE1NMWwzVEdwQmRVMXFRVFZDWjI5eVFtZEZSVUZaVHk5TlFVVkNRa04wYjJSSVVuZGplbTkyVEROU2RtRXlWblZNYlVacVpFZHNkbUp1VFhVS1dqSnNNR0ZJVm1sa1dFNXNZMjFPZG1KdVVteGlibEYxV1RJNWRFMUNWVWREYVhOSFFWRlJRbWMzT0hkQlVVbEZRak5LYkdKSFZtaGpNbFYzVG1kWlN3cExkMWxDUWtGSFJIWjZRVUpCZDFGdlRWUkthMXBxWXpST1ZGWnBUa1JDYWxsWFdUSk5WR3N6VFdwRk1WbHRSbXhaYlZreVRWUk5kMXBFVW1sT2VrcHRDbGxYVVhkTmVrRldRbWR2Y2tKblJVVkJXVTh2VFVGRlJVSkJaSGxhVjNoc1dWaE9iRTFEVVVkRGFYTkhRVkZSUW1jM09IZEJVVlZGUm01U2VWbFhiSE1LWWpKYWFXRllVbnBNTTBwdFdYcG5NMDlFVlhWalNHdDNTR2RaUzB0M1dVSkNRVWRFZG5wQlFrSm5VVkZqYlZadFkzazVNRmxYWkhwTU0xbDNUR3BCZFFwTmFrRTNRbWR2Y2tKblJVVkJXVTh2VFVGRlNVSkRNRTFMTW1nd1pFaENlazlwT0haa1J6bHlXbGMwZFZsWFRqQmhWemwxWTNrMWJtRllVbTlrVjBveENtTXlWbmxaTWpsMVpFZFdkV1JETldwaU1qQjNZVUZaUzB0M1dVSkNRVWRFZG5wQlFrTlJVbUZFUm1odlpFaFNkMk42YjNaTU1tUndaRWRvTVZscE5Xb0tZakl3ZG1SSVNtaGhWM2gyV20xS2NHUklUWFpqYlZwcVQwUmpORTVUTlhkbFV6aDFXakpzTUdGSVZtbE1NMlIyWTIxMGJXSkhPVE5qZVRsNVdsZDRiQXBaV0U1c1RHNXNkR0pGUW5sYVYxcDZURE5TYUZvelRYWmtha0YxVFVNMGVVMUVaMGREYVhOSFFWRlJRbWMzT0hkQlVXOUZTMmQzYjAxVVNtdGFhbU0wQ2s1VVZtbE9SRUpxV1ZkWk1rMVVhek5OYWtVeFdXMUdiRmx0V1RKTlZFMTNXa1JTYVU1NlNtMVpWMUYzVFhwQlpFSm5iM0pDWjBWRlFWbFBMMDFCUlV3S1FrRTRUVVJYWkhCa1IyZ3hXV2t4YjJJelRqQmFWMUYzVDFGWlMwdDNXVUpDUVVkRWRucEJRa1JCVVhKRVEyeHZaRWhTZDJONmIzWk1NbVJ3WkVkb01RcFphVFZxWWpJd2RtUklTbWhoVjNoMldtMUtjR1JJVFhaamJWcHFUMFJqTkU1VE5YZGxWRUUwUW1kdmNrSm5SVVZCV1U4dlRVRkZUa0pEYjAxTFJFVjVDbHBIV1ROUFJGVXhXV3BSZDFreVJtMU9ha1UxVG5wSmVFNVhTbWhhVjBwdFRtcEZlazFIVVRCWmFtTjVXbTFHYTAxRVRYZEpRVmxMUzNkWlFrSkJSMFFLZG5wQlFrUm5VVk5FUWtKNVdsZGFla3d6VW1oYU0wMTJaR3BCZFUxRE5IbE5RbXRIUTJselIwRlJVVUpuTnpoM1FWRTRSVU4zZDBwT2VsazBUV3BGZWdwUFZHc3pUVU0wUjBOcGMwZEJVVkZDWnpjNGQwRlNRVVZKUVhkbFlVaFNNR05JVFRaTWVUbHVZVmhTYjJSWFNYVlpNamwwVEROU2VWbFhiSE5pTWxwcENtRllVbnBOUW1OSFEybHpSMEZSVVVKbk56aDNRVkpGUlVOUmQwaE5hazE0VGtSUmVVMTZRbTlDWjI5eVFtZEZSVUZaVHk5TlFVVlRRa1p2VFZkSGFEQUtaRWhDZWs5cE9IWmFNbXd3WVVoV2FVeHRUblppVXprd1kyMUdjR0pIT1cxWmJXd3dZM2s1ZVZwdFRUUk9lbWN4VEc1Q05VeDVOVzVoV0ZKdlpGZEpkZ3BrTWpsNVlUSmFjMkl6WkhwTU0wcHNZa2RXYUdNeVZYVmxWekZ6VVVoS2JGcHVUWFprUjBadVkzazVNazFETkhkTWFrbDNUMEZaUzB0M1dVSkNRVWRFQ25aNlFVSkZkMUZ4UkVObmVFMXRVbTFPZW1jeFRsZEpNRTFIVG1oYWFsbDRUMVJqZVUxVVZtbFpWMVpwV21wWmVFMTZRbXRPUjBrelRXMWFhRnBFUVhvS1RVSmpSME5wYzBkQlVWRkNaemM0ZDBGU1VVVkRVWGRJWTIxV2MxcFhSbnBhVkVKalFtZHZja0puUlVWQldVOHZUVUZGVmtKRk5FMVVSMmd3WkVoQ2VncFBhVGgyV2pKc01HRklWbWxNYlU1MllsTTVNR050Um5CaVJ6bHRXVzFzTUdONU9YbGFiVTAwVG5wbk1VeHVRalZNTWtacVpFZHNkbUp1VFhaamJsWjFDbU41T0RSTlZHTTFUbXBuZVUxRVRUUk1Na1l3WkVkV2RHTklVbnBNZWtWM1JtZFpTMHQzV1VKQ1FVZEVkbnBCUWtablVVbEVRVnAzWkZkS2MyRlhUWGNLWjFsdlIwTnBjMGRCVVZGQ01XNXJRMEpCU1VWbVFWSTJRVWhuUVdSblJHUlFWRUp4ZUhOalVrMXRUVnBJYUhsYVducGpRMjlyY0dWMVRqUTRjbVlyU0FwcGJrdEJUSGx1ZFdwblFVRkJXVFJXTkhWWVkwRkJRVVZCZDBKSVRVVlZRMGxGYjFONmJEUlNkWHA2TW5jM2RqQXZWbFl4WWprMlJuQmtVRXMwZEhkekNuaDBUM2xtWkhKNVNYSnFhVUZwUlVFd1MwRjROSEkwV0cxaFFXOWpSa0Z0WTNvMUwxQXhOMFk0VDFSaFlucE9UbFJaTVdKV01UbG1lbGRyZDBObldVa0tTMjlhU1hwcU1FVkJkMDFFWVVGQmQxcFJTWGRJWmxVNVlqbElVVk50WTNCVWNqTnhSMVZpVTJWS1pHa3haa3AzUVRad1oyRTFaRTl5YmtGcFFtd3hWZ280ZWt0aFQzUkdlbkpIUTNWaGRsSnJNMXBHYmtGcVJVRXJjMUJsYlZwaVRuRnVObmt2UkhwVU5YQkRaMk5rUmpWc1JWZDJlRTlWUXpOaWN6QjVSMU53Q20weVJWWktSRWxJYkU5elRDdHVZWE15YVRscmRWcDVVUW90TFMwdExVVk9SQ0JEUlZKVVNVWkpRMEZVUlMwdExTMHRDZz09In19fX0=" + } + ] + }, + "message_signature": "MEUCIHIKWzSwwI62GqHdovcsWiW0KbNaHS0b42LBsxyRVNDrAiEA+tAKnG+vOfmzF5SfxBkFWnh7oGll5hOEizaWo+WXtvg=" +} \ No newline at end of file diff --git a/test/assets/rfc8785-0.0.2-py3-none-any.whl.sigstore b/test/assets/rfc8785-0.0.2-py3-none-any.whl.sigstore new file mode 100644 index 0000000..483e943 --- /dev/null +++ b/test/assets/rfc8785-0.0.2-py3-none-any.whl.sigstore @@ -0,0 +1,60 @@ +{ + "mediaType": "application/vnd.dev.sigstore.bundle+json;version=0.2", + "verificationMaterial": { + "x509CertificateChain": { + "certificates": [ + { + "rawBytes": "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" + } + ] + }, + "tlogEntries": [ + { + "logIndex": "76153733", + "logId": { + "keyId": "wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0=" + }, + "kindVersion": { + "kind": "hashedrekord", + "version": "0.0.1" + }, + "integratedTime": "1709764175", + "inclusionPromise": { + "signedEntryTimestamp": "MEUCIBf3PdlV0ZcrEbGAEgk4MqZ/2XrXVaoNgNY7SPtb4UAjAiEAtaNeC7d3W2nVXv3k/bO9kVn8tGm/VLqNmq6HmXip7gk=" + }, + "inclusionProof": { + "logIndex": "71990302", + "rootHash": "H4vwFjFMXB1wMptG/HzWZxTAgVMQeRLyxYVAjj0V77Q=", + "treeSize": "71990304", + "hashes": [ + "XXmMcYxafHY92ufCKlDRFQwaNYJ3DKDjVCuLrI3RgS4=", + "MAl1oWo7cvENduy3CJT4vKGWWm2THPYS857XyrIIGnQ=", + "oL5/l+vGeEBm3EMXuGB6bKzrDQgob9s16l2LqzsmZg0=", + "17Uy+jw+z+pkXH5rjqeDC2vmP9CRuCa6Ci7AZw99HjU=", + "yg2mFeuGCgCZW2AYrQZWlRs8VYsz1xInyl3sFzTOC84=", + "yTX4XLfADd7KZrB1B1BOaHpH122CDK+GzwDXlay8j+k=", + "mV2QNtrV+WDkXBNPjE3Im6+kLyPsZoMfQguW8X1KBKo=", + "hdXMInhF1t95XTq7cglzKp5fw6gL4Z/NKAOpfM8X0hE=", + "kpmAb9wGPYqvRAzw4czyQLynD593JMN/wxSv0qD66JE=", + "AcSXgK1w5baiJJ/RBVtApFQMMIZJtwE1Q6UASzPH+zs=", + "CayiUjRB6Htq5omYI+/lGCgRJmGU1DDkMvBLZiP1r4w=", + "XE7+Pykrktsdsy1ru6V4IsFAOKTJosu3KUa0//TCa0w=", + "7Z18YLBAvejEV4nJHIKoks/xlijnhR005qTW2w4QtHg=", + "98enzMaC+x5oCMvIZQA5z8vu2apDMCFvE/935NfuPw8=" + ], + "checkpoint": { + "envelope": "rekor.sigstore.dev - 2605736670972794746\n71990304\nH4vwFjFMXB1wMptG/HzWZxTAgVMQeRLyxYVAjj0V77Q=\n\n— rekor.sigstore.dev wNI9ajBFAiBXGe9HyQ/f0gV0XkOPNYPiKxN43AbeZdMD0SppBabIowIhAL+jT6Z/wWrGrtn7qdldgVH/jlRAweoFIhTnKvvLNysu\n" + } + }, + "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiIwYTE4NjY2NjM0MGI1N2MxYzhmMzY1NDI5YTA3ZjU1MjYxYTFhNjBiMjk1N2Y2M2IyOTQ4MjBjNGUzYzBiNWU0In19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FVUNJSElLV3pTd3dJNjJHcUhkb3Zjc1dpVzBLYk5hSFMwYjQyTEJzeHlSVk5EckFpRUErdEFLbkcrdk9mbXpGNVNmeEJrRlduaDdvR2xsNWhPRWl6YVdvK1dYdHZnPSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVZDZha05EUW14VFowRjNTVUpCWjBsVlQzaFNZbXd6VUVSU2VVWnNWbFZwU0ZwVFZHcElWRFZ3ZGpnNGQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcFJkMDE2UVRKTmFrbDVUMVJOTVZkb1kwNU5hbEYzVFhwQk1rMXFTWHBQVkUweFYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVUzTURsWVdGZHRSazlGZEhKWlMzcHFSRkJxWjIwMVRVdFRZV05QVVdGRVJXbFhkVWtLVVVwSU5IVjROV3h1U1Rndk1XTTFWVlIyVkVOMGRtZzNVR2RNVW05aVFXWmpiamhUWmtGdmJtbHJNa3RCUlhoak1HRlBRMEpZVFhkbloxWjJUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZWZVdaUENtRm1RMUY1UVV0cFYzQlRlVkZuUzFvMGFEUktURGRaZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDFwbldVUldVakJTUVZGSUwwSkdkM2RYYjFwWllVaFNNR05JVFRaTWVUbHVZVmhTYjJSWFNYVlpNamwwVEROU2VWbFhiSE5pTWxwcFlWaFNlZ3BNTTBwdFdYcG5NMDlFVlhWalNHdDJURzFrY0dSSGFERlphVGt6WWpOS2NscHRlSFprTTAxMlkyMVdjMXBYUm5wYVV6VTFZbGQ0UVdOdFZtMWplVGt3Q2xsWFpIcE1NMWwzVEdwQmRVMXFRVFZDWjI5eVFtZEZSVUZaVHk5TlFVVkNRa04wYjJSSVVuZGplbTkyVEROU2RtRXlWblZNYlVacVpFZHNkbUp1VFhVS1dqSnNNR0ZJVm1sa1dFNXNZMjFPZG1KdVVteGlibEYxV1RJNWRFMUNWVWREYVhOSFFWRlJRbWMzT0hkQlVVbEZRak5LYkdKSFZtaGpNbFYzVG1kWlN3cExkMWxDUWtGSFJIWjZRVUpCZDFGdlRWUkthMXBxWXpST1ZGWnBUa1JDYWxsWFdUSk5WR3N6VFdwRk1WbHRSbXhaYlZreVRWUk5kMXBFVW1sT2VrcHRDbGxYVVhkTmVrRldRbWR2Y2tKblJVVkJXVTh2VFVGRlJVSkJaSGxhVjNoc1dWaE9iRTFEVVVkRGFYTkhRVkZSUW1jM09IZEJVVlZGUm01U2VWbFhiSE1LWWpKYWFXRllVbnBNTTBwdFdYcG5NMDlFVlhWalNHdDNTR2RaUzB0M1dVSkNRVWRFZG5wQlFrSm5VVkZqYlZadFkzazVNRmxYWkhwTU0xbDNUR3BCZFFwTmFrRTNRbWR2Y2tKblJVVkJXVTh2VFVGRlNVSkRNRTFMTW1nd1pFaENlazlwT0haa1J6bHlXbGMwZFZsWFRqQmhWemwxWTNrMWJtRllVbTlrVjBveENtTXlWbmxaTWpsMVpFZFdkV1JETldwaU1qQjNZVUZaUzB0M1dVSkNRVWRFZG5wQlFrTlJVbUZFUm1odlpFaFNkMk42YjNaTU1tUndaRWRvTVZscE5Xb0tZakl3ZG1SSVNtaGhWM2gyV20xS2NHUklUWFpqYlZwcVQwUmpORTVUTlhkbFV6aDFXakpzTUdGSVZtbE1NMlIyWTIxMGJXSkhPVE5qZVRsNVdsZDRiQXBaV0U1c1RHNXNkR0pGUW5sYVYxcDZURE5TYUZvelRYWmtha0YxVFVNMGVVMUVaMGREYVhOSFFWRlJRbWMzT0hkQlVXOUZTMmQzYjAxVVNtdGFhbU0wQ2s1VVZtbE9SRUpxV1ZkWk1rMVVhek5OYWtVeFdXMUdiRmx0V1RKTlZFMTNXa1JTYVU1NlNtMVpWMUYzVFhwQlpFSm5iM0pDWjBWRlFWbFBMMDFCUlV3S1FrRTRUVVJYWkhCa1IyZ3hXV2t4YjJJelRqQmFWMUYzVDFGWlMwdDNXVUpDUVVkRWRucEJRa1JCVVhKRVEyeHZaRWhTZDJONmIzWk1NbVJ3WkVkb01RcFphVFZxWWpJd2RtUklTbWhoVjNoMldtMUtjR1JJVFhaamJWcHFUMFJqTkU1VE5YZGxWRUUwUW1kdmNrSm5SVVZCV1U4dlRVRkZUa0pEYjAxTFJFVjVDbHBIV1ROUFJGVXhXV3BSZDFreVJtMU9ha1UxVG5wSmVFNVhTbWhhVjBwdFRtcEZlazFIVVRCWmFtTjVXbTFHYTAxRVRYZEpRVmxMUzNkWlFrSkJSMFFLZG5wQlFrUm5VVk5FUWtKNVdsZGFla3d6VW1oYU0wMTJaR3BCZFUxRE5IbE5RbXRIUTJselIwRlJVVUpuTnpoM1FWRTRSVU4zZDBwT2VsazBUV3BGZWdwUFZHc3pUVU0wUjBOcGMwZEJVVkZDWnpjNGQwRlNRVVZKUVhkbFlVaFNNR05JVFRaTWVUbHVZVmhTYjJSWFNYVlpNamwwVEROU2VWbFhiSE5pTWxwcENtRllVbnBOUW1OSFEybHpSMEZSVVVKbk56aDNRVkpGUlVOUmQwaE5hazE0VGtSUmVVMTZRbTlDWjI5eVFtZEZSVUZaVHk5TlFVVlRRa1p2VFZkSGFEQUtaRWhDZWs5cE9IWmFNbXd3WVVoV2FVeHRUblppVXprd1kyMUdjR0pIT1cxWmJXd3dZM2s1ZVZwdFRUUk9lbWN4VEc1Q05VeDVOVzVoV0ZKdlpGZEpkZ3BrTWpsNVlUSmFjMkl6WkhwTU0wcHNZa2RXYUdNeVZYVmxWekZ6VVVoS2JGcHVUWFprUjBadVkzazVNazFETkhkTWFrbDNUMEZaUzB0M1dVSkNRVWRFQ25aNlFVSkZkMUZ4UkVObmVFMXRVbTFPZW1jeFRsZEpNRTFIVG1oYWFsbDRUMVJqZVUxVVZtbFpWMVpwV21wWmVFMTZRbXRPUjBrelRXMWFhRnBFUVhvS1RVSmpSME5wYzBkQlVWRkNaemM0ZDBGU1VVVkRVWGRJWTIxV2MxcFhSbnBhVkVKalFtZHZja0puUlVWQldVOHZUVUZGVmtKRk5FMVVSMmd3WkVoQ2VncFBhVGgyV2pKc01HRklWbWxNYlU1MllsTTVNR050Um5CaVJ6bHRXVzFzTUdONU9YbGFiVTAwVG5wbk1VeHVRalZNTWtacVpFZHNkbUp1VFhaamJsWjFDbU41T0RSTlZHTTFUbXBuZVUxRVRUUk1Na1l3WkVkV2RHTklVbnBNZWtWM1JtZFpTMHQzV1VKQ1FVZEVkbnBCUWtablVVbEVRVnAzWkZkS2MyRlhUWGNLWjFsdlIwTnBjMGRCVVZGQ01XNXJRMEpCU1VWbVFWSTJRVWhuUVdSblJHUlFWRUp4ZUhOalVrMXRUVnBJYUhsYVducGpRMjlyY0dWMVRqUTRjbVlyU0FwcGJrdEJUSGx1ZFdwblFVRkJXVFJXTkhWWVkwRkJRVVZCZDBKSVRVVlZRMGxGYjFONmJEUlNkWHA2TW5jM2RqQXZWbFl4WWprMlJuQmtVRXMwZEhkekNuaDBUM2xtWkhKNVNYSnFhVUZwUlVFd1MwRjROSEkwV0cxaFFXOWpSa0Z0WTNvMUwxQXhOMFk0VDFSaFlucE9UbFJaTVdKV01UbG1lbGRyZDBObldVa0tTMjlhU1hwcU1FVkJkMDFFWVVGQmQxcFJTWGRJWmxVNVlqbElVVk50WTNCVWNqTnhSMVZpVTJWS1pHa3haa3AzUVRad1oyRTFaRTl5YmtGcFFtd3hWZ280ZWt0aFQzUkdlbkpIUTNWaGRsSnJNMXBHYmtGcVJVRXJjMUJsYlZwaVRuRnVObmt2UkhwVU5YQkRaMk5rUmpWc1JWZDJlRTlWUXpOaWN6QjVSMU53Q20weVJWWktSRWxJYkU5elRDdHVZWE15YVRscmRWcDVVUW90TFMwdExVVk9SQ0JEUlZKVVNVWkpRMEZVUlMwdExTMHRDZz09In19fX0=" + } + ] + }, + "messageSignature": { + "messageDigest": { + "algorithm": "SHA2_256", + "digest": "ChhmZjQLV8HI82VCmgf1UmGhpgspV/Y7KUggxOPAteQ=" + }, + "signature": "MEUCIHIKWzSwwI62GqHdovcsWiW0KbNaHS0b42LBsxyRVNDrAiEA+tAKnG+vOfmzF5SfxBkFWnh7oGll5hOEizaWo+WXtvg=" + } +} \ No newline at end of file diff --git a/test/test_impl.py b/test/test_impl.py new file mode 100644 index 0000000..293e7b6 --- /dev/null +++ b/test/test_impl.py @@ -0,0 +1,67 @@ +"""Internal implementation tests.""" + +import json +from pathlib import Path + +import pypi_attestation_models._impl as impl +import pytest +from sigstore_protobuf_specs.dev.sigstore.bundle.v1 import Bundle + +bundle_path = Path(__file__).parent / "assets" / "rfc8785-0.0.2-py3-none-any.whl.sigstore" +attestation_path = Path(__file__).parent / "assets" / "rfc8785-0.0.2-py3-none-any.whl.json" + + +def test_sigstore_to_pypi() -> None: + 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.to_json()) == json.load(expected_file) + + +def test_sigstore_to_pypi_empty_certs() -> None: + with bundle_path.open("rb") as f: + sigstore_bundle = Bundle().from_json(f.read()) + sigstore_bundle.verification_material.certificate.raw_bytes = b"" + sigstore_bundle.verification_material.x509_certificate_chain.certificates = [] + + with pytest.raises(impl.InvalidBundleError): + impl.sigstore_to_pypi(sigstore_bundle) + + +def test_pypi_to_sigstore() -> None: + with attestation_path.open("rb") as f: + attestation = impl.Attestation.from_dict(json.load(f)) + 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. Not only that, + # but the fields present might be different (e.g: the original bundle might have a + # `x509_certificate_chain` field, but the converted bundle will use the `certificate` field + # instead). + assert bundle.media_type == "application/vnd.dev.sigstore.bundle+json;version=0.3" + assert bundle.message_signature.signature == original_bundle.message_signature.signature + assert ( + bundle.verification_material.tlog_entries + == original_bundle.verification_material.tlog_entries + ) + if original_bundle.verification_material.certificate.raw_bytes != b"": + assert ( + bundle.verification_material.certificate + == original_bundle.verification_material.certificate + ) + else: + assert ( + bundle.verification_material.certificate + == original_bundle.verification_material.x509_certificate_chain.certificates[0] + ) + + +def test_pypi_to_sigstore_invalid_certificate_base64() -> None: + with attestation_path.open("rb") as f: + attestation = impl.Attestation.from_dict(json.load(f)) + attestation.verification_material.certificate = "invalid base64 @@@@ string" + with pytest.raises(impl.InvalidAttestationError): + impl.pypi_to_sigstore(attestation) diff --git a/test/test_init.py b/test/test_init.py new file mode 100644 index 0000000..470b19c --- /dev/null +++ b/test/test_init.py @@ -0,0 +1,9 @@ +"""Initial testing module.""" + +import pypi_attestation_models + + +def test_version() -> None: + version = getattr(pypi_attestation_models, "__version__", None) + assert version is not None + assert isinstance(version, str)