66from __future__ import annotations
77
88import binascii
9- import json
109from 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
2119class 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-
3323class 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 )
0 commit comments