Skip to content

Canonicalization now strips a trailing FQDN-root dot; the spec defines no rule and ships no vector #986

Description

@KonstantinMirin

Summary

@target-uri / @authority canonicalization now strips a trailing FQDN-root dot from the host. This is a wire-visible change and the AdCP spec does not define the behaviour either way, so it is filed here to make it visible and to ask that upstream decide it.

Before: https://example.com./p → authority example.com.
After: https://example.com./p → authority example.com

Landed in #985 (feat(signing): reject malformed authorities, convert IDN hosts to A-labels).

Why the change was forced

#985 makes the signing path call _idna_canonicalize.canonicalize_host for non-ASCII hosts, which is the package's designated host normalizer and already has six consumers. That helper strips one trailing root dot as part of its UTS-46 preparation. The pre-existing ASCII path did a bare host.lower(), which keeps it.

Leaving both as they were would have produced a branch-dependent authority:

input ASCII branch IDNA branch
https://xn--bcher-kva.example./p xn--bcher-kva.example.
https://bücher.example./p xn--bcher-kva.example

Same host, two spellings, two different @authority values — so a signer emitting the A-label form and a verifier deriving the host from a Host header carrying the U-label would compute different signature bases and every signature between them would fail to verify. That is precisely the silent canonicalization drift the single-algorithm design exists to prevent.

Three options were available, none of them free:

  1. Strip once, before the branch (chosen). Both branches agree, and they agree with the designated normalizer. Costs a wire-visible change for FQDN-root URLs.
  2. Detach and re-append around the branch. Preserves today's bytes exactly, but it is bespoke normalization inside canonical.py — which would make it the seventh host-normalizing implementation in the tree, the duplication fix(signing): reject malformed authorities, convert IDN hosts to A-labels #985 exists to reduce.
  3. Reject a trailing root dot as a malformed authority. Unambiguous, but invents a rejection the spec does not state, and rejecting traffic the spec may require accepting is worse than the ambiguity.

What this issue is actually asking for

The spec should define root-dot handling and ship a vector for it. example.com. and example.com are the same DNS name; whether they are the same signature base is a protocol question, not an SDK question, and every implementation is currently guessing.

Note that the SDK could not simply add a local vector to settle it. tests/conformance/vectors/request-signing/ is vendored verbatim and pinned by SHA-256 in vector_manifest.json; test_vendored_tree_matches_manifest_exactly fails on extra files as well as modified ones, and its own message states the policy:

"Vectors are vendored verbatim from the spec; add the file upstream first, then re-vendor and regenerate the manifest."

That pin is #975/#980's whole point, so working around it locally would have reintroduced the drift those issues exist to stop. The behaviour is therefore pinned by an SDK-local test (test_trailing_root_dot_is_stripped_on_both_host_branches) which is explicitly marked as superseded once a real vector exists.

Suggested upstream ask (adcontextprotocol/adcp): add a root-dot-* case to dist/compliance/<version>/test-vectors/request-signing/canonicalization.json fixing whichever behaviour the spec wants, and state the rule in the @target-uri canonicalization steps alongside the existing scheme/port/path rules.

Impact

Low, but real. Affects only callers signing or verifying URLs whose host carries an explicit trailing dot. A new-SDK signer and an old-SDK verifier disagree for those URLs during rollout. Shipping as a minor (#985 is titled feat:) rather than a patch for this reason.

Related

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions