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.
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/@authoritycanonicalization insrc/adcp/signing/canonical.pyperforms no IDN processing._canon_authority()lowercases the host withstr.lower()and returns it as-is:The SDK already ships the correct helper —
adcp.signing._idna_canonicalize.canonicalize_host()— and wires it intojwks.py,key_origins.py,ip_pinned_transport.py,revocation_fetcher.pyandcanonical_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:@target-uriidn-to-punycodehttps://bücher.example/phttps://xn--bcher-kva.example/phttps://bücher.example/pidn-mixed-case-to-punycodehttps://BÜCHER.Example/phttps://xn--bcher-kva.example/phttps://bücher.example/p(
@authoritydiverges identically: expectedxn--bcher-kva.example, actualbücher.example.)negative/026-non-ascii-host.json— the verifier side of the same gap:request_signature_header_malformed, actualrequest_signature_invalidat 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:
Processing mode matters
The spec pins the mode, because this is a known cross-language divergence:
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.