@@ -56,6 +56,9 @@ def __init__(
5656 "signature_invalid" ,
5757 "body_mismatch" ,
5858 "malformed_jws" ,
59+ "malformed_jwks" ,
60+ "unrecognized_critical_header" ,
61+ "unusable_key" ,
5962 ],
6063 message : str ,
6164 ) -> None :
@@ -105,7 +108,7 @@ def generate_ucp_signing_key(*, kid: str, alg: Literal["EdDSA", "ES256"] = "EdDS
105108 # key.private_key — persist securely
106109 # key.public_jwk — publish at /.well-known/jwks.json
107110 """
108- joserfc = _load_joserfc ()
111+ _load_joserfc ()
109112
110113 if alg == "EdDSA" :
111114 from joserfc .jwk import OKPKey # type: ignore[import-not-found]
@@ -125,8 +128,6 @@ def generate_ucp_signing_key(*, kid: str, alg: Literal["EdDSA", "ES256"] = "EdDS
125128 public_jwk .setdefault ("alg" , alg )
126129 public_jwk .setdefault ("use" , "sig" )
127130
128- del joserfc
129-
130131 return GeneratedUCPKey (private_key = priv , public_jwk = public_jwk )
131132
132133
@@ -198,19 +199,30 @@ def sign_ucp_profile(
198199 profile = build_ucp_profile(..., signing_keys=[UCPSigningKey(**key.public_jwk)])
199200 signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid='merchant-2026-05')
200201 """
201- joserfc = _load_joserfc ()
202+ _load_joserfc ()
202203 from joserfc import jws # type: ignore[import-not-found]
203204 from joserfc .jws import JWSRegistry # type: ignore[import-not-found]
204205
206+ # Sign-time kid sanity check: the profile's `signing_keys[]` MUST contain
207+ # a JWK with the matching kid; otherwise verifiers can't resolve the
208+ # public key and the profile is dead-on-arrival.
209+ declared_kids = [
210+ k .get ("kid" ) if isinstance (k , dict ) else getattr (k , "kid" , None ) for k in profile .get ("signing_keys" , [])
211+ ]
212+ if kid not in declared_kids :
213+ msg = (
214+ f"sign_ucp_profile: kid { kid !r} is not present in profile.signing_keys[] "
215+ f"(declared kids: { declared_kids !r} ). Verifiers will not find the key."
216+ )
217+ raise ValueError (msg )
218+
205219 canonical_body = _canonicalize_profile (profile )
206220 header = {"alg" : alg , "kid" : kid , "typ" : _UCP_TYP }
207221 # joserfc treats EdDSA as "not recommended" by default; UCP §6 explicitly accepts
208222 # both EdDSA and ES256, so allow both.
209223 registry = JWSRegistry (algorithms = list (_ALLOWED_ALGS ))
210224 signature = jws .serialize_compact (header , canonical_body , signing_key , registry = registry )
211225
212- del joserfc
213-
214226 return {** profile , "signature" : signature }
215227
216228
@@ -251,11 +263,25 @@ def verify_ucp_profile(
251263
252264 ok = verify_ucp_profile(signed, build_jwks_response([key.public_jwk]))
253265 """
254- joserfc = _load_joserfc ()
266+ _load_joserfc ()
255267 from joserfc import jws # type: ignore[import-not-found]
256268 from joserfc .jwk import KeySet # type: ignore[import-not-found]
257269 from joserfc .jws import JWSRegistry # type: ignore[import-not-found]
258270
271+ # JWKS shape guard so a malformed argument emits a typed UCPVerificationError
272+ # rather than a confusing kid_not_found / AttributeError.
273+ if not isinstance (jwks , dict ) or not isinstance (jwks .get ("keys" ), list ):
274+ raise UCPVerificationError (
275+ "malformed_jwks" ,
276+ f"UCP verifier expected JWKS shape {{'keys': [...]}}; got { type (jwks ).__name__ } ." ,
277+ )
278+
279+ if not isinstance (signed_profile , dict ):
280+ raise UCPVerificationError (
281+ "no_signature" ,
282+ f"UCP verifier expected a profile dict; got { type (signed_profile ).__name__ } ." ,
283+ )
284+
259285 sig = signed_profile .get ("signature" )
260286 if not sig :
261287 raise UCPVerificationError (
@@ -294,6 +320,13 @@ def verify_ucp_profile(
294320 "duplicate_kid" ,
295321 f"JWKS contains { len (matches )} keys with kid={ kid !r} ; expected exactly one." ,
296322 )
323+ # RFC 7517 §4.2: reject keys not intended for signature verification.
324+ matched_use = matches [0 ].get ("use" )
325+ if matched_use is not None and matched_use != "sig" :
326+ raise UCPVerificationError (
327+ "unusable_key" ,
328+ f"JWK with kid={ kid !r} has use={ matched_use !r} ; expected 'sig'." ,
329+ )
297330
298331 stripped = {k : v for k , v in signed_profile .items () if k != "signature" }
299332 expected_payload = _canonicalize_profile (stripped )
@@ -303,14 +336,26 @@ def verify_ucp_profile(
303336 try :
304337 obj = jws .deserialize_compact (sig , key_set , registry = registry )
305338 except Exception as exc :
306- # joserfc raises various subclasses (BadSignatureError, DecodeError, ...).
307- # Wrap in our own type so callers don't need to import joserfc internals.
308- from joserfc .errors import BadSignatureError , DecodeError # type: ignore[import-not-found]
339+ # joserfc raises various subclasses. Wrap in our own type so callers
340+ # don't need to import joserfc internals.
341+ from joserfc .errors import ( # type: ignore[import-not-found]
342+ BadSignatureError ,
343+ DecodeError ,
344+ UnsupportedHeaderError ,
345+ )
309346
310347 if isinstance (exc , BadSignatureError ):
311348 raise UCPVerificationError ("signature_invalid" , f"UCP signature verification failed: { exc } " ) from exc
312349 if isinstance (exc , DecodeError ):
313350 raise UCPVerificationError ("malformed_jws" , f"Malformed JWS: { exc } " ) from exc
351+ # RFC 7515 §4.1.11 / RFC 8725 §3.10: a verifier MUST reject any JWS
352+ # whose `crit` header carries an extension the implementation doesn't
353+ # understand.
354+ if isinstance (exc , UnsupportedHeaderError ):
355+ raise UCPVerificationError (
356+ "unrecognized_critical_header" ,
357+ f"UCP signing rejected unrecognized critical header: { exc } " ,
358+ ) from exc
314359 raise
315360
316361 # Compare the bytes that were actually signed against the canonical body of the
@@ -323,7 +368,6 @@ def verify_ucp_profile(
323368 "UCP profile body does not match the signed payload (tampered or non-canonical)." ,
324369 )
325370
326- del joserfc
327371 return True
328372
329373
0 commit comments