Skip to content

prototype(verifier_token,execution_appraisal): enforce canonical 32-byte nonces, validate RelyingParty.challenge/verify_proof inputs, and reject non-finite JWT JSON (#629) - #634

Merged
lywinged merged 1 commit into
agentrust-io:mainfrom
drk-ram:drk-ram-fix-verifier-token-appraisal-629
Oct 30, 2026

Conversation

@drk-ram

@drk-ram drk-ram commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Hardens prototype/cmcp_gate.py, prototype/verifier_token.py, and prototype/execution_appraisal.py against 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

  1. CMCPTraceGate._prove (prototype/cmcp_gate.py):
    • Canonical 32-byte base64url nonce validation via self._decode(nonce) before consume_challenge(nonce) -> tests/test_629_independent_rejections.py::test_gate_refuses_nonce_alias_before_store_consumption
  2. RelyingParty.challenge and verify_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_inputs
    • Non-empty string session_id and sha256: action_digest in RelyingParty.challenge -> tests/test_verifier_token_api_contract.py::test_relying_party_challenge_and_verify_proof_reject_boolean_or_invalid_inputs
    • type(issued_at) is not int, type(expires_at) is not int, and 0 <= issued_at < expires_at <= 9007199254740991 in RelyingParty.challenge and verify_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_inputs
  3. authenticate_token and appraise_launch (prototype/execution_appraisal.py):
    • parse_constant=_no_nonfinite on 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_json
    • Safe-integer bounds (0 <= ... <= 9007199254740991) on now (clock_or_policy) and signed iat, 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_independently

Changelog entry

Validation

python -m pytest tests/test_629_independent_rejections.py tests/test_verifier_token_api_contract.py tests/test_verifier_token_profile.py tests/test_execution_appraisal.py -q
172 passed
python -m ruff check prototype/cmcp_gate.py prototype/execution_appraisal.py prototype/verifier_token.py tests/test_629_independent_rejections.py tests/test_verifier_token_api_contract.py
All checks passed!

Checklist

  • git commit -s (DCO sign-off on every commit)
  • Normative change: conformance test added
  • Breaking change: migration path documented

@drk-ram
drk-ram requested review from a team, lywinged and rajnisht7 as code owners October 25, 2026 05:29
@github-actions

github-actions Bot commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

🟡 Contributor Check: MEDIUM

Check Result
Profile MEDIUM
Credential LOW
Overall MEDIUM

Automated check by AgenTrust Contributor Check.

@github-actions github-actions Bot added the needs-review:MEDIUM Contributor check flagged MEDIUM risk label Oct 25, 2026

@dinakarjs dinakarjs left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

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 rajnisht7 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 lywinged left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@drk-ram
drk-ram force-pushed the drk-ram-fix-verifier-token-appraisal-629 branch from 24c3749 to 21f66f4 Compare October 29, 2026 15:17
@drk-ram

drk-ram commented Oct 29, 2026

Copy link
Copy Markdown
Contributor Author

Thank you @dinakarjs, @rajnisht7, and @lywinged! Addressed all points in 21f66f4 (rebased onto origin/main at e9ab302):

  1. Adopted the 7 independent rejection controls (tests/test_629_independent_rejections.py) + 4 RelyingParty.challenge input controls (test_challenge_refuses_each_new_input):
    • test_authenticated_claims_body_rejects_nonfinite_json (signed JWTs with NaN, Infinity, -Infinity in claims body)
    • test_signed_claim_time_bounds_are_reached_independently (signed JWTs with iat=-1, nbf=-1, exp=2**53 reaching time_bounds)
    • test_gate_refuses_nonce_alias_before_store_consumption ("A"*42 + "B" rejected before consume_challenge, with "A"*43 positive control)
    • test_challenge_refuses_each_new_input (nonce=123, issued_at=False, expires_at=True, expires_at=2**53 + 5)
  2. Cleaned up _prove in prototype/cmcp_gate.py and updated the PR description:
    • Removed the redundant if len(self._decode(nonce)) != 32: branch after len(nonce) == 43 and listed the rejection-to-test mapping in the PR description.

@dinakarjs dinakarjs left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 lywinged left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 two LC_ALL=C PYTHONUTF8=0 runs, the PYTHONOPTIMIZE=2 command and the two --help calls 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.py reports all 14 new test cases failing on behaviour at the merge base.
  • On main, challenge(nonce="A"*42+"B") and challenge(issued_at=False, expires_at=True) register, and verify_proof accepts now=True for a proof and challenge of 1 to 2; this head refuses all three with a ProfileError.

Things I would not hold the merge for:

  • verify_proof now refuses a proof the JavaScript verifier accepts, and no vector shows it. A copy of VT-CNF-001 whose nonce ends in 9 rather than 8, the same 32 bytes under a lax decoder, re-signed, is valid on main and in tools/independent-verifier, and verification_inputs_invalid here. codes.json lists that code for the token stage only, with a meaning that names verify_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 != 32 comparisons this change adds, in Proof, verify_proof and challenge, 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_proof accepts a challenge interval of 100 seconds that challenge refuses, on main too, bounding its timestamps by the token's exp and the safe-integer range; it checks the three separately, and challenge_issued_at above challenge_expires_at surfaces as holder_proof_expired. The description's 0 <= issued_at < expires_at <= 9007199254740991 fits neither exactly: challenge also caps the interval at 30 seconds, and verify_proof never 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 1e400 still comes back from authenticate_token as inf, in exp as well; Infinity is 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

@lywinged
lywinged merged commit 863c6f3 into agentrust-io:main Oct 30, 2026
12 of 14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-review:MEDIUM Contributor check flagged MEDIUM risk

Projects

None yet

4 participants