Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
a8a1d92
feat(identity): pin identity preview schemas to the final spec branches
bokelley Oct 15, 2026
bb4e56f
fix(identity): align Web Bot Auth, trust records and grants with the …
bokelley Oct 15, 2026
d78ec22
docs(identity): document the layered Web Bot Auth and trust outage rules
bokelley Oct 15, 2026
cb56200
fix(identity): address protocol and security review of the identity a…
bokelley Oct 15, 2026
85b569b
test(identity): assert revocation LRU eviction order exactly
bokelley Oct 15, 2026
160e202
refactor(identity): explain ignored nonce decode errors
bokelley Oct 15, 2026
6e6c828
fix(identity): accept short WBA nonces and inject the binding authorizer
bokelley Oct 15, 2026
29c7018
refactor(signing): take JWS, JWK and Standard Webhooks from maintaine…
bokelley Oct 16, 2026
9aad42f
refactor(signing): RFC 9421 on http-message-signatures; align Web Bot…
bokelley Oct 17, 2026
e81d800
fix(signing): address review of the RFC 9421 library migration
bokelley Oct 17, 2026
8821f34
test(signing): grade Web Bot Auth against the Cloudflare reference
KonstantinMirin Oct 18, 2026
99ce1a6
fix(signing): verify Web Bot Auth as standard WBA does
KonstantinMirin Oct 18, 2026
51f0bfa
fix(signing): decode JWKs and base64url strictly with joserfc
KonstantinMirin Oct 18, 2026
615055d
refactor(signing): take WBA keys and signature fields from the libraries
KonstantinMirin Oct 18, 2026
ee94e3b
test(signing): pin which Signature-Agent member types the verifier reads
KonstantinMirin Oct 18, 2026
e10a3a0
fix(signing): refuse a malformed member in any signature field
KonstantinMirin Oct 18, 2026
4def0dc
fix(signing): accept only the canonical origin string as Signature-Agent
KonstantinMirin Oct 18, 2026
021824b
test(signing): grade the governance issuer check outside the WBA profile
KonstantinMirin Oct 18, 2026
3317d99
test(signing): vendor the wba-profile vectors from the #7894 head
KonstantinMirin Oct 18, 2026
e682502
docs(signing): name the pyauth issue behind the async signature-base …
KonstantinMirin Oct 18, 2026
973bd55
refactor(governance): split and parse governance tokens with the shar…
KonstantinMirin Oct 18, 2026
cc005c2
refactor(governance): check crit markers once, with joserfc
KonstantinMirin Oct 18, 2026
5f21c22
refactor(governance): build one governance JWS registry
KonstantinMirin Oct 18, 2026
0c76abb
fix(signing): read a public JWK's key from its public members only
KonstantinMirin Oct 18, 2026
4324ba2
style(governance): restore the black formatting of the replay binding…
KonstantinMirin Oct 18, 2026
8057624
fix(governance): require each critical marker, its crit entry, and it…
KonstantinMirin Oct 18, 2026
68d74a8
refactor(signing): drop rsa-pss-sha512 from the RFC 9421 algorithm table
KonstantinMirin Oct 18, 2026
ff41c6e
test(signing): vendor the wba-profile vectors from the current #7894 …
KonstantinMirin Oct 18, 2026
f52410f
ci(signing): run the wba-interop harness at the current #8298 head
KonstantinMirin Oct 18, 2026
baebdb5
fix(signing): refuse a PS512 governance token before key discovery
KonstantinMirin Oct 18, 2026
528d5eb
fix(signing): answer a non-string WBA governance alg with governance_…
KonstantinMirin Oct 19, 2026
6f96e57
refactor(signing): split WBA governance tokens with the governance sp…
KonstantinMirin Oct 18, 2026
4ca8042
docs(signing): point the governance issuer check at its vectors' new …
KonstantinMirin Oct 18, 2026
90e59ea
test(signing): refuse a directory that publishes a key's private members
KonstantinMirin Oct 18, 2026
2fb5e4e
test(signing): grade against #8298 at ef966de1 and the #7894 vectors …
KonstantinMirin Oct 18, 2026
0b75188
fix(signing): ask for the key directory's media type only
KonstantinMirin Oct 19, 2026
c82d27a
fix(signing): read a private JWK from d, and check x against it
KonstantinMirin Oct 19, 2026
4635f75
fix(signing): verify a Web Bot Auth origin's revocation list as EdDSA…
KonstantinMirin Oct 19, 2026
67a844f
test(signing): refuse a directory that spells a private member in upp…
KonstantinMirin Oct 19, 2026
ab4f883
test(signing): let the interop adapter crash on anything but an SDK r…
KonstantinMirin Oct 19, 2026
369ddf3
docs(signing): list the stricter JWK and base64url decoding for adopters
KonstantinMirin Oct 19, 2026
40155e7
docs(signing): state the basis for signing Web Bot Auth with Ed25519 …
KonstantinMirin Oct 19, 2026
8ccabed
refactor(signing)!: adopt http-message-signatures 2.1 and drop the SD…
KonstantinMirin Oct 19, 2026
4de1237
test(signing): grade against #8298 at 306cdfa6 and the #7894 vectors …
KonstantinMirin Oct 19, 2026
275a067
test(reporting): load the filesystem row provider from @adcp/reportin…
KonstantinMirin Oct 19, 2026
6637374
Merge remote-tracking branch 'origin/next' into claude/issue-1500-ide…
bokelley Oct 19, 2026
88a67d6
Merge claude/issue-1500-identity-3-3-alignment (with origin/next) int…
bokelley Oct 19, 2026
87f3956
Merge origin/next (#1514 squash) into claude/issue-1511-rfc9421-library
bokelley Oct 19, 2026
4e0eb32
Merge #1523's branch (next, and the 2.0 pin) into fix/wba-standard-in…
KonstantinMirin Oct 19, 2026
e587440
Merge branch 'next' into fix/wba-standard-interop
KonstantinMirin Oct 19, 2026
e5c8207
fix(build): keep next's mypy TypeForm setting and 3.2.4 compliance da…
KonstantinMirin Oct 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,73 @@ jobs:
python -m pytest -n0 -p no:cacheprovider -v -ra \
tests/conformance/reporting/test_reporting_row_storage_interop_pg.py

wba-interop:
needs: changes
if: needs.changes.outputs.run_tests == 'true'
name: Web Bot Auth interop with the Cloudflare reference
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
# PURPOSE: the SDK's Web Bot Auth signer and verifier must interoperate with standard WBA.
# The AdCP harness (adcp/tests/wba-interop) grades tests/wba_interop/adapter.py against
# Cloudflare's web-bot-auth, pinned exactly in the harness lockfile, in both directions:
# the reference signs and the SDK verifies, and the SDK signs and the reference verifies.
env:
# adcontextprotocol/adcp commit that carries tests/wba-interop and the wba-profile
# vectors: the head of adcontextprotocol/adcp#8298. Pin its merge commit once it
# merges, and a release tag once one carries the harness.
ADCP_REF: "306cdfa6e565725e1a122838eafbcc980707306b"
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- name: Check out the AdCP harness at ADCP_REF
uses: actions/checkout@v6
with:
repository: adcontextprotocol/adcp
ref: ${{ env.ADCP_REF }}
path: adcp
filter: blob:none
sparse-checkout: |
tests/wba-interop
static/compliance/source/test-vectors/wba-profile
persist-credentials: false
- uses: actions/setup-python@v6
with:
python-version: "3.12"
- name: Set up uv
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
with:
version: "0.12.8"
enable-cache: true
cache-dependency-glob: pyproject.toml
cache-suffix: "3.12"
prune-cache: true
- name: Set up Node 24
uses: actions/setup-node@v6
with:
# The harness's package.json requires Node 24.
node-version: "24"
cache: npm
cache-dependency-path: adcp/tests/wba-interop/package-lock.json
- name: Install the harness
run: npm ci --prefix adcp/tests/wba-interop --ignore-scripts
- name: Run the harness against the SDK adapter
run: |
node adcp/tests/wba-interop/run.mjs \
--adapter "uv run python tests/wba_interop/adapter.py" \
--vectors adcp/static/compliance/source/test-vectors/wba-profile \
--junit wba-interop.xml
- name: Preserve the harness report
if: always()
uses: actions/upload-artifact@v7
with:
name: wba-interop-${{ github.run_attempt }}
retention-days: 7
path: wba-interop.xml
if-no-files-found: error

pg-conformance-required-gate:
name: Postgres conformance tests (Postgres 16)
runs-on: ubuntu-latest
Expand Down
13 changes: 6 additions & 7 deletions docs/agent-identity-3-3-migration.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Experimental AdCP 3.3 agent identity

This SDK supports an opt-in preview of issue [#1500](https://github.com/adcontextprotocol/adcp-client-python/issues/1500). The Web Bot Auth wire profile follows `wba-profile.mdx` in adcontextprotocol/adcp#7894 at `e24eeb09` (vectors in `tests/conformance/vectors/wba-profile/`, provenance in its `UPSTREAM.json`); the identity schemas are pinned to the commits in `src/adcp/signing/_identity_schemas/UPSTREAM.json`. Both may change before the protocol release. The stable wire pin and `COMPATIBLE_ADCP_VERSIONS` do not advertise 3.3 yet.
This SDK supports an opt-in preview of issue [#1500](https://github.com/adcontextprotocol/adcp-client-python/issues/1500). The Web Bot Auth wire profile follows `wba-profile.mdx` in adcontextprotocol/adcp#7894 at `e5929470` (vectors in `tests/conformance/vectors/wba-profile/`, provenance in its `UPSTREAM.json`); the identity schemas are pinned to the commits in `src/adcp/signing/_identity_schemas/UPSTREAM.json`. Both may change before the protocol release. The stable wire pin and `COMPATIBLE_ADCP_VERSIONS` do not advertise 3.3 yet.

The behavior below follows the final spec decisions (adcontextprotocol/adcp#8162, #8168, #8169, #8170) and matches the Go (`adcp-go`) and TypeScript (`@adcp/sdk/identity-3.3`) SDKs, except where [Differences from the other SDKs](#differences-from-the-other-sdks) says otherwise.

Expand Down Expand Up @@ -29,7 +29,7 @@ signing = SigningConfig(

Automatic signing also requires `web-bot-auth` in the seller's `request_signing.profiles`; omission advertises legacy support only. An unsupported configured profile raises before sending and never silently downgrades. Keep header emission opt-in until all counterparties tolerate the additional covered component.

Web Bot Auth signs with `ed25519` or `rsa-pss-sha512`, the profile's two algorithms. `sign_wba_request`, `public_directory_key` and `sign_key_directory` accept Ed25519 keys and RSA keys of 2048 to 8192 bits (signed with `rsa-pss-sha512`, as in the draft's Appendix E vectors). ES256 (P-256) stays on the AdCP RFC 9421 profile: a P-256 key is refused for Web Bot Auth, and a P-256 key in someone's directory is skipped.
Web Bot Auth signs with `ed25519`, the profile's one algorithm, following the current #7894 page (`5c3876073f`). The working group hasn't recorded a decision on that page; if it restores another algorithm, this SDK follows it. `sign_wba_request`, `public_directory_key` and `sign_key_directory` accept Ed25519 keys only. ES256 (P-256) stays on the AdCP RFC 9421 profile: a P-256 or RSA key is refused for Web Bot Auth, and such a key in someone's directory is ignored, with its signature. Every signature names `alg`; a signature without it is refused.

Serve the key directory at `/.well-known/http-message-signatures-directory` on that origin. The profile requires it signed (WBA Appendix B.1); verifiers refuse an unsigned directory:

Expand Down Expand Up @@ -99,18 +99,17 @@ Duplicate structured-field parameters, dictionary labels, and `Signature-Agent`

### Interop

- **One signature.** A request carries exactly one `web-bot-auth` signature and exactly one `Signature-Agent` member, keyed to that signature's label. Relays are gone: an intermediary that signs is the signer, and whether it may act for another party is an authorization question, not a second signature. `verify_wba_request` still returns a tuple, now always of one signer. `Signature-Agent` is read as a Structured Fields Dictionary only; the older bare sf-string form is not part of the final profile.
- **One signature.** A request carries exactly one `web-bot-auth` signature and exactly one `Signature-Agent` member, keyed to that signature's label. Relays are gone: an intermediary that signs is the signer, and whether it may act for another party is an authorization question, not a second signature. `verify_wba_request` still returns a tuple, now always of one signer. `Signature-Agent` is read as a Structured Fields Dictionary only; the older bare sf-string form is not part of the final profile. The member value is the signer's https origin in canonical form, compared as a string: a lowercase host, no default port, no trailing `/`, and no path, query, fragment, or userinfo. Any other spelling is refused with 403 before key discovery, and `sign_wba_request` sends the canonical form. A `Signature-Agent` member whose `type` parameter is not `directory` (a Token or a String) is ignored, so the signature has no member keyed to its label (403). Every `Signature-Input` member must be an inner list and every `Signature` member a Byte Sequence with no parameters, including members with other tags that the verifier doesn't read; otherwise the field is malformed (400). Key directory verification applies the same rule.
- **Nonce.** The nonce encodes exactly 64 bytes. The profile spells it as unpadded base64url; padded standard base64 of 64 bytes is also accepted, because Cloudflare's `web-bot-auth` signer and the draft's Appendix E vectors send it. The page has no shorter-nonce exception for `required_for` operations or reads.
- `@target-uri` is the target URI as received, built from the request line (`raw_path` and query string), so percent-encoding is preserved. `@authority` is that URI's authority, lowercased with any default port removed (RFC 9421 §2.2.3).
- RSA keys verify with `rsa-pss-sha512` (2048 to 8192 bits, odd exponent).
- `tests/conformance/vectors/wba-interop/` vendors adcp-client's Cloudflare interop fixtures with provenance; the SDK verifies both the Cloudflare-signed and the adcp-client-signed requests and directories.

### Key directories

`DiscoveredSignerKeys` resolves `(covered origin, thumbprint)`. It never calls capabilities or reads brand.json or trust.json.

- A directory must be signed (WBA Appendix B.1); an unsigned one is a discovery failure. The media type may be `application/http-message-signatures-directory+json`, `application/jwk-set+json` or `application/json`. Keys are found by their RFC 7638 thumbprint; a `kid` is optional and does not affect lookup. Keys the profile cannot use (P-256 included) are skipped; private key material fails the directory.
- Every directory signature must verify, cover at least the exact identifiers `"@authority";req` and `"content-digest"` (a keyed member or a bare `"@authority"` does not count), and fall inside its window (60 seconds of skew on `created`, none on `expires`). The digest covers the exact bytes served, a UTF-8 BOM included. Only keys with a verified signature are usable; an unsigned key beside them is ignored.
- A directory must be signed (WBA Appendix B.1); an unsigned one is a discovery failure. The media type must be `application/http-message-signatures-directory+json` (WBA section 5.5.1); any other media type fails the directory. Keys are found by their RFC 7638 thumbprint; a `kid` is optional and does not affect lookup. Keys the profile cannot use (P-256 and RSA included) are ignored, and so are their signatures (RFC 7517 section 5); private key material fails the directory.
- Every directory signature must verify, cover at least the exact identifiers `"@authority";req` and `"content-digest"` (a keyed member or a bare `"@authority"` does not count), and fall inside its window (60 seconds of skew on `created`, none on `expires`). The digest covers the exact bytes served, a UTF-8 BOM included. Every usable key signs (one signature per key, Appendix B.1): a usable key without a verified signature fails the directory.
- **Discovery failures** (WBA §6.10) cover a directory that does not resolve: a network or TLS error, a timeout, any status other than 200 (404, 410, 429 and 5xx included), or an invalid directory. A discovery failure never evicts a cached directory: its keys keep verifying for up to `max_stale_seconds` (24 hours by default) after the last successful fetch, never past the directory signatures' expiry. A key missing from such a stale copy is a discovery failure too. Failures are retried after a 60-second cooldown.
- With no usable cached copy, the request is unverified, and what happens is local policy: `RequestSignatureVerification(wba_discovery_failure=...)`. The default `"reject"` answers 403 with `Accept-Signature`. `"anonymous"` serves the request exactly as an unsigned one, except operations in `required_for` or `protocol_methods_required_for`.
- A directory that resolves without the key is evidence that the key was removed: a signature failure.
Expand Down Expand Up @@ -236,7 +235,7 @@ Onboarding posture is advisory. Sellers still implement requirements, applicatio

AdCP 3.3 deprecates the RFC 9421 profile; it keeps working through 3.x. `SigningConfig` and `RequestSignatureVerification` emit a `DeprecationWarning` for it only once the SDK advertises 3.3 (`COMPATIBLE_ADCP_VERSIONS`), so legacy constructions stay quiet while 3.3 is not served. `async_resolve_agent`, `verify_from_agent_url`, BrandJsonJwksResolver and governance/rights/document JWS verification retain their existing chain until AdCP 3.4.

**Governance tokens under Web Bot Auth.** `adcp.signing.wba.verify_wba_governance_issuer(token, signer_keys=..., governance_agents=...)` runs the profile's two governance checks (One agent per purpose): the token's `kid` is in the key directory of the `iss` origin and `jku` is that directory's URL (`governance_key_unknown`), and `iss` is the buyer's governance agent (`governance_issuer_not_authorized`), with the JWS, signature and time checks around them. It returns the issuer origin. The rest of the governance checklist (`aud`, `phase`, `sub`, the action binding, revocation, replay) stays with the caller. The `governance/` vectors grade it. Roll back opt-in signing only for origins that have not been bound; rolling a bound origin back to RFC 9421 requires explicit seller migration.
**Governance tokens under Web Bot Auth.** `adcp.signing.wba.verify_wba_governance_issuer(token, signer_keys=..., governance_agents=...)` runs the profile's two governance checks (One agent per purpose): the token's `kid` is in the key directory of the `iss` origin and `jku` is that directory's URL (`governance_key_unknown`), and `iss` is the buyer's governance agent (`governance_issuer_not_authorized`), with the JWS, signature and time checks around them. It returns the issuer origin. The rest of the governance checklist (`aud`, `phase`, `sub`, the action binding, revocation, replay) stays with the caller. The vectors in `tests/conformance/vectors/wba-governance/` grade it. Roll back opt-in signing only for origins that have not been bound; rolling a bound origin back to RFC 9421 requires explicit seller migration.

## Differences from the other SDKs

Expand Down
15 changes: 15 additions & 0 deletions docs/request-signing-migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,21 @@ Two ways across:

The same function is the identifier comparer for `adagents.json` `authorized_agents[].url`, `brand.json` `agents[].url`, TMP `seller_agent.agent_url`, `format-id.agent_url` and the governance issuer. There the change is strictly more permissive: `https://agent.example.com` and `https://agent.example.com/` now compare equal, where before a publisher who registered the first did not match a caller who arrived as the second, and the refusal named authorization rather than canonicalization. No registration needs editing, and no pair that matched before stops matching.

## 7. Upgrading to strict JWK and base64url decoding

These `adcp.signing` functions decode keys more strictly than they did in 9.x:

| Function | Change |
| --- | --- |
| `b64url_decode` | Accepts only RFC 7515 base64url: the URL-safe alphabet, no padding, and the one spelling `b64url_encode` produces. Padding, `+` or `/`, other characters, and non-zero trailing bits raise `ValueError`. |
| `public_key_from_jwk` | Decodes the members with `joserfc` and reads the public members only. Apart from the strict decoding above, it refuses the same keys, but the `ValueError` text changes, for example from `Ed25519 JWK x has length 3, expected 32` to `unusable OKP/Ed25519 JWK: ...`. |
| `private_key_from_jwk` | Reads `d` alone, so a `d`-only JWK still loads. When `x` (or `y`) is present and isn't the public key of `d`, it raises `ValueError` instead of returning the key that `d` names. |
| `publisher_pins.jwk_thumbprint` | Returns the RFC 7638 thumbprint as its base64url `str`, not the digest `bytes`. |

A key published with padded or standard-alphabet base64 no longer verifies, so check that your JWKS and `adagents.json` entries use unpadded base64url. Code that matched error text, or compared `jwk_thumbprint` with digest bytes, needs updating.

Signing and verification use `http-message-signatures` 2.1, which parses the URL per RFC 3986, so a URL with a raw `{`, `}`, or `|` in its query can't be signed: percent-encode it. `httpx` encodes those characters when they come through `params=`, but not when they are written into the URL.

## Related

- [`adcp.signing.install_signing_event_hook`](../src/adcp/signing/client.py) — buyer-side preset for adapters not using `ADCPClient`.
Expand Down
13 changes: 6 additions & 7 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -140,16 +140,15 @@ dependencies = [
"idna>=3.18,<4",
# RFC 9421 HTTP Message Signatures for every signing profile
# (``adcp.signing._rfc9421``): signature bases, ``Signature-Input``
# assembly, and the ed25519 / ecdsa-p256-sha256 / rsa-pss-sha512
# algorithms. The SDK supplies a ``component_resolver_class`` and reaches
# two protected subclass hooks (``_build_signature_base``,
# assembly, the ;key and ;req component parameters, prepare/apply for an
# awaited signature, and the ed25519 / ecdsa-p256-sha256 algorithms. 2.1
# is the floor: it resolves ;key and ;req (pyauth#30) and adds
# prepare/apply (pyauth#31). The diagnostic base builder reaches two
# protected hooks (``_build_signature_base``,
# ``_parse_covered_component_ids``); the upper bound and
# tests/conformance/signing/test_rfc9421_library_contract.py guard them.
# 2.1.0 (2026-10-19) rewrote the component resolver (it no longer exposes
# ``url``, and it resolves ``;key`` and ``;req`` itself), so this layer is
# capped below it until the SDK moves to 2.1 (#1537 carries that move).
# Apache-2.0, depends only on cryptography.
"http-message-signatures>=2.0.1,<2.1",
"http-message-signatures>=2.1.0,<3",
# RFC 8941 / RFC 9651 Structured Fields (successor to the archived
# http-sfv). 1.4.0 is the tested floor; ``on_duplicate_key`` lets the SDK
# refuse a duplicate dictionary key or parameter instead of keeping the
Expand Down
1 change: 1 addition & 0 deletions scripts/ci/ci_scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
JOB_GROUPS = {
"test": "run_tests",
"schema-check": "run_tests",
"wba-interop": "run_tests",
"pg-conformance": "run_postgres",
"pg-reporting-status": "run_postgres",
"pg-reporting-materializer": "run_postgres",
Expand Down
Loading
Loading