Found while vendoring the complete AdCP 3.1.1 request-signing vector set (#975). Reproductions are canonicalization.json cases, currently strict-xfail in tests/conformance/signing/test_canonicalization.py.
Problem
The spec requires signers to refuse to sign, and verifiers to reject, URLs whose authority is syntactically malformed:
If the authority has userinfo but no host (https://user@/p), no host at all (https://:443/p or https:///p), a bracketed host missing a closing bracket (https://[::1/p), or a bare IPv6 address outside brackets (https://fe80::1/p), the URL is malformed; signers MUST NOT sign it and verifiers MUST reject with request_target_uri_malformed.
— security.mdx, @target-uri canonicalization step 3
IPv6 zone identifiers (RFC 6874) MUST be rejected. ... verifiers MUST reject @target-uri values containing %25 inside [...] with request_target_uri_malformed.
— ibid., step 2
canonicalize_target_uri() rejects only one of the six shapes. The rest are canonicalized into a well-formed-looking result and signed.
Failing cases
| case |
input |
expected |
actual |
malformed-port-without-host |
https://:443/p |
reject |
accepted as https:///p |
malformed-userinfo-without-host |
https://user@/p |
reject |
accepted as https:///p |
malformed-empty-authority |
https:///p |
reject |
accepted as https:///p |
malformed-bare-ipv6 |
https://fe80::1/p |
reject |
accepted as https://fe80::1/p |
malformed-ipv6-zone-identifier |
https://[fe80::1%25eth0]/p |
reject |
accepted unchanged |
malformed-ipv6-missing-closing-bracket |
https://[::1/p |
reject |
✅ rejected (ValueError) |
The first three collapse to an empty authority, so @authority enters the signature base as the empty string and @target-uri as https:///p. Two different malformed inputs (https://:443/p and https://user@/p) canonicalize to the identical base — signatures become transferable between them.
malformed-bare-ipv6 is a distinct hazard: _canon_authority() splits fe80::1 on the last colon, yielding host fe80: and port 1, then reassembles it as fe80::1. The round-trip masks the malformation.
request_target_uri_malformed is not implemented
The spec's error code for all of these does not exist anywhere in the SDK — src/adcp/signing/errors.py has no such constant, and grep -rn request_target_uri_malformed src/ is empty. It has its own row in the transport error taxonomy:
| Request @target-uri is syntactically malformed (e.g., empty authority, bare IPv6, IPv6 zone identifier, raw non-ASCII host), OR canonicalized @authority does not byte-match the authority component of the canonical @target-uri (cross-vhost replay) | No | request_target_uri_malformed |
Because the code is missing, the vendored test asserts only that canonicalization refuses (pytest.raises(ValueError)) rather than grading the code. That assertion should be tightened to the spec code once it exists.
Note on the taxonomy row
That same row also covers a second obligation the SDK does not appear to implement: canonicalized @authority must byte-match the authority component of the canonical @target-uri, and verifiers must derive @authority from :authority/Host rather than proxy state. The spec flags it as a cross-vhost replay defense. No vector in the 3.1.1 set exercises it, so it is not covered by the strict-xfail ledger — flagging it here rather than filing separately, since the fix lands in the same place.
Suggested direction
Reject the five shapes in _canon_authority(), add request_target_uri_malformed to the error taxonomy, and map the canonicalization refusal onto it at the verifier boundary.
Found while vendoring the complete AdCP 3.1.1 request-signing vector set (#975). Reproductions are
canonicalization.jsoncases, currently strict-xfail intests/conformance/signing/test_canonicalization.py.Problem
The spec requires signers to refuse to sign, and verifiers to reject, URLs whose authority is syntactically malformed:
canonicalize_target_uri()rejects only one of the six shapes. The rest are canonicalized into a well-formed-looking result and signed.Failing cases
malformed-port-without-hosthttps://:443/phttps:///pmalformed-userinfo-without-hosthttps://user@/phttps:///pmalformed-empty-authorityhttps:///phttps:///pmalformed-bare-ipv6https://fe80::1/phttps://fe80::1/pmalformed-ipv6-zone-identifierhttps://[fe80::1%25eth0]/pmalformed-ipv6-missing-closing-brackethttps://[::1/pValueError)The first three collapse to an empty authority, so
@authorityenters the signature base as the empty string and@target-uriashttps:///p. Two different malformed inputs (https://:443/pandhttps://user@/p) canonicalize to the identical base — signatures become transferable between them.malformed-bare-ipv6is a distinct hazard:_canon_authority()splitsfe80::1on the last colon, yielding hostfe80:and port1, then reassembles it asfe80::1. The round-trip masks the malformation.request_target_uri_malformedis not implementedThe spec's error code for all of these does not exist anywhere in the SDK —
src/adcp/signing/errors.pyhas no such constant, andgrep -rn request_target_uri_malformed src/is empty. It has its own row in the transport error taxonomy:Because the code is missing, the vendored test asserts only that canonicalization refuses (
pytest.raises(ValueError)) rather than grading the code. That assertion should be tightened to the spec code once it exists.Note on the taxonomy row
That same row also covers a second obligation the SDK does not appear to implement: canonicalized
@authoritymust byte-match the authority component of the canonical@target-uri, and verifiers must derive@authorityfrom:authority/Hostrather than proxy state. The spec flags it as a cross-vhost replay defense. No vector in the 3.1.1 set exercises it, so it is not covered by the strict-xfail ledger — flagging it here rather than filing separately, since the fix lands in the same place.Suggested direction
Reject the five shapes in
_canon_authority(), addrequest_target_uri_malformedto the error taxonomy, and map the canonicalization refusal onto it at the verifier boundary.