Repository navigation
prototype(verifier_token,execution_appraisal): enforce canonical 32-byte nonces, validate RelyingParty.challenge/verify_proof inputs, and reject non-finite JWT JSON (#629) - #634
Conversation
|
🟡 Contributor Check: MEDIUM
Automated check by AgenTrust Contributor Check. |
dinakarjs
left a comment
There was a problem hiding this comment.
Initial review at exact head 24c3749: please complete the regression evidence required by CONTRIBUTING.md, 'Before a pull request is reviewed' (https://github.com/agentrust-io/trace-spec/blob/24c37491f7c48c296e9323004fd8fa595f0fc0a5/CONTRIBUTING.md).
The three supplied test functions do not independently exercise every new rejection. In particular, no added test calls CMCPTraceGate._prove with a same-byte noncanonical nonce; the JSON test covers a nonfinite JWT header but never a correctly signed JWT with a nonfinite claims body; and appraise_launch is tested only with unsafe now, rather than signed iat/nbf/exp values reaching the new time_bounds guard. Removing those specific checks leaves the supplied added cases unexercised at those boundaries.
Please add valid controls and a separate behavioral failing-without-check test for each added rejection, preserving new names/imports, and list the correspondence in the description. Include an authenticated claims-body control so a signature failure cannot mask the JSON rejection. I have not performed a full implementation approval while that required evidence is missing.
I inspected CI job 121086198621: lint, types and the ordinary test step pass; the portability step fails in test_integer_by_value.py::test_the_javascript_side_agrees with ASCII UnicodeDecodeError. That is the already isolated #624 repair, not a request to disable portability or write another workaround here. Reviewed with OpenAI Codex.
|
I prepared the supplemental rejection tests from my review as an adoption companion: dinakarjs#5 (commit 9966399, based exactly on 24c3749). Seven new cases independently reach authenticated non-finite claim JSON, signed iat/nbf/exp bounds, and early gate nonce refusal before store consumption, with positive controls. Focused 7 pass; related suite 161 pass; restored full suite 4796 pass / 48 skip. Removing the corresponding guards causes 3 / 3 / 1 new failures. Ruff and whitespace pass. The nonce case pins the early validation/error/store-call contract; the decoded-length subcheck is redundant after canonical 43-character validation and is not separately mutation-covered. No production source change or new bypass claim. Please adopt if useful and retain independent review; my changes-requested review remains pending adoption/review. |
rajnisht7
left a comment
There was a problem hiding this comment.
the overall changes looks good, but lacks tests, as per CONTRIBUTING, each newly added rejection needs its own test that fails when that specific guard is removed. Current tests don’t fully cover this:
- CMCPTraceGate._prove is never exercised with a same-byte noncanonical nonce ("A"*42 + "B"). We need a test that shows it is rejected before consume_challenge is called, plus a positive control with a canonical nonce.
- The non-finite JWT test only puts NaN in the header. We need a correctly signed JWT whose claims body contains NaN / Infinity / -Infinity so the claims-side parse_constant=_no_nonfinite is actually reached after signature verification.
- The time-bounds test only passes now=2**53 (hits clock_or_policy). We need independently signed cases for unsafe iat, nbf, and exp that reach the new time_bounds guard.
Smaller consistency points:
- RelyingParty.challenge enforces the ≤30s interval, but verify_proof does not. A direct caller can pass a much wider interval. Either enforce the same invariant in verify_proof or document that it intentionally trusts the challenge-store provenance.
- PR description says verify_proof validates session_id / action_digest. It currently only validates the timestamps + nonce; the others are only checked later via binding comparison. Either add the direct validation or tighten the description.
- The try/except around the new length check in _prove is a bit awkward since _decode already raises ProfileError
lywinged
left a comment
There was a problem hiding this comment.
Request changes. Reviewed at 24c3749 on da69b31.
Before a full review, what CONTRIBUTING's "Before a pull request is reviewed" asks for: a test for each rejection the change adds, failing when that rejection is switched off, and the description naming it. Seven reachable rejections have none; removing any one of them leaves the whole suite passing. Three are the ones dinakarjs listed: the claims parse_constant, the signed iat/nbf/exp bound and the gate's nonce decode. His companion, dinakarjs#5, describes tests for those three. Four more are in RelyingParty.challenge: isinstance(nonce, str), type(issued_at) is not int, type(expires_at) is not int and the 9007199254740991 cap. The new test's (False, True) is refused by either type clause, and its (g.NOW, True) and (g.NOW, 2**53) are refused on main too. This passes here and fails once for each of the four removed:
@pytest.mark.parametrize("change,code", [
({"nonce": 123}, "nonce_shape"),
({"issued_at": False, "expires_at": 1}, "challenge_interval"),
({"issued_at": 0, "expires_at": True}, "challenge_interval"),
({"issued_at": 2**53 - 10, "expires_at": 2**53 + 5}, "challenge_interval"),
])
def test_challenge_refuses_each_new_input(fixture, change, code):
body, _ = proof(fixture)
kwargs = {"nonce": "A" * 43, "session_id": "session-1", "action_digest": body["action_digest"],
"issued_at": g.NOW, "expires_at": g.NOW + 30}
v.RelyingParty(context(fixture)).challenge(**kwargs)
with pytest.raises(v.ProfileError, match=f"^{code}$"):
v.RelyingParty(context(fixture)).challenge(**{**kwargs, **change})I will review the rest once these are in.
CI at 24c3749 failed in the portability step for a reason this pull request did not cause: main failed the same step at da69b31, before #624, and passes at dcca4e0.
Generated by Claude Code
…es, non-finite JSON, and safe-int bounds (agentrust-io#629) Enforce canonical 32-byte base64url nonces in CMCPTraceGate._prove before consume_challenge and in RelyingParty.challenge / verify_proof, validate session_id, action_digest, and exact integer challenge timestamps within 0..9007199254740991, reject non-finite JSON constants in JWT headers and claims in prototype/execution_appraisal.py, and enforce safe-integer bounds on now, max_age, iat, nbf, and exp. Closes agentrust-io#629 Signed-off-by: Raghuvar Karthik Durga <177662415+drk-ram@users.noreply.github.com>
24c3749 to
21f66f4
Compare
|
Thank you @dinakarjs, @rajnisht7, and @lywinged! Addressed all points in
|
dinakarjs
left a comment
There was a problem hiding this comment.
Follow-up review at exact head 21f66f4, isolated checkout with hash-locked development dependencies, Linux/Python 3.12.14.
Verified incorporation of the signed-claims, time-bound and nonce-before-consumption controls from our companion, plus the four isolated RelyingParty.challenge input controls identified in lywinged's review. The redundant gate length branch is removed and the PR names the rejection-to-test mapping. Full local pytest: 5,234 passed, 98 skipped; Ruff, the dash check and mypy pass. The returned current-head CI, conformance and CodeQL workflows succeed. My earlier missing-test follow-up is addressed. Because this head includes tests I contributed, this is adoption verification, not independent approval; the other reviewers should assess the combined head.
Reviewed with OpenAI Codex.
lywinged
left a comment
There was a problem hiding this comment.
Approve. Reviewed at 21f66f4 on e9ab302.
What the first round asked for is in. Each of the seven rejections that had no test is pinned now: switching off parse_constant on the claims body or the signed iat/nbf/exp bound fails three cases, and switching off the nonce decode in CMCPTraceGate._prove, isinstance(nonce, str), either type(...) is not int check or the safe-integer cap in RelyingParty.challenge fails one each. The description lists the rejection to test mapping, and the first-round head's != 32 branch in the gate is gone.
What I checked rather than read:
- CI's commands, run here on 3.11 and 3.12: ruff and mypy as CI runs them, which do not reach
prototype/, and the dash check clean; the suite at 5234 passed and 98 skipped on both; the replay experiment, the twoLC_ALL=C PYTHONUTF8=0runs, thePYTHONOPTIMIZE=2command and the two--helpcalls pass. - The independent JavaScript verifier agrees with all 232 vectors, the 14 legacy and the 218 conformance ones, here and on
main. tools/review_bar.pyreports all 14 new test cases failing on behaviour at the merge base.- On
main,challenge(nonce="A"*42+"B")andchallenge(issued_at=False, expires_at=True)register, andverify_proofacceptsnow=Truefor a proof and challenge of 1 to 2; this head refuses all three with aProfileError.
Things I would not hold the merge for:
verify_proofnow refuses a proof the JavaScript verifier accepts, and no vector shows it. A copy ofVT-CNF-001whose nonce ends in9rather than8, the same 32 bytes under a lax decoder, re-signed, isvalidonmainand intools/independent-verifier, andverification_inputs_invalidhere.codes.jsonlists that code for the token stage only, with a meaning that namesverify_token's inputs; the entry wants the proof stage added, and a vector with such a nonce would show the two verifiers apart.- The three
!= 32comparisons this change adds, inProof,verify_proofandchallenge, cannot fire: each follows a 43 character check, and a 43 character string that survives the canonical round trip is 32 bytes. Dropping each comparison and keeping the decode leaves the suite green; deleting the decode with it does not. - rajnisht7's consistency point stands:
verify_proofaccepts a challenge interval of 100 seconds thatchallengerefuses, onmaintoo, bounding its timestamps by the token'sexpand the safe-integer range; it checks the three separately, andchallenge_issued_atabovechallenge_expires_atsurfaces asholder_proof_expired. The description's0 <= issued_at < expires_at <= 9007199254740991fits neither exactly:challengealso caps the interval at 30 seconds, andverify_proofnever requires the first to be below the second. - The pull request's title says "reject non-finite JWT JSON", but a signed claims body carrying
1e400still comes back fromauthenticate_tokenasinf, inexpas well;Infinityis refused. The description's "non-finite JSON constants" is the accurate scope; its changelog bullet says "reject non-finite JSON", as the title does. - The description's validation line counts 172 passed for its four test files; they collect and pass 165 here, on 3.11 and 3.12.
Generated by Claude Code
Summary
Hardens
prototype/cmcp_gate.py,prototype/verifier_token.py, andprototype/execution_appraisal.pyagainst non-canonical base64url nonces, boolean or out-of-range challenge/appraisal timestamps, and non-finite JSON constants in JWT headers and claims.Closes #629
Changes & Rejection-to-Test Mapping
CMCPTraceGate._prove(prototype/cmcp_gate.py):noncevalidation viaself._decode(nonce)beforeconsume_challenge(nonce)->tests/test_629_independent_rejections.py::test_gate_refuses_nonce_alias_before_store_consumptionRelyingParty.challengeandverify_proof(prototype/verifier_token.py):isinstance(nonce, str)and canonical_b64u_decode(nonce)->tests/test_629_independent_rejections.py::test_challenge_refuses_each_new_input,tests/test_verifier_token_api_contract.py::test_relying_party_challenge_and_verify_proof_reject_boolean_or_invalid_inputssession_idandsha256:action_digestinRelyingParty.challenge->tests/test_verifier_token_api_contract.py::test_relying_party_challenge_and_verify_proof_reject_boolean_or_invalid_inputstype(issued_at) is not int,type(expires_at) is not int, and0 <= issued_at < expires_at <= 9007199254740991inRelyingParty.challengeandverify_proof->tests/test_629_independent_rejections.py::test_challenge_refuses_each_new_input,tests/test_verifier_token_api_contract.py::test_relying_party_challenge_and_verify_proof_reject_boolean_or_invalid_inputsauthenticate_tokenandappraise_launch(prototype/execution_appraisal.py):parse_constant=_no_nonfiniteon JWT header and authenticated claims body ->tests/test_verifier_token_api_contract.py::test_execution_appraisal_rejects_nonfinite_jwt_json_and_unsafe_integer_times,tests/test_629_independent_rejections.py::test_authenticated_claims_body_rejects_nonfinite_json0 <= ... <= 9007199254740991) onnow(clock_or_policy) and signediat,nbf,exp(time_bounds) ->tests/test_verifier_token_api_contract.py::test_execution_appraisal_rejects_nonfinite_jwt_json_and_unsafe_integer_times,tests/test_629_independent_rejections.py::test_signed_claim_time_bounds_are_reached_independentlyChangelog entry
prototype/verifier_token.py,prototype/cmcp_gate.py,prototype/execution_appraisal.py: enforce canonical 32-byte base64url nonces before challenge consumption, validate challengesession_id/action_digest/integer interval bounds, reject non-finite JSON in JWT headers and claims, and enforce JCS safe-integer bounds onnow,iat,nbf, andexp(prototype(verifier_token,execution_appraisal): enforce canonical 32-byte nonces, validate RelyingParty.challenge/verify_proof inputs, and reject non-finite JWT JSON #629).Validation
Checklist
git commit -s(DCO sign-off on every commit)