Skip to content

fix(a2a): enforce IBCT capability tokens on client and server request paths - #6289

Merged
bug-ops merged 1 commit into
mainfrom
fix/6260-a2a-ibct-enforcement
Jul 14, 2026
Merged

fix(a2a): enforce IBCT capability tokens on client and server request paths#6289
bug-ops merged 1 commit into
mainfrom
fix/6260-a2a-ibct-enforcement

Conversation

@bug-ops

@bug-ops bug-ops commented Jul 14, 2026

Copy link
Copy Markdown
Owner

Summary

  • zeph-a2a's IBCT (Invocation-Bound Capability Token) primitive was fully implemented and unit-tested in isolation, but never wired into the live A2A request path (CWE-862 Missing Authorization). Config documented it as an active per-task authorization layer on top of the coarse bearer token; it wasn't.
  • Server: new ibct_middleware layered inside auth_middleware rejects /a2a//a2a/stream with 401 (missing/undecodable header) or 403 (signature/expiry/scope mismatch) whenever A2aServer::with_ibct_keys is configured with a non-empty key set; no-op when unconfigured (matches the existing bearer-auth opt-in pattern). src/daemon.rs resolves [a2a] ibct_keys / ibct_signing_key_vault_ref (vault-resolved, hard-fails startup if the ref is set but unresolvable, per the spec invariant).
  • Client: A2aClient::with_ibct_key issues and attaches a token scoped to the request's task_id and to the target endpoint's origin (ibct_scope_origin strips the path, since /a2a and /a2a/stream are both verified against the one pathless AgentCard::url). The same normalization is applied server-side to card.url so a non-canonical public_url can't silently reintroduce the mismatch.
  • Corrected the four doc surfaces (A2aServerConfig doc comments, crates/zeph-a2a/README.md, specs/014-a2a/spec.md, specs/010-security/spec.md) that overclaimed active enforcement, and added an explicit deployment caveat: no caller bundled in this repository currently attaches X-Zeph-IBCT, so this PR ships the enforcement primitive rather than a fully activated end-to-end control for the delegated-subagent scenario the issue describes.

Review history

Went through a full security chain (reduced: developer → impl-critic → reviewer, audit lead-in skipped since the issue already specified exact locations/remediation):

  • impl-critic first pass found a structural defect (S1: endpoint-scope mismatch meant /a2a and /a2a/stream could never both pass) and that no production caller attaches the header (S2) — both addressed, re-verified as fixed.
  • Code review found one more instance of the same bug class on the server side (M6: card.url wasn't normalized like the client's endpoint) plus missing mandatory testing-playbook/coverage-status entries — both addressed, re-reviewed and approved.

Test plan

  • cargo nextest run -p zeph-a2a --features server,ibct,card-signing — 235/235 passed
  • cargo nextest run -p zeph-a2a (ibct feature off) — 195/195 passed (backward compat)
  • Full workspace check suite (fmt, clippy --profile ci, nextest, rustdoc gate) — all green, confirmed independently by reviewer
  • gitleaks protect --staged — no leaks
  • Regression tests for the endpoint-scope bug class on both client and server sides (pathful_endpoint_scope_is_rejected, origin_scoped_token_accepted_on_both_routes, non_canonical_card_url_still_matches_client_issued_token, ibct_scope_origin_strips_explicit_default_port)
  • .local/testing/playbooks/a2a-ibct-enforcement.md and .local/testing/coverage-status.md updated (main repo)

Closes #6260

@github-actions github-actions Bot added documentation Improvements or additions to documentation rust Rust code changes labels Jul 14, 2026
@bug-ops
bug-ops enabled auto-merge (squash) July 14, 2026 17:56
@github-actions github-actions Bot added bug Something isn't working size/XL Extra large PR (500+ lines) labels Jul 14, 2026
@bug-ops
bug-ops disabled auto-merge July 14, 2026 17:57
@bug-ops
bug-ops enabled auto-merge (squash) July 14, 2026 17:57
… paths

The IBCT (Invocation-Bound Capability Token) primitive was fully
implemented, HMAC-signed, and unit-tested in isolation, but never
wired into the live A2A request path. Server config documented it as
an enforced authorization layer on top of the coarse bearer token,
but nothing verified the X-Zeph-IBCT header and nothing issued it
(CWE-862 Missing Authorization).

Server: new ibct_middleware layered inside auth_middleware rejects
/a2a and /a2a/stream with 401/403 when A2aServer::with_ibct_keys is
configured; no-op when unconfigured. Daemon wires config.a2a.ibct_keys
and ibct_signing_key_vault_ref (vault-resolved) into the builder.

Client: A2aClient::with_ibct_key issues and attaches a token scoped
to the request's task_id and to the target endpoint's origin
(ibct_scope_origin strips the path, since /a2a and /a2a/stream are
verified against the one pathless AgentCard::url). The same
normalization is applied server-side to card.url so a non-canonical
public_url can't silently reintroduce the mismatch.

No bundled caller currently attaches the header, so this ships the
enforcement primitive rather than a fully activated end-to-end
control; corrected the four doc surfaces that overclaimed otherwise.

Closes #6260
@bug-ops
bug-ops force-pushed the fix/6260-a2a-ibct-enforcement branch from a123279 to 3372e60 Compare July 14, 2026 17:57
@bug-ops
bug-ops merged commit a9301db into main Jul 14, 2026
47 checks passed
@bug-ops
bug-ops deleted the fix/6260-a2a-ibct-enforcement branch July 14, 2026 18:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working documentation Improvements or additions to documentation rust Rust code changes size/XL Extra large PR (500+ lines)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant