88from cryptography .hazmat .primitives .asymmetric import padding as crypto_padding
99from cryptography .x509 import load_der_x509_certificate
1010from django .conf import settings
11- from pydantic import BaseModel , ConfigDict , Field
11+ from pydantic import Base64Bytes , BaseModel , ConfigDict , Field , model_validator
1212from pydantic .alias_generators import to_snake
1313from pypi_attestations import Attestation as _UpstreamAttestation
1414from pypi_attestations import (
1515 Distribution ,
1616 Envelope , # noqa - needed in module namespace for Pydantic model rebuild
1717 Publisher ,
1818 VerificationError ,
19- VerificationMaterial ,
2019)
20+ from pypi_attestations import VerificationMaterial as _UpstreamVerificationMaterial
2121from sigstore .dsse import Envelope as DSSEEnvelope
2222from sigstore .dsse import _pae
2323
2828SLSA_PROVENANCE_V02 = "https://slsa.dev/provenance/v0.2"
2929
3030
31+ class VerificationMaterial (_UpstreamVerificationMaterial ):
32+ """Extended verification material that supports optional certificate and public key.
33+
34+ PEP 740 requires a certificate, but this extension allows attestations signed
35+ with a custom key where the certificate is absent. The public_key field is
36+ accepted as an extra field and only present in the output when provided.
37+ """
38+
39+ model_config = ConfigDict (extra = "allow" )
40+
41+ certificate : Base64Bytes | None = None
42+
43+ @model_validator (mode = "after" )
44+ def _validate_fields (self ):
45+ # public_key cannot be present when certificate is present
46+ unexpected = set (self .model_extra or {}) - {"public_key" }
47+ if unexpected :
48+ raise ValueError (f"unexpected fields in verification_material: { unexpected } " )
49+ public_key = getattr (self , "public_key" , None )
50+ if self .certificate is not None and public_key is not None :
51+ raise ValueError ("verification_material cannot contain both certificate and public_key" )
52+ return self
53+
54+
3155class _PermissivePolicy :
3256 """A permissive verification policy that always succeeds."""
3357
@@ -110,6 +134,8 @@ def _has_valid_certificate(attestation):
110134 if vm is None :
111135 return False
112136 cert_bytes = vm .certificate
137+ if cert_bytes is None :
138+ return False
113139 load_der_x509_certificate (cert_bytes )
114140 return True
115141 except (ValueError , Exception ):
@@ -180,6 +206,30 @@ def _verify_signature(attestation, public_key):
180206 raise VerificationError (f"signature verification failed: { e } " )
181207
182208
209+ def _verify_embedded_key (attestation , server_key ):
210+ """Verify that the embedded public key matches the server-configured key."""
211+ vm = attestation .verification_material
212+ public_key = getattr (vm , "public_key" , None ) if vm else None
213+ if public_key is None :
214+ return
215+ try :
216+ embedded_key = serialization .load_pem_public_key (public_key .encode ())
217+ except (ValueError , Exception ) as e :
218+ raise VerificationError (f"invalid embedded public key: { e } " )
219+ server_key_bytes = server_key .public_bytes (
220+ serialization .Encoding .PEM ,
221+ serialization .PublicFormat .SubjectPublicKeyInfo ,
222+ )
223+ embedded_key_bytes = embedded_key .public_bytes (
224+ serialization .Encoding .PEM ,
225+ serialization .PublicFormat .SubjectPublicKeyInfo ,
226+ )
227+ if server_key_bytes != embedded_key_bytes :
228+ raise VerificationError (
229+ "embedded public key does not match server-configured ATTESTATION_VERIFICATION_KEY"
230+ )
231+
232+
183233def verify_provenance (filename , sha256 , provenance , offline = True ):
184234 """Verify the provenance object is valid for the package.
185235
@@ -205,8 +255,10 @@ def verify_provenance(filename, sha256, provenance, offline=True):
205255 _enrich_publisher_from_statement (stmt , publisher )
206256 if verification_key :
207257 _verify_signature (attestation , verification_key )
258+ _verify_embedded_key (attestation , verification_key )
208259 else :
209260 raise VerificationError (
210- "Attestation has no Sigstore certificate and no custom "
211- "verification key is configured (ATTESTATION_VERIFICATION_KEY)"
261+ "Attestation has no Sigstore certificate or no custom "
262+ "verification key configured via ATTESTATION_VERIFICATION_KEY "
263+ "(embedded key in verification_material is optional)"
212264 )
0 commit comments