Commit a016eca
feat(signing): v3-identity Tier 1 — BrandJsonJwksResolver + CapabilityCache (port from JS) (#345)
* feat(signing): port BrandJsonJwksResolver + CapabilityCache + capability priming from JS
Tier 1 of the v3-identity bundle (RFC at
docs/proposals/v3-identity-bundle-design.md).
Direct port of three JS-side modules:
* BrandJsonJwksResolver — implements adcp.signing.AsyncJwksResolver.
Walks brand.json/agents[], follows authoritative_location/house
redirects with bare-host validation, picks agent by
(type, id?, brand_id?), falls back to origin-bound
/.well-known/jwks.json (security: prevents cross-origin trust pivot),
honors Cache-Control + ETag, cascade refresh on unknown kid.
10 typed error codes match JS for cross-language conformance.
Composes with AsyncCachingJwksResolver for inner JWK caching —
does NOT reinvent that layer.
* CapabilityCache + build_capability_cache_key — TTL-based per-agent
cache for request_signing capability blocks. Cache-key format
byte-identical to JS so a future shared Redis cache works
cross-language.
* ensure_capability_loaded — async primer with fail-open negative-
cache TTL (60s) on fetch failures, in-flight dedup via
asyncio.Future, MCP/A2A transport unwrapping (structuredContent,
content[].text, result.artifacts[].parts[].data,
result.parts[].data).
This is the JWKS-resolution layer of the v3 trust chain. The
verifier machinery already exists in adcp.signing/ (verify_starlette_request,
AsyncCachingJwksResolver, ReplayStore, etc.); this connects the
brand-side discovery to the existing verifier.
Composes with future Tier 2 (BuyerAgentRegistry per
adcontextprotocol/adcp-client#1269) and Tier 3
(BrandAuthorizationResolver, gated on adcontextprotocol/adcp#3690
for eTLD+1 binding + authorized_operators[]).
Tests: 61 new (34 brand_jwks, 27 capability_cache + priming),
2,975 total pass. ruff + mypy clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(signing): address expert review on PR #345 — SSRF, body cap, URL canon
Six findings from the four expert reviews on Tier 1 JWKS port:
**Critical (security):**
* SSRF on brand.json fetch — brand.json walker now uses
``build_async_ip_pinned_transport`` per hop with ``trust_env=False``,
matching the JWKS fetcher's posture. Without this, an
attacker-controlled ``authoritative_location`` could redirect the
fetch chain to private IPs / cloud-metadata endpoints. The brand.json
walker is the verifier's trust root — its SSRF posture must be at
least as strict as JWKS.
* No response-body size cap — counterparty serving a multi-megabyte
brand.json would OOM the verifier. Added ``DEFAULT_MAX_BRAND_JSON_BYTES``
(256 KiB) cap, rejected before parse with ``invalid_body``.
**Critical (correctness):**
* URL canonicalization gap — ``urlsplit`` lowercases scheme but NOT
host, and does not strip default ports. JS ``new URL()`` does both.
Without these, the redirect-loop detector saw ``https://X.example/``
and ``https://x.example/`` as distinct entries; the well-known
fallback origin check spuriously rejected ``https://x:443`` vs
``https://x``. Fixed by normalizing host to lowercase and stripping
default ports (443 for https, 80 for http).
**Real bugs (concurrency):**
* Refresh single-flighting was serializing — replaced ``asyncio.Lock``
with a Future-based dedup, mirroring the JS pattern. N concurrent
``resolve()`` calls on a cold cache now share ONE brand.json fetch
instead of doing N fetches in series.
* ``Future`` lifecycle on ``BaseException`` in ``ensure_capability_loaded``
— the bare ``except Exception`` previously swallowed most errors but
let ``BaseException`` (including ``CancelledError``) propagate without
resolving the in-flight future, leaving joined waiters hung forever.
Now narrowed to expected discovery errors; BaseException propagates
with the future explicitly excepted.
**Real bugs (DX):**
* Capability-priming silent error swallow — bare ``except Exception``
meant operators had no signal when a counterparty's discovery
endpoint was poisoning their negative cache. Added WARNING-level
logging on every fail-open path.
**Tests:**
* Tests now use a ``client_factory`` injection seam instead of
monkey-patching ``httpx.AsyncClient.__init__`` globally — only
resolvers built in the test scope pick up the mock; concurrent
AsyncClient construction for unrelated reasons is unaffected.
* Added 7 new tests covering: host case lowering, default port
stripping, scheme lowering, non-default port preservation,
oversized-body rejection, case-aliasing redirect-loop detection,
and N-concurrent-resolve fetch dedup.
68 tests pass (up from 61), 2960 total. ruff + mypy clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>1 parent 4bd34e9 commit a016eca
7 files changed
Lines changed: 3039 additions & 0 deletions
File tree
- docs/proposals
- src/adcp/signing
- tests
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
92 | 92 | | |
93 | 93 | | |
94 | 94 | | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
95 | 101 | | |
96 | 102 | | |
97 | 103 | | |
98 | 104 | | |
99 | 105 | | |
100 | 106 | | |
101 | 107 | | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
102 | 119 | | |
103 | 120 | | |
104 | 121 | | |
| |||
255 | 272 | | |
256 | 273 | | |
257 | 274 | | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
258 | 281 | | |
259 | 282 | | |
| 283 | + | |
260 | 284 | | |
261 | 285 | | |
262 | 286 | | |
| |||
273 | 297 | | |
274 | 298 | | |
275 | 299 | | |
| 300 | + | |
276 | 301 | | |
277 | 302 | | |
278 | 303 | | |
| |||
325 | 350 | | |
326 | 351 | | |
327 | 352 | | |
| 353 | + | |
328 | 354 | | |
329 | 355 | | |
330 | 356 | | |
331 | 357 | | |
332 | 358 | | |
333 | 359 | | |
| 360 | + | |
334 | 361 | | |
335 | 362 | | |
| 363 | + | |
336 | 364 | | |
337 | 365 | | |
338 | 366 | | |
| |||
0 commit comments