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 (
45+ "verification_material must contain either a certificate or a public_key"
46+ )
47+ return self
48+
49+
3150class _PermissivePolicy :
3251 """A permissive verification policy that always succeeds."""
3352
@@ -110,6 +129,8 @@ def _has_valid_certificate(attestation):
110129 if vm is None :
111130 return False
112131 cert_bytes = vm .certificate
132+ if cert_bytes is None :
133+ return False
113134 load_der_x509_certificate (cert_bytes )
114135 return True
115136 except (ValueError , Exception ):
@@ -180,6 +201,29 @@ def _verify_signature(attestation, public_key):
180201 raise VerificationError (f"signature verification failed: { e } " )
181202
182203
204+ def _verify_embedded_key (attestation , server_key ):
205+ """Verify that the embedded public key matches the server-configured key."""
206+ vm = attestation .verification_material
207+ if vm is None or vm .public_key is None :
208+ return
209+ try :
210+ embedded_key = serialization .load_pem_public_key (vm .public_key .encode ())
211+ except (ValueError , Exception ) as e :
212+ raise VerificationError (f"invalid embedded public key: { e } " )
213+ server_key_bytes = server_key .public_bytes (
214+ serialization .Encoding .PEM ,
215+ serialization .PublicFormat .SubjectPublicKeyInfo ,
216+ )
217+ embedded_key_bytes = embedded_key .public_bytes (
218+ serialization .Encoding .PEM ,
219+ serialization .PublicFormat .SubjectPublicKeyInfo ,
220+ )
221+ if server_key_bytes != embedded_key_bytes :
222+ raise VerificationError (
223+ "embedded public key does not match server-configured ATTESTATION_VERIFICATION_KEY"
224+ )
225+
226+
183227def verify_provenance (filename , sha256 , provenance , offline = True ):
184228 """Verify the provenance object is valid for the package.
185229
@@ -205,8 +249,10 @@ def verify_provenance(filename, sha256, provenance, offline=True):
205249 _enrich_publisher_from_statement (stmt , publisher )
206250 if verification_key :
207251 _verify_signature (attestation , verification_key )
252+ _verify_embedded_key (attestation , verification_key )
208253 else :
209254 raise VerificationError (
210- "Attestation has no Sigstore certificate and no custom "
211- "verification key is configured (ATTESTATION_VERIFICATION_KEY)"
255+ "Attestation has no Sigstore certificate or no custom "
256+ "verification key configured via ATTESTATION_VERIFICATION_KEY "
257+ "(embedded key in verification_material is optional)"
212258 )
0 commit comments