Skip to content

@target-uri canonicalization applies no IDNA A-label conversion, and the verifier does not reject raw U-labels #977

Description

@KonstantinMirin

Found while vendoring the complete AdCP 3.1.1 request-signing vector set (#975). Reproductions are newly vendored cases, currently strict-xfail.

Problem

@target-uri / @authority canonicalization in src/adcp/signing/canonical.py performs no IDN processing. _canon_authority() lowercases the host with str.lower() and returns it as-is:

host = host.lower()
if port is not None and port != _DEFAULT_PORTS.get(scheme):
    return f"{host}:{port}"
return host

The SDK already ships the correct helper — adcp.signing._idna_canonicalize.canonicalize_host() — and wires it into jwks.py, key_origins.py, ip_pinned_transport.py, revocation_fetcher.py and canonical_formats/references.py. canonical.py, the RFC 9421 signing path, is the one place that never calls it.

Consequence: a request to an IDN host signs and verifies over a signature base containing a raw non-ASCII host. A conformant counterparty computes the A-label base, so the two disagree and the signature fails to verify — the classic silent 9421 interop break.

Failing cases

canonicalization.json:

case input expected @target-uri actual
idn-to-punycode https://bücher.example/p https://xn--bcher-kva.example/p https://bücher.example/p
idn-mixed-case-to-punycode https://BÜCHER.Example/p https://xn--bcher-kva.example/p https://bücher.example/p

(@authority diverges identically: expected xn--bcher-kva.example, actual bücher.example.)

negative/026-non-ascii-host.json — the verifier side of the same gap:

  • expected request_signature_header_malformed, actual request_signature_invalid at step 10.

The spec is explicit that these are two different obligations. Signers MUST emit A-labels; verifiers MUST reject a raw U-label rather than silently re-normalize it:

A host containing raw non-ASCII bytes that has not been ToASCII-normalized by the signer MUST be rejected by the verifier; verifiers do not silently re-normalize.
security.mdx, @target-uri canonicalization step 2

Processing mode matters

The spec pins the mode, because this is a known cross-language divergence:

UTS-46 Nontransitional processing with CheckHyphens=true, CheckBidi=true, UseSTD3ASCIIRules=true, Transitional_Processing=false ... the idna package — not str.encode('idna'), which is IDNA2003

Worth confirming _idna_canonicalize.canonicalize_host() matches that pin before reusing it here.

Suggested direction

Call canonicalize_host() from _canon_authority() for the signer/base-construction path, and add a verifier-side rejection for authorities carrying raw non-ASCII bytes.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions