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:
- 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.
- 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
Vulnerability
zeph-a2a's IBCT (Invocation-Bound Capability Token) mechanism — documented acrossspecs/014-a2a/spec.md,specs/010-security/spec.md,crates/zeph-a2a/README.md, and theA2aServerConfigdoc 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. TheIbcttype (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 callsIbct::issueoutside its own tests, and nothing in the server's axum handlers ever callsIbct::verifyor even reads theX-Zeph-IBCTheader.Config fields
A2aServerConfig::ibct_keys,ibct_signing_key_vault_ref, andibct_ttl_secsexist, are parsed, redacted correctly inDebug/Serialize, and are documented with a hard requirement:A2aServerConfig::ibct_keysdoc comment states "When non-empty, all A2A task requests must include a validX-Zeph-IBCTheader signed with one of these keys." This is false — the fields are read nowhere outsidezeph-configitself.Severity
High (P1) — this is a documented, spec'd, and configurable authorization control (analogous to
## 6. Authentication and Authorization → Missing authorization checksin the security-audit checklist) that silently provides zero enforcement. An operator who setsibct_keys/ibct_signing_key_vault_refbelieving that per-task_id/endpointcapability scoping is enforced on top of the shared bearer token gets no such protection. Not Critical because the coarse bearer-token/require_authgate (which is correctly enforced — seecrates/zeph-a2a/src/server/router.rsauth_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 invokemessage/send,tasks/get,tasks/cancelfor anytask_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
crates/zeph-a2a/src/server/handlers.rs(jsonrpc_handler,stream_handler) — noX-Zeph-IBCTheader extraction, noIbct::verifycall anywhere incrates/zeph-a2a/src/server/(confirmed viagrep -rln ibct crates/zeph-a2a/src/server/→ zero matches).src/daemon.rs:75-89buildsA2aServervia.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_secsare never referenced.A2aServer(crates/zeph-a2a/src/server/mod.rs) has nowith_ibct_keys-style builder method at all.crates/zeph-a2a/src/client.rsrpc_call(line ~398) andstream_message(line ~232) only callreq.bearer_auth(t)— noIbct::issuecall, noX-Zeph-IBCTheader ever attached, contradicting the crate README's claim ("When theibctfeature is enabled, Zeph signs outbound A2A requests with HMAC-SHA256 capability tokens... sent in theX-Zeph-IBCTrequest header").crates/zeph-config/src/channels.rs:816-819(ibct_keysdoc 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 bearerauth_tokenacross 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 calltasks/get/tasks/cancel/message/sendfor atask_idthat was never delegated to it — e.g., cancel or read another peer's in-flight task, or submitmessage/sendunder an arbitrarycontext_id— because the server performs no IBCT check to confirm the caller was actually issued a capability scoped to thattask_id+endpoint. Therequire_auth/bearer gate passes (same shared token), and the request proceeds exactly as if IBCT were never configured.Remediation
Either:
crates/zeph-a2a/src/server/router.rsthat, whenAppState/A2aServeris configured withibct_keys, extractsX-Zeph-IBCT, decodes it, and callsIbct::verify(&keys, expected_endpoint, expected_task_id)before dispatching tojsonrpc_handler/stream_handler, returning 401/403 on failure or missing header (matching the doc's "must include a valid header" contract); wire a matchingA2aServer::with_ibct_keys(...)builder and threadconfig.a2a.ibct_keys/ibct_signing_key_vault_refthrough insrc/daemon.rs; wireA2aClient::issue-and-attach on the client side (rpc_call/stream_message) when the caller supplies anIbctKey.A2aServerConfig::ibct_keysdoc 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 configuringibct_keys.Option 1 is preferred since the primitive is already fully implemented, tested, and the crate's own docs promise this behavior.
References
IbctKey/IbctKeyConfig— confirms the config plumbing is actively maintained, making this enforcement gap more likely an oversight than an intentional deferral), docs(a2a): IBCT wire-format description diverges between specs/014-a2a and specs/010-security #6197/docs(a2a): reconcile IBCT wire-format drift between specs and implementation #6198 (IBCT wire-format doc drift — prior work assumed IBCT was live)