Repository navigation
Description
What is wrong
At main (a8e3559), src/agentrust_trace/intent_bridge.py and src/agentrust_trace/successor_observation.py have three verification-ordering and input-isolation gaps:
-
verify_bridgeandverify_successor_artifactcheckkidequality before validatingtrusted_authorizer_jwk/trusted_observer_jwkwith_pubkey_from_jwk:- In
src/agentrust_trace/intent_bridge.py:173-198(verify_successor_artifact) andlines 364-371(verify_bridge), the verifier checkstrusted_kid != observer_key_id/authorizer_key_id(and inverify_successor_artifact, also checksobserver != expected_observer,successor_observation_digest, andsignaturebase64url) BEFORE calling_pubkey_from_jwk(trusted_*_jwk). - When a caller passes an unusable
trusted_authorizer_jwkortrusted_observer_jwk(for example, carrying private key material"d", wrongkty="RSA", missing"x", or non-canonical base64url"x") whose"kid"happens not to match,verify_bridgeandverify_successor_artifactblame the artifact ("authorizer_key_id does not identify the trusted key") instead of rejecting the unusabletrusted_*_jwkfirst.
- In
-
verify_bridgevalidates caller arguments (now,declaration,tool_call,pic_intent_digest,pic_args_digest) only after verifying the Ed25519 signature:- In
verify_bridge(lines 393-431), caller-supplied options (now=-1,now=True,declaration="not-a-dict",tool_call="not-a-dict",pic_intent_digest="bad",pic_args_digest="bad") are checked only after Ed25519 signature verification and afterAuthorizationDenied(decision == "deny"). - If a caller passes
now=-1ortool_call=123with a bridge whose signature is invalid or whose decision is"deny",verify_bridgeraises"authorization signature is invalid"orAuthorizationDeniedinstead of rejecting the caller's malformed input at entry (the same defect sign: validate caller options at verify_record entry, reject negative now, and validate trusted_bundle_keys when revocation_bundle is None #556 / sign: validate caller options at verify_record entry (#556) #562 closed insign.verify_record).
- In
-
evaluate_successor_observationpassesbound["observation"]directly topredicatewithoutcopy.deepcopy:- In
src/agentrust_trace/successor_observation.py:126,result = predicate(observation)passes the caller's innerafter["observation"]dict by reference (since_bind_successor_observationreturns{**after, "observed_at": observed_at}, a shallow copy), allowingpredicateto mutateafter["observation"]in place (unlikeplatform_measurement.py:139, which passescopy.deepcopy(runtime)).
- In
Reproduction on a8e3559:
from agentrust_trace import intent_bridge, sign
key = sign.generate_key()
bridge = intent_bridge.sign_bridge({
"authorization_id": "a1", "decision": "allow", "authorizer": "auth",
"authorizer_key_id": "k1", "authorized_at": 100, "expires_at": 200,
"scope": {"tools": ["t"], "impacts": ["i"]},
"pic": {"profile": "PIC-CJSON/1.0", "intent_digest": "sha256:" + "1" * 64, "args_digest": "sha256:" + "2" * 64},
"declaration_digest": "sha256:" + "3" * 64, "tool_call_digest": "sha256:" + "4" * 64,
"transcript_required": False,
}, key)
# 1. Private key 'd' in trusted_authorizer_jwk is ignored and reported as kid mismatch:
intent_bridge.verify_bridge(
bridge, {"kty": "OKP", "crv": "Ed25519", "x": sign.key_to_jwk(key)["x"], "d": "secret", "kid": "wrong"},
declaration={"impact": "i"}, pic_intent_digest="sha256:" + "1" * 64,
pic_args_digest="sha256:" + "2" * 64, tool_call={"name": "t"}, now=150,
)Spec section or file
src/agentrust_trace/intent_bridge.py(verify_bridge,verify_successor_artifact)src/agentrust_trace/successor_observation.py(evaluate_successor_observation)
Expected behavior
- Validate
trusted_authorizer_jwkandtrusted_observer_jwkwith_pubkey_from_jwk(and checkisinstance(trusted_kid, str) and bool(trusted_kid)) at entry before comparingkidor inspecting the untrusted artifact's signature. - Validate caller-supplied arguments (
now,declaration,tool_call,pic_intent_digest,pic_args_digest,trusted_observer,authorization_id) at entry inverify_bridgeandverify_successor_artifactbefore inspecting the bridge/successor artifact or verifying signatures. - Pass
copy.deepcopy(observation)topredicateinevaluate_successor_observationso a caller predicate cannot mutateafter["observation"].
Impact
Malformed caller options or private-key-carrying trusted_*_jwk dicts are misreported as artifact kid mismatches or signature failures in intent_bridge, and evaluate_successor_observation exposes after["observation"] to in-place mutation by predicate.
Activity
github-actions commentedon Oct 23, 2026
🟡 Contributor Check: MEDIUM
Automated check by AgenTrust Contributor Check.
fix(intent_bridge,successor_observation): validate caller options and…
fix(intent_bridge,successor_observation): validate caller options and…