Skip to content

Commit 1bc9ef3

Browse files
vvillait88claude
andcommitted
hardening(identity): round-26 UCP signing reviewer findings
verify_ucp_profile now treats explicit JSON null on JWK.use / JWK.alg as absent (skip-on-null) rather than rejecting via joserfc's stricter KeySet.import_key_set validation; both fields are optional per RFC 7517 and the Node sibling now uses ``!= null`` semantics, so a JWK with ``"use": null`` / ``"alg": null`` should pass language-symmetric verify. The ``is not None`` checks above already skip null; we now also drop the explicit-null entries before handing the JWK to joserfc so its key parameter registry doesn't reject them downstream. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 48bced1 commit 1bc9ef3

2 files changed

Lines changed: 42 additions & 0 deletions

File tree

agentscore_commerce/identity/ucp_jwks.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,10 @@ def verify_ucp_profile(
430430
)
431431
matched = matches[0]
432432
# RFC 7517 §4.2: reject keys not intended for signature verification.
433+
# ``use`` and ``alg`` are optional per RFC 7517; an explicit JSON null is
434+
# out-of-spec but treat it as absent (skip-on-null) so a JWK with
435+
# ``"use": null`` matches the Node sibling's ``!= null`` semantics in
436+
# ucp-jwks.ts and the two languages stay symmetric.
433437
matched_use = matched.get("use")
434438
if matched_use is not None and matched_use != "sig":
435439
raise UCPVerificationError(
@@ -444,6 +448,11 @@ def verify_ucp_profile(
444448
"unusable_key",
445449
f"JWK alg {matched_alg!r} does not match JWS header alg {header_alg!r}.",
446450
)
451+
# joserfc's KeySet.import_key_set runs a stricter dict-key validation that
452+
# rejects ``use: None`` / ``alg: None`` outright. Strip explicit nulls for
453+
# those two fields before handing the JWK off so skip-on-null actually
454+
# propagates to the import step.
455+
matches = [{k: v for k, v in matched.items() if not (k in ("use", "alg") and v is None)}]
447456

448457
stripped = {k: v for k, v in signed_profile.items() if k != "signature"}
449458
try:

tests/test_ucp_jwks.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -756,6 +756,39 @@ def test_accepts_u2027_sanity_case(self) -> None:
756756
assert verify_ucp_profile(signed, build_jwks_response([signer.public_jwk])) is True
757757

758758

759+
class TestJWKUseAlgNullTreatedAsAbsent:
760+
"""RFC 7517 lists ``use`` and ``alg`` as optional. Explicit JSON null is
761+
out-of-spec but harmless; treat null as absent (skip-on-null) so a JWK
762+
carrying ``"use": null`` or ``"alg": null`` matches the Node sibling's
763+
``!= null`` semantics in ucp-jwks.ts and the two languages stay
764+
symmetric.
765+
"""
766+
767+
def test_verify_succeeds_when_matched_jwk_has_null_use(self) -> None:
768+
key = generate_ucp_signing_key(kid="null-use")
769+
profile = _base_profile([key.public_jwk])
770+
signed = sign_ucp_profile(profile, signing_key=key.private_key, kid="null-use")
771+
jwks_with_null_use = build_jwks_response([{**key.public_jwk, "use": None}])
772+
assert verify_ucp_profile(signed, jwks_with_null_use) is True
773+
774+
def test_verify_succeeds_when_matched_jwk_has_null_alg(self) -> None:
775+
key = generate_ucp_signing_key(kid="null-alg", alg="EdDSA")
776+
profile = _base_profile([key.public_jwk])
777+
signed = sign_ucp_profile(profile, signing_key=key.private_key, kid="null-alg")
778+
jwks_with_null_alg = build_jwks_response([{**key.public_jwk, "alg": None}])
779+
assert verify_ucp_profile(signed, jwks_with_null_alg) is True
780+
781+
def test_verify_still_rejects_use_enc_with_unusable_key(self) -> None:
782+
# Sanity: non-null wrong values continue to fail with unusable_key.
783+
key = generate_ucp_signing_key(kid="enc-sanity")
784+
profile = _base_profile([key.public_jwk])
785+
signed = sign_ucp_profile(profile, signing_key=key.private_key, kid="enc-sanity")
786+
enc_jwk = {**key.public_jwk, "use": "enc"}
787+
with pytest.raises(UCPVerificationError) as exc:
788+
verify_ucp_profile(signed, build_jwks_response([enc_jwk]))
789+
assert exc.value.code == "unusable_key"
790+
791+
759792
class TestVerifierErrorPrecedence:
760793
def test_null_profile_with_malformed_jwks_returns_no_signature(self) -> None:
761794
with pytest.raises(UCPVerificationError) as exc:

0 commit comments

Comments
 (0)