Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 11 additions & 0 deletions .changeset/webhook-mode-capability-only-selection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"adcontextprotocol": minor
---

Webhook-signing mode is now selected only by the seller's `legacy_hmac_fallback` capability and its per-counterparty onboarding record, not by a field in the registering request's body. Condition (b) of `request_signature_required` ("payload carries a field that triggers signing regardless of `required_for`") and the per-request `authentication` block as a mode trigger are deprecated in 3.x and removed in 4.0. The mode no longer comes from the body; rejection with the new code still reads the body, but it never changes mode or escalates authentication. This resolves the conflict with the "policy before body parsing" rule (adcp#7563). During the transition, a seller with `legacy_hmac_fallback: false` rejects a registration carrying `authentication.credentials` with the new `webhook_hmac_not_supported` (correctable by changing the registration, not retryable as-is) (HTTP 400) instead of `request_signature_required`; the code is added to `request-signing-error-code.json` on both `enumDescriptions` and `enumMetadata`. Sellers with `legacy_hmac_fallback: true` and no onboarding record keep the 3.x behavior. The documented-exception alternative is recorded as rejected: it makes the verifier parse unauthenticated bytes to decide whether they required authentication.

A verifier now rejects a JSON-RPC batch (a JSON array body) with `request_body_malformed` (SHOULD in 3.2.x, MUST from 3.3, per the existing hardening schedule), replacing the 3.2 interim rule that let a verifier resolve every element (adcp#7565). A signed AdCP request carries exactly one JSON-RPC message.

Conformance: vector `request-signing/negative/027` (and its 3.2 mirror) now declares `applies_when: "webhook_signing.legacy_hmac_fallback == true"` and is graded only for such agents. The functional create fixtures in `media_buy_seller` (two steps), `create_media_buy_async`, `creative_generative/seller` and `sales_broadcast_tv` no longer send `push_notification_config.authentication`, so a signing seller with `legacy_hmac_fallback: false` is not forced to fail either 027 or an unrelated create step. Webhook-emission grading of the 9421 baseline is unchanged.

Composition with the 3.3 rule that `reporting_webhook` and `artifact_webhook` may omit `authentication` (adcp#8177): the same capability-only selection applies to every webhook registration. On those two registrations the new code and condition (b) apply only to a request served at 3.3 or later, because `authentication` stays required on a request served below 3.3 and so carries no preference signal there.
2 changes: 1 addition & 1 deletion docs/accounts/tasks/sync_accounts.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ Each entry has:
- `url` — HTTPS endpoint URL. Sellers MUST complete an endpoint activation challenge or equivalent proof-of-control before treating a new or changed active subscriber as active.
- `event_types[]` — types the subscriber wants. Only account-anchored types are permitted (today: `creative.status_changed`, `creative.assignment_changed`, `indicators.changed`, `creative.purged`, `account.status_changed`, `account.change_recorded`, `product.created`, `product.updated`, `product.priced`, `product.removed`, `signal.created`, `signal.updated`, `signal.priced`, `signal.removed`, `wholesale_feed.bulk_change`, and the experimental `collection_card.updated`, `collection_card.removed` and `collection_catalog.bulk_change`, which a buyer registers only with a seller that lists them in `wholesale_feed_webhooks.event_types`; see [`list_collections`](/docs/media-buy/task-reference/list_collections#collection-card-webhooks)). Sellers MUST reject any media-buy-anchored type (`scheduled`, `final`, `delayed`, `adjusted`, `window_update`, `impairment`) or agent-anchored type (`capabilities.changed`) as a per-account validation failure with [`INVALID_REQUEST`](/docs/building/verification/compliance-catalog#error-code-invalid-request) or [`VALIDATION_ERROR`](/docs/building/verification/compliance-catalog#error-code-validation-error) in `accounts[].errors[]`, and `error.field` MUST point at the invalid `event_types` entry.
- `product_payload_view` — `canonical` for a [`list_products`](/docs/media-buy/task-reference/list_products) mirror or `legacy` for the 3.x `get_products` shape. Omission defaults to `legacy`; valid only when a product event is selected.
- `authentication` (optional) — legacy Bearer or HMAC-SHA256. Omit to use the default RFC 9421 webhook profile. When present, the same signed-registration downgrade-resistance rules as `push_notification_config.authentication` apply. Credentials are write-only — sellers omit them on reads.
- `authentication` (optional) — legacy Bearer or HMAC-SHA256. Omit to use the default RFC 9421 webhook profile; presence as a mode trigger is deprecated in 3.x, and a seller with `legacy_hmac_fallback: false` rejects it with `webhook_hmac_not_supported`. When present, the same signed-registration downgrade-resistance rules as `push_notification_config.authentication` apply. Credentials are write-only — sellers omit them on reads.
- `active` (default `true`) — set `false` to pause a subscriber without removing the registration. Sellers MAY skip only the outbound proof challenge while `active: false`; they MUST still enforce HTTPS parsing, hostname normalization, and reserved-range rejection on write. Paused subscribers MUST NOT receive fires until reactivated. Reactivation MUST repeat full SSRF validation with connect pinning plus proof-of-control for any tuple without current valid proof.

### Endpoint proof of control
Expand Down
2 changes: 1 addition & 1 deletion docs/building/by-layer/L1/request-signing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -531,7 +531,7 @@ async def mcp(request: Request):

`authenticate_fallback`, `auth_error`, and `handle_mcp` stand for your own code, which is why the block is marked `test=false`. Some points the example depends on:

- **Unsigned requests.** `authenticate_fallback` returns `None` for a credential you don't accept. It also has to refuse an operation where signing must be unconditional (the strict posture), and a payload that carries a webhook `authentication` block when you support signing. Those cases get `request_signature_required` too ([Composition with fallback authenticators](/docs/building/by-layer/L1/security#composition-with-fallback-authenticators)).
- **Unsigned requests.** `authenticate_fallback` returns `None` for a credential you don't accept. It also has to refuse an operation where signing must be unconditional (the strict posture), and, while `legacy_hmac_fallback` is `true` (deprecated in 3.x), a payload that carries a webhook `authentication` block when you support signing. Those cases get `request_signature_required` too; a seller with `legacy_hmac_fallback: false` answers that block with `webhook_hmac_not_supported` ([Composition with fallback authenticators](/docs/building/by-layer/L1/security#composition-with-fallback-authenticators)).
- **One strict parse.** The SDK verifier doesn't run the step-14 duplicate-key check, so the example parses the body once with duplicate keys rejected and hands `envelope` to `handle_mcp`. Business logic never parses the body again.
- **Key rotation.** A `kid` published between rebuilds fails with `request_signature_key_unknown` until the next rebuild, so rebuild at least as often as your JWKS cache TTL. Never fall back to trying every agent's keys.
- **Blocking fetches.** `StaticJwksResolver` never fetches, which keeps the request path off the network. `CachingJwksResolver(jwks_uri=...)` refetches on a `kid` miss, but it fetches synchronously. If you use it in an async handler, prime it at startup or call `verify_request_signature` through `run_in_threadpool`.
Expand Down
Loading
Loading