Skip to content

feat(signing): accept any well-formed adcp_use purpose in keygen - #1708

Merged
bokelley merged 2 commits into
nextfrom
claude/issue-1677-keygen-open-purpose
Oct 29, 2026
Merged

bokelley merged 2 commits into
nextfrom
claude/issue-1677-keygen-open-purpose

Conversation

@bokelley

Copy link
Copy Markdown
Contributor

Problem

generate_signing_keypair, pem_to_adcp_jwk and adcp-keygen --purpose narrowed purpose to Literal["request-signing", "webhook-signing"] and raised on anything else. That set is narrower than the spec vocabulary and than the SDK's own internals:

  • response-signing (the designated-task response JWS profile, core/response-payload-jws-envelope.json) could not be minted or published.
  • governance-signing is what adcp.governance already requires of a governance JWK, but keygen could not produce one.
  • A surface the vocabulary does not name yet (e.g. an agent signing its own OAuth access tokens) had no way to set adcp_use.

The helpers underneath (generate_ed25519, generate_es256, _adcp_public_jwk) already took a plain str.

Fix

  • purpose is validated syntactically: a lowercase hyphenated token ([a-z0-9]+(-[a-z0-9]+)*), at most 64 characters. Empty, upper-case, underscore, whitespace, leading/trailing/double-hyphen and non-str values still raise ValueError; the CLI exits 2 before writing a file.
  • Constants: adcp.signing now exports ADCP_USE_REQUEST, ADCP_USE_WEBHOOK (commented as deprecated upstream, adcp#5555), and the new ADCP_USE_RESPONSE and ADCP_USE_GOVERNANCE. governance.py uses the constant.
  • Docstrings say why an open vocabulary is safe (purpose matching is fail-closed) and no longer claim every verifier refuses a request-signing key on webhooks: 3.3 webhook verifiers accept {request-signing, webhook-signing}, and new webhook signers publish request-signing.

The signature is widened from a Literal to str, so existing callers are unaffected. This isn't a breaking change.

Tests

  • response-signing, governance-signing and an unregistered token round-trip into adcp_use, through both entry points and the CLI.
  • Malformed purposes raise.
  • Fail-closed property: a key minted under response-signing, governance-signing or an unregistered purpose is refused by the request verifier (request_signature_key_purpose_invalid) and by the webhook verifier's accepted-uses set.

Not changed: WebhookSender.from_jwk still requires adcp_use == "webhook-signing" even though 3.3 says new webhook signers use request-signing. That's a sender-policy question, separate from what keygen can express, so I left it as a follow-up.

Closes #1677

🤖 Generated with Claude Code

generate_signing_keypair, pem_to_adcp_jwk and adcp-keygen --purpose took a
closed Literal["request-signing", "webhook-signing"] and refused every other
value, so the SDK could not mint or publish a key for response-signing (the
designated-task response JWS profile, core/response-payload-jws-envelope.json),
governance-signing (which governance.py already verifies), or a surface the
vocabulary does not name yet. The helpers they delegate to already took a str.

purpose is now validated by syntax instead of membership: a lowercase
hyphenated token of at most 64 characters. Malformed values (empty, upper
case, underscores, whitespace, leading/trailing/double hyphens, non-str)
still raise ValueError, and the CLI exits 2 before writing a file. An open
vocabulary is safe because purpose matching is fail-closed: a verifier
accepts a key only when adcp_use equals the purpose of the surface it
verifies, so a key under any other purpose verifies nothing there. A test
pins that against the request and webhook verifiers.

adcp.signing exports ADCP_USE_REQUEST, ADCP_USE_WEBHOOK (marked deprecated
upstream), and the new ADCP_USE_RESPONSE and ADCP_USE_GOVERNANCE, so callers
need not retype literals; governance.py uses the constant. The docstrings no
longer say request- and webhook-signing keys must be distinct for every
verifier: 3.3 webhook verifiers accept request-signing keys, and new webhook
signers publish request-signing.

Closes #1677

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@aao-secretariat aao-secretariat Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Ladon verdict: Approve

Approve — clean PR, no blocking or medium findings.

Checked: widening keygen purpose from a closed Literal to a syntactically-validated open str is a contravariant input change (non-breaking, correctly shipped under feat(signing):). Reviewer verified the fail-closed claim holds against the real verifier (membership-checked adcp_use), re.fullmatch anchors token validation, and the CLI exits 2 before writing. Tests cover every new branch.

High-risk flag is true only because three src/adcp/signing/** files were modified, but no medium-or-higher finding attaches to them, so the modification is presumed safe (no deletions). No gated paths, no author team gate, no escalation trigger. Falls through the decision table to row 9.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@aao-secretariat aao-secretariat Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Ladon verdict: Approve

Approve.

Delta since the prior approve is a single additive change: tests/fixtures/public_api_snapshot.json gains four adcp.signing constants (ADCP_USE_GOVERNANCE/REQUEST/RESPONSE/WEBHOOK). Values match constants.py and the __all__ additions verbatim, ordering is consistent, and the feat(signing): prefix is correct for additive exports plus a non-breaking Literal→str widening of purpose.

Checks:

  • No critical/high/medium findings in this run.
  • high_risk is true only due to (modified) entries under src/adcp/signing/**, but the reviewer found no medium-or-higher concerns on those files — modifications presumed safe. No (deleted) reasons, so row 3 does not fire.
  • gated_paths: false; review_decision: APPROVED — no gate blocks.
  • No no-auto-approve team match.
  • Prior decision was approve, so no sticky escalation applies.

No escalation trigger, no team gate, zero medium findings → falls through to row 9 (approve).

bokelley added a commit that referenced this pull request Oct 29, 2026
generate_signing_keypair, pem_to_adcp_jwk and adcp-keygen --purpose took a
closed Literal["request-signing", "webhook-signing"] and refused every other
value, so the SDK could not mint or publish a key for response-signing (the
designated-task response JWS profile, core/response-payload-jws-envelope.json),
governance-signing (which governance.py already verifies), or a surface the
vocabulary does not name yet. The helpers they delegate to already took a str.

purpose is now validated by syntax instead of membership: a lowercase
hyphenated token of at most 64 characters. Malformed values (empty, upper
case, underscores, whitespace, leading/trailing/double hyphens, non-str)
still raise ValueError, and the CLI exits 2 before writing a file. An open
vocabulary is safe because purpose matching is fail-closed: a verifier
accepts a key only when adcp_use equals the purpose of the surface it
verifies, so a key under any other purpose verifies nothing there. A test
pins that against the request and webhook verifiers.

adcp.signing exports ADCP_USE_REQUEST, ADCP_USE_WEBHOOK (marked deprecated
upstream), and the new ADCP_USE_RESPONSE and ADCP_USE_GOVERNANCE, so callers
need not retype literals. The docstrings no
longer say request- and webhook-signing keys must be distinct for every
verifier: 3.3 webhook verifiers accept request-signing keys, and new webhook
signers publish request-signing.

Backport of #1708 to 9.x. Closes #1677 on main.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@bokelley
bokelley merged commit 612f97a into next Oct 29, 2026
41 checks passed
@bokelley
bokelley deleted the claude/issue-1677-keygen-open-purpose branch October 29, 2026 23:42
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.

1 participant