Found while vendoring the complete AdCP 3.1.1 request-signing vector set (#975). Reproductions are the newly vendored vectors, currently strict-xfail in tests/conformance/signing/test_verifier_vectors.py.
Problem
verify_request_signature has no step-1 strict-parse stage. Structured-field input that is malformed or ambiguous is not rejected at parse time; it flows on to a later checklist step and is rejected under a different error code — or, worse, is silently resolved to one interpretation.
Conformance requires byte-for-byte match on expected_outcome.error_code, so rejecting with the wrong code is a graded failure, not a near-miss. More importantly, each of these is a parser-differential surface: signer and verifier can disagree on what was actually signed.
Failing vectors
| vector |
expected |
actual |
step reached |
negative/021-duplicate-signature-input-label.json |
request_signature_header_malformed |
request_signature_components_incomplete |
6 |
negative/022-multi-valued-content-type.json |
request_signature_header_malformed |
request_signature_invalid |
10 |
negative/023-multi-valued-content-digest.json |
request_signature_header_malformed |
request_signature_invalid |
10 |
021 — duplicate Signature-Input dictionary key
Signature-Input declares label sig1 twice. RFC 8941 §3.2 permits an implementation to reject or retain the last value; the AdCP profile requires rejection at step 1 as downgrade protection.
This is the one with teeth. The SDK silently resolves the duplicate and then rejects for an unrelated reason — request_signature_components_incomplete: required covered component missing: @authority — which means it took the second sig1, the one covering fewer components. That is precisely the smuggling scenario the spec's rejection rule exists to prevent: a proxy appends a weaker sig1 and a verifier that reads the last value grades a different component set than the signer committed to. The vector's own $comment calls this out.
022 — multi-valued Content-Type
Content-Type: application/json, text/plain on a field covered by the signature. Content-Type is not a List-Structured-Field, so concatenation leaves the covered value undefined relative to the signature base. The SDK derives a base from the raw value and fails at crypto verify instead of rejecting at parse.
023 — multi-valued Content-Digest
Two sha-256 members in one Content-Digest. Per RFC 9530 §2 this is a Dictionary field and duplicate algorithms are ambiguous for signature coverage. Same failure mode as 022.
Suggested direction
A strict-parse stage ahead of the checklist that rejects with request_signature_header_malformed on: duplicate keys in the Signature-Input / Signature dictionaries, and multiple field values (or duplicate structured-field members) for any covered non-list field.
Note this changes the error code returned for inputs that are currently rejected as request_signature_invalid / request_signature_components_incomplete, which is a visible behavior change for consumers keying on the code.
Found while vendoring the complete AdCP 3.1.1 request-signing vector set (#975). Reproductions are the newly vendored vectors, currently strict-xfail in
tests/conformance/signing/test_verifier_vectors.py.Problem
verify_request_signaturehas no step-1 strict-parse stage. Structured-field input that is malformed or ambiguous is not rejected at parse time; it flows on to a later checklist step and is rejected under a different error code — or, worse, is silently resolved to one interpretation.Conformance requires byte-for-byte match on
expected_outcome.error_code, so rejecting with the wrong code is a graded failure, not a near-miss. More importantly, each of these is a parser-differential surface: signer and verifier can disagree on what was actually signed.Failing vectors
negative/021-duplicate-signature-input-label.jsonrequest_signature_header_malformedrequest_signature_components_incompletenegative/022-multi-valued-content-type.jsonrequest_signature_header_malformedrequest_signature_invalidnegative/023-multi-valued-content-digest.jsonrequest_signature_header_malformedrequest_signature_invalid021 — duplicate
Signature-Inputdictionary keySignature-Inputdeclares labelsig1twice. RFC 8941 §3.2 permits an implementation to reject or retain the last value; the AdCP profile requires rejection at step 1 as downgrade protection.This is the one with teeth. The SDK silently resolves the duplicate and then rejects for an unrelated reason —
request_signature_components_incomplete: required covered component missing: @authority— which means it took the secondsig1, the one covering fewer components. That is precisely the smuggling scenario the spec's rejection rule exists to prevent: a proxy appends a weakersig1and a verifier that reads the last value grades a different component set than the signer committed to. The vector's own$commentcalls this out.022 — multi-valued
Content-TypeContent-Type: application/json, text/plainon a field covered by the signature.Content-Typeis not a List-Structured-Field, so concatenation leaves the covered value undefined relative to the signature base. The SDK derives a base from the raw value and fails at crypto verify instead of rejecting at parse.023 — multi-valued
Content-DigestTwo
sha-256members in oneContent-Digest. Per RFC 9530 §2 this is a Dictionary field and duplicate algorithms are ambiguous for signature coverage. Same failure mode as 022.Suggested direction
A strict-parse stage ahead of the checklist that rejects with
request_signature_header_malformedon: duplicate keys in theSignature-Input/Signaturedictionaries, and multiple field values (or duplicate structured-field members) for any covered non-list field.Note this changes the error code returned for inputs that are currently rejected as
request_signature_invalid/request_signature_components_incomplete, which is a visible behavior change for consumers keying on the code.