Repository navigation
Description
What is wrong
Checked on main at commit c08974c3c0b33d9bd77bc1778b238a86f770b788 (agentrust-trace 0.11.0):
-
verify_bridge(andverify_successor_artifact) wraps_pubkey_from_jwkinside the signature-verificationtry ... except Exception:block, andverify_bridgechecksauthorizer_key_idonly afterpub.verify(signature, body).
Insrc/agentrust_trace/intent_bridge.py:362-375:body = _jcs({"profile": BRIDGE_PROFILE, "authorization": authorization}, "the authorization") try: _pubkey_from_jwk(trusted_authorizer_jwk).verify(signature, body) except Exception as exc: raise IntentBridgeError("authorization signature is invalid") from exc trusted_kid = trusted_authorizer_jwk.get("kid") if not isinstance(trusted_kid, str) or trusted_kid != authorization["authorizer_key_id"]: raise IntentBridgeError("authorizer_key_id does not identify the trusted key")
- When an authorization signed by
key-b(authorizer_key_id="key-b") is verified againsttrusted_authorizer_jwkforkey-a(kid="key-a"),pub.verify(signature, body)runs first withkey-a's public key and raisesIntentBridgeError("authorization signature is invalid"). Line 374 ("authorizer_key_id does not identify the trusted key") is only reachable when the artifact was signed bykey-awhile naming anotherauthorizer_key_id, opposite toverify_successor_artifact(lines 182-185), which checksobserver_key_idbefore signature verification. verify_bridgedoes not checkisinstance(trusted_authorizer_jwk, dict)before calling_pubkey_from_jwk(trusted_authorizer_jwk), and bothverify_bridge(line 369) andverify_successor_artifact(line 210) call_pubkey_from_jwk(...)insidetry ... except Exception: raise IntentBridgeError("... signature is invalid"). Any malformed caller JWK ("not-a-dict",{"kty": "RSA", "kid": ...}, missing"x", or non-canonical"x") raisesAttributeErrororValueErrorinside_pubkey_from_jwkand is misreported as"authorization signature is invalid"or"successor signature is invalid"when no signature check was ever run.
- When an authorization signed by
-
sign_bridgeaccepts arbitrary non-object and malformedauthorizationinputs thatverify_bridgeunconditionally rejects.
tools/sweep_public_surface.pyshows'a-string', 123, None, [1, 2], True, False, 0, {}, '', {'a': 1}, [{}], 1.5in theACCEPTEDcolumn forintent_bridge.sign_bridge <first positional>. Unlikeprovenance.build_recordandcontent_marking.build_assertion,intent_bridge.sign_bridgeis not listed inPRODUCERSintests/test_public_functions_raise_what_they_document.py:478-488and does not validateauthorizationbefore signing{"profile": BRIDGE_PROFILE, "authorization": authorization}, so it emits signed bridge artifacts over"a-string",None,1.5, or{}thatverify_bridgealways rejects.
Reproduction at c08974c3c0b33d9bd77bc1778b238a86f770b788:
from agentrust_trace.intent_bridge import (
IntentBridgeError,
digest_jcs,
sign_bridge,
sign_successor_artifact,
verify_bridge,
verify_successor_artifact,
)
from agentrust_trace.sign import generate_key, key_to_jwk
key_a = generate_key()
key_b = generate_key()
decl = {"impact": "external-side-effect"}
call = {"name": "send_invoice", "arguments": {"id": 1}}
auth_b = {
"authorization_id": "auth-1",
"decision": "allow",
"authorizer": "policy",
"authorizer_key_id": "key-b",
"authorized_at": 100,
"expires_at": 200,
"scope": {"tools": ["send_invoice"], "impacts": ["external-side-effect"]},
"pic": {
"profile": "PIC-CJSON/1.0",
"intent_digest": "sha256:" + "1" * 64,
"args_digest": "sha256:" + "2" * 64,
},
"declaration_digest": digest_jcs(decl),
"tool_call_digest": digest_jcs(call),
"transcript_required": False,
}
bridge_b = sign_bridge(auth_b, key_b)
try:
verify_bridge(
bridge_b,
{**key_to_jwk(key_a), "kid": "key-a"},
declaration=decl,
pic_intent_digest=auth_b["pic"]["intent_digest"],
pic_args_digest=auth_b["pic"]["args_digest"],
tool_call=call,
now=150,
)
except IntentBridgeError as exc:
print("wrong trusted key (kid='key-a' vs authorizer_key_id='key-b'):", exc)
for bad_jwk in ["not-a-dict", {"kty": "RSA", "kid": "key-b"}]:
try:
verify_bridge(
bridge_b,
bad_jwk,
declaration=decl,
pic_intent_digest=auth_b["pic"]["intent_digest"],
pic_args_digest=auth_b["pic"]["args_digest"],
tool_call=call,
now=150,
)
except IntentBridgeError as exc:
print(f"malformed trusted_authorizer_jwk={bad_jwk!r}:", exc)
after = {"observation": {"ok": True}, "observer": "obs-1", "observed_at": 150}
succ = sign_successor_artifact("auth-1", after, "obs-key-1", key_a)
try:
verify_successor_artifact(
succ,
{"kty": "RSA", "kid": "obs-key-1"},
trusted_observer="obs-1",
authorization_id="auth-1",
after=after,
)
except IntentBridgeError as exc:
print("malformed trusted_observer_jwk={'kty': 'RSA', 'kid': 'obs-key-1'}:", exc)
for bad_auth in ["a-string", None, 1.5, {}]:
signed = sign_bridge(bad_auth, key_a)
print(f"sign_bridge({bad_auth!r}) succeeded -> keys={sorted(signed)}")Actual output:
wrong trusted key (kid='key-a' vs authorizer_key_id='key-b'): authorization signature is invalid
malformed trusted_authorizer_jwk='not-a-dict': authorization signature is invalid
malformed trusted_authorizer_jwk={'kty': 'RSA', 'kid': 'key-b'}: authorization signature is invalid
malformed trusted_observer_jwk={'kty': 'RSA', 'kid': 'obs-key-1'}: successor signature is invalid
sign_bridge('a-string') succeeded -> keys=['authorization', 'profile', 'signature']
sign_bridge(None) succeeded -> keys=['authorization', 'profile', 'signature']
sign_bridge(1.5) succeeded -> keys=['authorization', 'profile', 'signature']
sign_bridge({}) succeeded -> keys=['authorization', 'profile', 'signature']
Spec section or file
src/agentrust_trace/intent_bridge.py(sign_bridge,verify_bridge,verify_successor_artifact)docs/integration/pic-trace-bridge-v1.mdtests/test_intent_bridge.pytests/test_public_functions_raise_what_they_document.py
Expected behavior
- In
verify_bridgeandverify_successor_artifact, validate the caller's JWK (isinstance(..., dict),kidmatch, and_pubkey_from_jwk(...)wrapped inIntentBridgeErrorwith its own key error message) before callingpub.verify(signature, body), so that akidmismatch reports"authorizer_key_id does not identify the trusted key"and a malformed JWK is not misreported as an invalid signature. - In
sign_bridge, requireauthorizationto be a JSON object (isinstance(authorization, dict)) and validate its required bridge fields before signing so the producer does not emit artifacts its own verifier refuses.
Impact
Callers of verify_bridge and verify_successor_artifact receive "authorization signature is invalid" / "successor signature is invalid" when the caller-supplied JWK is malformed or when authorizer_key_id does not match trusted_authorizer_jwk["kid"], and sign_bridge produces signed artifacts over non-objects or incomplete dicts that cannot verify.
Contributor: Raghuvar Durga (@drk-ram)
Activity
fix(intent_bridge): validate JWK and kid before signature verificatio…