Skip to content

feat(security): sign webhooks with the request-signing key; deprecate webhook-signing#5552

Merged
bokelley merged 10 commits into
adcontextprotocol:mainfrom
BaiyuScope3:feat/webhook-allow-request-signing-key-reuse
Jun 16, 2026
Merged

feat(security): sign webhooks with the request-signing key; deprecate webhook-signing#5552
bokelley merged 10 commits into
adcontextprotocol:mainfrom
BaiyuScope3:feat/webhook-allow-request-signing-key-reuse

Conversation

@BaiyuScope3

@BaiyuScope3 BaiyuScope3 commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator

Why

This PR implements the chosen direction for webhook key reuse: deprecate dedicated adcp_use: "webhook-signing" keys while continuing to accept them for backward compatibility. Paired implementation PR: adcontextprotocol/adcp-client#2239.

Today a signer that already publishes an adcp_use: "request-signing" key must also mint and publish a second, dedicated adcp_use: "webhook-signing" key to emit webhooks. The webhook verifier rejects any other purpose at step 8. That means two keys, two rotation procedures, and two JWKS entries for an operator who would rather reuse one key.

This PR lets the signer choose: reuse the request-signing key for webhooks, or keep a dedicated webhook-signing key. The verifier accepts both during the deprecation window.

Why it's safe

Cross-protocol confusion on webhooks is already prevented by two controls that are independent of key purpose:

  1. The tag: adcp/webhook-signing/v1 is part of the RFC 9421 signed base and checked at step 3. A captured request signature carries tag=adcp/request-signing/v1 and is rejected there. The spec already says this: "The distinct tag prevents a request signature from being replayed as a webhook signature and vice versa."
  2. Mandatory content-digest coverage at step 6.

The step-8 adcp_use discriminator adds blast-radius isolation, not the load-bearing domain-separation control. Dedicated webhook keys remain RECOMMENDED for operators that want that isolation.

The relaxation is one-directional: a webhook MAY be verified with a request-signing key, but request verification still requires adcp_use == "request-signing" exactly. response-signing and governance-signing keys are never valid for webhooks.

What changed

  • security.mdx webhook verifier checklist step 8 accepts adcp_use in {webhook-signing, request-signing}.
  • Wrong, absent, or unverifiable key purposes all reject with webhook_signature_key_purpose_invalid. webhook_mode_mismatch remains reserved for the HMAC-vs-9421 auth-mode selector and is not used for key-purpose failures.
  • security.mdx key publication and cross-purpose reuse guidance now permits one-directional reuse for webhooks and downgrades the dedicated webhook key from REQUIRED to RECOMMENDED.
  • security.mdx one-JWK-per-adcp_use publication-shape section documents the reuse exception. adcp_use remains a string, never an array.
  • L1 and security-model docs now align with the new rule: webhook delivery uses request-signing keys; deprecated webhook-signing keys are still accepted for compatibility.
  • Capabilities schema descriptions clarify that identity.key_origins.webhook_signing names the webhook delivery surface, not a required live key purpose.
  • Conformance vectors: former negative 008-wrong-adcp-use became positive 008-request-signing-key-reuse; new negative 008-wrong-adcp-use covers a response-signing key and still fails step 8.
  • Compliance universal webhook-emission checks now accept a webhook-valid signing key: canonical request-signing or deprecated webhook-signing.
  • build-check.yml force-refreshes the ephemeral local origin/3.0.x ref with +3.0.x:refs/remotes/origin/3.0.x so CI does not fail after a non-fast-forward update of the comparison branch.
  • Changeset added as minor: this widens verifier acceptance and deprecates the old purpose without removing support.

Notes for reviewers

  • This PR takes the implicit form: a request-signing key is accepted for webhooks. An explicit multi-purpose form would require changing adcp_use from a string into an array or introducing a combined value, which is intentionally out of scope.
  • Follow-up surface: several non-normative docs still say to mint a webhook-signing key. They remain wire-compatible during the deprecation window, but should be swept after this lands.
  • dist/compliance/** is left to the release pipeline; only source compliance artifacts are edited here.

Generated with Claude Code

Webhook verifier checklist step 8 now accepts adcp_use of 'webhook-signing'
OR 'request-signing' — a signer may reuse the request-signing key it already
publishes instead of minting a dedicated webhook key. Other purposes
(response-signing, governance-signing) remain rejected with
webhook_mode_mismatch; absent adcp_use with webhook_signature_key_purpose_invalid.

Safe and one-directional: domain separation is carried by the RFC 9421 tag
(adcp/webhook-signing/v1, step 3) and mandatory content-digest coverage, not
by the key-purpose discriminator. The reverse (webhook key verifying a
request) stays forbidden. A dedicated webhook key remains RECOMMENDED.

Conformance vectors: former negative 008-wrong-adcp-use -> positive
008-request-signing-key-reuse; new negative 008 covers a response-signing key.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@aao-release-bot aao-release-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cryptographic argument is sound — the signed tag at step 3 is the real domain-separation control, and the adcp_use check was never load-bearing for confusion-resistance on the webhook path. The relaxation is safe. But the spec now contradicts itself in three places and ships a conformance vector that enforces one side of the contradiction, and the PR is explicitly an RFC with open design questions. Holding for the author/maintainer call rather than blocking or rubber-stamping.

security-reviewer: no High/Medium. A captured request signature carries tag=adcp/request-signing/v1 in its signed @signature-params base and is rejected at webhook step 3 (security.mdx:1469) before the relaxed step 8 is ever reached — and would fail crypto verify at step 10 regardless. The only party who can mint a webhook-tagged signature verifiable under the request-signing key is the holder of that private key, i.e. the legitimate signer reusing its own key. Asymmetry intact: request step 8 (security.mdx:1237) still requires adcp_use == "request-signing" exactly. No replay path, no cross-protocol confusion.

ad-tech-protocol-expert: sound-with-caveats. Domain-separation reasoning verified against the vectors (tag is inside the signed @signature-params). The one-directional asymmetry is coherent.

Things I checked

  • Domain separation holds. Webhook step 3 tag check (security.mdx:1469) fires before step 8; request signature replay onto the webhook path fails at step 3 and again at step 10. The adcp_use discriminator adds zero confusion-resistance on the webhook path — it is blast-radius hygiene, as the PR states.
  • Asymmetry is real. Request verifier step 8 (security.mdx:1237) still demands adcp_use == "request-signing" exactly. A webhook-signing key cannot verify a request; response/governance keys are rejected on both paths.
  • Positive vector 008 reuses the original signature bytes (OT_F-yQs..., keyid test-wrong-purpose-2026). Legitimate — Ed25519 is deterministic and only the verifier's accept/reject decision changed. Was always cryptographically valid; only step 8 rejected it.
  • Changeset present, minor. Defensible for a verifier-acceptance widening — but see the error-code point below, which is the part that is observably breaking.

Blocking the merge in the author's own framing (this is the RFC's open question)

This PR's own "Notes for reviewers" offers two paths on the error code. The path it took ships a self-contradictory normative document. Three sites were not updated:

  1. Taxonomy table contradicts step 8. security.mdx:1556 still reads JWK adcp_use ≠ webhook-signing → webhook_signature_key_purpose_invalid. After this PR step 8 emits webhook_mode_mismatch for a wrong purpose and accepts request-signing outright. The row is now false.
  2. webhook_mode_mismatch is overloaded. security.mdx:1565 defines it as the HMAC-vs-9421 auth-mode selector mismatch (reinforced at :1462). Reusing it for a key-purpose failure collapses two distinct failure classes onto one stable code — which defeats the taxonomy's stated goal that "SDK error handling distinguishes the two profiles" (:1545). Reusing a stable error code for a new failure class is the observably breaking part of this change for any receiver that branches on it.
  3. Blast-radius sentence contradicts the new design. security.mdx:1438 (untouched by the diff) still states kid collisions are forbidden "specifically so a request-signing-key compromise cannot be repurposed as a webhook-signing capability." That repurposing is now the intended behavior. The sentence's rationale is now false.

And the conformance README was not touched: static/compliance/source/test-vectors/webhook-signing/README.md L34-36, L45, L105 still describe 008 as the negative request-signing → webhook_signature_key_purpose_invalid vector and claim test-wrong-purpose-2026 "causes step 8 to reject." It now causes step 8 to accept. An implementer building a runner from that README, or a verifier built from the L1556 table, fails the new negative/008 vector — the spec disagrees with itself and the bundled vector picks a side.

What flips this to approve

  1. Pick one error-code story and make the whole document tell it. Either revert step 8 to webhook_signature_key_purpose_invalid for all purpose failures (your offered fallback — lowest surface, no overload), OR keep the adcp#2467 split and update security.mdx:1556 and :1565 to match, plus negative/008's error_code.
  2. Fix the security.mdx:1438 blast-radius sentence so the kid-collision rationale reflects the new model (collision forbidden so each kid carries one declared purpose; dedicated webhook key is the isolation mechanism, RECOMMENDED).
  3. Update webhook-signing/README.md (L34-36, L45, L105) for the positive/negative 008 split and the new response-signing key.

Follow-ups (non-blocking — file as issues)

  • The PR scopes itself to normative security.mdx; migration guides, whats-new, and brand-rights/governance/collection-lists task specs still say webhook signing uses a webhook-signing key. Sweep once direction is agreed, as the PR notes.
  • Confirm with the WG whether the implicit reuse form is preferred over an explicit multi-purpose adcp_use (the PR raises this; it is a real fork worth a deliberate decision, not a default).

Crypto's right. The spec just needs to agree with itself before it goes on the wire. Resolve the three doc sites and the README and I'll approve.

Per reviewer feedback: resolve the self-contradiction. Step 8 uses
webhook_signature_key_purpose_invalid for ALL key-purpose failures (absent
adcp_use, missing verify key_op, or adcp_use outside {webhook-signing,
request-signing}); webhook_mode_mismatch is left reserved for the HMAC-vs-9421
auth-mode selector. Fixes the taxonomy table row, the blast-radius kid-collision
sentence (reuse is now intended; dedicated key is the isolation mechanism), the
negative 008 vector error_code, and the conformance README (008 split + new
response-signing key).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@BaiyuScope3

Copy link
Copy Markdown
Collaborator Author

Thanks — this is a great review, and you're right on all four. I picked option A (your offered fallback): one error-code story, webhook_signature_key_purpose_invalid for every key-purpose failure, and webhook_mode_mismatch left reserved for the HMAC-vs-9421 auth-mode selector. No overload, lowest surface, and it keeps the taxonomy's "distinguish the two profiles" goal intact.

Pushed c737525 resolving the three doc sites + the README + the vector:

  1. Step 8 (security.mdx) — now emits webhook_signature_key_purpose_invalid for absent adcp_use, missing verify key_op, or an adcp_use outside {webhook-signing, request-signing}. Added an explicit note that webhook_mode_mismatch is NOT used here.
  2. Taxonomy table (was :1556) — row updated to adcp_use ∉ {webhook-signing, request-signing}, absent, or key_ops lacks verify → webhook_signature_key_purpose_invalid. The webhook_mode_mismatch row is unchanged.
  3. Blast-radius sentence (was :1438) — rewritten: kid collisions are forbidden so each kid carries one declared purpose; reuse is now an intended option, so a request-key compromise does extend to webhooks, and the dedicated webhook-signing key is the isolation mechanism (RECOMMENDED).
  4. Conformance README — updated the keys.json description, the negative-008 line (adcp_use=response-signing), added the positive/008-request-signing-key-reuse entry, the jwks_ref example, and the exploitable-test-kids list (added test-response-purpose-2026).
  5. Vectornegative/008-wrong-adcp-use error_code is now webhook_signature_key_purpose_invalid.

The paired SDK PR (adcontextprotocol/adcp-client#2239, commit 760be39) makes the same change — step 8 collapses to a single webhook_signature_key_purpose_invalid branch, and the vendored vectors/keys/README mirror this exactly.

Follow-ups (filing as separate issues, per your note): (a) sweep the non-normative docs — migration guides, whats-new, brand-rights/governance/collection-lists — that still say webhook uses a dedicated webhook-signing key; (b) WG decision on implicit reuse vs an explicit multi-purpose adcp_use. The PR still carries that as an open design question and I'm happy to switch to the explicit form if the WG prefers it.

bokelley
bokelley previously approved these changes Jun 15, 2026

@bokelley bokelley left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All three blocking items from the prior review are resolved in c737525:

  • Taxonomy table — now reads adcp_use ∉ {webhook-signing, request-signing}, absent, or key_ops lacks verify → webhook_signature_key_purpose_invalid. Consistent with step 8. ✓
  • Error-code overload — step 8 emits webhook_signature_key_purpose_invalid for all purpose failures. Explicit inline note that webhook_mode_mismatch is reserved for the HMAC-vs-9421 selector and is NOT used here. ✓
  • Blast-radius sentence — rewritten: kid collisions are forbidden so each kid carries one declared purpose; reuse is an intended option so a request-signing-key compromise does extend to webhooks; dedicated webhook-signing key is the isolation mechanism and remains RECOMMENDED. ✓
  • Conformance README — L34-36, L45, L105 all updated; test-response-purpose-2026 added to the exploitable-kids list; positive/008 and negative/008 entries correctly described. ✓

Crypto reasoning still holds. Option A (single webhook_signature_key_purpose_invalid story) is the right call — no overload, lowest surface, taxonomy intact.

Two non-blocking follow-ups noted; no change needed in this PR:

  • Non-normative doc sweep (migration guides, whats-new, brand-rights/governance/collection-lists) — tracked per your note.
  • WG decision on implicit vs explicit multi-purpose adcp_use — PR carries it as an open question; correct place for it.

Approved on the technical merits. Maintainer sign-off still needed for the normative design direction reversal per the RFC framing.


Generated by Claude Code

@aao-release-bot aao-release-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The design is sound — the one-directional relaxation (request-signing key OK for webhooks, webhook key never OK for requests) holds up — but the PR ships a normative error-code contract that contradicts itself across four surfaces, and one of those surfaces is the conformance vector it also ships. Not approving until the contradiction is resolved; this is also flagged in the PR body as an RFC reversal of a normative design choice, so it's the maintainers' call regardless.

The architectural principle is right: domain separation on the webhook path is carried by the RFC 9421 tag (in the signed base, checked at step 3) and mandatory content-digest, not by the adcp_use discriminator. security-reviewer confirmed the crypto: a captured request signature carries tag=adcp/request-signing/v1 and dies at step 3 before step 8 ever runs — the tag is bound into the base, so it can't be rewritten to pass. No replay or signature-confusion path. Accepting a request-signing key does not let a request signature through.

Blocking the approve — error-code contradiction (fix flips this to approve)

The new "Key publication" paragraph at security.mdx:1436 says the verifier:

MUST reject any other purpose with `webhook_mode_mismatch` (and absent `adcp_use` with `webhook_signature_key_purpose_invalid`)

That directly contradicts the other four surfaces this PR touches, all of which use `webhook_signature_key_purpose_invalid` for a declared-but-wrong purpose like `response-signing`:

  • Step 8 (security.mdx:1474): rejects response/governance/absent/missing-verify all with `webhook_signature_key_purpose_invalid`, and explicitly states `webhook_mode_mismatch` is "NOT used for key-purpose failures."
  • Error-taxonomy table (security.mdx:1556): adcp_use ∉ {webhook-signing, request-signing} → `webhook_signature_key_purpose_invalid`.
  • Changeset: "rejected with `webhook_signature_key_purpose_invalid`. `webhook_mode_mismatch` is unchanged and remains reserved for the HMAC-vs-9421 auth-mode selector."
  • Conformance vector negative/008-wrong-adcp-use.json: response-signing key → error_code: webhook_signature_key_purpose_invalid, failed_step: 8.

So four surfaces say `key_purpose_invalid`; only line 1436 says `webhook_mode_mismatch`. This is the more dangerous half of the split: line 1436 is also the only surface that matches the SDK behavior the PR body's "Error-code nuance" note advertises ("the SDK already emits `webhook_mode_mismatch` for declared-but-wrong purposes"). An SDK that does that would emit `webhook_mode_mismatch` for a `response-signing` key and fail the 008-wrong-adcp-use vector this same PR ships, which asserts `webhook_signature_key_purpose_invalid`. The PR can't have it both ways. `webhook_mode_mismatch` is also semantically wrong here independent of consistency — it's the HMAC-vs-9421 auth-mode selector code (security.mdx:1565), not a key-purpose code.

Fix: change line 1436 to `webhook_signature_key_purpose_invalid` and drop the `webhook_mode_mismatch` reference there, matching step 8 + the table + the vector. Confirm the paired adcp-client#2239 SDK emits `key_purpose_invalid` for `response-signing`/`governance-signing` (not `webhook_mode_mismatch`), or it fails 008. That fix plus that confirmation flips this to approve.

Things I checked

  • Crypto domain-separation argument is sound (security-reviewer): tag is in the @signature-params signed base, checked at step 3 ahead of step 8 — no cross-protocol replay, no key-confusion path the tag doesn't already cover. Component-set overlap is irrelevant once the tag differs byte-for-byte.
  • Positive vector reuses the original signature correctly. positive/008-request-signing-key-reuse.json keeps keyid test-wrong-purpose-2026 and the original Ed25519 signature OT_F-yQ… over an unchanged base — Ed25519 is deterministic, so reuse is valid. Right move.
  • One-directional asymmetry preserved. Request step 8 still requires adcp_use == \"request-signing\" exactly; a webhook key is still rejected for requests. The relaxation does not leak backward.
  • Changeset type is correct. minor — relaxes a constraint, removes no previously-valid behavior; existing dedicated-key signers stay valid. Blast-radius tradeoff is disclosed, not hidden (security.mdx:1438).
  • Negative-coverage preserved. New test-response-purpose-2026 keeps a genuinely-wrong purpose under test at step 8.

Follow-ups (non-blocking — file as issues)

  • Non-normative docs still assert a dedicated webhook-signing key is required (migration guides, whats-new, brand-rights/governance/collection-lists task specs). The PR body already scopes these out; track the sweep.

Minor nits (non-blocking)

  1. Stale README Scope line. static/compliance/source/test-vectors/webhook-signing/README.md:27 still reads "adcp_use MUST be \"webhook-signing\" on the verifying JWK" — directly contradicts the relaxed rule. The PR edited the file-layout block right below it but left this normative sentence untouched. Update it to the {webhook-signing, request-signing} set.
  2. kid name now misleading. test-wrong-purpose-2026 is used by a positive vector (accepted reuse) but still reads "wrong-purpose." The $comment explains it, but the name will tempt a future maintainer to re-tighten step 8. A rename or inline note would age better.

The design is right. Resolve the line-1436 error code so the spec stops contradicting its own conformance suite, confirm the SDK agrees, and this is an approve.

@BaiyuScope3

BaiyuScope3 commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator Author

Update: Let's consider completely removed the webhook adcp_use and just simply use the existing request adcp_use.
If any agent want to have dedicated key for webhook, they can simply create 2 kids
Webhook is part of request, same algo on signature , no need to have a dedicated adcp_use

@bokelley

Copy link
Copy Markdown
Contributor

The direction is coherent (rely entirely on the RFC 9421 tag + KID isolation; skip the adcp_use discriminator for webhooks altogether), but completely removing webhook-signing acceptance would be a breaking change: any operator who publishes only adcp_use: "webhook-signing" keys today would fail step 8 under the new verifier. This PR is intentionally non-breaking — it adds request-signing to the accepted set without removing anything.

Two concerns worth putting in a follow-up issue for WG input before going further:

  • JWKS-level purpose metadata disappears. With two request-signing KIDs, there's no machine-readable signal in the JWKS for "this KID is the webhook key." Tooling, rotation procedures, and audit logs that rely on adcp_use to understand key purpose would have to fall back to KID naming convention — which is human convention, not schema-enforceable.
  • Blast-radius framing shifts. Today a dedicated webhook-signing key gives operators provable isolation: a request-signing-key compromise doesn't touch webhook delivery. The two-KID-same-purpose model keeps isolation achievable but convention-only — the JWKS itself no longer encodes the intent.

Recommendation: land this PR as the minimal non-breaking relaxation (accepts both; dedicated webhook key RECOMMENDED), then file a webhook-signing deprecation RFC for the WG to weigh as a 3.2 or 4.0 decision. The "tag is sufficient" argument is solid enough to bring to the group — but a deprecation that breaks existing key publishers is a breaking-change decision, not a scope change inside a minor PR.


Triaged by Claude Code. Session: https://claude.ai/code/session_01LcC8kwr6UUN4Vz9nin1VBB


Generated by Claude Code

@BaiyuScope3

BaiyuScope3 commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator Author

Will mark the webhook-signing adcp_use deprecated and currently supporting both
the full removal will happened in next major 4.0

@bokelley

Copy link
Copy Markdown
Contributor

Deprecation direction is coherent — marking webhook-signing as deprecated with both still accepted cleanly extends this PR's scope.

One unresolved finding from the last automated review to fix before adding more: security.mdx:1436 still reads webhook_mode_mismatch for a declared-but-wrong key purpose, while step 8, the taxonomy table, the changeset, and vector 008 all use webhook_signature_key_purpose_invalid. That line needs to be updated to webhook_signature_key_purpose_invalid so the spec is internally consistent before the deprecation text lands.

On the 4.0 removal: recommend referencing a follow-up issue in the deprecation notice rather than naming the version directly. The WG hasn't formally committed to 4.0 as the removal target, and hard-coding it in normative text pre-commits to a breaking-change timeline that should go through the RFC process separately.


Triaged by Claude Code. Session: https://claude.ai/code/session_01HrKRMc5Lj75nXDXvfHPgUh


Generated by Claude Code

…ebhook-signing

Reframe to the unified model: webhooks are signed with the agent's
request-signing key (differentiated from requests by the RFC 9421 tag), with
no separate webhook key purpose. Step 8 requires adcp_use == request-signing;
the deprecated webhook-signing value is still accepted for backward
compatibility (removed in 4.0). Key isolation, when wanted, is a second
request-signing key under a distinct kid — not a distinct adcp_use.

Rewrites the webhook key-publication section, the publication-shape note, the
blast-radius sentence, the adcp_use enum row, and the conformance README.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@BaiyuScope3

Copy link
Copy Markdown
Collaborator Author

Design update — went with the unified model (deprecate webhook-signing).

After discussion on our side, this PR no longer frames webhook signing as "reuse your request key OR publish a dedicated webhook-signing key." Instead:

  • Webhooks are signed with the agent's request-signing key. There is no separate webhook key purpose. Domain separation from request signatures is carried entirely by the tag (adcp/webhook-signing/v1), as established earlier in the review.
  • adcp_use: "webhook-signing" is deprecated (removed in 4.0). Verifiers MUST still accept it on the webhook path for backward compatibility, so nothing breaks for existing 3.x deployments — but new signers SHOULD use request-signing.
  • Key isolation is via a distinct kid, not a distinct purpose. An operator wanting separate webhook key material publishes a second request-signing key under its own kid and signs webhooks with it.

This resolves the "implicit reuse vs explicit multi-purpose adcp_use" open question from the original PR notes — we're removing the second purpose rather than entrenching it.

Error-code resolution from your review is unchanged: all webhook key-purpose failures use webhook_signature_key_purpose_invalid; webhook_mode_mismatch stays reserved for the HMAC-vs-9421 selector.

Updated in 7f3c54a: step 8, the key-publication section, the publication-shape note, the blast-radius sentence, the adcp_use enum row, and the conformance README. Paired SDK PR adcontextprotocol/adcp-client#2239 (a8b1e6d) mirrors it — verifier/signer accept request-signing (+ deprecated webhook-signing), and the AdcpUse member is marked deprecated. Conformance vectors are unchanged: positive/008-request-signing-key-reuse covers the request-signing path, positive/001–007 exercise the deprecated-but-accepted webhook-signing path, negative/008 rejects response-signing.

Follow-up (non-blocking, will file): the non-normative docs (migration guides, whats-new, brand-rights/governance/collection-lists) still describe a webhook-signing key; they'll be swept to the request-signing model, with the deprecation noted, once this lands.

@BaiyuScope3 BaiyuScope3 changed the title feat(security): allow webhook delivery to reuse a request-signing key feat(security): sign webhooks with the request-signing key; deprecate webhook-signing Jun 15, 2026
@bokelley

Copy link
Copy Markdown
Contributor

Support the behavior, with sequencing caveats.

The core model is sound: webhook delivery can accept either a dedicated adcp_use: "webhook-signing" key or the signer’s existing adcp_use: "request-signing" key. The load-bearing domain separation is the signed RFC 9421 tag plus mandatory content-digest coverage, not the key-purpose discriminator. A captured request signature still carries tag=adcp/request-signing/v1 and should fail the webhook verifier before the relaxed purpose check matters.

I’d merge this direction once the spec is internally consistent:

  1. Keep dedicated webhook keys RECOMMENDED, not REQUIRED, because they still provide blast-radius isolation and independent rotation.
  2. Resolve the implicit-vs-explicit decision here before the SDK ships. I’m comfortable with the implicit form in this PR, but if the WG wants explicit operator intent then that should be settled now rather than after verifier behavior lands.
  3. Clean up the error-code story before merge. My preference is to keep key-purpose failures under webhook_signature_key_purpose_invalid and reserve webhook_mode_mismatch for HMAC-vs-9421 mode-selection mismatch, but either path is fine if the taxonomy, checklist, and vectors all agree.
  4. Update the stale MUST-language and vector README in this PR so implementers do not get contradictory instructions from the normative text vs. conformance vectors.
  5. Sequence rollout as spec first, SDK second, downstream implementations third. The agentic-api simplification is fine once receivers are on an SDK/spec version that accepts request-signing keys for webhooks.

So: +1 on the protocol behavior; hold merge until the doc/vector consistency and error-code cleanup are done.

@aao-release-bot aao-release-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean execution of a normative reversal. The problem is that a normative reversal is a WG call, not a reviewer's signature — and you said as much in the PR body.

Things I checked

  • Domain-separation argument holds. The tag is in the signed base and rejected byte-for-byte at step 3 (security.mdx:1469); existing negative vector 001-wrong-tag already proves a request-signing tag dies at step 3. So the step-8 adcp_use check adds zero confusion resistance on the webhook path — it was blast-radius hygiene, not domain separation. security-reviewer: Low, no reachable confusion / replay / cross-protocol attack is reopened by the relaxation.
  • One-directional asymmetry preserved. Request verification still requires adcp_use == "request-signing" exactly; response-signing and governance-signing keys are still rejected for webhooks. New negative 008-wrong-adcp-use covers the response-signing case, so cross-purpose rejection coverage isn't lost.
  • Vectors are right, including the step-ordering subtlety. Positive 008-request-signing-key-reuse reuses the original valid Ed25519 signature over the same webhook-tagged base — a holder of the request-signing private key can legitimately sign a webhook-tagged base, so it verifies cleanly. Negative 008 supplies a fresh valid signature so the rejection is provably attributable to step 8 (purpose), which runs before step 10 (crypto). Both success outcomes are correct.
  • In-file normative coherence. The PR reconciles the load-bearing statements that contradict the new rule: step 8 (security.mdx:1474), both error-taxonomy tables, the request-signing JWK table row (:1073), the "One JWK per `adcp_use`" paragraph (:955), and the Key-publication block (:1426). Checked each — they are not left stale.

The open question (why this is a comment, not an approve)

  • The changeset is minor. Flipping conformance vector 008 negative→positive means a verifier that correctly implemented the old normative MUST now fails the suite. ad-tech-protocol-expert reads that as major. I read it as a defensible minor — nothing is removed on the wire, webhook-signing keys still verify, no existing production traffic breaks; it's an additive relaxation plus a deprecation. Reasonable people differ, and that's a maintainer ruling.
  • More to the point: this reverses a deliberate, currently-normative single-purpose-key rule, and you flagged it yourself as a proposal for the maintainers. I'm not going to auto-approve a normative reversal on a reviewer's signature.

What flips this to approve: a WG/maintainer sign-off on the reversal plus a decision on minor vs major. The execution is sound; the call isn't mine.

Follow-ups (non-blocking)

  • security.mdx "five signing systems" list (~:1585) still enumerates webhook signing (RFC 9421) — adcp_use: "webhook-signing" as a live purpose with no deprecation marker. Residual; fold it into this PR rather than the later sweep, since it sits in the same normative file you just edited.
  • PR body "What changed" bullet says wrong purposes reject with `webhook_mode_mismatch`. The diff, changeset, and vectors correctly use `webhook_signature_key_purpose_invalid` and reserve `webhook_mode_mismatch` for the HMAC-vs-9421 selector. The spec is right; the description is stale. Fix it before it misleads the paired adcp-client#2239 review.
  • Blast-radius posture flips from "a request-key compromise cannot forge webhooks" to "it can, unless you split by `kid`." Documented and acceptable, but make the RECOMMENDED second-`kid` guidance loud in the migration note — it's the one thing operators will get wrong.

Minor nits (non-blocking)

  1. Key name lags its job. The reused key keeps kid="test-wrong-purpose-2026" while now serving the positive reuse vector, and its sibling keys.json $comment still frames it as cross-purpose material. A key named "wrong-purpose" that is now the right purpose is a small standing irony. Rename in a follow-up.

Holding for a maintainer call on the reversal and the changeset type.

@bokelley

Copy link
Copy Markdown
Contributor

Checking @bokelley's five conditions against the 7f3c54a diff:

  1. RECOMMENDED not REQUIRED ✅ — webhook-signing is now deprecated entirely; request-signing is canonical. No REQUIRED key-purpose language remains.
  2. Implicit-vs-explicit ✅ — resolved as implicit.
  3. Error-code story ✅ — step 8 and the taxonomy table both use webhook_signature_key_purpose_invalid for all key-purpose failures; webhook_mode_mismatch correctly reserved for HMAC-vs-9421 only.
  4. Stale MUST-language ⚠️ — three items missed in the sweep:
    • Line 955 prose vs. lines 970–975 code example: The prose was updated to say "an operator wanting blast-radius isolation publishes a second "request-signing" key with a distinct kid," but the JSON example immediately below still shows a "webhook-signing" second entry. The example needs to become two "request-signing" keys (different kids) to match the prose.
    • Line 870: still reads "the seller signs outbound with an adcp_use: "webhook-signing" key and the buyer verifies" — should be "request-signing".
    • Line 878: still reads "(…is the job of signed webhooks (adcp_use: "webhook-signing"))" — should be "request-signing".
    • Line 1589 (TMP cross-reference): still lists webhook signing (RFC 9421) — adcp_use: "webhook-signing" as the canonical purpose — should be updated to "request-signing" with a deprecation parenthetical.
  5. Rollout sequencing — no code action needed.

4.0 timing (prior comment): lines 1073, 1438, and the changeset body still hard-code "removed in AdCP 4.0." Recommend a follow-up issue reference if the WG hasn't formally committed to that removal window.


Triaged by Claude Code. Session: https://claude.ai/code/session_01LbBeWCWKeELirPfwjDQ6WC


Generated by Claude Code

… not 4.0

Address review on adcontextprotocol#5552: update the remaining stale webhook-signing sites
missed in the first pass — the request-signing profile prose (lines 870/878),
the JWKS publication example (second entry is now a request-signing key under
a distinct kid, matching the prose), the five-signing-systems list, and the
conformance README scope line. Replace hard-coded 'AdCP 4.0' removal language
with a reference to the deprecation tracking issue (adcontextprotocol#5555), per reviewer note
that the removal window is a WG/RFC decision.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@BaiyuScope3

Copy link
Copy Markdown
Collaborator Author

Thanks @bokelley — all of item 4 and the 4.0 timing are addressed in 8e3a410 (spec) and bf7992b (SDK).

Item 4 — stale webhook-signing sites:

  • Line 955 example vs prose ✅ — the second JWKS entry is now adcp_use: "request-signing" under a distinct kid (acme-webhook-2026-04), matching the "second request-signing key for isolation" prose. Added a sentence tying the example to the Key-publication isolation guidance.
  • Line 870 ✅ — now "the seller signs outbound with its adcp_use: "request-signing" key (the deprecated "webhook-signing" is still accepted)."
  • Line 878 ✅ — durable-attestation reference now reads "signed with the adcp_use: "request-signing" key."
  • Line 1589 (five-signing-systems / TMP list) ✅ — request-signing now notes it also signs webhooks; the webhook-signing bullet is marked deprecated and points at the tracking issue. (Same site @aao-release-bot flagged at ~:1585.)
  • README scope line (:29) ✅ — "adcp_use MUST be "request-signing" (deprecated "webhook-signing" also accepted)."

4.0 timing ✅ — removed the hard-coded "AdCP 4.0" from the deprecation notice (:1438), the enum row (:1073), and the changeset. They now reference #5555, a new tracking issue for the removal decision + non-normative doc sweep + the JWKS purpose-metadata question you and I raised. The version/window is left to the WG/RFC there rather than pre-committed in normative text. (The other "4.0" mentions in the file — HMAC fallback, request-signing mandate — are pre-existing and untouched.)

The paired SDK PR (#2239, bf7992b) mirrors the deprecation/issue-reference wording.

Deferred (non-blocking nit you and the bots flagged): renaming kid="test-wrong-purpose-2026" — it now serves the positive reuse vector, so the name is ironic. Renaming changes the signed @signature-params base, so it needs a regenerated signature; I'll fold it into the #5555 follow-up to avoid churn here unless you'd rather I do it in this PR.

@aao-release-bot aao-release-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Argus review could not complete

The automated review encountered an issue (possibly reached max turns, timed out, or failed to post the final gh pr review). A human reviewer should take this PR.

View workflow run

This is an automated message from the Argus AI review workflow.

@aao-release-bot aao-release-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Argus review could not complete

The automated review encountered an issue (possibly reached max turns, timed out, or failed to post the final gh pr review). A human reviewer should take this PR.

View workflow run

This is an automated message from the Argus AI review workflow.

aao-release-bot[bot]
aao-release-bot Bot previously approved these changes Jun 16, 2026

@aao-release-bot aao-release-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverses the single-purpose-key rule on the webhook path only, and does it correctly: domain separation was always carried by the RFC 9421 tag, not the adcp_use discriminator, so the step-8 purpose check was redundant on a surface that already gates on the tag at step 3.

The safety argument holds in both directions. A captured request signature carries tag=adcp/request-signing/v1 and dies at webhook step 3 (webhook_signature_tag_invalid, security.mdx:1469) before the relaxed step 8 is ever reached. A captured webhook signature carries tag=adcp/webhook-signing/v1 and dies at request step 3 — and request step 8 still demands adcp_use == "request-signing" exactly (security.mdx:1237, untouched by this PR). The relaxation is one-directional, the tag is in @signature-params so it's signed, and the reverse stays strict. security-reviewer: APPROVE, no replay or bypass path, Low findings only.

minor is the right changeset type. The verifier accepts a strict superset ({webhook-signing}{webhook-signing, request-signing}) and rejects nothing it used to accept; webhook-signing is deprecated but still honored, with removal deferred to a future major (#5555). The only major-triggering act — removing the value — is explicitly not done here.

Things I checked

  • Error-code flip-flop settled correctly. Five commits bounced between webhook_mode_mismatch and webhook_signature_key_purpose_invalid. Final state is consistent: every key-purpose failure (wrong purpose, absent adcp_use, missing verify key_op) returns webhook_signature_key_purpose_invalid at step 8 (security.mdx:157, taxonomy security.mdx:166), and webhook_mode_mismatch stays reserved for the HMAC-vs-9421 selector. Negative vector 008-wrong-adcp-use.json and the taxonomy row agree.
  • Conformance vectors are cryptographically coherent. Positive 008-request-signing-key-reuse reuses the exact Signature bytes (OT_F-yQs…) and keyid (test-wrong-purpose-2026) from the former negative 008 over a byte-identical base — genuinely valid, only the verdict flipped from reject-at-8 to accept. New negative 008 swaps in test-response-purpose-2026 (real response-signing key added to keys.json) and still rejects at step 8. Both jwks_ref kids resolve.
  • response-signing / governance-signing stay rejected for webhooks. Named explicitly in step 8 and exercised by the new negative vector.
  • Schema change is description-only. get-adcp-capabilities-response.json touches only the key_origins and webhook_signing description strings — no field/type/enum/required change. Non-breaking, and the webhook_signing description now matches the security.mdx reframing of webhook signing as a delivery surface. Changeset present, minor, correct.

Follow-ups (non-blocking — file as issues)

  • request-signing.mdx:635 directly contradicts the new normative rule. It still reads: "Never reuse the same key for both purposes — receivers enforce purpose at the JWK adcp_use level, not the RFC 9421 tag." That is the exact statement this PR reverses, stated as an imperative, in an unmodified L1 doc sitting next to the one you changed. Two L1 normative docs now disagree on the load-bearing question. The author scoped the sweep to security.mdx + vectors and disclosed it — fair — but this one is sharper than the rest of the un-swept set (webhooks.mdx, security-model.mdx, quickstart.mdx, etc.) because those stay wire-correct while webhook-signing is still accepted, whereas this one is an active contradiction. Worth pulling into this PR or the immediate follow-up rather than the general sweep.
  • #5555 should carry a hard deprecation date plus a conformance vector asserting webhook-signing is rejected after the window (security-reviewer L1). The two-value allow-set on the webhook path is the kind of temporary breadth that ossifies if nothing forces it closed.
  • Make the step-3-before-step-8 ordering an explicit invariant in the webhook checklist (security-reviewer L2). The whole relaxation rests on the tag check short-circuiting before the purpose check. The checklist already carries a "future edits MUST preserve ordering" note for the step-13 cache insert; an analogous one-liner here protects against a well-meaning future reorder.

Minor nits (non-blocking)

  1. PR description disagrees with what shipped. The body says wrong-purpose keys reject with webhook_mode_mismatch and that the PR "aligns the spec to that split." The committed artifacts do the opposite — no split, all purpose failures use webhook_signature_key_purpose_invalid. The files are self-consistent and correct; only the prose is stale. Worth correcting so the history isn't misleading.
  2. Changeset opening sentence imprecise. "now requires adcp_use == \"request-signing\"" reads as if webhook-signing is already rejected; step 8 accepts both. The changeset's own second paragraph says "both accepted." "now accepts" fixes it.
  3. test-wrong-purpose-2026 is now the accepted positive case but keeps a kid named "wrong-purpose." The $comment was updated to explain the role flip; renaming the kid would churn the fixtures, so the comment is the right call. Noted only because the name now means the opposite of what it says.

Approving on the strength of the tag-based domain-separation argument plus clean conformance-vector coverage. Fix the request-signing.mdx:635 contradiction soon — two L1 docs contradicting each other on key reuse is the one thing here that will actually confuse an implementer.

@aao-release-bot aao-release-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically sound and safe to ship — but it reverses a deliberate normative choice and leaves a normative sibling doc asserting the opposite, so I'm holding for confirmation rather than approving. The architectural call is right: domain separation on webhooks is carried by the RFC 9421 tag (step 3) and mandatory content-digest (step 6), not by the step-8 adcp_use discriminator — so relaxing step 8 removes no confusion resistance.

Things I checked

  • Safety argument holds. security-reviewer: SOUND, no High/Medium. A captured request signature carries tag=adcp/request-signing/v1 and dies at webhook step 3 before step 8 is ever reached (negative/001-wrong-tag.json still exercises this). Accepting a request-signing key on the webhook path opens no new replay, cross-protocol-confusion, or escalation path.
  • One-directional asymmetry is intact. The request-side checklist (security.mdx request step 8) is untouched — still adcp_use == "request-signing" exactly. The relaxation is webhook-only. No hunk makes it bidirectional. A webhook-signing key still cannot verify a request.
  • minor is the right semver. ad-tech-protocol-expert: this is an additive relaxation that retains webhook-signing as an accepted value — nothing removed, no type/enum/required flip. major would be over-rotation. Existing webhook-signing signers verify cleanly against both old and new verifiers.
  • Schema change is description-only. get-adcp-capabilities-response.json touches only two description strings on key_origins / key_origins.webhook_signing. Wire shape identical, webhook_signing field retained. Not spec drift.
  • Error-code taxonomy is internally consistent in the shipped artifacts. Step 8, the taxonomy table, the changeset, and the vector \$comments all agree: webhook_signature_key_purpose_invalid for wrong/absent purpose + missing verify; webhook_mode_mismatch reserved for the HMAC-vs-9421 selector.
  • positive/008 is sound by construction. It reuses the original valid Ed25519 signature over a byte-identical base for test-wrong-purpose-2026; Ed25519 is deterministic, so a sig valid in the old negative role (which died at step 8, never reaching crypto verify) stays valid now that it must pass it. code-reviewer confirmed the 64-byte sig length and recomputed the content-digest; it could not execute a raw crypto.verify in the sandbox.
  • CI refspec change is benign. build-check.yml:25 adds + to force-update the 3.0.x drift-comparison ref. Unrelated to the webhook change, harmless.

What flips this to approve

  1. Resolve the normative contradiction in request-signing.mdx:635. That line — same L1 layer, normative hands-on guide — still says: "Never reuse the same key for both purposes — receivers enforce purpose at the JWK adcp_use level, not the RFC 9421 tag." That is the exact inverse of this PR's central claim. An implementer following it ships a verifier that rejects your new positive/008. Your PR body scoped the sweep to "non-normative docs," but this one is normative and directly load-bearing. Sweep it here (or add a one-line deprecation banner pointing at #webhook-callbacks). security-model.mdx:145,164 is the next-most-load-bearing offender — it's linked directly from the paragraph you edited.
  2. Confirm the directional reversal is sanctioned. You flagged this yourself as an RFC reversing a deliberate single-purpose-key rule and "the maintainers' call." I'm not going to auto-approve a normative reversal on an automated pass — that's a WG decision. If the WG has signed off, say so and I'll move.

Follow-ups (non-blocking — file as issues)

  • PR description contradicts the diff on error codes. The "What changed" bullet and "Notes for reviewers" say step 8 rejects wrong purposes with webhook_mode_mismatch. Every committed artifact correctly uses webhook_signature_key_purpose_invalid. The diff is right; the prose is stale. Fix it before the paired adcp-client#2239 implements the wrong mapping off your description.
  • Reverse-direction coverage is spec-only. No request-signing conformance vector exercises "a webhook-signing key is rejected for a request" — the now-load-bearing forbidden direction is asserted in prose but not graded. Add request-signing/negative/*-webhook-purpose-key.json.
  • No governance-signing webhook negative vector. The new negative/008 covers response-signing; the step-8 prose also names governance-signing as rejected. response-signing is the representative cross-purpose case, but a governance-signing vector would match the enumerated prose.
  • Run one crypto.verify over positive/008 in CI. Closes the one check the review sandbox couldn't execute. Sound by construction, but measure it once.

Minor nits (non-blocking)

  1. kid name is now misleading. positive/008 accepts a key whose kid is literally test-wrong-purpose-2026. The keys.json \$comment was updated to explain it, so it's documented — but a kid named "wrong-purpose" on an accepted positive vector is a notable choice. Rename on the next vector-regen pass.

--comment, not --approve: the execution is clean and the threat model is unchanged, but I won't rubber-stamp a normative reversal that still has a sibling spec saying the opposite. Resolve request-signing.mdx:635 and confirm the WG call, and I'll approve.

@aao-release-bot aao-release-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The relaxation is sound and I can't fault the security model — but this is a normative reversal you've flagged as the maintainers' call, and it ships with a conformance suite that now contradicts itself. Holding at comment, not approve, until both clear.

The crux is right: domain separation between requests and webhooks is carried by the RFC 9421 tag, which is inside @signature-params, covered by the signature, and checked at step 3 — before key resolution. adcp_use == "webhook-signing" at step 8 was redundant domain separation, not the load-bearing control. security-reviewer: no-finding. I walked both replay directions with it — a captured request signature carries tag=adcp/request-signing/v1 and dies at webhook step 3; a captured webhook signature carries tag=adcp/webhook-signing/v1 and dies at request step 3. No single signature verifies on both paths. The one-directional asymmetry (webhook MAY use a request key; request MUST still require request-signing exactly) is enforced at security.mdx:1237 and unchanged.

Things I checked

  • Schema is description-only. get-adcp-capabilities-response.json touches two description strings (key_origins, webhook_signing); type/format/additionalProperties:false/x-adcp-validation anchoring all retained. Wire shape unchanged. ad-tech-protocol-expert: minor is correct — a verifier-acceptance relaxation plus a deprecation-with-no-removal is additive; no previously-accepted message becomes rejected. Removal in #5555 is the future major.
  • Error-code routing is internally consistent. Step 8 (security.mdx:230), the taxonomy table (security.mdx:239), the changeset, and negative/008 all route every purpose failure — wrong purpose, absent adcp_use, missing verify key_op — to webhook_signature_key_purpose_invalid, and reserve webhook_mode_mismatch for the HMAC-vs-9421 selector. error-code.json correctly untouched (these transport codes don't live in that enum).
  • Vector 008 wiring. positive/008-request-signing-key-reuse reuses byte-identical kid + base + signature from the prior valid negative vector and flips expected_outcome to success — validity inherited, self-consistent. negative/008 retargets to the new test-response-purpose-2026 (response-signing), still rejected at step 8. Governance- and response-signing keys remain invalid on the webhook path — confirmed by the new negative vector.
  • build-check.yml +3.0.x. Correct and safe — forces the ephemeral CI tracking ref to update on a non-fast-forward after a 3.0.x rebase; mutates only the checkout, never the remote.

What would flip this to approve

  1. WG/maintainer sign-off on the direction. You've framed this as a proposal reversing a currently-normative rule. No do-not-auto-approve label is set, so I'm not auto-holding — but I'm not going to auto-approve a normative security-model reversal on my own signature either. That's a human call.
  2. Resolve the conformance-suite contradiction. static/compliance/source/universal/webhook-emission.yaml:630 (assert_webhook_signing_key_present) still asserts a webhook-emitting agent's JWKS MUST contain an adcp_use: "webhook-signing" key and explicitly states request-signing keys "do not satisfy this check," failing with webhook_signing_keys_wrong_purpose. A signer that adopts this PR's recommended pattern — sign webhooks with a request-signing key, publish no webhook-signing key — fails the suite's own on-ramp gate. The verifier vectors say accept request-signing; the emission grader says signers must publish webhook-signing. Update the grader to accept request-signing on emission, or explicitly track it as a blocking follow-up that lands before #5555. ad-tech-protocol-expert: sound-with-caveats, this is the caveat.

Follow-ups (non-blocking — file as issues)

  • PR body contradicts the implementation on the error code. The description's "What changed" says step 8 rejects wrong purposes with webhook_mode_mismatch. The shipped spec, changeset, taxonomy table, and negative/008 all use webhook_signature_key_purpose_invalid and explicitly reserve webhook_mode_mismatch for the auth-mode selector. The artifacts are right; only the prose is stale — fix it so the paired adcp-client#2239 doesn't follow the wrong routing.
  • negative/008 signature is not CI-validated. It carries a fresh signature over a regenerated base for test-response-purpose-2026 and asserts failed_step: 8. Nothing in build-compliance.cjs or test:hmac-vectors cryptographically verifies the 9421 webhook vectors — if that signature is invalid, a runner rejects at step 10 (crypto), not step 8 (purpose), and the assertion silently diverges. Confirm it was generated against that key's published x.
  • Non-normative doc sweep (acknowledged in the PR): docs/reference/test-vectors/index.mdx:50 omits the newly-published test-response-purpose-2026 from the public-keys warning — a security-warning completeness gap, since its private material is now on the CDN. Plus the L3/webhooks.mdx, seller-integration.mdx, and get_adcp_capabilities.mdx references the PR body lists.

Minor nits (non-blocking)

  1. keys.json kid test-wrong-purpose-2026 is now a misnomer. It is an accepted request-signing key after this change, not a wrong-purpose one. All in-repo references are internally consistent with its new role, so nothing dangles — renaming would churn the signature base, so leaving it is defensible. Notable that the negative-path key kept its name while switching sides.

Sound model, clean schema. Comment, not approve — get WG agreement on the reversal and fix the emission grader (webhook-emission.yaml:630) so the suite doesn't reject the pattern it now recommends, and this flips to approve.

@aao-release-bot aao-release-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The implementation is clean and ships as-is; the open question is direction, not code. You labeled this "Proposal / RFC for discussion … it's the maintainers' call" and the committed artifacts read as a decided change with a removal issue (#5555). Those can't both be true. Resolve that and I approve.

The technical relaxation is sound. Domain separation moves off the adcp_use discriminator and onto the RFC 9421 tag, which is the right control: the tag is inside @signature-params, so it's part of the signed base and can't be swapped without the private key. Verified bidirectionally:

  • Request sig replayed on the webhook route → dies at webhook step 3 (security.mdx:1469, webhook_signature_tag_invalid). Unchanged by this PR.
  • Webhook sig (now signed with a request-signing key, tag=adcp/webhook-signing/v1) replayed on the request route → request step 8 adcp_use == "request-signing" now passes, but request step 3 (security.mdx:1232) requires adcp/request-signing/v1 and rejects first. Step 3 runs before step 8, so the tag is the load-bearing gate and it holds.

The relaxation is correctly one-directional: request verification still requires adcp_use == "request-signing" exactly (security.mdx:218), and response-signing/governance-signing are never valid on the webhook path — new negative 008-wrong-adcp-use exercises exactly that.

security-reviewer: no High, no Medium. ad-tech-protocol-expert: sound-with-caveats, minor is correct (additive acceptance + deprecation, nothing previously-valid starts rejecting; repo is on the 3.1.0-rc.0 line). code-reviewer: all six machine artifacts internally consistent.

Things I checked

  • Bidirectional replay defense. Request step 3 and webhook step 3 tag checks are both untouched (security.mdx:1232, :1469); the tag is in the signed base. This is the whole safety argument and it's correct.
  • Conformance vectors. Positive 008-request-signing-key-reuse reuses byte-identical Signature/expected_signature_base from the former negative 008 under test-wrong-purpose-2026 (an Ed25519 request-signing key) — same base + same key + deterministic Ed25519 → still verifies, success:true is right. New negative 008 fails at step 8 before crypto, so its fresh signature is non-load-bearing.
  • Schema is description-only. get-adcp-capabilities-response.json edits two description strings; webhook_signing capability, the ["adcp/webhook-signing/v1"] profile_version enum, key_origins.webhook_signing, and the x-adcp-validation rule are all retained. No wire-shape change, no fail-open. Not drift — the new prose matches the docs.
  • Changeset present, type correct. minor matches a relaxation + deprecation that keeps webhook-signing accepted.
  • Error taxonomy (committed) is internally consistent. Step 8 routes all key-purpose failures to webhook_signature_key_purpose_invalid; webhook_mode_mismatch stays reserved for the HMAC-vs-9421 selector (security.mdx:230, :239).

What flips me to approve

  1. Resolve the RFC framing. Drop the "Proposal / RFC for discussion" banner if this is a decided WG direction, or mark the PR draft if it isn't. Reversing the single-purpose-key rule across the ecosystem is a direction call; I won't auto-approve something the author flagged as still open.
  2. Reconcile the PR description with the diff. The "What changed" / "Error-code nuance" section says step 8 should reject wrong purposes with webhook_mode_mismatch and reserve key_purpose_invalid for absent purpose. The committed security.mdx and changeset do the opposite — all purpose failures get key_purpose_invalid. The committed taxonomy is the better one (webhook_mode_mismatch is an auth-mode concept, not a key-purpose one). Just update the description; the code is right. The PR body is reviewing an earlier draft than the one in the diff.

Follow-ups (non-blocking — file as issues)

  • Doc sweep debt. Non-normative surfaces (migration guides, what's-new, brand-rights/governance/collection-list task specs) still say "mint a webhook-signing key." You scoped that out deliberately — track it so adopters don't hit stale guidance during the deprecation window.
  • Protect the asymmetry in shared verifier code (security-reviewer L1). Safety depends on request-signing being accepted on the webhook path while webhook-signing is NOT accepted on the request path. The spec recommends sharing verifier code — add a request-suite negative vector asserting a webhook-signing-tagged signature is rejected at request step 3, so a future "symmetry" refactor can't silently reopen reverse replay.
  • #5555 narrowing. Confirm removal of webhook-signing acceptance is scheduled so the webhook-path accepted set narrows back to one value once signers migrate. Note in #5555 that the tag/profile name (adcp/webhook-signing/v1) deliberately outlives the key-purpose name — a cleanup shouldn't try to rename the tag.

Minor nits (non-blocking)

  1. Drive-by CI fix. build-check.yml:25 adds the + force prefix to origin +3.0.x:refs/remotes/origin/3.0.x — correct (forces a non-FF update of the ephemeral local tracking ref, can't touch the remote), but unrelated to webhook signing. Fine to keep; worth a line in the description so it isn't a surprise in git blame.

Clean work. Flip the two items above and this is an approve.

aao-release-bot[bot]
aao-release-bot Bot previously approved these changes Jun 16, 2026

@aao-release-bot aao-release-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Widens the webhook verifier to accept a request-signing key and deprecates the dedicated webhook-signing purpose. Right call: domain separation on webhooks is carried by the RFC 9421 tag in the signed base, not by the adcp_use discriminator — so dropping the discriminator's strictness on the webhook path costs nothing the tag wasn't already buying.

Things I checked

  • The load-bearing change is step 8 of the webhook verifier checklist (security.mdx). Final state accepts adcp_use ∈ {request-signing, webhook-signing}, rejects everything else (absent adcp_use, missing verify key_op, response-signing, governance-signing) with webhook_signature_key_purpose_invalid. webhook_mode_mismatch stays reserved for the HMAC-vs-9421 selector. Checklist, taxonomy table row, negative/008 expected_outcome, and the keys.json $comment for test-response-purpose-2026 all agree.
  • The relaxation is one-directional and the asymmetry is enforced by two independent gates. Webhook path accepts request-signing; request path (untouched) still demands adcp_use == "request-signing" exactly. A captured request signature carries tag=adcp/request-signing/v1 and dies at webhook step 3 before step 8 is reached; a webhook signature carries tag=adcp/webhook-signing/v1 and dies at request step 3. No replay or downgrade path opens. security-reviewer: no High — the tag is the load-bearing separator both directions, content-digest stays mandatory with no opt-out, request verification is unchanged and strict.
  • Conformance vectors are consistent by construction. positive/008-request-signing-key-reuse reuses kid test-wrong-purpose-2026 and the verbatim Ed25519 signature from the former negative 008 — valid because that vector always failed at step 8 (purpose), never step 10 (crypto), over an identical signed base. New negative/008 swaps in test-response-purpose-2026 (response-signing). jwks_ref, Signature-Input keyid, and expected_signature_base keyid line up in both.
  • No residual normative contradiction in security.mdx. The old "MUST reject any JWK whose adcp_use is not exactly webhook-signing" passages (key-publication prose, step 8, taxonomy row) are all removed-and-replaced in-hunk, not left dangling. Verified against the diff directly and via code-reviewer's base-line→replacing-hunk mapping.
  • Schema change is description-only (get-adcp-capabilities-response.json: key_origins and key_origins.webhook_signing). No field/type/enum/required change; adcp_use stays a string; webhook_signing correctly reframed as the delivery surface, not a required live key purpose. ad-tech-protocol-expert: sound-with-caveats, no wire drift, minor defensible.
  • Changeset present (.changeset/webhook-allow-request-signing-key-reuse.md, minor). Additive widening + deprecation-without-removal — minor is the right shape. The tag/profile name stays adcp/webhook-signing/v1 (correct — that's the wire identifier; only the key purpose moved).
  • build-check.yml +3.0.x:refs/remotes/origin/3.0.x force-updates a local remote-tracking ref in a throwaway CI checkout. Pushes nothing, makes the drift comparison resilient to a non-fast-forward 3.0.x. Benign.

Follow-ups (non-blocking — file as issues)

  • State the semver rationale in the changeset. This relaxes a verifier MUST — an implementation built to the old "exactly webhook-signing" text now fails the new positive/008 grading. minor is correct under AdCP's wire-contract semver policy (no signer/verifier on the network desyncs), but a reviewer applying the strict "MUST relaxed = major" reading will challenge it. Name the verifier-relaxation-vs-wire-contract distinction in the changeset. The eventual #5555 removal of webhook-signing acceptance will be major — say so to set the deprecation arc cleanly.
  • Sweep the remaining non-normative "mint a webhook-signing key" prose. The PR body already flags this. The authoritative passages are reconciled; adjacent examples/asides during the compat window are not. Sweep before #5555 lands so the deprecation removal isn't fighting stale guidance.
  • Confirm the regenerated negative/008 signature in CI. code-reviewer could not re-run Ed25519 verification on the new test-response-purpose-2026 signature bytes (sandbox). Structurally consistent; let the .context/generate-webhook-vectors.mjs grading path confirm the bytes before merge.
  • (Security, Low — already disclosed.) Default posture drops from isolation-by-default to isolation-opt-in: an operator publishing a single request-signing key now shares key material across requests and webhooks. The PR documents the distinct-kid mitigation honestly. Consider a SHOULD nudging a webhook-dedicated kid for agents whose request-signing key also authenticates mutating tool calls — parallel to the existing publisher-pin MUST for mutating-scope agents.

Minor nits (non-blocking)

  1. Imprecise anchor. security.mdx step 8 links [Downgrade and injection resistance](#webhook-callbacks) — the label names a subsection with no heading of its own, so the anchor resolves to the parent #### Webhook callbacks. Link works; label is loose.

The error-code taxonomy changed its mind once between commits 1 and 2 (webhook_mode_mismatchwebhook_signature_key_purpose_invalid for the response-signing case); the final state is internally consistent across all five sites. Worth noting only so the next reader doesn't re-derive the contradiction from the commit history.

Approving on the strength of the tag-carries-separation argument plus consistent conformance vectors. Follow-ups noted.

aao-release-bot[bot]
aao-release-bot Bot previously approved these changes Jun 16, 2026

@aao-release-bot aao-release-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sound relaxation. Domain separation on webhooks was always carried by the RFC 9421 tag and mandatory content-digest, never by the adcp_use discriminator — so widening step 8 to accept the request-signing key costs no confusion resistance.

Things I checked

  • The relaxation is one-directional. Webhook step 8 now accepts adcp_use ∈ {request-signing, webhook-signing} (security.mdx verifier checklist step 8), but the request checklist is untouched: request step 3 still requires tag=adcp/request-signing/v1 and request step 8 still requires adcp_use == "request-signing" exactly. A webhook-signing key verifying a request is forbidden by two independent gates, neither relaxed. security-reviewer: no-issue.
  • Replay is blocked before step 8 is reached. The tag is inside @signature-params (signed base), and webhook step 3 rejects adcp/request-signing/v1 with webhook_signature_tag_invalid — five steps before the key-purpose check. A captured request signature dies at step 3 regardless of which key the webhook path now accepts. Regression-covered by negative/001-wrong-tag.
  • Schema source changes are description-only. All four files (get-adcp-capabilities-response.json key_origins, sync-accounts-request.json, push-notification-config.json, webhook-challenge.json) touch only description strings — no field rename, type, enum, required↔optional, additionalProperties, or oneOf change. ad-tech-protocol-expert: no drift. Property name webhook_signing and its x-adcp-validation anchor are unchanged; it now names the delivery surface, not a required live purpose.
  • minor is correct. Acceptance-set widening + deprecation without removal. No conformant signer or verifier breaks. The breaking removal of webhook-signing is correctly deferred to #5555 / a major bump.
  • Error-code consistency holds across all four surfaces — spec step 8, the taxonomy table row, the negative-vector error_code, and the test assertion all use webhook_signature_key_purpose_invalid for wrong/absent/missing-verify purposes, and reserve webhook_mode_mismatch strictly for the HMAC-vs-9421 selector.
  • Vectors are cryptographically self-consistent. Positive 008-request-signing-key-reuse reuses the identical key, signature, and base as the former negative, flipping only expected_outcome — so the crypto is unchanged and only the policy outcome moved. New negative 008-wrong-adcp-use swaps in a fresh response-signing key, still failing step 8. tests/webhook-signing-vectors.test.cjs proves negative/008 verifies cryptographically before the step-8 policy rejection — exactly the right way to encode this.
  • build-check.yml force-fetch (+3.0.x:refs/remotes/origin/3.0.x) is the correct fix for surviving a non-fast-forward update of the comparison branch under --depth=1; the only downside (silent overwrite of a diverged local ref) is irrelevant in a fresh CI checkout.

Follow-ups (non-blocking — file as issues)

  • The test that proves this change is the one test not wired into CI. tests/webhook-signing-vectors.test.cjs is only reachable via npm run test:webhook-signing-vectors (package.json:47); build-check.yml:123 runs only the two HMAC scripts, and vitest.config.ts excludes **/*.test.cjs, so test:unit skips it. The cryptographic proof the PR rests on is enforced on local pre-commit, not on PRs. Append && npm run test:webhook-signing-vectors to the conformance step at build-check.yml:123. (code-reviewer: Minor.)
  • Non-normative docs sweep: several task docs still say to mint a webhook-signing key and still link /docs/building/implementation/security#... anchors against the canonical docs/building/by-layer/L1/security.mdx. Wire-compatible during the deprecation window; sweep after this lands (already noted in the PR description).

Minor nits (non-blocking)

  1. Changeset opener reads as a tightening. .changeset/webhook-allow-request-signing-key-reuse.md:11 leads with "step 8 now requires adcp_use == \"request-signing\"" before the parenthetical restores webhook-signing. Not contradictory, but "accepts request-signing (canonical) or webhook-signing (deprecated)" removes the one spot a reader could misinfer a breaking change.
  2. sf-binary parser is base64url-only. tests/webhook-signing-vectors.test.cjs:24 regex ^sig1=:([A-Za-z0-9_-]+):$ matches only the base64url alphabet. All current vectors happen to emit base64url, so it works; a future vector with standard base64 (+///=) would throw rather than parse. Fails loud, not a false-pass — widen the charset when convenient.

Approving on the strength of the verified step-3 tag gate plus the cryptographic vector test. Follow-ups noted.

@aao-release-bot aao-release-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean key-reuse relaxation: webhooks now verify under request-signing keys, with webhook-signing deprecated-but-accepted. The right call, because the load-bearing domain-separation control was never the adcp_use discriminator — it's the RFC 9421 tag in the signed base, checked at step 3 before step 8 is ever reached.

Things I checked

  • Replay direction is blocked. A captured request signature carries tag=adcp/request-signing/v1 in its @signature-params; webhook step 3 (security.mdx:1469) rejects on tag byte-mismatch before the relaxed step-8 purpose check runs. Step 3 gates step 8 — the ordering is load-bearing and correct. security-reviewer: no new key-confusion vector.
  • Relaxation is one-directional. Request verifier step 8 still requires adcp_use == "request-signing" exactly and is untouched by this PR. A webhook frame can't replay as a request (request step 3 rejects adcp/webhook-signing/v1). The widening leaks nowhere.
  • response-signing/governance-signing still reject. Step 8 prose (diff L300) and the new negative/008-wrong-adcp-use.json (now a response-signing key) both enforce it. Accepted set is exactly {request-signing, webhook-signing}.
  • Vector flip is sound by construction. positive/008-request-signing-key-reuse.json reuses the byte-identical signature (sig1=:OT_F-yQs…:) and base from the former negative/008 under test-wrong-purpose-2026 — that signature was always cryptographically valid; only the verdict flips from reject to accept. New negative/008 is a freshly generated valid signature under test-response-purpose-2026. The new test (tests/webhook-signing-vectors.test.cjs) asserts the negative signature verifies before the step-8 policy rejection — so it fails for the right reason.
  • No schema drift. The four static/schemas/source/** edits are description-only — no field/type/enum/required-optional change, adcp_use stays a string. The one live enum in blast radius, webhook_signing.versions: ["adcp/webhook-signing/v1"] at get-adcp-capabilities-response.json:1192, is the wire-profile tag and is correctly left alone. ad-tech-protocol-expert: sound.
  • minor is correct. Widens the verifier accept-set and deprecates without removing — every previously-conformant signer/verifier stays conformant. The removal of webhook-signing is deferred to #5555 and flagged major. Changeset present and accurately worded.
  • CI wiring. test:webhook-signing-vectors added to the dedicated build-check step and chained into the test aggregate. The 3.0.x:+3.0.x: refspec force-update is standard syntax, safe under --depth=1 in an ephemeral checkout.

Follow-ups (non-blocking — file as issues)

  • The doc-path sweep missed the strings it was standing on. This PR migrates docs/building/implementation/securitydocs/building/by-layer/L1/security across the corpus, but get-adcp-capabilities-response.json:~1015 still points its key_origins description at the old docs/building/implementation/security.mdx §Origin separation. Cosmetic prose inside a description string — fold it into the follow-up sweep the PR body already acknowledges.
  • Blast-radius migration wording. Default key reuse means a request-signing-key compromise now extends to webhooks. The docs disclose this and name the mitigation (a second request-signing key under a distinct kid). For operators who today run separate webhook-signing material, strengthen the migration note to "keep separate key material to preserve your existing isolation" so a collapse-to-one-key migration isn't a silent posture downgrade. security-reviewer L1.

Minor nits (non-blocking)

  1. Run the suite once on PR head. Static + cryptographic analysis says green (8 positive + negative/008), but capture the actual test:webhook-signing-vectors tally before merge — code-reviewer and I both verified by inspection, not execution.

LGTM. Follow-ups noted below.

@bokelley bokelley merged commit 7a48ee4 into adcontextprotocol:main Jun 16, 2026
30 checks passed
BaiyuScope3 added a commit to BaiyuScope3/adcp-client that referenced this pull request Jun 16, 2026
…re paths

Align with the merged spec (adcontextprotocol/adcp#5552): webhooks are signed
with the request-signing key, so the sell-side emission grader
(assertJwksPurpose) and the tenant-registry webhook auto-wire
(assertWebhookSigningUse) must accept adcp_use 'request-signing' as well as the
deprecated 'webhook-signing'. Previously both hard-required 'webhook-signing'
and would reject a conformant request-signing agent. Updates the auto-wire
tests and the signWebhook doc comment to match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
bokelley pushed a commit to BaiyuScope3/adcp-client that referenced this pull request Jun 17, 2026
…re paths

Align with the merged spec (adcontextprotocol/adcp#5552): webhooks are signed
with the request-signing key, so the sell-side emission grader
(assertJwksPurpose) and the tenant-registry webhook auto-wire
(assertWebhookSigningUse) must accept adcp_use 'request-signing' as well as the
deprecated 'webhook-signing'. Previously both hard-required 'webhook-signing'
and would reject a conformant request-signing agent. Updates the auto-wire
tests and the signWebhook doc comment to match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
bokelley pushed a commit to adcontextprotocol/adcp-client that referenced this pull request Jun 17, 2026
* feat(signing): accept reused request-signing key for webhook delivery

Webhook verifier step 8 now accepts a key whose adcp_use is either
'webhook-signing' or 'request-signing', letting a signer reuse the
request-signing key it already publishes instead of minting a dedicated
webhook key. Any other purpose (response-signing, governance-signing,
unknown) is still rejected with webhook_mode_mismatch.

Safe because domain separation is carried by the RFC 9421 tag
(adcp/webhook-signing/v1, part of the signed base) plus mandatory
content-digest coverage, not by the key-purpose discriminator: a captured
request signature can never be replayed against the webhook verifier
because step 3 rejects its tag.

signWebhook / signWebhookAsync accept the same set. Conformance vector
008 flipped from negative (request-signing rejected) to positive
(request-signing reuse accepted); a new negative 008 covers a
response-signing key. A dedicated webhook key remains RECOMMENDED but is
no longer REQUIRED.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(signing): use key_purpose_invalid for all webhook key-purpose failures

Per reviewer feedback on the paired spec PR: stop overloading
webhook_mode_mismatch (reserved for the HMAC-vs-9421 auth-mode selector) for
key-purpose failures. Step 8 now emits webhook_signature_key_purpose_invalid
for absent adcp_use, a missing verify key_op, OR an adcp_use outside
{webhook-signing, request-signing}. Updates the negative 008 vector, keys.json
and fixture README, and the test assertion to match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(signing): webhooks use the request-signing key; deprecate webhook-signing

Reframe to the unified model: webhooks are signed with a request-signing key
(no separate webhook key purpose). Verifier step 8 and the signer gates still
accept request-signing and the deprecated webhook-signing (backward compat,
removed in 4.0); the error message and all doc comments now lead with
request-signing. Marks the AdcpUse 'webhook-signing' member deprecated and
updates the webhook-emitter key-purpose guidance (reuse the request-signing
provider; isolate via a second request-signing kid, not a distinct adcp_use).
No behavior change — code already accepted both purposes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(signing): reference webhook-signing removal issue instead of 4.0

Mirror the spec review fix: replace hard-coded '4.0' removal language in the
AdcpUse deprecation notes, verifier/signer/emitter comments, changeset, and
vendored conformance README with a reference to the tracking issue
(adcontextprotocol/adcp#5555), since the removal window is a WG decision.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(signing): accept request-signing keys on webhook emission/auto-wire paths

Align with the merged spec (adcontextprotocol/adcp#5552): webhooks are signed
with the request-signing key, so the sell-side emission grader
(assertJwksPurpose) and the tenant-registry webhook auto-wire
(assertWebhookSigningUse) must accept adcp_use 'request-signing' as well as the
deprecated 'webhook-signing'. Previously both hard-required 'webhook-signing'
and would reject a conformant request-signing agent. Updates the auto-wire
tests and the signWebhook doc comment to match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants