@@ -671,6 +671,73 @@ def test_sign_accepts_bool_dict_keys(self) -> None:
671671 assert verify_ucp_profile (signed , build_jwks_response ([signer .public_jwk ])) is True
672672
673673
674+ # U+2028 / U+2029 named via escape so the RUF001 ambiguous-character lint
675+ # doesn't fire on the test inputs (the codepoints are intentional, not typos).
676+ _U2028 = "\u2028 "
677+ _U2029 = "\u2029 "
678+
679+
680+ class TestLineParagraphSeparatorRejection :
681+ """U+2028 / U+2029 are escaped by pre-ES2019 V8 (``JSON.stringify`` emits
682+ the escaped sequences) but emitted raw by ``json.dumps(ensure_ascii=False)``.
683+
684+ Modern V8 emits them raw too, so the divergence is theoretical on today's
685+ Node, but the rejection mirrors core/api/src/lib/canonicalize.ts so the
686+ contract stays symmetric for any pre-ES2019 verifier path (older V8,
687+ browser-side verifier code).
688+ """
689+
690+ def test_rejects_u2028_at_top_level (self ) -> None :
691+ signer = generate_ucp_signing_key (kid = "k" )
692+ profile = {** _base_profile ([signer .public_jwk ]), "extras" : {"note" : f"before{ _U2028 } after" }}
693+ with pytest .raises (ValueError , match = "U\\ +2028" ):
694+ sign_ucp_profile (profile , signing_key = signer .private_key , kid = "k" )
695+
696+ def test_rejects_u2029_at_top_level (self ) -> None :
697+ signer = generate_ucp_signing_key (kid = "k" )
698+ profile = {** _base_profile ([signer .public_jwk ]), "extras" : {"note" : f"before{ _U2029 } after" }}
699+ with pytest .raises (ValueError , match = "U\\ +2029" ):
700+ sign_ucp_profile (profile , signing_key = signer .private_key , kid = "k" )
701+
702+ def test_rejects_u2028_nested_in_list (self ) -> None :
703+ signer = generate_ucp_signing_key (kid = "k" )
704+ profile = {** _base_profile ([signer .public_jwk ]), "extras" : {"items" : ["ok" , f"bad{ _U2028 } tail" ]}}
705+ with pytest .raises (ValueError , match = "U\\ +2028" ):
706+ sign_ucp_profile (profile , signing_key = signer .private_key , kid = "k" )
707+
708+ def test_rejects_u2029_nested_in_list (self ) -> None :
709+ signer = generate_ucp_signing_key (kid = "k" )
710+ profile = {** _base_profile ([signer .public_jwk ]), "extras" : {"items" : ["ok" , f"bad{ _U2029 } tail" ]}}
711+ with pytest .raises (ValueError , match = "U\\ +2029" ):
712+ sign_ucp_profile (profile , signing_key = signer .private_key , kid = "k" )
713+
714+ def test_rejects_u2028_nested_in_dict_value (self ) -> None :
715+ signer = generate_ucp_signing_key (kid = "k" )
716+ profile = {** _base_profile ([signer .public_jwk ]), "extras" : {"deep" : {"inner" : f"before{ _U2028 } after" }}}
717+ with pytest .raises (ValueError , match = "U\\ +2028" ):
718+ sign_ucp_profile (profile , signing_key = signer .private_key , kid = "k" )
719+
720+ def test_rejects_u2029_nested_in_dict_value (self ) -> None :
721+ signer = generate_ucp_signing_key (kid = "k" )
722+ profile = {** _base_profile ([signer .public_jwk ]), "extras" : {"deep" : {"inner" : f"before{ _U2029 } after" }}}
723+ with pytest .raises (ValueError , match = "U\\ +2029" ):
724+ sign_ucp_profile (profile , signing_key = signer .private_key , kid = "k" )
725+
726+ def test_rejects_u2028_in_dict_key (self ) -> None :
727+ signer = generate_ucp_signing_key (kid = "k" )
728+ profile = {** _base_profile ([signer .public_jwk ]), "extras" : {f"bad{ _U2028 } key" : "value" }}
729+ with pytest .raises (ValueError , match = "U\\ +2028" ):
730+ sign_ucp_profile (profile , signing_key = signer .private_key , kid = "k" )
731+
732+ def test_accepts_u2027_sanity_case (self ) -> None :
733+ # U+2027 (HYPHENATION POINT) is a different codepoint, not a target of
734+ # the rejection. Confirms we're matching exactly U+2028 / U+2029.
735+ signer = generate_ucp_signing_key (kid = "k" )
736+ profile = {** _base_profile ([signer .public_jwk ]), "extras" : {"note" : "before\u2027 after" }}
737+ signed = sign_ucp_profile (profile , signing_key = signer .private_key , kid = "k" )
738+ assert verify_ucp_profile (signed , build_jwks_response ([signer .public_jwk ])) is True
739+
740+
674741class TestVerifierErrorPrecedence :
675742 def test_null_profile_with_malformed_jwks_returns_no_signature (self ) -> None :
676743 with pytest .raises (UCPVerificationError ) as exc :
0 commit comments