Repository navigation
Three step-1 header rules strict_header_precheck does not apply, and the gate is private #1356
Description
Summary
adcp/signing/_header_precheck.py (8.0.0) rejects all four negative conformance
vectors that grade checklist step 1 — 021-duplicate-signature-input-label,
022-multi-valued-content-type, 023-multi-valued-content-digest,
026-non-ascii-host — and its rule table has three holes that no vector in the
kit exercises:
- the RFC 8941 §3.2 duplicate-dictionary-key rule runs on
Signature-Input
andContent-Digest, not onSignature; - dictionary keys compare as written, so
Content-Digest: SHA-256=:…:, sha-256=:…:carries one RFC 9530 §2 algorithm twice and passes; - the
Hostheader is checked for raw non-ASCII bytes only, so the authority
shapes the URL canonicalization algorithm's steps 2–3 bind comparers to
reject — a bare IPv6 literal, an unclosed bracket, a zone identifier,
userinfo or a port with no host — pass on the header.
strict_header_precheck is also absent from adcp.signing.verifier.__all__ and
from adcp.signing, and canonical._malformed_authority_reason — the one
callable form of the authority rule, since canonicalize_target_uri applies it
but cannot be asked without raising — is private. A verifier that runs step 1
ahead of its own key-resolution work imports both from under the package's API.
#1022 is the ancestor: it reported the duplicate-label defect on
Signature-Input against 6.6.0, and _header_precheck.py is the fix. These are
the rows that fix does not cover.
Why this is API design, not schema or codegen
The rules are hand-written and hand-tabled. _SINGLE_LINE_SIGNED_HEADERS
already lists signature, so the repeated-line rule covers it; the
duplicate-key rule is applied by two if name == … branches that name the other
two headers. _non_ascii_authority_reason reads the Host header and applies
one of the two authority rules the module's sibling already defines. Nothing
generated or pinned drives any of it.
Repro, against 8.0.0
from adcp.signing.verifier import strict_header_precheck
URL = "https://seller.example.com/adcp/create_media_buy"
SIG_INPUT = (
'sig1=("@method" "@target-uri" "@authority" "content-type");created=1776520800;'
'expires=1776521100;nonce="KXYnfEfJ0PBRZXQyVXfVQA";keyid="test-ed25519-2026";'
'alg="ed25519";tag="adcp/request-signing/v1"'
)
SIG = "sig1=:" + "A" * 86 + ":"
def precheck(**extra: str) -> str | None:
headers = {"Content-Type": "application/json", "Signature-Input": SIG_INPUT, "Signature": SIG}
headers.update(extra)
raw = [(k.lower().encode(), v.encode("latin-1")) for k, v in headers.items()]
return strict_header_precheck(headers=headers, raw_headers=raw, url=URL)
print("dup Signature label:", precheck(Signature=f"sig1=:{'A' * 86}:, sig1=:{'B' * 86}:"))
print("Content-Digest case:", precheck(**{
"Content-Digest": "SHA-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:, "
"sha-256=:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=:",
}))
print("bare IPv6 Host: ", precheck(Host="::1"))
print("zone-id Host: ", precheck(Host="[fe80::1%25eth0]"))Output on adcp==8.0.0:
dup Signature label: None
Content-Digest case: None
bare IPv6 Host: None
zone-id Host: None
None is "nothing is malformed", and verify_request_signature proceeds to
step 7 on each of the four.
Downstream consequence
Each of the four requests above is one a conforming verifier rejects at step 1,
and none of them is caught at a later step either:
- a repeated
sig1onSignaturereaches the RFC 8941 parser, which retains
one of the two signature values. A verifier that keeps the first and a
consumer that keeps the last read different credentials over a
Signature-Inputboth agree on — the parser-differential RFC 8941 §3.2
exists to close, on the one header where the disagreement is about the
credential itself. Content-Digestcarrying the same algorithm twice is what
negative/023rejects. A change of case reopens it:content_digest_matches
reads one of the two values, and the signer may have digested the other.- a malformed
Hostreaches the adopter unexamined.build_signature_base
derives@authorityfrom the URL, so the header's shape is never judged
anywhere, and the header is the only place these shapes arrive — an ASGI
framework buildingrequest.urldoes not carry them over.
The four vectors pass either way, so conformance stays green while the rules do
not hold. That is the reason to close them in the SDK rather than leave them to
each adopter.
Fix
Covered by the attached pull request:
Signaturejoins a dictionary-header table that replaces the two
per-header branches;- dictionary keys compare case-folded — RFC 8941 §3.2's
keyproduction admits
lowercase only, so two keys differing in case are never two keys; - the
Hostheader runs throughmalformed_authority_reason, which
canonicalization already applies to the URL. Same rule, two codes, two steps,
which is the split_header_precheck's own docstring states; strict_header_precheckandmalformed_authority_reasonare exported from
adcp.signing.
All 70 request-signing vectors keep their verdicts and no positive vector is
refused.
Related
The three cases are expressible as conformance vectors and the kit ships none of
them, so an implementation passes request-signing and still accepts all three.
Filed against the spec repository separately.
The per-header spelling of the duplicate-key rule is one instance of a pattern in
src/adcp/signing/: a rule declared in one place and decided in another. #1354
enumerates the others.
Activity
bokelley commentedon Oct 3, 2026
Triage
Classification: Bug
Bucket(s): signing (label absent — noted below)
Status: deferred
Blocked-on: #1369 — resurfaces on merge
PR #1369 ("fix(signing): close three step-1 header rules and export the precheck"), opened by the same author minutes after this issue, directly
Closes #1356and addresses all three rule gaps: addingSignatureto the dictionary-header table, case-folding dictionary key comparisons per RFC 8941 §3.2, and routingHostthroughmalformed_authority_reasonfor the authority shapes URL canonicalization already rejects. All four repro vectors and 920 existing vectors pass on the PR head.No further action needed on the issue until PR #1369 merges or is closed.
Triaged by Claude Code. Session: https://claude.ai/code/session_017r8LgzvaemgTE61dC9ipgQ
Generated by Claude Code