Repository navigation
fix(signing): match standard Web Bot Auth, and finish the library migration - #1537
Conversation
Update the experimental AdCP 3.3 identity preview schemas from the open spec branches, with provenance in UPSTREAM.json: - relationship.status uses pending_approval from its own enum, and relationship is emitted on get_principal only when served at 3.3 or later. - The buyer onboarding posture lives only in trust.json, at profiles.adcp.onboarding on the record and on an agent entry; sellers and buyers share one mode enum. - The agent_identity requirement kind and documentation_url are gone, and aao_verified uses verified_specialisms. Regenerate the bundle and the preview models, and export PrincipalRelationshipStatus in place of BuyerOnboardingMode. Refs adcontextprotocol#1500 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…final spec Bring the experimental AdCP 3.3 agent identity up to date with the final spec decisions and with the Go and TypeScript SDKs. Signature path, per DR-0023 and WBA 5.5/6.10: - Answer with WBA statuses and headers only, with one generic AUTH_INVALID body and never SERVICE_UNAVAILABLE. A revoked key is 403 without Accept-Signature. - Key-directory discovery failures never evict a cached directory, which keeps verifying up to max_stale_seconds; with nothing cached the request is unverified: 403 with Accept-Signature by default, or anonymous via wba_discovery_failure, except operations in required_for. - verify_wba_request needs only the directory and replay store. Revocation, bindings and grants are the authorization layer; replay and revocation store failures are application-level SERVICE_UNAVAILABLE. Authorization layer: - OriginRevocationChecker.status reports revoked, current, no_list, stale or unavailable. No list means identity-established and unbindable; a stale list fails spend closed. - An unbound caller outside unbound_for gets what an anonymous caller gets. A delisted bound origin falls back to identity-established with no task-control exception, keeps its binding against downgrade, and cannot be bound again while unlisted. - Trust records: the last record read stays authoritative; a 404 or invalid record after a read is an outage, never a removal; spend gets SERVICE_UNAVAILABLE and non-spend continues within a configurable bound. A grantor that served 200 never falls back to authorized_operators and an outage is never grant_absent. - Grants are read from the house record, with registrable-domain comparison, self-operated brands, narrowing and window intersection. Interop: 64-byte nonces, the target URI as received, unsigned JWKS directories by thumbprint, RSA-PSS-SHA512, and the vendored Cloudflare interop fixtures. Hardening: per owner/host trust budgets with a separate foreign-hop budget, PSL PRIVATE-aware per-operator history quotas, backdated directory created, replay at exactly expires plus skew, and fail-closed account claims. Refs adcontextprotocol#1500 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Rewrite the AdCP 3.3 agent identity migration guide for the final spec decisions: errors split by layer, discovery failures, revocation states, delisting and outage continuation, house-record grants, interop details, and the deliberate differences from the Go and TypeScript SDKs. Refs adcontextprotocol#1500 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…lignment - A reused WBA nonce is 429 without Accept-Signature, as the spec requires; replay_status=403 only reproduces the older #7894 vector. The signer enforces 64-byte nonces too. - Spend classification fails closed: update_media_buy and control_media_buy commit spend unless every change is a rename, pause or cancellation. - Grantees compare as registrable domains, and the house resolver can map the brand to its child-brand id in the house portfolio. - Unsigned requests to required operations get the WBA Accept-Signature challenge, and a web-bot-auth-only verifier answers them and RFC 9421 signatures with 403. - authorities refuses signatures captured for another host, with a boot warning when unset. - Revocation list state is an LRU cache with revoked facts kept separately, so a flood of origins cannot lock new buyers out; concurrent overflow is unavailable. Trust history admits up to 65536 operators. - Seller-side claim failures are PERMISSION_DENIED without details, and grant_absent is reserved for uncovered claims. - Document the breaking experimental API changes, bearer fallback for signed callers, and the missing durable history seams. Refs adcontextprotocol#1500 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Compare the admitted origins as an ordered list rather than by substring, which CodeQL reads as URL sanitization. Refs adcontextprotocol#1500 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Comment the expected decode failures in valid_wba_nonce and give the revocation status protocol an explicit body. Refs adcontextprotocol#1500 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- The verifier accepts 22 to 256 character nonces again: 64 bytes is RECOMMENDED for signers and httpsig-02 adds no requirement. The signer still sends 64 bytes by default and accepts the same range from callers. - Binding, trust-record and spend logic move out of the transport middleware behind one injected WbaAuthorizer; OriginBindingGate is the default, derived from the config so dataclasses.replace rebuilds it. - The RFC 9421 DeprecationWarning fires only once the SDK advertises 3.3. - Snapshot adcp.signing and adcp.types.experimental_identity in the public API snapshot, with the PgReplayStore stub resolving like the real class. Refs adcontextprotocol#1511, adcontextprotocol#1500 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…d libraries The SDK keeps AdCP domain rules and takes these protocols from libraries: - jws.py verifies with joserfc. The AdCP profile stays on top: the alg allow-list, exact typ, no crit, a protected kid, strict JSON in header and payload (joserfc keeps the last duplicate key), EdDSA pinned to Ed25519, and the fetcher's own size cap instead of joserfc's defaults. - keygen.py generates keys and exports public JWKs with joserfc; the curve policy, AdCP JWK members and the CLI stay in the SDK. - standard_webhooks.py signs with the specification's standardwebhooks package. Verification calls Webhook.sign and matches v1 tokens in constant time, because Webhook.verify reads the wall clock with a fixed window and cannot honor the caller's now and tolerance. Adds joserfc and standardwebhooks, and raises the cryptography floor to 45.0.1, which joserfc requires. Refs adcontextprotocol#1511, adcontextprotocol#1500 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… Auth with adcp#7894 Moves every RFC 9421 layer onto http-message-signatures 2.0.1 and RFC 8941 onto http-sf, replacing the archived http-sfv: - adcp.signing._rfc9421 holds the component resolver that supplies the ;key and ;req parameters (pyauth/http-message-signatures#29) and the AdCP @target-uri/@authority canonicalization, plus thin sign/verify wrappers over HTTPMessageSigner and HTTPMessageVerifier. - The 3.2 request and webhook profiles verify at step 10 through HTTPMessageVerifier and sign through HTTPMessageSigner.sign; the async SigningProvider path takes the library's own base. Error codes and step order are unchanged; every conformance vector passes. - crypto.py dispatch uses the library algorithm classes; Signature, Signature-Input and Content-Digest parse with http-sf, which refuses duplicate keys and parameters through on_duplicate_key. - Governance JWS signs and verifies with joserfc; publisher-pin thumbprints use joserfc; JWS splitting uses joserfc extract_compact. Aligns the experimental Web Bot Auth profile with the final adcontextprotocol/adcp#7894 at e24eeb09: one signature and one Signature-Agent member per request, signed directories only, ed25519 and rsa-pss-sha512 only, a 64-byte nonce, and 429 with Accept-Signature carrying a fresh nonce for a replay. Re-vendors the wba-profile vectors, governance vectors included, with provenance. Refs adcontextprotocol#1511 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Read one header spelling everywhere: the library request takes the first spelling of a name, as the profile lookups do, and the step-1 precheck refuses a signed field that a mapping carries under two spellings. - Refuse parameters on a Signature member and a req parameter that is not a true flag. - verify_wba_governance_issuer registers the action-binding crit markers, checks marker/claim pairing, and checks the issuer before key discovery. - Typed duplicate-field errors, an explicit window guard on the shared verify helper, a tab-tolerant profile-selection walk, and contract tests for the http-sf primitives it uses. Refs adcontextprotocol#1511 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Ladon verdict: Approve
Approve — clean library-migration refactor of the RFC 9421 / WBA signing stack (33 files, +1744/-713).
No blocking findings. The reviewer verified the load-bearing paths:
crypto.import_jwkconsolidation keeps the 32-byte P-256 member check and strictb64url_decoderejects padding/non-canonical bits._check_signature_members(wba.py ~252) enforces RFC 9421 §4.1/4.2 shape before selection, making the droppedis_inner_listguard safe.- Canonical-origin check (wba.py ~1096) rejects before key discovery; directory rules (one media type, ignore-unusable, every-usable-signs) are consistent and tested.
- Governance
critical_claims_failurebuilds on joserfccheck_headerplus SDK checks;StrictJsonErrorsubclassesValueError, so the governanceexcept (TypeError, ValueError)still catches malformed JSON / RecursionError.
Semver: Ed25519-only removal of ALG_RSA_PSS_SHA512 ships under fix without a breaking marker — defensible here because the WBA profile is experimental and the surface is unreleased (stacked on #1523), so not flagged as a public-surface break.
Decision table: no critical/high/medium findings; gated_paths false; high_risk is true but all signing-path entries are (modified) with no medium-or-higher finding attached, so rows 3–5 do not fire. No team gate, no prior decision. Falls through to row 9 → approve.
Non-blocking follow-up (already on author's "Before merge" list): ci.yml ADCP_REF pins adcp#8298's PR-head commit; re-pin to the merge commit so wba-interop stays reproducible.
crypto.import_jwk hands the whole JWK to joserfc, and joserfc derives the public key from d when d is present. So public_key_from_jwk, the request and webhook verifiers' step 8, and the JWS verifier used the key that d names, not the key that x names. A kid or a publisher pin names a key by its RFC 7638 thumbprint, which covers x (and y) only. A JWKS entry that carried the pinned x beside another key's d passed the pin check, and the signature that d made verified. Before adcontextprotocol#1537, public_key_from_jwk read x alone. import_jwk now drops the private members unless private=True, which only private_key_from_jwk passes. The Web Bot Auth module takes the private-member set from crypto instead of keeping its own copy. test_public_key_is_read_from_the_public_members and test_reject_signature_by_a_private_member_beside_the_key failed before this change.
There was a problem hiding this comment.
Ladon verdict: Approve
Approve — signing/governance hardening plus test coverage, no blocking findings.
Checked:
critical_claims_failureloop rewrite (governance.py:1457) enforces marker/crit-entry/claim coherence withmarker is True; verified across all four cases and new parametrized tests.import_jwk(private=False)drops private JWK members so a straydnever selects the verification key (crypto.py); well covered.type(alg) is not strguards both governance verifiers against non-string alg.- WBA governance alg allow-list narrowed to Ed25519 (drops PS512), matching the profile; RSA_PSS_SHA512 removed from the private
_rfc9421.ALGORITHMS(no public break). - Pin/doc/vector bumps to 2546cf5f are internally consistent.
Gate posture:
- high_risk is true but all reasons are
(modified)files under src/adcp/signing/** with zero medium-or-higher findings → row 5 does not fire; no deletions → row 3 does not fire. - gated_paths is false; review_decision is APPROVED.
- No no-auto-approve team match.
- Prior decision was approve (not sticky-escalate).
No critical/high/medium findings → rows 1–8 do not fire. Falls through to row 9: approve.
There was a problem hiding this comment.
Ladon verdict: Approve
Approve — clean test-only delta since prior approve.
The delta since the prior clean approve (head 853c841) is test-only: a parametrized test (P-256/RSA/Ed25519) asserting that a key directory publishing private JWK material (d) is refused with "private key material". The reviewer verified it exercises real production behavior — the fail-closed scan at wba.py:632-633 rejects private JWK members before unusable keys are skipped — and the test helper correctly handles all three key types.
No critical/high/medium findings. high_risk is true (signing paths modified), but per the decision rules the flag alone is not escalation-worthy: the reviewer surfaced no medium-or-higher concerns on the modified files, so the modifications are presumed safe. gated_paths is false and review_decision is APPROVED. No no-auto-approve team match. Rows 1–8 do not fire → row 9 approve.
There was a problem hiding this comment.
Ladon verdict: Approve
Approve — vendored WBA profile test-vector refresh with no actionable findings.
This PR re-pins experimental adcp#7894 conformance vectors (2546cf5f → f311e1b5), with the commit SHA moving consistently across UPSTREAM.json, UPSTREAM-README.md, README.md, docs/agent-identity-3-3-migration.md, and the test_wba_profile.py provenance assertion. It bumps the interop harness ADCP_REF and adds conformance scenarios (R18 base refinement, new R19b/R21b, directory cases D09–D14b, and the D03 wba→adcp tier reclassification). All four pin references agree on f311e1b5; the recorded scenarios.json SHA-256 is self-enforced by CI; the load-bearing D13/D14 ordering reads consistently.
No SDK source, public surface, credential, forward-compat, import-layering, or generated-code changes. No semver-signal concerns.
Decision rationale: No critical/high/medium findings (table rows 1, 4, 5, 8 don't fire). gated_paths is false (row 2 n/a; review_decision is also APPROVED). high_risk is true but all reasons are (modified) with zero medium-or-higher findings — rows 3 and 5 don't fire. Prior decision was approve, so sticky row 6 n/a. No no-auto-approve team match (row 7 n/a). Falls through to row 9 → approve.
bokelley
left a comment
There was a problem hiding this comment.
What I verified
I checked out the head at 3f04bd2b in a clean worktree on Python 3.12, with .[dev], fastapi, and svix installed.
- pytest. The signing conformance suite plus the identity, governance, and WBA test files (
test_identity_3_3_alignment,test_governance_authorization,test_wba_*,test_signed_request_verification,test_webhook_signing_capabilities, and the other identity files): 2,401 passed, 2 skipped. The fulltests/conformancesuite: 4,116 passed, with 2,507 skipped (Postgres). The six #1524 files: 92 passed, 0 failed. - ruff and mypy.
ruff check src/is clean.mypy src/adcp/reports no issues in 1,536 files. - Harness at the PR's pin (#8298
ef966de1). 63 of 63 pass (wba23/23,adcp25/25). The same pin against #1523's heade81d8009gives 55 of 63. The 8 failures are exactly the ones in your table: R10, R17, R17c, R17d, R17e, R20b, D08, and D11. - Harness at the current #8298 head (
ac2b3ffa). 64 of 64 pass, including the new D14c (upper-caseD). I logged every adapter refusal. All 41 are deliberateWbaVerificationErrorreasons, and none is a crash. - The order-dependent failure you describe.
test_stateful_mcp_session_reads_each_requests_own_wba_signerfails aftertests/test_wba_task_ownership.py. The cause is the_HANDLER_TOOLSregistry, which is keyed by class name, so the twoHandlerclasses collide. It's a problem in the tests, not in this PR.
Findings, by severity
Medium
-
This PR bundles AdCP 3.2 fixes into a draft-spec stack. RELEASING.md says "Fixes land on
mainfirst", and this stack only reachesnext. Three groups of changes in this PR fix the ratified 3.2 line:- The governance
critcoherence check and the non-stringalgcheck (governance.py,critical_claims_failure). I checked them againstsecurity.mdxchecklist step 4 on adcp main ("require each boolean protected-header marker, itscritentry, and its payload claim to appear together"), and they are correct. - The #1524 test fixes.
- Arguably the strict
b64url_decodeand the private-member drop inimport_jwk, becausepublic_key_from_jwkis on the RFC 9421 3.2 verifier path.
On
mainat5df00364with fastapi and svix installed, the six #1524 files still fail: 26 failed and 48 passed out of 74 collected. Main also still accepts thecrit/marker combinations this PR refuses.Suggestion: split these into a PR against
main, and letnextpick them up through the regular sync. Keep this PR to the WBA changes. This PR doesn't close #1524 in any case, because CI still installs neitherfastapinorsvix(ci.yml:134,.[dev]). - The governance
-
No CI has run on this PR or on #1523.
ci.ymlrunspull_requestonly formain,next, and theconductor/*branches. This PR's base,claude/issue-1511-rfc9421-library, isn't one of them. As a result,test,schema-check, and the newwba-interopjob have never run on GitHub. The only checks so far are GitGuardian, IPR,check, andcode_review, so every result in the description is local. They reproduce for me, but the jobs need to run on GitHub before merge.After retargeting to
next, confirm the run. Also check whetherWeb Bot Auth interop with the Cloudflare referenceis a required check: I couldn't read branch protection, and the job isn't in any aggregate gate. -
The stack inherits a conflict with
next.git merge-tree origin/nextconflicts insrc/adcp/signing/verifier.py(imports) for both #1523 and #1537.nextnow importstarget_uri_precheckfrom_header_precheck, which came in with #1479. The resolution in #1523 has to keep it and its call site, or the 3.2.4 target-URI precheck is silently lost. #1514 merges cleanly ontonext, but it is 8 commits behind.
Low
-
The directory
Acceptheader still offers the media types the verifier now refuses.wba.py:777-779sendsapplication/jwk-set+json;q=0.9, application/json;q=0.8, while_verify_directoryaccepts onlyDIRECTORY_MEDIA_TYPE. A server that negotiates content could pickapplication/jsonand then be refused. Send onlyDIRECTORY_MEDIA_TYPE. -
private_key_from_jwknow requiresxbut never checks it (crypto.py:139-148). I passeddfrom one key andxfrom another. The function accepted the pair, and the derived public key doesn't matchx. Ad-only JWK used to work inWebhookSender.from_jwkand now raises. Either check thatxmatches the public key derived fromd(a misconfiguration then fails at load time instead of at the receiver), or don't requirex. As written, it's an API break with no benefit. -
The WBA revocation-list path still allows
ES256.OriginRevocationChecker._discover(wba.py:1601) buildsAsyncCachingRevocationChecker.from_issuer_originwithout an algorithm list.averify_jws_document(revocation_fetcher.py:1124) therefore falls back toALLOWED_JWS_ALGS = {EdDSA, ES256}. It can't be exploited, because directory keys are Ed25519 only and joserfc refusesES256with an OKP key. But your #7894 answer (item 5) says this path narrows toEdDSAfor Python, and this PR doesn't do it. Either pass{"EdDSA"}or correct the answer. -
The PR is inconsistent about the
Ed25519algorithm name, and the profile doesn't define a usable key.- A directory key that carries
"alg": "Ed25519"(the RFC 9864 fully-specified name) is ignored (wba.py:340). The governance path now accepts the JWSalgEd25519(jws.py:113,wba.py:1223). - An Ed25519 key with
use: "enc", or withkey_opslackingverify, is ignored instead of being required to sign. The page says "Every Ed25519 key in the directory signs the response."
Neither case can be exploited, because an ignored key never verifies anything. Still, the profile should say what "a key the profile uses" means, so that all three SDKs ignore the same keys.
- A directory key that carries
-
The pins are behind the heads.
ADCP_REF(ci.yml:385) isef966de1and the vendored vectors aref311e1b5. The #8298 head is nowac2b3ffaand the #7894 head ise5929470, which add D14c. The SDK already passes D14c, becausewba.py:589lower-cases member names. No in-repo test covers an upper-caseD, so add one when you re-vendor. -
The adapter's catch-all
exceptturns a crash into a correct refusal.adapter.py:148catches everyException. The harness grades a directory refusal as a pass whenokis false and the scenario has no expected status (run.mjs:76). It also maps an adapter that crashes outright took: false(run.mjs:51). So aTypeErrorin the SDK grades as a correct refusal on D08 to D14c.- In the adapter: convert only
WbaVerificationErrorandValueError, and let anything else crash. - In #8298: don't count
rule: "adapter-crash"as a refusal. This affects every SDK's adapter.
- In the adapter: convert only
-
The multi-key directory signing path is untested.
sign_key_directorynow usesappend=True(wba.py:746). Every in-repo call passes one key, and the harness also signs directories with one key (run.mjs:117). I checked a 3-key round trip by hand and it works. Add a test with at least two keys. -
Some changed APIs need a breaking-change marker. You asked whether these do.
b64url_decode(strict),private_key_from_jwk(xrequired),public_key_from_jwk(error text), andpublisher_pins.jwk_thumbprint(returnsstr, notbytes) are exports that also exist onmain9.x. Onnextthey ship in 10.0.0a. ABREAKING CHANGE:footer in the squash commit puts them inCHANGELOG.next.md. The WBA changes are fine without one, since the WBA profile is experimental. -
The basis for Ed25519-only. The description cites "the profile decision of 2026-10-17 on #7894". The change is
5c3876073fon #7894, which is still open, and the page says nothing on it is normative until the working group records a decision. I agree with the change; the page now saysed25519only. Please word it as following the current page, so that if the working group reverses it, the RSA removal is reverted too.
Nit: in the interop table, "pass" in the e81d8009 column means the SDK accepted the request. "accepted" would read more clearly.
Where this is right
-
R10. A signature without
algis now refused. The page's parameter row requiresalg, so defaulting it to the key's algorithm was a defect in #1523. -
R17 family.
Signature-Agentis compared exactly against the canonical origin.type(agent[0]) is not strstill refuses a Token. -
Directories.
- Only the one media type is accepted.
- Unusable keys and their signatures are ignored.
- Every usable key must sign.
- The private-member scan, which ignores case, runs before the skip.
This matches the page and Appendix B.1. Skipping an ignored key's member before the tag and window checks is the right reading.
-
R20b. Every member must be well-formed before one is selected. This matches step 1 of the page.
-
One
import_jwk. Private members are dropped before joserfc sees them. Before this PR, a straydbesidexon the JWS path selected the key fromd. I confirmed thatpublic_key_from_jwknow ignoresd. -
Strict base64url. JWK members are now strict. I confirmed that padded, standard-alphabet, and non-zero-trailing-bit spellings of
xare refused, and that each has no thumbprint. -
Removed residue. Removing
_check_critical_markersand_governance_registryfromwba.pyin favour of one shared helper and one registry removes real duplication that #1523 left. -
#1524 test fixes. The changes are correct: the retry is re-signed (the receiver refuses a replayed nonce before dedupe), the digest-off case signs with the 3.1 profile, and
operation_idis passed tosend_mcp. -
Unchanged from #1514/#1523. The diff doesn't touch these:
- the nonce rule (same accepted set, matching the page);
- one signature (R21 and R21b pass);
- signed directories required;
- 429 with a fresh
Accept-Signature; - the unsigned-request rule;
WbaAuthorizer;_WbaComponentResolver;- the duplicate-header body-swap fix.
Base branch and stacking
The base is correct for the WBA code. This PR targets #1523's branch, so it lands on next, not main, and it doesn't conflict with the branch model. It is a direct continuation of #1523: every behavioural change here corrects #1523 against the #7894 page as it stands now (RSA, alg, origin, media type, directory keys). Nothing here duplicates #1514.
Recommended order:
- Merge
nextinto #1514, then merge #1514. - Retarget #1523 to
next, resolve theverifier.pyconflict keepingtarget_uri_precheck, and merge it. Alternatively, fold this PR into #1523 first, since #1523 doesn't match #7894 without it. - Retarget this PR to
nextand bumpADCP_REFand the vectors to the current heads. Let CI run, includingwba-interop, and merge. - Separately, open a PR against
mainwith the 3.2 governance fixes and the #1524 test fixes, and addfastapiandsvixto the CI install (finding 1).
🤖 Generated with Claude Code
Add tests/wba_interop/adapter.py, which answers the four operations of the AdCP interop harness (sign_request, sign_directory, verify_request, and verify_directory) through the public adcp.signing API. Add a wba-interop CI job. It checks out tests/wba-interop and the wba-profile vectors from adcontextprotocol/adcp at ADCP_REF, the head of the harness pull request (adcontextprotocol/adcp#8298), installs the harness with npm ci on Node 24, and runs run.mjs against the adapter.
Apply the Web Bot Auth rules that the Cloudflare web-bot-auth reference and the profile state: - Sign and verify with ed25519 only. RSA-PSS leaves the WBA signer, verifier, and directory, and the dead RSA branch of the governance issuer check goes with it. ES256 stays on the AdCP RFC 9421 profile. - Refuse a signature that has no alg parameter. The verifier no longer infers alg from the key. - Ignore a directory key the profile can't use, and the signature that key makes (RFC 7517 section 5). Refuse a directory in which a usable key doesn't sign (WBA Appendix B.1, one signature per key). - Refuse a directory served with any media type other than application/http-message-signatures-directory+json (WBA section 5.5.1).
b64url_decode used the standard library's lenient decoder, which drops characters outside the alphabet and ignores padding and trailing bits. Verifier step 8 decodes the signer's JWK through public_key_from_jwk, so it accepted a JWK whose x member had "!!" inserted. Build b64url_decode and b64url_encode on joserfc.util, and import Ed25519 and P-256 JWKs with joserfc's OKPKey and ECKey through one helper, crypto.import_jwk. The JWS and governance paths use the same helper and decoder instead of their own copies. The SDK keeps one check that joserfc leaves open: a P-256 member is exactly 32 bytes (RFC 7518 section 6.2.1.2).
Remove hand-written code that joserfc, http-sf, and http-message-signatures already provide: - public_directory_key exports the JWK with joserfc's OKPKey.as_dict and thumbprint. - jwk_thumbprint passes joserfc the members that the key type's registry marks as required, instead of its own RFC 7638 member table. It returns the base64url thumbprint, so thumbprint_key_id no longer decodes and re-encodes it. - A directory key's x member is checked by importing it with joserfc, and the nonce check decodes base64url strictly, so neither re-encodes a value to compare it. - The async signer and the verifier build Signature-Input with http-sf (signature_input_field) instead of an f-string. - sign_key_directory adds each key's signature to the same message with HTTPMessageSigner.sign(append_if_signature_exists=True) instead of joining the fields with ", ".
Under -00 section 5.2.1, a Signature-Agent member with type=directory names a key directory, and the verifier ignores a member with any other type (profile rule 6). The verifier already reads type=directory as a Token and as a String, as Cloudflare's parser does, and ignores jwks_uri and cimd, which leaves the signature without a member keyed to its label (403, harness scenarios R18 and R18b). No test covered either spelling. The nonce test's signing moves into a helper, _sign_vector_request, that signs the positive vector's request with the library, so a test can sign a Signature-Agent shape the SDK signer never emits.
RFC 9421 sections 4.1 and 4.2 define every Signature-Input member as an inner list and every Signature member as a Byte Sequence with no parameters. The verifier checked this only for the web-bot-auth member, so a sibling such as cdn=token verified. Cloudflare's http-message-sig refuses such a field as MalformedField, which the profile answers with 400 (harness scenario R20b). _check_signature_members runs on both fields before a member is selected, for requests and for key directories, so a malformed web-bot-auth member is 400 too. A member with another tag is still not read otherwise. _verify_crypto no longer checks its own member.
The Signature-Agent member value is the signer's https origin, and the profile compares it as a string: a lowercase host, no default port, no trailing slash, and no path, query, fragment, or userinfo. The verifier canonicalized the value first, so https://BUYER.example.com:443/agents/a verified as https://buyer.example.com. The verifier now refuses any value that differs from its canonical origin with 403, before key discovery (harness scenarios R17, R17c, R17d, and R17e). http:// and userinfo were already refused. sign_wba_request already sends the canonical origin, and a test pins it.
The governance vectors leave the Web Bot Auth profile's vectors, and the profile drops its governance checks: verify_wba_governance_issuer is an AdCP-layer check that runs after WBA verification. The function stays. Move the three governance vectors, with the e24eeb09 keys file they sign with, to vectors/wba-governance, with their own provenance record. Move their tests from test_wba_profile.py to test_wba_governance.py, which shares the profile tests' directory resolver.
Re-vendor static/compliance/source/test-vectors/wba-profile from adcontextprotocol/adcp at 49b3a315, the head of #7894. The request and directory vectors keep their signatures and change only their comments. The directory no longer carries governance vectors or the governance key, and it adds scenarios.json, the failure scenarios that the interop harness signs at run time.
…hook ProfileSigner reaches HTTPMessageSigner's protected base builder because the library has no public way to get a signature base before signing. pyauth/http-message-signatures#31 asks for one, so the module and the class docstrings name it as the condition for dropping the hook.
…ed helpers The governance verifier had its own compact JWS splitter, its own strict JSON parser, and two checks that decoded base64url and re-encoded it to refuse a non-canonical spelling. Split the token with jws.parse_compact_jws and parse each segment with parse_strict_json, the splitter and parser that the revocation-list and Web Bot Auth paths use. b64url_decode is strict, so the re-encode checks go too. Outcomes and error codes are unchanged, with two exceptions where the old parser was weaker than every other JWS path in the SDK: a segment with an unpaired surrogate escape is refused (governance_token_invalid), and nesting deep enough to raise RecursionError is governance_token_invalid instead of an uncaught exception. Some failure messages change, because the splitter refuses a malformed header before the JSON step. The governance authorization tests (tests/test_governance_authorization.py and tests/conformance/vectors/governance-authorization.json) gate it.
…efusal
The adapter turned every exception into {"ok": false}, and the harness
grades a directory scenario with no expected status as passed when ok is
false. A TypeError in the SDK therefore graded as a correct refusal on
D08 to D14c.
The adapter now reports only the SDK's refusals: WbaVerificationError,
and the ValueError it raises when it refuses to sign
(WbaVerificationError subclasses ValueError). Any other exception
propagates and the adapter exits with a traceback.
test_the_interop_adapter_reports_only_sdk_refusals failed for TypeError
and KeyError before this change.
adcontextprotocol#1537 changes four exported adcp.signing functions that also exist on main 9.x. docs/request-signing-migration.md section 7 lists each change and what an adopter checks before upgrading. BREAKING CHANGE: adcp.signing.b64url_decode accepts only RFC 7515 base64url (no padding, no standard-alphabet characters, no non-zero trailing bits) and raises ValueError for anything else. public_key_from_jwk reads a JWK through joserfc, so its ValueError text changes. private_key_from_jwk raises ValueError when a JWK's x (or y) isn't the public key of its d. adcp.signing.publisher_pins.jwk_thumbprint returns the RFC 7638 thumbprint as a base64url str instead of the digest bytes.
…only The migration guide now says that Web Bot Auth signs with ed25519 following the current #7894 page (5c3876073f). The working group has not ratified that page, so a reversal there restores the removed algorithm here too.
…K's resolvers http-message-signatures 2.1.0 resolves the RFC 9421 ;key and ;req component parameters (pyauth/http-message-signatures#30) and adds HTTPMessageSigner.prepare and apply for a signature produced elsewhere (adcontextprotocol#31). It also replaced the resolver's url attribute with url_parts, so the SDK's resolver subclasses, which read url, failed every signature base under 2.1. The SDK now uses the library for what the subclasses did: - Every profile resolves components with HTTPSignatureComponentResolver. ComponentResolver (the ;key and ;req branches, a derived-component allow-list, and its own trimming and control-character checks), ComponentError, AdcpComponentResolver, and Web Bot Auth's _WbaComponentResolver are removed. - The AdCP RFC 9421 profile hands the library a request whose URL is the canonical target URI (canonical.signing_message), so the library derives the AdCP @target-uri and @authority from it. Web Bot Auth hands it the target URI as received. - The async signer uses prepare and apply instead of the protected base hook. build_signature_base still builds a base through the protected _build_signature_base: the library has no public call for a base over parameters the caller orders. The library is the reference for every observable change, and each one follows it: - A covered derived component beyond @method, @target-uri, and @authority, such as @path, resolves and verifies instead of being refused. Neither profile refuses extra components. test_an_extra_covered_derived_component_verifies replaces test_unsupported_derived_component_wraps_as_header_malformed. - A Web Bot Auth @target-uri for a URL with no path ends in "/", and one with a fragment or userinfo drops them. @authority strips a zero-padded default port. - A covered header's obsolete line folding is unfolded (RFC 9421 section 2.1) instead of refused. - A lowercase method, a malformed percent escape, and a raw "{", "}", or "|" in the URL, which RFC 3986 doesn't allow, are refused. The signers' ValueError says to percent-encode the URL. - A Web Bot Auth target URI that isn't a valid URI is 403, the profile's status for a failed verification, instead of 400. Tests that asserted on the removed internals are rewritten against the library: test_rfc9421_library_contract.py drops four tests and adds test_library_resolves_key_and_req and test_verify_tells_a_base_failure_from_a_bad_signature, and the WBA test helpers sign with the library resolver. BREAKING CHANGE: adcp.signing requires http-message-signatures 2.1.0 or later. sign_request, async_sign_request, and sign_wba_request raise ValueError for a lowercase method or a URL that isn't valid RFC 3986, such as one with a raw "{", "}", or "|" in its query, and the verifiers refuse such a request. A covered derived component beyond the required ones verifies.
…at e5929470 ADCP_REF moves to 306cdfa6, the #8298 head, where the harness grades an adapter crash as a failure, never as a refusal. The wba-profile vectors move to e5929470, the #7894 head: scenarios.json adds D14c (a private member spelled in upper case) and equals the harness's copy at 306cdfa6. The request and directory vectors, keys.json, and the upstream README are byte-identical to f311e1b5. With http-message-signatures 2.1.0 the adapter passes 64 of 64 checks, with no adapter crash.
3f04bd2 to
4de1237
Compare
There was a problem hiding this comment.
Ladon verdict: Approve
Approve — completes the http-message-signatures 2.1 migration with a clean reviewer pass.
No critical/high/medium findings. Walking the decision table top-down:
- Rows 1 (no critical/high), 4, 8 (no medium findings) — no match.
- Row 2 (gated_paths) — gated_paths is false; also review_decision is APPROVED. No match.
- Row 3 (deleted high-risk path) — all high_risk_reasons are
(modified), none deleted. No match. - Row 5 (high_risk modified + medium finding) — high_risk is true for signing/** modifications, but there are zero medium findings. No match.
- Row 6 (sticky escalate) — prior decision was approve, not escalate. No match.
- Row 7 (no-auto-approve team) — no team match. No match.
- Falls through to Row 9 → approve.
What was checked: the diff drops custom ComponentResolver subclasses for the library resolver, routes AdCP canonicalization through canonical.signing_message, moves the async signer to prepare/apply, narrows WBA to ed25519, and tightens JWK/base64url decoding onto joserfc. _rfc9421.verify() fails closed on alg/key mismatch; wba.py refuses private-material directories case-insensitively; one-signature-per-usable-key is enforced. Semver signal is satisfied — refactor(signing)! with BREAKING CHANGE footers plus migration prose in request-signing-migration.md §7; the unmarked WBA-preview removals are correct for the experimental 3.3 surface.
Non-blocking note: wba-interop CI pins ADCP_REF to the unmerged #8298 head, but the job comment already flags re-pinning on merge — low, not surfaced as a blocker.
…g-storage Ports the driver change from adcontextprotocol#1521 (bde770b) on main. The JS SDK moved createFilesystemReportingRowObjectProviderV1 out of the ledger package, so the row-storage interop lane fails on any branch cut before adcontextprotocol#1521. The same change is already on main and next, so the sync merges cleanly.
There was a problem hiding this comment.
Ladon verdict: Approve
Approve — clean delta, no blocking findings.
The incremental change since the prior approve is a single test-only file: the JS half of the cross-SDK row-storage interop driver (tests/interop/reporting_row_storage_js_driver.cjs). It adds a fallback resolving ReportingRowStorageV1 and createFilesystemReportingRowObjectProviderV1 from @adcp/reporting-storage when SDK 16 stops exporting them from @adcp/sdk's reporting/ledger entry. The gate verifies both symbols are functions before use, rowStorage.* references track the resolved binding, and the fallback fails closed via the existing requireFromSdk helper. No production surface, no correctness issue.
Decision-table walk:
- Rows 1 (critical/high): no findings — skip.
- Row 2 (gated_paths): gated_paths is false — skip.
- Row 3 (high-risk deletion): all high-risk reasons are (modified), none deleted — skip.
- Row 4 (medium data-loss/schema/infra): no findings — skip.
- Row 5 (high-risk modified + medium): high_risk is true with modified signing files, but there are zero medium findings — does not fire.
- Row 6 (sticky escalate): prior decision was approve, not escalate — skip.
- Row 7 (no-auto-approve team): no team match — skip.
- Row 8 (3+ medium): zero medium — skip.
- Row 9: approve.
high_risk is true only via (modified) signing-path files with no reviewer-flagged concern; per the high-risk guidance a modification with no medium-or-higher finding is presumed safe. review_decision is APPROVED. Net result is an approve.
|
Thanks for the review. Each finding, with its commit or answer:
Nit: the interop table says "accepted".
The other profile edge cases need no change:
Testing. Local CI on
Because upstream CI doesn't trigger on this pull request's base, the same |
…ntity-3-3-alignment
…o claude/issue-1511-rfc9421-library Brings in next through adcontextprotocol#1514's branch. The one conflict was the import block of src/adcp/signing/verifier.py: this branch imports `_rfc9421`, and next (adcontextprotocol#1479, AdCP 3.2.4) imports `target_uri_precheck`. The resolution keeps both, and keeps the precheck's call site, so a malformed wire authority is still refused as request_target_uri_malformed at step 6 before key lookup and replay claims. A new regression test in test_target_uri_malformed_boundary.py fails if the precheck is dropped. http-message-signatures 2.1.0, released 2026-10-19, rewrote the component resolver this layer subclasses (no `url` attribute). Cap the dependency below 2.1 until the SDK moves to 2.1, which adcontextprotocol#1537 carries. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…511-rfc9421-library next's tree at 2b08ced equals adcontextprotocol#1514's head 6637374, which this branch already merged, so the merge records the squash commit without changing the tree. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
@KonstantinMirin, #1514 ( This PR now conflicts with git fetch upstream # adcontextprotocol/adcp-client-python
# 1. Merge #1523's final head. It is next's tree exactly, plus the merge with next.
git merge 87f3956b6a854e3fdd810a68d92cd1a593719765
# The only conflict is pyproject.toml: keep your "http-message-signatures>=2.1.0,<3",
# and drop the three comment lines about the "<2.1" cap.
# 2. Record next's squash of #1523 without changing the tree (next's tree == 87f3956b's).
git merge -s ours upstream/nextAfter the two merges, Two things in #1523 as merged that you'll see in the merge:
Harness pin. Once 🤖 Generated with Claude Code |
…x/wba-standard-interop
|
Ladon cannot review this PR until merge conflicts are resolved. |
There was a problem hiding this comment.
Ladon verdict: Approve
Approve — subsequent pass on an already-approved 3.2.4 regen + version-bump PR; no blocking findings.
What I checked:
- No critical/high/medium findings in this run; prior pass had no blockers and none re-surfaced.
high_risk: trueis driven entirely by(modified)entries undersrc/adcp/signing/**— all modifications, no deletions. Reviewer found no medium-or-higher concerns on those files, so the modifications are presumed safe (no Row 5 trigger).gated_paths: falseandreview_decision: APPROVED— Row 2 not applicable.- No no-auto-approve team match (Row 7 N/A).
- Verified reviewer's notes: deliberate server-side fail-closed vs buyer-side fail-open asymmetry is correct; fetcher cache bounded with cursor-cycle detection and credential-scoped keys; new public exports (adcp.identifiers, adcp.property_lists) present in
__init____all__+ TYPE_CHECKING and documented in AGENTS.md (no missing handler-table entry); CI gates remain armed (no blanket type:ignore, no disabled tests, no dropped ruff rules).
Decision table: none of rows 1–8 fire → Row 9 approve. Net improvement / clean subsequent pass.
Brings #1539 (governance crit-marker and non-string alg checks, the #1524 signing test fixes, fastapi and svix in the dev extra) into next. Conflicts: - src/adcp/governance.py: keep next's critical_claims_failure, which already requires each marker, crit entry and claim together (#1537), and take main's refusal of a non-string alg. - src/adcp/signing/crypto.py: keep next's joserfc-based strict b64url_decode, which already refuses padding, other alphabets and non-zero trailing bits. - tests/conformance/signing/test_crypto.py: take main's standard-alphabet case and keep next's key tests. - tests/test_governance_authorization.py: keep next's unit tests on critical_claims_failure and add main's end-to-end re-signed vector tests (the crit test renamed with an _end_to_end suffix) and the non-string alg test. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This pull request is stacked on #1523. It adds the adapter that the AdCP Web Bot Auth (WBA) interop harness runs against this SDK, wires the harness into CI, and fixes the places where the SDK and standard WBA disagree. It also removes the hand-written code that #1523 left beside the libraries, as listed in the table on #1523. Cloudflare's npm
web-bot-auth0.2.0 andhttp-message-sig0.3.0 are the judge. The protocol draft's editor maintains both packages.The harness is adcontextprotocol/adcp#8298, for adcontextprotocol/adcp#8297.
#1539 carries the AdCP 3.2 fixes to
main: thecritcoherence check, the non-stringalgcheck, the #1524 test fixes, andfastapiandsvixin the CI install.nextpicks them up through the regular sync. This pull request no longer carries the #1524 test fixes or the 3.2 half of thealgcheck. Two of the fixes stay here as well, because the Web Bot Auth path depends on them: thecritcheck lives incritical_claims_failure, whichverify_wba_governance_issuershares with the 3.2 verifier, and strict base64url decoding comes fromjoserfchere, throughimport_jwk. If themainsync intonextconflicts incrypto.b64url_decodeor in thecritcheck ofverify_governance_authorization, keepnext's side, which already carries each fix.Changes
The commits make the following changes:
test(signing),ci(signing)).tests/wba_interop/adapter.pyreads one JSON operation on standard input (sign_request,sign_directory,verify_request, orverify_directory) and writes one JSON result. It calls only the publicadcp.signingAPI. It reports the SDK's refusals,WbaVerificationErrorand theValueErrorof a refused signature, asok: false. Any other exception crashes it, so the harness never grades an SDK defect as a correct refusal. Thewba-interopjob inci.ymlchecks outtests/wba-interop/and thewba-profilevectors atADCP_REF(306cdfa6), runsnpm cion Node 24, and runs the harness againstuv run python tests/wba_interop/adapter.pywith JUnit output.fix(signing), scenarios R10, D08, and D11). The changes are the following:ed25519only. RSA-PSS leaves the signer, the verifier, the directory, the governance issuer check, and the RFC 9421 algorithm table. The AdCP RFC 9421 profile keeps Ed25519 and ES256.alg. It used to defaultalgto the key's algorithm.application/http-message-signatures-directory+jsonis refused, and the directory fetch asks for that media type only. The SDK used to accept, and ask for,application/jsonandapplication/jwk-set+json.EdDSAonly:OriginRevocationCheckerpassesallowed_algs={"EdDSA"}, a new parameter ofAsyncCachingRevocationChecker.fix(signing)).b64url_decodeused the standard library's lenient decoder, so verifier step 8 accepted a JWK whosexhad!!inserted.b64url_encode,b64url_decode, and public JWK import come fromjoserfcthrough one helper,crypto.import_jwk, which the JWS and governance paths share. The SDK keeps the 32-byte P-256 member check, whichjoserfcleaves open.import_jwkdrops the private members of a public key beforejoserfcsees them:joserfcderives the public key from adit is given, and akidor a publisher pin names the key by a thumbprint overx.private_key_from_jwkbuilds the key fromd, so ad-only JWK loads, and it refuses a JWK whosex(ory) isn't the public key ofd.refactor(signing)).public_directory_keyexports withOKPKey.as_dictandthumbprint.jwk_thumbprintpassesjoserfcthe members its key registry marks as required, instead of an RFC 7638 member table. The async signer and the verifier buildSignature-Inputwithhttp-sf, andsign_key_directoryappends each key's signature withHTTPMessageSigner.sign(append_if_signature_exists=True)instead of", ".join.test(signing), scenarios R18, R18c, and R18d). Tests pin the existing behavior: aSignature-Agentmember withtype=directoryverifies, as a Token and as a String, and a member with any othertype, such asjwks_uriorcimd, is ignored (403).fix(signing), scenario R20b). EverySignature-Inputmember must be an inner list and everySignaturemember a Byte Sequence with no parameters, under every label (400). A member with another tag is still not read otherwise. The check runs before a member is selected, so a malformedweb-bot-authmember is400too, and directory verification applies it as well.fix(signing), scenarios R17, R17c, R17d, and R17e). TheSignature-Agentvalue must be exactly the canonical https origin string: a lowercase host, no default port, no trailing/, and no path, query, fragment, or userinfo. The verifier canonicalized the value first, sohttps://BUYER.example.com:443/agents/averified ashttps://buyer.example.com. Any other spelling is403before key discovery. The signer already sent the canonical origin, and a test pins it.test(signing), scenarios D14, D14b, and D14c). Tests pin the existing behavior: a directory in which any key carries a private member is refused, whatever the case of the member name (d,D), before unusable keys are skipped.test(signing)). The governance vectors leave the WBA profile vectors, andverify_wba_governance_issuerstays as an AdCP-layer check after WBA verification. Its three vectors move tovectors/wba-governance, with their own provenance record, and its tests move totest_wba_governance.py.test(signing),ci(signing)). Thewba-profilevectors are vendored from adcontextprotocol/adcp ate5929470, the #7894 head. The request and directory vectors keep their signatures and change only their comments, andscenarios.jsoncomes with them, D14c included.ADCP_REFis306cdfa6, the #8298 head, where the harness grades an adapter crash as a failure.http-message-signatures2.1 (refactor(signing)!). 2.1.0 resolves the RFC 9421;keyand;reqcomponent parameters (Resolve the RFC 9421 key and req component parameters pyauth/http-message-signatures#30) and addsHTTPMessageSigner.prepareandapply(chore(main): release 1.3.1 #31). It also replaced the resolver'surlattribute, which the SDK's resolver subclasses read, so they failed every signature base under 2.1. The SDK's resolver subclasses are gone: every profile uses the library'sHTTPSignatureComponentResolver. The AdCP RFC 9421 profile hands the library a request whose URL is the canonical target URI (canonical.signing_message), so the library derives the AdCP@target-uriand@authority, and Web Bot Auth hands it the target URI as received. The async signer usesprepareandapplyinstead of the protected base hook. The floor is 2.1.0.refactor(governance),refactor(signing)). Both governance verifiers split tokens withjws.parse_compact_jwsand parse segments withparse_strict_json. Both checkcritwith one helper,critical_claims_failure, built onJWSRegistry.check_header, and build their registry with one factory. Each commit body says which tests gate it and what changes.fix(governance),fix(signing)). Each critical marker, itscritentry, and its claim must appear together, as security.mdx checklist step 4 requires. The WBA issuer check refuses a tokenalgthat isn't a string asgovernance_token_invalid, and refuses aPS512token before key discovery.docs/agent-identity-3-3-migration.mdfollows each change.API changes
Outside the WBA preview
369ddf37ecarries aBREAKING CHANGE:footer for these changes, which affect functions that also exist onmain9.x.docs/request-signing-migration.mdsection 7 lists them for adopters.adcp.signing.b64url_decodeaccepts only RFC 7515 base64url and raisesValueErrorfor padding, characters outside the URL-safe alphabet, and non-zero trailing bits.public_key_from_jwkimports withjoserfc, so itsValueErrortext changes. It reads only the public members, as it did before this pull request.private_key_from_jwkraisesValueErrorwhen a JWK'sx(ory) isn't the public key of itsd. Ad-only JWK loads, as before.adcp.signing.publisher_pins.jwk_thumbprintreturns the base64url thumbprint (str) instead of the digest bytes.8ccabed42carries aBREAKING CHANGE:footer for thehttp-message-signatures2.1 adoption.http-message-signatures2.1 is the reference for these changes, and neither the #7894 profile nor RFC 9421 requires otherwise:adcp.signingrequireshttp-message-signatures2.1.0 or later.sign_request,async_sign_request, andsign_wba_requestraiseValueErrorfor a lowercase method or a URL that isn't valid RFC 3986, such as one with a raw{,}, or|in its query or a malformed percent escape, and the verifiers refuse such a request. The error says to percent-encode the URL;httpxencodes those characters when they come throughparams=, but not when they are written into the URL.@method,@target-uri, and@authority, such as@path, resolves and verifies. The SDK used to refuse it.@target-urifor a URL with no path ends in/, a fragment or userinfo is dropped,@authoritystrips a zero-padded default port, and a target URI that isn't a valid URI is403, the profile's status for a failed verification, instead of400.In the WBA preview
The WBA profile is experimental and this stack targets the 3.3 draft line, so these ship without a breaking-change marker:
adcp.signing.wba.ALG_RSA_PSS_SHA512is removed, andWBA_ALGORITHMSis{"ed25519"}.sign_wba_request,public_directory_key, andsign_key_directoryraiseValueErrorfor an RSA key.verify_wba_requestrefuses aSignature-Agentvalue that isn't the canonical origin string.verify_directory_responseandDiscoveredSignerKeysrefuse a directory served asapplication/jsonorapplication/jwk-set+json, and a directory in which a usable key doesn't sign.AsyncCachingRevocationCheckerand itsfrom_issuer_origintakeallowed_algs, which defaults toALLOWED_JWS_ALGS.Behavior changes in the 3.2 governance path
The governance commits keep every outcome and error code, with the following exceptions. Each one is
governance_token_invalid:critentry whose marker is absent, or isn't a boolean, is refused even when its claim is absent too. RFC 7515 section 4.1.11 forbids a producer to list a name that doesn't occur in the header, and lets a recipient refuse it. security.mdx checklist step 4 requires the refusal.critentry with afalsemarker, or a marker without acritentry. security.mdx checklist step 4 (AdCP 3.2.1 to 3.2.5) requires "each boolean protected-header marker, itscritentry, and its payload claim to appear together".RecursionErroris refused instead of raising.The WBA issuer check shares these rules. On the 3.2 path, a header
algthat isn't a string is fixed in #1539.Spec basis
5c3876073f). The working group hasn't recorded a decision on that page, so if it reverses this, the RSA removal is reverted too. The profile'salgised25519, and the revocation-listalgisEdDSA.algrequired:wba-profile.mdx, Profile rules ("created,expires,keyid,alg, and anonce"; "named inalg, which must match the key").wba-profile.mdxate5929470refuses a directory in which any key carries a private member, checked before unusable keys are skipped (scenarios D14, D14b, and D14c).draft-ietf-webbotauth-httpsig-protocol-00section 5.5.1.Signature-Agent:draft-ietf-webbotauth-httpsig-protocol-00section 5.2.1 (the bare origin, andtype=directory), aswba-profile.mdxate5929470narrows it: the value is exactly the canonical https origin string, compared without normalizing, andtype=directoryis accepted as a Token or a String. The covered member is the one keyed to the signature's label.validateNonceaccepts, and it still does.verify_wba_governance_issuerstays, as the AdCP-layer check that the R4 erratum relies on. Thecritrules follow security.mdx checklist step 4 at AdCP 3.2.1 to 3.2.5.request-signing.mdx("every accepted signature on a request with a body must coverContent-Digest").Interop results
The harness at adcontextprotocol/adcp
306cdfa6(#8298, the CI pin), withhttp-message-signatures2.1.0 installed, passes 64 of 64 checks on this head: tierwba23 of 23, tieradcp26 of 26, D14c included, with no adapter crash. At the earlier pinef966de1, #1523's head (e81d8009) passes 55 of 63.The following table lists every check whose result changed. "Accepted" means the SDK accepted a request or directory that the scenario expects it to refuse:
e81d8009algparameterwba403403Signature-Agentwith a pathadcp403403Signature-Agentorigin with a trailing/adcp403403Signature-Agentorigin with an uppercase hostadcp403403Signature-Agentorigin with an explicit:443adcp403403Signaturevalue is a tokenwba400400application/jsonadcpadcpThe harness prototype, with the same judge, also covers RSA and one more directory case:
e81d8009POSTandGET403Testing
{{TESTING}}
Not in this pull request
jws.parse_general_json_jwsconverts the general JSON form to the compact form (RFC 7515 section 7.2.1).joserfc.jws.extract_general_jsonisn't in that module's__all__, and it doesn't decode the signature.iat,nbf, andexpchecks carry the SDK's rules: finite numbers only, integers only on the WBA path,expafteriat, and a valid verifier clock.JWTClaimsRegistryneeds the same checks plus a mapping of its errors to the AdCP codes.build_signature_base, and the Web Bot Auth diagnostic base, still build a base through the library's protected_build_signature_base. The library has no public call that builds a base for parameters the caller supplies in their own order;tests/conformance/signing/test_rfc9421_library_contract.pypins the hook.Before merge
ADCP_REFinci.ymlto the merge commit of #8298 once it merges.http-message-signatures2.1 adoption: its resolvers read theurlattribute that 2.1 removed, so it breaks as soon as 2.1 installs.nextafter fix(identity): align AdCP 3.3 agent identity with the final spec and the Go and TS SDKs #1514 and refactor(signing): RFC 9421 on http-message-signatures; align Web Bot Auth with adcp#7894 #1523 merge, and let CI run,wba-interopincluded. The WBA module onnextpredates #7894: itsverify_wba_requestrequiresrevocation_checker, which the adapter doesn't pass, and with one passed,nextrefuses every Cloudflare-signed directory.