Skip to content

spec(security): pin sf-binary encoding + tighten URL canonicalization (#2341, #2343)#2348

Merged
bokelley merged 2 commits into
mainfrom
bokelley/upstream-filed
Apr 18, 2026
Merged

spec(security): pin sf-binary encoding + tighten URL canonicalization (#2341, #2343)#2348
bokelley merged 2 commits into
mainfrom
bokelley/upstream-filed

Conversation

@bokelley

Copy link
Copy Markdown
Contributor

Summary

Two clarifications to the AdCP RFC 9421 request-signing profile surfaced by TypeScript reference SDK work (adcp-client#575) and follow-up expert review. Both are spec tightenings — no behavioral change for signers that already match the reference vectors.

#2341 — binary value encoding

  • Signature and Content-Digest sf-binary tokens are normatively base64url-no-padding (RFC 4648 §5), overriding RFC 8941 §3.3.5's standard-base64 default. Matches the existing nonce rule; avoids two proxy hazards (/ rewriting, = parsed as a structured-field delimiter).
  • Verifiers MUST reject mixed-alphabet tokens (any of [+/=] plus any of [-_] in one value) — mixed tokens can decode to different bytes depending on "translate-then-decode" order, letting an attacker stage a Content-Digest mismatch that one verifier accepts and another rejects.
  • Verifiers MAY lenient-decode pure standard-base64 through AdCP 3.2 (time-bounded compatibility affordance).

#2343 — URL canonicalization

Expanded the @target-uri algorithm with normative text for every case that previously admitted implementation variance:

  • UTS-46 Nontransitional pinned (CheckHyphens/CheckBidi/UseSTD3ASCIIRules=true, Transitional_Processing=false) for IDN → Punycode. Closes the single biggest silent divergence between TS (url.domainToASCII), Go (x/net/idna), and Python (idna package).
  • IPv6 zone identifiers rejected (RFC 6874 §1 — node-local, no meaning cross-host). Signers MUST NOT sign, verifiers MUST reject.
  • Consecutive slashes preserved byte-for-byte (was: collapsed). Closes the /admin//foo vs /admin/foo path-confusion surface. Deployments MUST disable slash-folding on signed routes.
  • @authority derived from wire Host (or HTTP/2+ :authority), MUST byte-for-byte match canonical @target-uri authority. Closes cross-vhost replay on shared verifier pools.
  • Malformed-authority rejection expanded: bracket-mismatch IPv6, bare IPv6, empty authority, userinfo-only, raw non-ASCII host.
  • Percent-encoding spelled out: reserved stays encoded (%3A), full unreserved set decoded (%41A, not just %7E~).
  • Combined dot-segment + consecutive-slash cases pinned (/a/.//b/a//b, /a//../b/a/b).

New conformance set

static/compliance/source/test-vectors/request-signing/canonicalization.json31 fixed input/output cases (25 positive, 6 malformed-reject) covering every algorithm step. Version-pinned ("version": "3.0"). Independent of crypto — SDKs run it as a fast unit test. Published at /compliance/{version}/test-vectors/request-signing/canonicalization.json.

Closes #2341, #2343. Original profile: #2323 (3.0 GA).

Test plan

  • npm run precommit passes (29 test files, 587 tests, typecheck clean)
  • Mintlify docs validation passes
  • canonicalization.json parses, version-pinned, no duplicate input_urls, all required fields present on every case
  • Punycode output for bücher.example independently verified via Node's url.domainToASCII
  • TypeScript SDK (adcp-client#575) runs the 31 cases after this merges
  • Go and Python reference SDKs wire up canonicalization.json as a unit-test suite

🤖 Generated with Claude Code

…#2341, #2343)

Clarifications to the AdCP RFC 9421 request-signing profile surfaced by
TypeScript reference SDK work (adcp-client#575) and expert review.

#2341: Signature and Content-Digest sf-binary tokens are base64url-no-
padding (RFC 4648 §5), overriding RFC 8941's standard-base64 default.
Matches the nonce rule; avoids proxy hazards with '/' and '='. Verifiers
MUST reject mixed-alphabet tokens (closes digest-mismatch ambiguity) and
MAY lenient-decode pure standard-base64 through AdCP 3.2.

#2343: @target-uri algorithm expanded:
 - UTS-46 Nontransitional pinned for IDN to Punycode (TS / Go / Python
   default differently).
 - IPv6 zone identifiers (RFC 6874) are rejected -- node-local, no
   meaning cross-host.
 - Consecutive slashes preserved byte-for-byte (was: collapsed). Closes
   the /admin//foo vs /admin/foo path-confusion surface. Servers MUST
   disable slash-folding on signed routes.
 - Malformed-authority rejection expanded: bracket-mismatch IPv6, bare
   IPv6, empty authority, userinfo-only, raw non-ASCII host.
 - @authority MUST be derived from wire Host / :authority and MUST
   match the @target-uri authority byte-for-byte (closes cross-vhost
   replay on shared verifier pools).
 - Percent-encoding: reserved stays encoded (%3A), full unreserved set
   decoded (%41 to A, not just %7E to ~).
 - Combined dot-segment + consecutive-slash cases pinned.

New canonicalization.json (version 3.0): 31 fixed input/output cases
(25 positive, 6 malformed-reject) covering every algorithm step. No
crypto needed -- SDKs run it as a fast unit test.

Closes #2341, #2343. Original profile: #2323.

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

github-actions Bot commented Apr 18, 2026

Copy link
Copy Markdown
Contributor

Schema Link Check Results

Commit: c7883d8 - spec(security): rename URL-malformed error code to request_target_uri_malformed

⚠️ Warnings (schema not yet released)

These schemas exist in source but haven't been released yet. The links will be broken until the next version is published:

  • https://adcontextprotocol.org/schemas/v3/enums/specialism.json
    • Schema exists in latest (source) but not yet released in v3
    • Action: This link will work after next 3.x release is published

To fix: Either:

  1. Wait for the next release and merge this PR after the release is published
  2. Use latest instead of a version alias if you need the link to work immediately (note: latest is the development version and may change)
  3. Coordinate with maintainers to cut a new release before merging

…_malformed

Addresses expert-review nit on #2348. The previous taxonomy used
request_signature_header_malformed for both actual header malformation
AND URL-parse rejections — semantically confusing because URL rejections
happen before any signature header is inspected.

Split into two codes:
 - request_target_uri_malformed: URL syntax invalid (empty authority,
   bare IPv6, IPv6 zone identifier, bracket-mismatch, raw non-ASCII
   host, @authority / Host mismatch).
 - request_signature_header_malformed: unchanged — covers actual
   Signature / Signature-Input header problems and the Signature /
   Content-Digest mixed-alphabet rejection.

Safe to rename now — 3.0 has not shipped. Updates the 6 reject cases
in canonicalization.json; existing negative vectors that test actual
header malformation (011, 019) stay on the original code.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley bokelley merged commit 40aacfc into main Apr 18, 2026
16 checks passed
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.

request-signing: clarify sf-binary encoding — vectors use base64url, RFC 8941 specifies standard base64

1 participant