Skip to content

P1: IBCT (X-Zeph-IBCT) capability tokens are fully specified/configured but never enforced on either A2A client or server request path #6260

Description

@bug-ops

Vulnerability

zeph-a2a's IBCT (Invocation-Bound Capability Token) mechanism — documented across specs/014-a2a/spec.md, specs/010-security/spec.md, crates/zeph-a2a/README.md, and the A2aServerConfig doc comments as an active, enforced per-task/per-endpoint authorization layer on top of the coarse bearer token — is never wired into either the client request path or the server request path. The Ibct type (crates/zeph-a2a/src/ibct.rs) is fully implemented, HMAC-SHA256-signed, constant-time-verified, and unit-tested in isolation, but nothing in the crate ever calls Ibct::issue outside its own tests, and nothing in the server's axum handlers ever calls Ibct::verify or even reads the X-Zeph-IBCT header.

Config fields A2aServerConfig::ibct_keys, ibct_signing_key_vault_ref, and ibct_ttl_secs exist, are parsed, redacted correctly in Debug/Serialize, and are documented with a hard requirement: A2aServerConfig::ibct_keys doc comment states "When non-empty, all A2A task requests must include a valid X-Zeph-IBCT header signed with one of these keys." This is false — the fields are read nowhere outside zeph-config itself.

Severity

High (P1) — this is a documented, spec'd, and configurable authorization control (analogous to ## 6. Authentication and Authorization → Missing authorization checks in the security-audit checklist) that silently provides zero enforcement. An operator who sets ibct_keys/ibct_signing_key_vault_ref believing that per-task_id/endpoint capability scoping is enforced on top of the shared bearer token gets no such protection. Not Critical because the coarse bearer-token/require_auth gate (which is correctly enforced — see crates/zeph-a2a/src/server/router.rs auth_middleware) still applies; but any caller holding a valid bearer token (e.g., a compromised delegated subagent sharing one token across many peer tasks) can invoke message/send, tasks/get, tasks/cancel for any task_id/endpoint with no IBCT check whatsoever, which is exactly the escalation IBCT exists to prevent per its own spec text (specs/014-a2a/spec.md §"IBCT: Invocation-Bound Capability Tokens": "This scopes the token to a specific task and endpoint...").

Location

  • Server: crates/zeph-a2a/src/server/handlers.rs (jsonrpc_handler, stream_handler) — no X-Zeph-IBCT header extraction, no Ibct::verify call anywhere in crates/zeph-a2a/src/server/ (confirmed via grep -rln ibct crates/zeph-a2a/src/server/ → zero matches).
  • Server construction: src/daemon.rs:75-89 builds A2aServer via .with_auth(...), .with_require_auth(...), .with_rate_limit(...), .with_max_body_size(...), .with_request_timeout(...), .with_task_ttl(...)config.a2a.ibct_keys / ibct_signing_key_vault_ref / ibct_ttl_secs are never referenced. A2aServer (crates/zeph-a2a/src/server/mod.rs) has no with_ibct_keys-style builder method at all.
  • Client: crates/zeph-a2a/src/client.rs rpc_call (line ~398) and stream_message (line ~232) only call req.bearer_auth(t) — no Ibct::issue call, no X-Zeph-IBCT header ever attached, contradicting the crate README's claim ("When the ibct feature is enabled, Zeph signs outbound A2A requests with HMAC-SHA256 capability tokens... sent in the X-Zeph-IBCT request header").
  • Docs asserting enforcement that doesn't exist: crates/zeph-config/src/channels.rs:816-819 (ibct_keys doc comment), crates/zeph-a2a/README.md:24-30, specs/014-a2a/spec.md:231-269, specs/010-security/spec.md:356-387.

Attack Scenario

Deployment: operator configures [a2a] ibct_signing_key_vault_ref = "ZEPH_A2A_IBCT_KEY" intending per-task capability scoping, and shares one long-lived bearer auth_token across several delegated subagents/peers as the transport-level credential (a documented, supported pattern — bearer auth is coarse-grained by design, IBCT is meant to add the fine-grained layer). One delegated peer (or an attacker who intercepts/exfiltrates the shared bearer token from any single leg of that fan-out) can call tasks/get/tasks/cancel/message/send for a task_id that was never delegated to it — e.g., cancel or read another peer's in-flight task, or submit message/send under an arbitrary context_id — because the server performs no IBCT check to confirm the caller was actually issued a capability scoped to that task_id+endpoint. The require_auth/bearer gate passes (same shared token), and the request proceeds exactly as if IBCT were never configured.

Remediation

Either:

  1. Wire IBCT enforcement end-to-end: add an axum middleware/extractor in crates/zeph-a2a/src/server/router.rs that, when AppState/A2aServer is configured with ibct_keys, extracts X-Zeph-IBCT, decodes it, and calls Ibct::verify(&keys, expected_endpoint, expected_task_id) before dispatching to jsonrpc_handler/stream_handler, returning 401/403 on failure or missing header (matching the doc's "must include a valid header" contract); wire a matching A2aServer::with_ibct_keys(...) builder and thread config.a2a.ibct_keys/ibct_signing_key_vault_ref through in src/daemon.rs; wire A2aClient::issue-and-attach on the client side (rpc_call/stream_message) when the caller supplies an IbctKey.
  2. Or, if this remains intentionally deferred, immediately correct all four documentation surfaces (A2aServerConfig::ibct_keys doc comment, crates/zeph-a2a/README.md, specs/014-a2a/spec.md, specs/010-security/spec.md) to state plainly that IBCT issuance/verification is implemented as a standalone, unit-tested primitive but not wired into the live A2A client/server request path, and downgrade/remove the "must include a valid header" enforcement claim so operators don't get a false sense of security from configuring ibct_keys.

Option 1 is preferred since the primitive is already fully implemented, tested, and the crate's own docs promise this behavior.

References

Metadata

Metadata

Assignees

Labels

P1High ROI, low complexity — do next sprinta2aA2A protocol integrationbugSomething isn't workingsecuritySecurity-related issue

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions