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 ,
19+ VerificationMaterial as _UpstreamVerificationMaterial ,
2020)
2121from sigstore .dsse import Envelope as DSSEEnvelope
2222from sigstore .dsse import _pae
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.
36+ """
37+
38+ certificate : Base64Bytes | None = None
39+ public_key : str | None = None
40+
41+ @model_validator (mode = "after" )
42+ def _require_certificate_or_public_key (self ):
43+ if self .certificate is None and self .public_key is None :
44+ raise ValueError ("verification_material must contain either a certificate or a public_key" )
45+ return self
46+
47+
3148class _PermissivePolicy :
3249 """A permissive verification policy that always succeeds."""
3350
@@ -110,6 +127,8 @@ def _has_valid_certificate(attestation):
110127 if vm is None :
111128 return False
112129 cert_bytes = vm .certificate
130+ if cert_bytes is None :
131+ return False
113132 load_der_x509_certificate (cert_bytes )
114133 return True
115134 except (ValueError , Exception ):
@@ -180,6 +199,29 @@ def _verify_signature(attestation, public_key):
180199 raise VerificationError (f"signature verification failed: { e } " )
181200
182201
202+ def _verify_embedded_key (attestation , server_key ):
203+ """Verify that the embedded public key matches the server-configured key."""
204+ vm = attestation .verification_material
205+ if vm is None or vm .public_key is None :
206+ return
207+ try :
208+ embedded_key = serialization .load_pem_public_key (vm .public_key .encode ())
209+ except (ValueError , Exception ) as e :
210+ raise VerificationError (f"invalid embedded public key: { e } " )
211+ server_key_bytes = server_key .public_bytes (
212+ serialization .Encoding .PEM ,
213+ serialization .PublicFormat .SubjectPublicKeyInfo ,
214+ )
215+ embedded_key_bytes = embedded_key .public_bytes (
216+ serialization .Encoding .PEM ,
217+ serialization .PublicFormat .SubjectPublicKeyInfo ,
218+ )
219+ if server_key_bytes != embedded_key_bytes :
220+ raise VerificationError (
221+ "embedded public key does not match server-configured ATTESTATION_VERIFICATION_KEY"
222+ )
223+
224+
183225def verify_provenance (filename , sha256 , provenance , offline = True ):
184226 """Verify the provenance object is valid for the package.
185227
@@ -205,8 +247,10 @@ def verify_provenance(filename, sha256, provenance, offline=True):
205247 _enrich_publisher_from_statement (stmt , publisher )
206248 if verification_key :
207249 _verify_signature (attestation , verification_key )
250+ _verify_embedded_key (attestation , verification_key )
208251 else :
209252 raise VerificationError (
210- "Attestation has no Sigstore certificate and no custom "
211- "verification key is configured (ATTESTATION_VERIFICATION_KEY)"
253+ "Attestation has no Sigstore certificate or no custom "
254+ "verification key configured via ATTESTATION_VERIFICATION_KEY "
255+ "(embedded key in verification_material is optional)"
212256 )
0 commit comments