Skip to content

Commit c6bdbda

Browse files
committed
Use sigstore-python for the data models
1 parent 0fd2819 commit c6bdbda

10 files changed

+193
-252
lines changed

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,22 +20,22 @@ See the full API documentation [here].
2020

2121
```python
2222
from pathlib import Path
23-
from pypi_attestation_models import sigstore_to_pypi
24-
from sigstore_protobuf_specs.dev.sigstore.bundle.v1 import Bundle
23+
from pypi_attestation_models import pypi_to_sigstore, sigstore_to_pypi, Attestation
24+
from sigstore.models import Bundle
2525

2626
# Sigstore Bundle -> PEP 740 Attestation object
2727
bundle_path = Path("test_package-0.0.1-py3-none-any.whl.sigstore")
2828
with bundle_path.open("rb") as f:
29-
sigstore_bundle = Bundle().from_json(f.read())
29+
sigstore_bundle = Bundle.from_json(f.read())
3030
attestation_object = sigstore_to_pypi(sigstore_bundle)
31-
print(attestation_object.to_json())
31+
print(attestation_object.model_dump_json())
3232

3333

3434
# PEP 740 Attestation object -> Sigstore Bundle
3535
attestation_path = Path("attestation.json")
3636
with attestation_path.open("rb") as f:
37-
attestation = impl.Attestation.from_dict(json.load(f))
38-
bundle = impl.pypi_to_sigstore(attestation)
37+
attestation = Attestation.model_validate_json(f.read())
38+
bundle = pypi_to_sigstore(attestation)
3939
print(bundle.to_json())
4040
```
4141

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ classifiers = [
1515
"Programming Language :: Python :: 3",
1616
"License :: OSI Approved :: Apache Software License",
1717
]
18-
dependencies = ["sigstore-protobuf-specs"]
18+
# TODO pin sigstore since we deppend on Bundle._inner, which is an object from the protobuf models and could change
19+
dependencies = ["cryptography", "pydantic", "sigstore @ git+https://github.com/sigstore/sigstore-python.git@7583a787ab808d3780e1fcdae86b8420fde939b8"]
1920
requires-python = ">=3.9"
2021

2122
[project.optional-dependencies]

src/pypi_attestation_models/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
Attestation,
77
ConversionError,
88
InvalidAttestationError,
9-
InvalidBundleError,
109
VerificationMaterial,
1110
pypi_to_sigstore,
1211
sigstore_to_pypi,
@@ -16,7 +15,6 @@
1615
"Attestation",
1716
"ConversionError",
1817
"InvalidAttestationError",
19-
"InvalidBundleError",
2018
"VerificationMaterial",
2119
"pypi_to_sigstore",
2220
"sigstore_to_pypi",

src/pypi_attestation_models/_impl.py

Lines changed: 28 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -6,30 +6,20 @@
66
from __future__ import annotations
77

88
import binascii
9-
import json
109
from base64 import b64decode, b64encode
11-
from dataclasses import asdict, dataclass
12-
from typing import Any, Literal
10+
from typing import Annotated, Any, Literal, NewType
1311

14-
import sigstore_protobuf_specs.dev.sigstore.bundle.v1 as sigstore
15-
from sigstore_protobuf_specs.dev.sigstore.common.v1 import MessageSignature, X509Certificate
16-
from sigstore_protobuf_specs.dev.sigstore.rekor.v1 import TransparencyLogEntry
17-
18-
_NO_CERTIFICATES_ERROR_MESSAGE = "No certificates found in Sigstore Bundle"
12+
from annotated_types import MinLen # noqa: TCH002
13+
from cryptography import x509
14+
from cryptography.hazmat.primitives import serialization
15+
from pydantic import BaseModel
16+
from sigstore.models import Bundle, LogEntry
1917

2018

2119
class ConversionError(ValueError):
2220
"""The base error for all errors during conversion."""
2321

2422

25-
class InvalidBundleError(ConversionError):
26-
"""The Sigstore Bundle given as input is not valid."""
27-
28-
def __init__(self: InvalidBundleError, msg: str) -> None:
29-
"""Initialize an `InvalidBundleError`."""
30-
super().__init__(f"Could not convert input Bundle: {msg}")
31-
32-
3323
class InvalidAttestationError(ConversionError):
3424
"""The PyPI Attestation given as input is not valid."""
3525

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

4030

41-
@dataclass
42-
class VerificationMaterial:
31+
TransparencyLogEntry = NewType("TransparencyLogEntry", dict[str, Any])
32+
33+
34+
class VerificationMaterial(BaseModel):
4335
"""Cryptographic materials used to verify attestation objects."""
4436

4537
certificate: str
4638
"""
4739
The signing certificate, as `base64(DER(cert))`.
4840
"""
4941

50-
transparency_entries: list[dict[str, Any]]
42+
transparency_entries: Annotated[list[TransparencyLogEntry], MinLen(1)]
5143
"""
5244
One or more transparency log entries for this attestation's signature
5345
and certificate.
5446
"""
5547

56-
@staticmethod
57-
def from_dict(dict_input: dict[str, Any]) -> VerificationMaterial:
58-
"""Create a VerificationMaterial object from a dict."""
59-
return VerificationMaterial(
60-
certificate=dict_input["certificate"],
61-
transparency_entries=dict_input["transparency_entries"],
62-
)
6348

64-
65-
@dataclass
66-
class Attestation:
49+
class Attestation(BaseModel):
6750
"""Attestation object as defined in PEP 740."""
6851

6952
version: Literal[1]
@@ -82,90 +65,36 @@ class Attestation:
8265
is the raw bytes of the signing operation.
8366
"""
8467

85-
def to_json(self: Attestation) -> str:
86-
"""Serialize the attestation object into JSON."""
87-
return json.dumps(asdict(self))
88-
89-
@staticmethod
90-
def from_dict(dict_input: dict[str, Any]) -> Attestation:
91-
"""Create an Attestation object from a dict."""
92-
return Attestation(
93-
version=dict_input["version"],
94-
verification_material=VerificationMaterial.from_dict(
95-
dict_input["verification_material"],
96-
),
97-
message_signature=dict_input["message_signature"],
98-
)
99-
100-
101-
@dataclass
102-
class Provenance:
103-
"""Provenance object as defined in PEP 740."""
104-
105-
version: Literal[1]
106-
"""
107-
The provenance object's version, which is always 1.
108-
"""
109-
110-
publisher: object | None
111-
"""
112-
An optional open-ended JSON object, specific to the kind of Trusted
113-
Publisher used to publish the file, if one was used.
114-
"""
11568

116-
attestations: list[Attestation]
117-
"""
118-
One or more attestation objects.
119-
"""
120-
121-
122-
def sigstore_to_pypi(sigstore_bundle: sigstore.Bundle) -> Attestation:
123-
"""Convert a Sigstore Bundle into a PyPI attestation object, as defined in PEP 740."""
124-
certificate = sigstore_bundle.verification_material.certificate.raw_bytes
125-
if certificate == b"":
126-
# If there's no single certificate, we check for a leaf certificate in the
127-
# x509_certificate_chain.certificates` field.
128-
certificates = sigstore_bundle.verification_material.x509_certificate_chain.certificates
129-
if not certificates:
130-
raise InvalidBundleError(_NO_CERTIFICATES_ERROR_MESSAGE)
131-
# According to the spec, the first member of the sequence MUST be the leaf certificate
132-
# conveying the signing key
133-
certificate = certificates[0].raw_bytes
134-
135-
certificate = b64encode(certificate).decode("ascii")
136-
tlog_entries = [t.to_dict() for t in sigstore_bundle.verification_material.tlog_entries]
137-
verification_material = VerificationMaterial(
138-
certificate=certificate,
139-
transparency_entries=tlog_entries,
69+
def sigstore_to_pypi(sigstore_bundle: Bundle) -> Attestation:
70+
"""Convert a Sigstore Bundle into a PyPI attestation as defined in PEP 740."""
71+
certificate = sigstore_bundle.signing_certificate.public_bytes(
72+
encoding=serialization.Encoding.DER
14073
)
14174

75+
signature = sigstore_bundle._inner.message_signature.signature # noqa: SLF001
14276
return Attestation(
14377
version=1,
144-
verification_material=verification_material,
145-
message_signature=b64encode(sigstore_bundle.message_signature.signature).decode("ascii"),
78+
verification_material=VerificationMaterial(
79+
certificate=b64encode(certificate).decode("ascii"),
80+
transparency_entries=[sigstore_bundle.log_entry._to_dict_rekor()], # noqa: SLF001
81+
),
82+
message_signature=b64encode(signature).decode("ascii"),
14683
)
14784

14885

149-
def pypi_to_sigstore(pypi_attestation: Attestation) -> sigstore.Bundle:
86+
def pypi_to_sigstore(pypi_attestation: Attestation) -> Bundle:
15087
"""Convert a PyPI attestation object as defined in PEP 740 into a Sigstore Bundle."""
15188
try:
15289
certificate_bytes = b64decode(pypi_attestation.verification_material.certificate)
15390
signature_bytes = b64decode(pypi_attestation.message_signature)
15491
except binascii.Error as err:
15592
raise InvalidAttestationError(str(err)) from err
15693

157-
certificate = X509Certificate(raw_bytes=certificate_bytes)
158-
tlog_entries = [
159-
TransparencyLogEntry().from_dict(x)
160-
for x in pypi_attestation.verification_material.transparency_entries
161-
]
94+
tlog_entry = pypi_attestation.verification_material.transparency_entries[0]
16295

163-
verification_material = sigstore.VerificationMaterial(
164-
certificate=certificate,
165-
tlog_entries=tlog_entries,
166-
)
167-
return sigstore.Bundle(
168-
media_type="application/vnd.dev.sigstore.bundle+json;version=0.3",
169-
verification_material=verification_material,
170-
message_signature=MessageSignature(signature=signature_bytes),
96+
return Bundle.from_parts(
97+
cert=x509.load_der_x509_certificate(certificate_bytes),
98+
sig=signature_bytes,
99+
log_entry=LogEntry._from_dict_rekor(tlog_entry), # noqa: SLF001
171100
)

test/assets/rfc8785-0.0.2-py3-none-any.whl.json

Lines changed: 0 additions & 48 deletions
This file was deleted.

0 commit comments

Comments
 (0)