Skip to content

intent_bridge: verify_bridge misreports malformed or mismatched trusted_authorizer_jwk as an invalid signature, and sign_bridge accepts non-authorization inputs #507

Closed

Description

@drk-ram

What is wrong

Checked on main at commit c08974c3c0b33d9bd77bc1778b238a86f770b788 (agentrust-trace 0.11.0):

  1. verify_bridge (and verify_successor_artifact) wraps _pubkey_from_jwk inside the signature-verification try ... except Exception: block, and verify_bridge checks authorizer_key_id only after pub.verify(signature, body).
    In src/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 against trusted_authorizer_jwk for key-a (kid="key-a"), pub.verify(signature, body) runs first with key-a's public key and raises IntentBridgeError("authorization signature is invalid"). Line 374 ("authorizer_key_id does not identify the trusted key") is only reachable when the artifact was signed by key-a while naming another authorizer_key_id, opposite to verify_successor_artifact (lines 182-185), which checks observer_key_id before signature verification.
    • verify_bridge does not check isinstance(trusted_authorizer_jwk, dict) before calling _pubkey_from_jwk(trusted_authorizer_jwk), and both verify_bridge (line 369) and verify_successor_artifact (line 210) call _pubkey_from_jwk(...) inside try ... except Exception: raise IntentBridgeError("... signature is invalid"). Any malformed caller JWK ("not-a-dict", {"kty": "RSA", "kid": ...}, missing "x", or non-canonical "x") raises AttributeError or ValueError inside _pubkey_from_jwk and is misreported as "authorization signature is invalid" or "successor signature is invalid" when no signature check was ever run.
  2. sign_bridge accepts arbitrary non-object and malformed authorization inputs that verify_bridge unconditionally rejects.
    tools/sweep_public_surface.py shows 'a-string', 123, None, [1, 2], True, False, 0, {}, '', {'a': 1}, [{}], 1.5 in the ACCEPTED column for intent_bridge.sign_bridge <first positional>. Unlike provenance.build_record and content_marking.build_assertion, intent_bridge.sign_bridge is not listed in PRODUCERS in tests/test_public_functions_raise_what_they_document.py:478-488 and does not validate authorization before signing {"profile": BRIDGE_PROFILE, "authorization": authorization}, so it emits signed bridge artifacts over "a-string", None, 1.5, or {} that verify_bridge always 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.md
  • tests/test_intent_bridge.py
  • tests/test_public_functions_raise_what_they_document.py

Expected behavior

  • In verify_bridge and verify_successor_artifact, validate the caller's JWK (isinstance(..., dict), kid match, and _pubkey_from_jwk(...) wrapped in IntentBridgeError with its own key error message) before calling pub.verify(signature, body), so that a kid mismatch reports "authorizer_key_id does not identify the trusted key" and a malformed JWK is not misreported as an invalid signature.
  • In sign_bridge, require authorization to 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)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions