From ff716ba75bb65a6596741f66d93bc9385492a715 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sun, 19 Apr 2026 22:52:33 -0400 Subject: [PATCH 1/2] spec(webhook-verifier): webhook_mode_mismatch + webhook_target_uri_malformed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two named error codes and one inlined rule on the webhook verifier path. **webhook_mode_mismatch** — promote the "MUST alarm, not silently downgrade" bullet on received-signature-vs-registered-auth mismatch to MUST reject with a stable code. Alarm-without-reject leaves the payload accepted by the mismatched scheme; 401 + stable code lets sender-side retry logic route mismatch to incident response rather than replay identically. **webhook_target_uri_malformed on step 10** — inline the @authority derivation rule that already exists in the request-signing profile but was easy to miss when implementing from the webhook checklist alone. Verifiers MUST derive @authority from the as-received Host header (or HTTP/2+ :authority pseudo-header), NOT from reverse-proxy routing state, and reject with webhook_target_uri_malformed if the canonicalized @authority does not byte-match the authority component of the canonical @target-uri. Closes the cross-vhost replay vector (attacker intercepts a TLS-terminated webhook and replays to a second vhost on the same verifier pool: same cert SAN, different Host). Retry-semantics paragraph broadened from webhook_signature_* to webhook_* so the two new structural codes are covered by the terminal-failure + incident-routing guidance. Extracted from #2433 as a standalone editorial + taxonomy fix. No wire schema change. Co-authored-by: Emma Mulitz Co-Authored-By: Claude Opus 4.7 (1M context) --- .../webhook-verifier-mode-authority-clarifications.md | 10 ++++++++++ docs/building/implementation/security.mdx | 10 ++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) create mode 100644 .changeset/webhook-verifier-mode-authority-clarifications.md diff --git a/.changeset/webhook-verifier-mode-authority-clarifications.md b/.changeset/webhook-verifier-mode-authority-clarifications.md new file mode 100644 index 0000000000..b3b3cd183d --- /dev/null +++ b/.changeset/webhook-verifier-mode-authority-clarifications.md @@ -0,0 +1,10 @@ +--- +--- + +Two named error codes and one explicit rule on the webhook verifier checklist: + +- **`webhook_mode_mismatch`.** Upgrade the "buyers MUST alarm, not silently downgrade" rule to MUST reject with the new stable code when the signature mode on a received webhook does not match the mode the buyer registered with (`authentication.credentials` present vs absent). Alarm-without-reject leaves the payload accepted by the mismatched scheme; the 401 + stable code lets sender-side retry logic route mismatch to incident response rather than replay identically. +- **`webhook_target_uri_malformed` on step 10.** Inline the `@authority` derivation rule that already exists in the request-signing profile but was easy to miss when implementing from the webhook checklist alone: verifiers MUST derive `@authority` from the as-received HTTP `Host` header (or HTTP/2+ `:authority` pseudo-header), NOT from reverse-proxy routing state, and reject with `webhook_target_uri_malformed` if the canonicalized `@authority` does not byte-match the authority component of the canonical `@target-uri`. Closes the cross-vhost replay vector (attacker intercepts a TLS-terminated webhook and replays to a second vhost on the same verifier pool: same cert SAN, different `Host`). +- **Retry-semantics paragraph broadened** from `webhook_signature_*` to `webhook_*` so the two new structural codes are covered by the terminal-failure + incident-routing guidance. + +Extracted from PR #2433 as a standalone editorial + taxonomy fix. No wire-schema change; no client-side breakage for implementations that already verified correctly. diff --git a/docs/building/implementation/security.mdx b/docs/building/implementation/security.mdx index fc6a91f4c5..5560516052 100644 --- a/docs/building/implementation/security.mdx +++ b/docs/building/implementation/security.mdx @@ -1065,10 +1065,10 @@ Buyers MUST NOT derive signer identity from webhook payload fields (`task_id`, ` - **Sellers MUST log** every request that arrives with a non-empty `authentication` block. Ops alarms on unexpected HMAC selection protect the buyer side when the buyer thought it was getting 9421. - **Sellers that support request signing SHOULD require** the inbound request to be 9421-signed (per the [request verifier checklist](#verifier-checklist-requests)) when `authentication` is present on `push_notification_config`. When a signed request cryptographically commits to the body, the `authentication` block cannot be injected without also invalidating the signature. -- **Buyers MUST alarm, not silently downgrade**, when they receive a 9421-signed webhook after registering with `authentication.credentials`, or when they receive HMAC-signed webhooks after registering without `authentication`. Silent mode-mismatch acceptance is exactly the denial-of-information surface. +- **Buyers MUST reject with `webhook_mode_mismatch` and alarm**, not silently downgrade, when they receive a 9421-signed webhook after registering with `authentication.credentials`, or when they receive HMAC-signed webhooks after registering without `authentication`. Rejection is the safety property; alarming is the telemetry — a buyer that alarms but accepts the payload has already handed authority to the mismatched signing scheme. The rejection surfaces as HTTP `401` with the stable error code so sender-side retry logic can route it to incident response rather than replaying identically. - **Buyers SHOULD negotiate HMAC-mode out-of-band** at onboarding when interoperating with sellers that have not yet implemented 9421. Durable per-counterparty mode selection in operator records is not MITM-mutable the way a per-request field is. -**Verifier checklist for webhooks.** Apply these 14 checks in order, short-circuiting on the first failure. The steps below are the [request verifier checklist](#verifier-checklist-requests) with exactly two substitutions: the `tag` value (`adcp/webhook-signing/v1` instead of `adcp/request-signing/v1`) and the direction-of-trust resolution (seller's brand.json `agents[]` entry instead of the buyer's). Implementations SHOULD share verifier code between the two profiles and branch only on those two substitutions. Error codes are prefixed `webhook_signature_*` so caller-side error handling distinguishes the two profiles. +**Verifier checklist for webhooks.** Apply these 14 checks in order, short-circuiting on the first failure. The steps below are the [request verifier checklist](#verifier-checklist-requests) with exactly two substitutions: the `tag` value (`adcp/webhook-signing/v1` instead of `adcp/request-signing/v1`) and the direction-of-trust resolution (seller's brand.json `agents[]` entry instead of the buyer's). Implementations SHOULD share verifier code between the two profiles and branch only on those two substitutions. Error codes are prefixed `webhook_*` (mostly `webhook_signature_*`, plus a small set of structural codes like `webhook_target_uri_malformed` and `webhook_mode_mismatch`) so caller-side error handling distinguishes the two profiles. 1. Parse `Signature-Input` and `Signature` headers per RFC 9421 §4. Reject if malformed (`webhook_signature_header_malformed`). If `Signature` or `Signature-Input` is present without the other, reject with the same code — a bound pair, not a guessable one. 2. Reject if any of `created`, `expires`, `nonce`, `keyid`, `alg`, or `tag` is absent from the `Signature-Input` parameters (`webhook_signature_params_incomplete`). @@ -1082,7 +1082,7 @@ Buyers MUST NOT derive signer identity from webhook payload fields (`task_id`, ` **9a. Per-keyid cap check.** Check the [webhook replay-cache cap](#webhook-replay-dedup-sizing). Reject with `webhook_signature_rate_abuse` if exceeded. Runs before cryptographic verify (step 10) for the same cheap-rejection rationale as request signing. -10. Compute the canonical signature base per RFC 9421 §2.5 using the covered components (after applying `@target-uri` canonicalization from [the request-signing profile](#adcp-rfc-9421-profile)). Verify the signature against the JWK (`webhook_signature_invalid` on failure). +10. Compute the canonical signature base per RFC 9421 §2.5 using the covered components, after applying `@target-uri` canonicalization AND `@authority` derivation per [the request-signing profile](#adcp-rfc-9421-profile). **The `@authority` rule is load-bearing for webhook security:** verifiers MUST derive `@authority` from the as-received HTTP `Host` header (or the HTTP/2+ `:authority` pseudo-header), NOT from reverse-proxy routing state or load-balancer metadata, and reject with `webhook_target_uri_malformed` if the canonicalized `@authority` does not byte-for-byte match the authority component of the canonical `@target-uri`. Implementers building from this checklist alone — without cross-referencing the profile — MUST apply this rule; skipping it silently accepts a cross-vhost replay vector (an attacker intercepts a TLS-terminated webhook and replays it to a second vhost on the same verifier pool: same cert SAN, different `Host`). After canonicalization completes, verify the signature against the JWK (`webhook_signature_invalid` on failure). 11. Recompute `content-digest` from the received body bytes and compare (`webhook_signature_digest_mismatch` on mismatch). REQUIRED — no policy branch. 12. Check the nonce against the replay cache. Reject if `(keyid, nonce)` has been seen within the replay-cache TTL (`webhook_signature_replayed`). 13. **Only after steps 1–12 have all passed**, insert `(keyid, nonce)` into the replay cache with TTL = `(expires − now) + 60 s`. @@ -1125,10 +1125,12 @@ Codes parallel the [request-signing error taxonomy](#transport-error-taxonomy), | Revocation list not refreshed within grace | `webhook_signature_revocation_stale` | | Cryptographic verification failed | `webhook_signature_invalid` | | `content-digest` mismatch | `webhook_signature_digest_mismatch` | +| `@authority` does not match signed `@target-uri` authority component (cross-vhost replay) | `webhook_target_uri_malformed` | | Nonce already seen within window | `webhook_signature_replayed` | | Per-keyid replay cache exceeded cap | `webhook_signature_rate_abuse` | +| Registered auth mode does not match signature mode on received webhook | `webhook_mode_mismatch` | -**Retry semantics for signature failures.** At-least-once delivery tells senders to retry on any non-2xx response, but a signature verification failure is not a transient error — the signature bytes arrive identically on every retry, so every retry fails identically. Senders MUST treat a `401` response carrying `WWW-Authenticate: Signature error="webhook_signature_*"` as a terminal failure for that specific delivery attempt: stop retrying the current event, log the failure with the error code for operator attention, and continue the normal retry queue for subsequent events. Senders SHOULD route sustained `webhook_signature_*` rates above an operator-defined threshold to incident response rather than continuing to emit them — persistent signature failure indicates a key-rotation coordination problem or a compromise, both of which need human action. Receivers MUST NOT silently discard signature failures; surfacing them in operator logs is part of the security posture. +**Retry semantics for signature failures.** At-least-once delivery tells senders to retry on any non-2xx response, but a signature verification failure is not a transient error — the signature bytes arrive identically on every retry, so every retry fails identically. Senders MUST treat a `401` response carrying `WWW-Authenticate: Signature error="webhook_*"` (any code defined in the taxonomy above, including `webhook_signature_*`, `webhook_target_uri_malformed`, and `webhook_mode_mismatch`) as a terminal failure for that specific delivery attempt: stop retrying the current event, log the failure with the error code for operator attention, and continue the normal retry queue for subsequent events. Senders SHOULD route sustained `webhook_*` error rates above an operator-defined threshold to incident response rather than continuing to emit them — persistent signature, authority, or mode failures indicate a key-rotation coordination problem, a misconfigured verifier, or a compromise, all of which need human action. Receivers MUST NOT silently discard these failures; surfacing them in operator logs is part of the security posture. ##### Webhook migration timeline From dab93f10c89d4635c3c864e4cce7fb989af226f5 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sun, 19 Apr 2026 23:08:13 -0400 Subject: [PATCH 2/2] spec(webhook-verifier): iterate on expert review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address security-reviewer and protocol-expert findings: 1. Pin :authority/Host precedence and byte-equality requirement in webhook step 10 (RFC 7540 §8.1.2.3). Pick-one behavior when both headers are present is a silent downgrade surface. 2. Clarify that the byte-match against the signed @target-uri — not the choice of source header — is the load-bearing safety gate, because Host itself can be rewritten in transit by a forward proxy or CDN. 3. Reconcile taxonomy intro sentence: "signature-level failures are non-retryable" → "every code in this table is non-retryable." The table now contains non-signature-level codes. 4. Rename "Retry semantics for signature failures" → "Retry semantics for verification failures." Broadened paragraph covers non-signature codes too. 5. Add editor-note on future-additions: any new webhook_* code inherits terminal-per-delivery semantics via the wildcard match; new retryable codes MUST carve out the exception at the point of addition. 6. Reword checklist preamble parenthetical so the structural-code list reads as open-ended rather than exhaustive. No wire-schema change. No new error codes beyond those in the original PR #2467. Co-authored-by: Emma Mulitz Co-Authored-By: Claude Opus 4.7 (1M context) --- ...webhook-verifier-mode-authority-clarifications.md | 12 +++++++----- docs/building/implementation/security.mdx | 10 ++++++---- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/.changeset/webhook-verifier-mode-authority-clarifications.md b/.changeset/webhook-verifier-mode-authority-clarifications.md index b3b3cd183d..324eb38777 100644 --- a/.changeset/webhook-verifier-mode-authority-clarifications.md +++ b/.changeset/webhook-verifier-mode-authority-clarifications.md @@ -1,10 +1,12 @@ --- --- -Two named error codes and one explicit rule on the webhook verifier checklist: +Webhook-verifier hardening on the 9421 signing path. Two new named error codes, the `@authority` derivation rule inlined in checklist step 10, and taxonomy/retry-semantics cleanups. -- **`webhook_mode_mismatch`.** Upgrade the "buyers MUST alarm, not silently downgrade" rule to MUST reject with the new stable code when the signature mode on a received webhook does not match the mode the buyer registered with (`authentication.credentials` present vs absent). Alarm-without-reject leaves the payload accepted by the mismatched scheme; the 401 + stable code lets sender-side retry logic route mismatch to incident response rather than replay identically. -- **`webhook_target_uri_malformed` on step 10.** Inline the `@authority` derivation rule that already exists in the request-signing profile but was easy to miss when implementing from the webhook checklist alone: verifiers MUST derive `@authority` from the as-received HTTP `Host` header (or HTTP/2+ `:authority` pseudo-header), NOT from reverse-proxy routing state, and reject with `webhook_target_uri_malformed` if the canonicalized `@authority` does not byte-match the authority component of the canonical `@target-uri`. Closes the cross-vhost replay vector (attacker intercepts a TLS-terminated webhook and replays to a second vhost on the same verifier pool: same cert SAN, different `Host`). -- **Retry-semantics paragraph broadened** from `webhook_signature_*` to `webhook_*` so the two new structural codes are covered by the terminal-failure + incident-routing guidance. +- **`webhook_mode_mismatch`.** Upgrade the "buyers MUST alarm, not silently downgrade" rule to MUST reject with the new stable code when the signature mode on a received webhook does not match the mode the buyer registered (`authentication.credentials` present vs absent). Alarm-without-reject leaves the payload accepted by the mismatched scheme; the 401 + stable code lets sender-side retry logic route mismatch to incident response rather than replay identically. +- **`webhook_target_uri_malformed` on step 10.** Inline the `@authority` rule that already exists in the request-signing profile but was easy to miss when implementing from the webhook checklist alone. Verifiers MUST derive `@authority` from the HTTP/2+ `:authority` pseudo-header when present, otherwise from the as-received HTTP/1.1 `Host` header — not from reverse-proxy routing state, load-balancer metadata, or any `Host` value a forward proxy may have rewritten in transit. If both `:authority` and `Host` are present they MUST be byte-equal (RFC 7540 §8.1.2.3); divergence rejects with `webhook_target_uri_malformed`. The canonicalized value MUST byte-for-byte match the authority component of the canonical `@target-uri` (the load-bearing safety gate, because `Host` itself can be rewritten in transit). Closes the cross-vhost replay vector. +- **Retry-semantics paragraph** broadened from `webhook_signature_*` to `webhook_*` so the two new structural codes are covered by the terminal-failure + incident-routing guidance. Heading renamed from "Retry semantics for signature failures" to "Retry semantics for verification failures." An editor-note paragraph warns future editors that any new `webhook_*` code inherits terminal-per-delivery semantics and must be explicitly carved out if it SHOULD be retryable. +- **Taxonomy intro sentence** reconciled so "signature-level failures are non-retryable" is replaced with "every code in this table is non-retryable" — the table now contains non-signature-level codes. +- **Checklist preamble** reworded so the parenthetical listing structural codes reads as open-ended rather than an exhaustive set. -Extracted from PR #2433 as a standalone editorial + taxonomy fix. No wire-schema change; no client-side breakage for implementations that already verified correctly. +Extracted from PR #2433 and iterated in response to security-reviewer and protocol-expert review. No wire-schema change; no client-side breakage for implementations that already verified correctly. diff --git a/docs/building/implementation/security.mdx b/docs/building/implementation/security.mdx index 5560516052..92132730d4 100644 --- a/docs/building/implementation/security.mdx +++ b/docs/building/implementation/security.mdx @@ -1068,7 +1068,7 @@ Buyers MUST NOT derive signer identity from webhook payload fields (`task_id`, ` - **Buyers MUST reject with `webhook_mode_mismatch` and alarm**, not silently downgrade, when they receive a 9421-signed webhook after registering with `authentication.credentials`, or when they receive HMAC-signed webhooks after registering without `authentication`. Rejection is the safety property; alarming is the telemetry — a buyer that alarms but accepts the payload has already handed authority to the mismatched signing scheme. The rejection surfaces as HTTP `401` with the stable error code so sender-side retry logic can route it to incident response rather than replaying identically. - **Buyers SHOULD negotiate HMAC-mode out-of-band** at onboarding when interoperating with sellers that have not yet implemented 9421. Durable per-counterparty mode selection in operator records is not MITM-mutable the way a per-request field is. -**Verifier checklist for webhooks.** Apply these 14 checks in order, short-circuiting on the first failure. The steps below are the [request verifier checklist](#verifier-checklist-requests) with exactly two substitutions: the `tag` value (`adcp/webhook-signing/v1` instead of `adcp/request-signing/v1`) and the direction-of-trust resolution (seller's brand.json `agents[]` entry instead of the buyer's). Implementations SHOULD share verifier code between the two profiles and branch only on those two substitutions. Error codes are prefixed `webhook_*` (mostly `webhook_signature_*`, plus a small set of structural codes like `webhook_target_uri_malformed` and `webhook_mode_mismatch`) so caller-side error handling distinguishes the two profiles. +**Verifier checklist for webhooks.** Apply these 14 checks in order, short-circuiting on the first failure. The steps below are the [request verifier checklist](#verifier-checklist-requests) with exactly two substitutions: the `tag` value (`adcp/webhook-signing/v1` instead of `adcp/request-signing/v1`) and the direction-of-trust resolution (seller's brand.json `agents[]` entry instead of the buyer's). Implementations SHOULD share verifier code between the two profiles and branch only on those two substitutions. Error codes are prefixed `webhook_*` — most carry the `webhook_signature_*` infix, plus structural codes without it (currently `webhook_target_uri_malformed`, `webhook_mode_mismatch`) — so caller-side error handling distinguishes the two profiles. 1. Parse `Signature-Input` and `Signature` headers per RFC 9421 §4. Reject if malformed (`webhook_signature_header_malformed`). If `Signature` or `Signature-Input` is present without the other, reject with the same code — a bound pair, not a guessable one. 2. Reject if any of `created`, `expires`, `nonce`, `keyid`, `alg`, or `tag` is absent from the `Signature-Input` parameters (`webhook_signature_params_incomplete`). @@ -1082,7 +1082,7 @@ Buyers MUST NOT derive signer identity from webhook payload fields (`task_id`, ` **9a. Per-keyid cap check.** Check the [webhook replay-cache cap](#webhook-replay-dedup-sizing). Reject with `webhook_signature_rate_abuse` if exceeded. Runs before cryptographic verify (step 10) for the same cheap-rejection rationale as request signing. -10. Compute the canonical signature base per RFC 9421 §2.5 using the covered components, after applying `@target-uri` canonicalization AND `@authority` derivation per [the request-signing profile](#adcp-rfc-9421-profile). **The `@authority` rule is load-bearing for webhook security:** verifiers MUST derive `@authority` from the as-received HTTP `Host` header (or the HTTP/2+ `:authority` pseudo-header), NOT from reverse-proxy routing state or load-balancer metadata, and reject with `webhook_target_uri_malformed` if the canonicalized `@authority` does not byte-for-byte match the authority component of the canonical `@target-uri`. Implementers building from this checklist alone — without cross-referencing the profile — MUST apply this rule; skipping it silently accepts a cross-vhost replay vector (an attacker intercepts a TLS-terminated webhook and replays it to a second vhost on the same verifier pool: same cert SAN, different `Host`). After canonicalization completes, verify the signature against the JWK (`webhook_signature_invalid` on failure). +10. Compute the canonical signature base per RFC 9421 §2.5 using the covered components, after applying `@target-uri` canonicalization AND `@authority` derivation per [the request-signing profile](#adcp-rfc-9421-profile). **The `@authority` rule is load-bearing for webhook security:** verifiers MUST derive `@authority` from the HTTP/2+ `:authority` pseudo-header when present, otherwise from the as-received HTTP/1.1 `Host` header — NOT from reverse-proxy routing state, load-balancer metadata, or any `Host` value a forward proxy may have rewritten in transit. If both `:authority` and `Host` are present on the as-received request, they MUST be byte-equal after canonicalization (RFC 7540 §8.1.2.3 equivalence); divergence rejects with `webhook_target_uri_malformed`. The canonicalized `@authority` MUST byte-for-byte match the authority component of the canonical `@target-uri`; mismatch rejects with `webhook_target_uri_malformed`. That byte-match against the signed `@target-uri` — not the choice of source header — is the only safe gate, because `Host` itself can be rewritten in transit. Implementers building from this checklist alone — without cross-referencing the profile — MUST apply this rule; skipping it silently accepts a cross-vhost replay vector (an attacker intercepts a TLS-terminated webhook and replays it to a second vhost on the same verifier pool: same cert SAN, different `Host`). After canonicalization completes, verify the signature against the JWK (`webhook_signature_invalid` on failure). 11. Recompute `content-digest` from the received body bytes and compare (`webhook_signature_digest_mismatch` on mismatch). REQUIRED — no policy branch. 12. Check the nonce against the replay cache. Reject if `(keyid, nonce)` has been seen within the replay-cache TTL (`webhook_signature_replayed`). 13. **Only after steps 1–12 have all passed**, insert `(keyid, nonce)` into the replay cache with TTL = `(expires − now) + 60 s`. @@ -1109,7 +1109,7 @@ Signers MUST publish revocations via the same combined revocation list used for ##### Webhook error taxonomy -Codes parallel the [request-signing error taxonomy](#transport-error-taxonomy), prefixed `webhook_` so SDK error handling distinguishes the two profiles. Buyers MAY return `401` to the seller on any of these; a seller's retry loop will replay with the same signature, so signature-level failures are non-retryable to the sender even though HTTP semantics permit retry. +Codes parallel the [request-signing error taxonomy](#transport-error-taxonomy), prefixed `webhook_` so SDK error handling distinguishes the two profiles. Buyers MAY return `401` to the seller on any of these; a seller's retry loop will replay with the same signature bytes, so every code in this table is non-retryable to the sender — signature failures, authority-mismatch, and mode-mismatch all produce identical outputs on retry — even though HTTP semantics permit retry. | Failure | Code | |---|---| @@ -1130,7 +1130,9 @@ Codes parallel the [request-signing error taxonomy](#transport-error-taxonomy), | Per-keyid replay cache exceeded cap | `webhook_signature_rate_abuse` | | Registered auth mode does not match signature mode on received webhook | `webhook_mode_mismatch` | -**Retry semantics for signature failures.** At-least-once delivery tells senders to retry on any non-2xx response, but a signature verification failure is not a transient error — the signature bytes arrive identically on every retry, so every retry fails identically. Senders MUST treat a `401` response carrying `WWW-Authenticate: Signature error="webhook_*"` (any code defined in the taxonomy above, including `webhook_signature_*`, `webhook_target_uri_malformed`, and `webhook_mode_mismatch`) as a terminal failure for that specific delivery attempt: stop retrying the current event, log the failure with the error code for operator attention, and continue the normal retry queue for subsequent events. Senders SHOULD route sustained `webhook_*` error rates above an operator-defined threshold to incident response rather than continuing to emit them — persistent signature, authority, or mode failures indicate a key-rotation coordination problem, a misconfigured verifier, or a compromise, all of which need human action. Receivers MUST NOT silently discard these failures; surfacing them in operator logs is part of the security posture. +**Retry semantics for verification failures.** At-least-once delivery tells senders to retry on any non-2xx response, but a verification failure is not a transient error — the signature bytes and request context arrive identically on every retry, so every retry fails identically. Senders MUST treat a `401` response carrying `WWW-Authenticate: Signature error="webhook_*"` (any code defined in the taxonomy above, including `webhook_signature_*`, `webhook_target_uri_malformed`, and `webhook_mode_mismatch`) as a terminal failure for that specific delivery attempt: stop retrying the current event, log the failure with the error code for operator attention, and continue the normal retry queue for subsequent events. Senders SHOULD route sustained `webhook_*` error rates above an operator-defined threshold to incident response rather than continuing to emit them — persistent signature, authority, or mode failures indicate a key-rotation coordination problem, a misconfigured verifier, or a compromise, all of which need human action. Receivers MUST NOT silently discard these failures; surfacing them in operator logs is part of the security posture. + +**Editor note on future additions.** The wildcard `webhook_*` terminal-failure classification above is an eager sweep: any new code added to the taxonomy inherits terminal-per-delivery semantics without individual review. Editors adding a new `webhook_*` code that SHOULD be retryable (e.g., a future transient-infrastructure signal) MUST update this paragraph to carve out the exception at the point of addition — do not rely on the pattern match to remain safe for codes not yet defined. ##### Webhook migration timeline