Commit 35f581d
* feat(signing): key_origins consistency check + brand.json chain error codes (#350 stage 4)
Per ADCP request-signing spec #3690, an agent advertising signing
posture declares ``identity.key_origins`` on its capabilities response —
keyed by purpose (request_signing / webhook_signing / governance_signing
/ tmp_signing) and valued with the origin URI that hosts the JWKS for
that purpose. After resolving an agent's keys via the brand.json chain,
the verifier MUST confirm the resolved jwks_uri host matches the
declared origin for the purpose under check.
**4a — new error codes** (errors.py):
Nine new ``request_signature_*`` codes covering the brand.json discovery
chain (#3690 §"Discovering an agent's signing keys") and the
``identity.key_origins`` consistency check:
- _brand_json_url_missing (capabilities.identity.brand_json_url absent)
- _capabilities_unreachable (transport failure fetching capabilities)
- _brand_json_unreachable (transport failure fetching brand.json)
- _brand_json_malformed (strict-parse failure on brand.json)
- _brand_origin_mismatch (agent eTLD+1 ≠ brand eTLD+1 + no delegation)
- _agent_not_in_brand_json (agent URL absent from agents[])
- _brand_json_ambiguous (multiple agents[] entries byte-equal)
- _key_origin_mismatch (resolved jwks_uri host ≠ declared origin)
- _key_origin_missing (signing posture asserted, declaration absent)
Plus the nine ``webhook_signature_*`` mirror constants and entries in
REQUEST_TO_WEBHOOK_CODE so the webhook verifier wrapper retags
request-family codes into webhook-family ones without each callsite
re-declaring the mapping.
**4b — key_origins consistency check** (key_origins.py, new):
``check_key_origin_consistency(jwks_uri, key_origins, purpose,
posture=None, code_family="request")`` — the standalone primitive for
the consistency check. Pure function on (resolved jwks_uri, declared
key_origins map, purpose); raises SignatureVerificationError with the
right spec code. ``code_family="webhook"`` swaps to the webhook code
family.
Canonicalization: ASCII-lowercase + stdlib host.encode("idna") to
A-label form. The spec asks for IDNA-2008 strictly while stdlib
encodings.idna is IDNA-2003; this divergence is rare in practice and
matching the package's existing convention (jwks.py:201,
ip_pinned_transport.py:110, revocation_fetcher.py:380) keeps the
canonicalization story coherent. A future IDNA-2008 migration would
update all four callsites together.
The verifier integration (calling this check after RFC 9421 verification
when the JWKS source was a brand.json walk vs. a publisher pin) belongs
to stage 5's dispatch wire-up — the primitive lands here, ready to
compose with the BrandAuthorizationResolver gate.
12 new tests cover: success (declared matches resolved), missing
declaration, mismatch on different host, mismatch on subdomain drift,
case-insensitive comparison, posture propagation in diagnostics,
bare-host declarations, ``None`` and empty key_origins maps, invalid
jwks_uri (fail-closed), and webhook code-family routing.
Re-exports from ``adcp.signing`` so adopters can import:
- check_key_origin_consistency
- All 9 new REQUEST_SIGNATURE_* code constants
(Webhook mirrors stay submodule-only; webhook code routing is internal
to the webhook verifier wrapper.)
Refs #350, adcontextprotocol/adcp#3690
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(signing): expert-review fixes for key_origins consistency check
Folds three expert review passes into the stage 4 PR (ad-tech-protocol-expert,
code-reviewer, security-reviewer all run in parallel on the original
commit 62d2379).
**1. Spec-mandated structured detail fields (MUST-FIX, protocol expert)**
ADCP #3690 security.mdx step 7 mandates that
``request_signature_key_origin_mismatch`` carry
``{purpose, expected_origin, actual_origin}`` and
``_key_origin_missing`` carry ``{purpose, posture}`` as structured
fields, not just an opaque message string. Middleware adapters surface
them on the 401 / in a DLQ; without this, the SDK can't be spec-
conformant once Stage 5 wires the verifier.
- Extend ``SignatureVerificationError`` with optional
``detail: Mapping[str, str] | None``. ``str(exc)`` still renders the
free-form message for unstructured logs; structured callers read
``exc.detail``.
- ``check_key_origin_consistency`` now passes the spec-mandated keys.
- Two new tests pin the detail shape for both code paths.
**2. Bare-host vs URL canonicalization asymmetry (HIGH, security reviewer)**
The previous ``_origin_host`` fallback path for bare-host inputs only
guarded against ``/`` and space — accepting ``user@host``,
``host:port``, ``host?query``, ``host#fragment`` verbatim. An attacker
with capability-write access could declare a bare-host-with-port
(``keys.brand.com:8443``) to force a mismatch against the operator's
brand.json origin and DoS the honest verification path.
Fix: factor a ``_extract_host`` helper that re-parses bare-host inputs
through ``urlsplit`` with a synthetic ``https://`` scheme prepended.
URL form and bare-host form now strip port / userinfo / query /
fragment symmetrically. Two new tests cover ``host:port`` and
``user@host`` declaration shapes.
**3. Trailing-dot FQDN asymmetry (HIGH, security reviewer)**
``host.example.`` and ``host.example`` are the same FQDN — the trailing
dot denotes the root zone. Previous code preserved the dot, so a
brand.json serving the dot form against a capability declaring the
no-dot form (or vice versa) byte-mismatched. An attacker controlling
capabilities could weaponize this to deny verification against the
real counterparty.
Fix: strip a single trailing dot before IDNA-encoding. Test covers
both directions.
**4. IDN U-label vs A-label equivalence test (NICE, all three)**
The docstring promised IDN canonicalization but no test exercised it.
Added a test using ``münchen.example`` ↔ ``xn--mnchen-3ya.example``
in both directions.
**5. Carve-out for publisher-pin source in function docstring (MEDIUM, security)**
Previously the publisher-pin skip was documented only in the module
docstring. A caller reading just the function doc would miss it. Added
a "Caller contract" paragraph at the top of the function docstring
flagging that callers MUST skip this call for publisher-pinned tuples.
**6. Symmetric fail-closed test on declared side (LOW, security)**
The existing ``test_consistency_raises_mismatch_on_invalid_jwks_uri``
covers the resolved side but not the declared side. Added a symmetric
test so a future refactor can't silently invert the fail direction on
one side.
**Deferred to follow-up issues** (not in scope for this PR):
- Plumbing the ``source`` discriminant ("brand.json walk" vs
"publisher adagents.json pin") through ``BrandJsonJwksResolver`` so
Stage 5 can enforce the carve-out automatically. Belongs in the
Stage 5 verifier-integration PR.
- Migrating all four codebase callsites (jwks.py, ip_pinned_transport.py,
revocation_fetcher.py, key_origins.py) from stdlib IDNA-2003 to the
``idna`` PyPI package's IDNA-2008 in one commit. Package-wide
conformance pass, separate concern.
Tests: 20 in test_key_origins (up from 12). Full signing surface
(582 tests across tests/test_*.py + tests/conformance/signing/)
remains green. ruff + mypy clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci(storyboard): vendor AAO reference-formats fixture into @adcp/sdk install
Three of the four storyboard runners (seller_agent.py, sales-proposal-mode,
v3 reference seller (translator)) have been failing on main and on every
PR for the same upstream reason: ``@adcp/sdk@latest`` (which the storyboard
jobs install unpinned for drift-detection) does not ship
``aao-reference-formats.json`` in its npm tarball. Every step that
touches AAO format resolution rejects with:
AAO catalog (reference-formats.json) not found. Looked in:
.../node_modules/@adcp/sdk/test/lib/v2-projection-fixtures/aao-reference-formats.json
.../node_modules/@adcp/sdk/.context/adcp-3307/server/src/creative-agent/reference-formats.json
Vendor a copy at test/lib/v2-projection-fixtures/aao-reference-formats.json.
The SDK's error message itself recommends vendoring the file at the
expected path. Upstream tracking issue is adcp#3307.
Fix:
- Vendor the file at ``tests/fixtures/aao-reference-formats.json``
(sourced from ``adcontextprotocol/adcp:server/src/creative-agent/reference-formats.json``,
the canonical reference catalog the SDK is supposed to ship).
- Add three lines to each of the four storyboard jobs' ``Pre-install
@adcp/sdk`` steps to drop the file into
``$(npm root -g)/@adcp/sdk/test/lib/v2-projection-fixtures/`` after
the npm install. Idempotent — if upstream later ships the file the
``cp`` overwrites with the same bytes; if upstream moves it the
overwrite is a safer floor than the missing-file failure.
The four storyboard jobs affected:
- AdCP storyboard runner — examples/seller_agent.py
- AdCP storyboard runner — examples/multi_platform_seller (PlatformRouter)
(currently passing — doesn't exercise the AAO path — but covered for
symmetry against future drift)
- AdCP storyboard runner — v3 reference seller (translator)
- AdCP storyboard runner — sales-proposal-mode (proposal_finalize)
The vendored file is 145 KiB JSON with 50 format entries. Not committed
to the package distribution (lives under ``tests/fixtures/``).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci(storyboard): also vendor v1-canonical-mapping schema fixture
The AAO reference-formats fix in the previous commit cleared one of the
two missing-fixture errors and unmasked a second one of the same shape:
v1-canonical-mapping.json not found. Looked in:
.../node_modules/@adcp/sdk/schemas/cache/3.1.0-beta.2/registries/v1-canonical-mapping.json
.../node_modules/@adcp/sdk/schemas/cache/3.1.0-beta.1/registries/v1-canonical-mapping.json
.../node_modules/@adcp/sdk/schemas/cache/3.1.0-beta.0/registries/v1-canonical-mapping.json
.../node_modules/@adcp/sdk/schemas/cache/latest/registries/v1-canonical-mapping.json
Run `npm run sync-schemas` for a 3.1+ AdCP version.
Same root cause: ``@adcp/sdk`` published an npm tarball that omits the
v1-canonical-mapping schema cache for 3.1.0-beta.* versions. The
storyboard runner's v1→v2 format projection walks this registry; missing
the file causes a cascade of step failures (and incorrectly-routed error
codes like ``PRODUCT_NOT_FOUND`` instead of ``TERMS_REJECTED`` because
the canonical mapping is consulted before product lookup).
Vendor ``v1-canonical-mapping.json`` from
``adcontextprotocol/adcp:dist/schemas/3.1.0-beta.2/registries/``
(14 KiB) into ``tests/fixtures/`` and drop it into the SDK install path
the SDK looks up first (the SDK's lookup order falls back through
.2 → .1 → .0 → latest, so vendoring .2 is sufficient).
Same four storyboard jobs as the prior fix:
- examples/seller_agent.py
- examples/multi_platform_seller (PlatformRouter)
- v3 reference seller (translator)
- sales-proposal-mode (proposal_finalize)
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 c23c9d5 commit 35f581d
7 files changed
Lines changed: 6033 additions & 1 deletion
File tree
- .github/workflows
- src/adcp/signing
- tests
- fixtures
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
391 | 391 | | |
392 | 392 | | |
393 | 393 | | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
394 | 405 | | |
395 | 406 | | |
396 | 407 | | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
397 | 413 | | |
398 | 414 | | |
399 | 415 | | |
| |||
548 | 564 | | |
549 | 565 | | |
550 | 566 | | |
| 567 | + | |
| 568 | + | |
551 | 569 | | |
552 | 570 | | |
553 | 571 | | |
| 572 | + | |
| 573 | + | |
| 574 | + | |
| 575 | + | |
| 576 | + | |
554 | 577 | | |
555 | 578 | | |
556 | 579 | | |
| |||
757 | 780 | | |
758 | 781 | | |
759 | 782 | | |
| 783 | + | |
| 784 | + | |
760 | 785 | | |
761 | 786 | | |
762 | 787 | | |
| 788 | + | |
| 789 | + | |
| 790 | + | |
| 791 | + | |
| 792 | + | |
763 | 793 | | |
764 | 794 | | |
765 | 795 | | |
| |||
858 | 888 | | |
859 | 889 | | |
860 | 890 | | |
| 891 | + | |
| 892 | + | |
861 | 893 | | |
862 | 894 | | |
863 | 895 | | |
| 896 | + | |
| 897 | + | |
| 898 | + | |
| 899 | + | |
| 900 | + | |
864 | 901 | | |
865 | 902 | | |
866 | 903 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
162 | 162 | | |
163 | 163 | | |
164 | 164 | | |
| 165 | + | |
165 | 166 | | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
166 | 173 | | |
167 | 174 | | |
168 | 175 | | |
169 | 176 | | |
170 | 177 | | |
171 | 178 | | |
172 | 179 | | |
| 180 | + | |
| 181 | + | |
173 | 182 | | |
174 | 183 | | |
175 | 184 | | |
| |||
219 | 228 | | |
220 | 229 | | |
221 | 230 | | |
| 231 | + | |
222 | 232 | | |
223 | 233 | | |
224 | 234 | | |
| |||
336 | 346 | | |
337 | 347 | | |
338 | 348 | | |
| 349 | + | |
339 | 350 | | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
340 | 357 | | |
341 | 358 | | |
342 | 359 | | |
343 | 360 | | |
344 | 361 | | |
345 | 362 | | |
346 | 363 | | |
| 364 | + | |
| 365 | + | |
347 | 366 | | |
348 | 367 | | |
349 | 368 | | |
| |||
395 | 414 | | |
396 | 415 | | |
397 | 416 | | |
| 417 | + | |
398 | 418 | | |
399 | 419 | | |
400 | 420 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
| 10 | + | |
| 11 | + | |
10 | 12 | | |
11 | 13 | | |
12 | | - | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
13 | 25 | | |
14 | 26 | | |
15 | 27 | | |
16 | 28 | | |
17 | 29 | | |
18 | 30 | | |
19 | 31 | | |
| 32 | + | |
20 | 33 | | |
21 | 34 | | |
22 | 35 | | |
23 | 36 | | |
| 37 | + | |
24 | 38 | | |
25 | 39 | | |
26 | 40 | | |
| |||
42 | 56 | | |
43 | 57 | | |
44 | 58 | | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
45 | 81 | | |
46 | 82 | | |
47 | 83 | | |
| |||
64 | 100 | | |
65 | 101 | | |
66 | 102 | | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
67 | 117 | | |
68 | 118 | | |
69 | 119 | | |
| |||
87 | 137 | | |
88 | 138 | | |
89 | 139 | | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
90 | 149 | | |
0 commit comments