Repository navigation
Conversation
… 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 #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 #1511 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
I checked this PR at The async Target branch. This PR and #1514 carry code written to the 3.3 drafts. Merge them into a separate branch, such as The following table lists the hand-written code that installed libraries still replace, plus three places where the SDK repeats its own governance JWS code:
The first row also fixes a defect. Verifier step 8 ( The following gaps belong upstream:
I'm happy to finish the cleanup in the table, on top of this PR or in the follow-up, if that helps. |
There was a problem hiding this comment.
Ladon verdict: Approve
Approve — library-migration refactor of the signing/WBA stack with no surviving defects.
This PR migrates RFC 9421 → http-message-signatures, structured fields → http-sf, and JOSE → joserfc, plus #7894 WBA profile alignment. The reviewer verified the load-bearing paths and found no Critical/High/Medium defects:
- wba.py (largest file): one-signature/one-agent fail-closed enforcement, correctly-shaped replay mapping (429+Accept-Signature on replay vs. 429 on capacity), governance SSRF guard load-bearing (issuer==iss, jku==iss, issuer-authorized before key discovery), signature tightened to bare sf-binary.
- No stale imports of the deleted
_structured_fields/split_structured_field. - Digest compare now constant-time;
_rfc9421.verifyfail-closes on every branch. - 17,100 tests + vectors pass; no tests disabled, no blanket ignores.
High-risk flag is true, but every (modified) high-risk file passed review with no medium-or-higher findings, and the one (deleted) file — _structured_fields.py — is a refactor artifact with all references removed and tests green, not a destructive change (its replacement _rfc9421.py is added in the same diff). Row 3's deletion-escalation is aimed at risky removals; this is a verified library swap with full test coverage.
Two non-blocking arbiter notes raised by the reviewer: (1) public params removed from the WBA/server surface and P-256 dropped under refactor:/fix: rather than feat!: — but all are documented in the migration guide, the surface is experimental, and this follows the cited #1514 maintainer decision; (2) verifier.py reorders JWK decode ahead of base build, but no vector hits the combined-malformed path and both inputs still reject. Neither rises to a finding.
No no-auto-approve team match; gated_paths false; no blocking findings. Falls through the decision table to row 9.
|
@KonstantinMirin, thanks for checking it at
#7894 at CI. The PR checks are green. A full CI run dispatched on this branch is green except "Row storage interop with the JavaScript SDK" and its gate. That lane checks out adcp-client at Your new table. It is a cleanup on top of this PR. Yes please: a follow-up stacked here works well. I agree with its first row. The lenient Target branch. A |
Stacked on #1514 (base
claude/issue-1500-identity-3-3-alignment). Two parts: (A) every RFC 9421 layer now comes fromhttp-message-signatures, Structured Fields fromhttp-sf, and the remaining JOSE code fromjoserfc, as @KonstantinMirin's table on #1514 asked; (B) the experimental Web Bot Auth path follows the final adcontextprotocol/adcp#7894 profile ate24eeb09.No prototype branch was shared, so this follows the approach described in the #1514 comment:
http-message-signatures2.0.1 from PyPI with acomponent_resolver_classsubclass that supplies;keyand;req(pyauth/http-message-signatures#29; the upstream fix is pyauth#30).(A) Library migration: every row of the table
src/adcp/signing/_rfc9421.pyis the one module that talks to the libraries:ComponentResolveris thecomponent_resolver_classfor every signer and verifier. It adds;keyand;req, trims field values, refuses control characters, and allows only@method,@target-uriand@authority. Drop thekey/reqbranches once a release carries pyauth#30.AdcpComponentResolverputs the AdCP URL canonicalization in itsget_target_uri/get_authorityoverrides. The Web Bot Auth resolver covers the URI as received.ProfileVerifierturns off the library's wall-clock window check, because each profile checks its window first against the injected clock, with its own codes.29c7018ewba.py:393-506,:589-625,:781-806: signature base, algorithm dispatch,Signature-InputassemblyHTTPMessageSigner.sign/HTTPMessageVerifier.verify._base,_sign,_verifyand_target_uriare gone. The profile checks stay: window, nonce,Signature-Agentmember, origin, key policy, replay, revocation.canonical.py(507):Signature-Inputparser, structured-field split, signature basehttp-sf, the base is the library's, andsplit_structured_fieldis deleted.parse_signature_input_headerandbuild_signature_baseremain as public wrappers. Kept hand-written: the AdCP URL canonicalization (canonicalize_target_uri,canonicalize_authority,canonical_origin). These are AdCP-defined rules (dot segments, percent-encoding, IDNA, the malformed-authority rejections), and they plug into the library through the resolver overrides.signer.py:135-215:Signature-Inputbuilt by f-string, hand-written escapingsign_requestcallsHTTPMessageSigner.sign, and the f-string assembly and sf-string escaping are deleted. The asyncSigningProviderpath needs the base before it awaits the signature, and the library has no public call that returns one. That path usesHTTPMessageSigner's own base builder through its protected subclass hooks (_build_signature_base,_parse_covered_component_ids), pinned bytest_rfc9421_library_contract.pyand<3. Sync and async output match byte for byte (existing test).verifier.py:305-487,webhook_verifier.py: verification overcanonical.pyhttp-sf. Step 10 builds the base and verifies withHTTPMessageVerifier. The checklist order and the mapping of each failure to its AdCP code stay.webhook_verifier.pykeeps its webhook-v1 mixed-alphabet check, a profile rule, now on the shared token walker.crypto.py:156-269: Ed25519/ES256 dispatch, DER to raw,Signatureparsinghttp_message_signatures.algorithms, which also does the ES256r‖sconversion.Signatureis parsed and serialized withhttp-sf. Kept: emitting the 3.0/3.1 Base64URL spelling, which is not RFC 8941 and which no library emits, and the JWK helpers (public API).governance.py:1026-1210,:1443-1477: second compact JWSjoserfc.jws.serialize_compact, verified withdeserialize_compactand split withextract_compact, using_AdcpJwsRegistrywith the three critical-claim markers registered forcrit. Token bytes are unchanged (sorted header, same claims bytes). Kept: strict JSON decoding of the segments (joserfc'sjson.loadskeeps the last duplicate member) and the governance claim checks.publisher_pins.py:14-25: RFC 7638 thumbprintjoserfckeythumbprint(). A key missing a required public member still matches nothing.digest.py: hand-writtenContent-Digestsplithttp-sf.jws.py:144-203: compact and JSON splittingjoserfc.jws.extract_compact. A general-JSON document with one protected signature is the compact form in another shape (RFC 7515 §7.2.1), so it goes through the same extractor.deserialize_jsonwould verify before the profile's header checks run, so it is not used for splitting._structured_fields.py(177),_header_precheck.py:144-178: duplicate-key rejection_structured_fields.pydeletedhttp-sfreports every duplicate dictionary key and parameter throughon_duplicate_key, and the SDK turns that into a step-1 rejection. Kept, small:dictionary_has_parameter, the WBA profile-selection walk. It is about 40 lines onhttp-sf's ownparse_key/parse_item/parse_bare_item, and it is needed because no parser reports an explicittag="web-bot-auth"that a later duplicate overwrites or that a syntax error follows. Also kept: theContent-Typecomma check, which is HTTP field syntax, not RFC 8941.http-sfvdependencyhttp-sfThe legacy 3.0/3.1 Base64URL compatibility also stays hand-written. No RFC 8941 parser accepts that alphabet, so a minimal walker translates those
:…:tokens before the stricthttp-sfparse (translate_legacy_binary). The 3.2 profile never calls it.Behavior notes (3.2 profile, non-experimental):
profile-3.2) and every webhook vector passes with the same error codes.created;keyid;alg;expires;nonce;tag; it wascreated;expires;nonce;keyid;alg;tag). Verifiers rebuild@signature-paramsfrom the received header, so this interoperates, and Cloudflare's signer uses the same order.@signature-paramsis the RFC 8941 serialization of the received member.Signature-Input,SignatureandContent-Digestmust parse as whole Dictionaries.request_signature_header_malformed.parse_compact_jwsrequires a JSON protected header withalg.(B) Web Bot Auth aligned with adcp#7894 (
e24eeb09)Source:
docs/building/by-layer/L1/wba-profile.mdxandstatic/compliance/source/test-vectors/wba-profile/ate24eeb09, plus the replay decision.web-bot-authsignature and exactly oneSignature-Agentmember, keyed to its label. A second signature or member gets 403 withAccept-Signature(negative/006-two-signatures). The relay coverage rules are deleted.verify_wba_requeststill returns a tuple, now always of one signer, so callers unpack unchanged.DiscoveredSignerKeys(require_signed_directories=)andverify_directory_response(require_signed=)are removed.ed25519andrsa-pss-sha512. The ES256 branch is removed from Web Bot Auth: a P-256 key is refused bypublic_directory_key,sign_wba_request,sign_key_directory, andSigningConfig/sign_requestwithagent_origin, and skipped in a directory.ecdsa-p256-sha256stays on the 3.2 profile.required_foroperations or for reads: a short nonce gets 403 withAccept-Signature.Accept-Signaturecarrying a fresh 64-byte nonce. Other signature failures get 403 withAccept-Signature, which now namesalgand carries a barenonceparameter, as in the page's example. Local capacity refusals stay 429 without a challenge. The 403 compatibility switch is removed:verify_wba_request(replay_status=)andRequestSignatureVerification(wba_replay_status=)are gone.web-bot-authanswers 403 withAccept-Signature. One that also advertises the RFC 9421 profile answers 401request_signature_requiredplusAccept-Signature.e24eeb09, governance vectors included.UPSTREAM.jsonrecords each file's SHA-256, a test checks them, and the upstream README is kept asUPSTREAM-README.md. All pass, see below. The governance vectors use the newadcp.signing.wba.verify_wba_governance_issuer(One agent per purpose): thekidmust be in theissorigin's directory withjkuequal to that directory (governance_key_unknown), andissmust be the buyer's governance agent (governance_issuer_not_authorized). The issuer is checked before key discovery, so a token cannot make the verifier fetch a directory from an unlisted origin. The JWS checks, thecritmarker pairing of checklist step 4, the signature (joserfc) and the time checks run around those two.docs/agent-identity-3-3-migration.mdandCHANGELOG.md(Unreleased) are updated. This is an experimental surface, so per the maintainer decision on fix(identity): align AdCP 3.3 agent identity with the final spec and the Go and TS SDKs #1514 there is no BREAKING CHANGE marker. The removed options are listed under "Breaking changes to the experimental API" in the migration guide.Vector and interop results
e24eeb09: 2 positive, 6 negative and 3 governance vectors pass. Negative statuses andAccept-Signature(label, components, parameters, and a verifier-chosen nonce on the 429) matchexpected_outcome. Every vector'sexpected_signature_baseequals the basehttp-message-signaturesbuilds. Signing the committed base with the committed seed reproduces the committedSignature.profile-3.2vectors pass with byte-identical error codes. All webhook vectors pass too.wba-interopfixtures pass unchanged: Cloudflareweb-bot-auth@0.2.0-signed requests andhttp-message-sigdirectories, adcp-client-signed requests and directories, the percent-encoded paths, and the draft's Appendix E RSA-PSS vector. All four fixture directories are signed, so none conflicts with the signed-directory rule. One spelling difference: Cloudflare's signer emits a padded standard-base64 nonce where the page says base64url, and the verifier accepts both encodings of exactly 64 bytes.Dependency changes
http-message-signatures>=2.0.1,<3(Apache-2.0, depends only oncryptography).http-sf>=1.4.0,<2(MIT; the maintained successor tohttp-sfv;on_duplicate_key).http-sfv(archived).uv.lockis updated. A mypy override markshttp_message_signaturesre-exports as implicit, because the package shipspy.typedwithout__all__.Validation
pytest: 17100 passed, 2618 skipped, 1 xfailed (with thepgextra installed). The base branch gave 17030 passed in the same environment withoutpg.ruff,black,mypy src/adcp,mypy --strict tests/type_checks, Pyright adopter fixtures (62), the type-ignore contract,make validate-generatedandpre-commitare clean.Content-Digestandcontent-digestcould make the digest check and the library read different values. The library request now takes the first spelling, as the profile lookups do, and step 1 refuses a signed field that appears under two spellings.verify_wba_governance_issuerrejectedcrit-bearing action tokens. It now also checks the issuer before discovery.Signaturemember parameters and a non-truereqare refused.Content-Digestspelling is still tolerated on every profile. It was before too, and both spellings decode to the bytes the signature covers.Closes #1511
🤖 Generated with Claude Code