Skip to content

Commit

Permalink
Use sigstore-python for the data models
Browse files Browse the repository at this point in the history
  • Loading branch information
facutuesca committed Apr 30, 2024
1 parent 0fd2819 commit 8782437
Show file tree
Hide file tree
Showing 9 changed files with 187 additions and 246 deletions.
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: Apache Software License",
]
dependencies = ["sigstore-protobuf-specs"]
# TODO pin sigstore since we deppend on Bundle._inner, which is an object from the protobuf models and could change
dependencies = ["cryptography", "pydantic", "sigstore @ git+ssh://git@github.com/sigstore/sigstore-python@7583a787ab808d3780e1fcdae86b8420fde939b8"]
requires-python = ">=3.9"

[project.optional-dependencies]
Expand Down
2 changes: 0 additions & 2 deletions src/pypi_attestation_models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
Attestation,
ConversionError,
InvalidAttestationError,
InvalidBundleError,
VerificationMaterial,
pypi_to_sigstore,
sigstore_to_pypi,
Expand All @@ -16,7 +15,6 @@
"Attestation",
"ConversionError",
"InvalidAttestationError",
"InvalidBundleError",
"VerificationMaterial",
"pypi_to_sigstore",
"sigstore_to_pypi",
Expand Down
127 changes: 28 additions & 99 deletions src/pypi_attestation_models/_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,20 @@
from __future__ import annotations

import binascii
import json
from base64 import b64decode, b64encode
from dataclasses import asdict, dataclass
from typing import Any, Literal
from typing import Annotated, Any, Literal, NewType

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"
from annotated_types import MinLen # noqa: TCH002
from cryptography import x509
from cryptography.hazmat.primitives import serialization
from pydantic import BaseModel
from sigstore.models import Bundle, LogEntry


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."""

Expand All @@ -38,32 +28,25 @@ def __init__(self: InvalidAttestationError, msg: str) -> None:
super().__init__(f"Could not convert input Attestation: {msg}")


@dataclass
class VerificationMaterial:
TransparencyLogEntry = NewType("TransparencyLogEntry", dict[str, Any])


class VerificationMaterial(BaseModel):
"""Cryptographic materials used to verify attestation objects."""

certificate: str
"""
The signing certificate, as `base64(DER(cert))`.
"""

transparency_entries: list[dict[str, Any]]
transparency_entries: Annotated[list[TransparencyLogEntry], MinLen(1)]
"""
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:
class Attestation(BaseModel):
"""Attestation object as defined in PEP 740."""

version: Literal[1]
Expand All @@ -82,90 +65,36 @@ class Attestation:
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,
def sigstore_to_pypi(sigstore_bundle: Bundle) -> Attestation:
"""Convert a Sigstore Bundle into a PyPI attestation as defined in PEP 740."""
certificate = sigstore_bundle.signing_certificate.public_bytes(
encoding=serialization.Encoding.DER
)

signature = sigstore_bundle._inner.message_signature.signature # noqa: SLF001
return Attestation(
version=1,
verification_material=verification_material,
message_signature=b64encode(sigstore_bundle.message_signature.signature).decode("ascii"),
verification_material=VerificationMaterial(
certificate=b64encode(certificate).decode("ascii"),
transparency_entries=[sigstore_bundle.log_entry._to_dict_rekor()], # noqa: SLF001
),
message_signature=b64encode(signature).decode("ascii"),
)


def pypi_to_sigstore(pypi_attestation: Attestation) -> sigstore.Bundle:
def pypi_to_sigstore(pypi_attestation: Attestation) -> 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
]
tlog_entry = pypi_attestation.verification_material.transparency_entries[0]

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),
return Bundle.from_parts(
cert=x509.load_der_x509_certificate(certificate_bytes),
sig=signature_bytes,
log_entry=LogEntry._from_dict_rekor(tlog_entry), # noqa: SLF001
)
48 changes: 0 additions & 48 deletions test/assets/rfc8785-0.0.2-py3-none-any.whl.json

This file was deleted.

Loading

0 comments on commit 8782437

Please sign in to comment.