Skip to content

Request-signing verifier accepts duplicate-label Signature-Input (conformance vector negative/021 expects header_malformed) #1022

Description

@fgranata

Summary

adcp.signing.verifier.verify_request_signature (adcp 6.6.0, Python 3.12) accepts a request whose Signature-Input header contains the same label twice — e.g. sig1=(...), sig1=(...). The conformance vector negative/021-duplicate-signature-input-label (dist/compliance/3.1.13/test-vectors/request-signing/) expects rejection with request_signature_header_malformed.

RFC 8941 §3.2 permits parsers to reject duplicate dictionary keys, and the vector's $comment cites the AdCP profile's downgrade-protection rule requiring it: with last-wins parsing, two different signature bases can hide behind one label — the verifier validates one of them while a downstream consumer may read the other.

We found this while grading our seller against the vectors: the live failure initially surfaced as request_signature_window_invalid (the vector's frozen timestamps fail the window check first), which masked the parser leniency. The repro below uses a fresh window and a signature that genuinely verifies — the duplicate label is the only defect, and the verifier accepts it.

Likely the same parser layer affects negative/022-multi-valued-content-type and negative/023-multi-valued-content-digest (we haven't isolated those to the parser as cleanly; the duplicate-label case is proven).

Repro (self-contained, adcp 6.6.0)

import time
from adcp.signing import crypto, keygen
from adcp.signing.signer import sign_request
from adcp.signing.verifier import (
    VerifierCapability, VerifyOptions, verify_request_signature,
)

URL = "https://seller.example.com/adcp/create_media_buy"
BODY = b'{"plan_id":"plan_001"}'
pem, jwk = keygen.generate_ed25519("dup-kid", adcp_use="request-signing")
pk = crypto.load_private_key_pem(pem)
headers = {"content-type": "application/json"}
sh = sign_request(method="POST", url=URL, headers=headers, body=BODY,
                  private_key=pk, key_id="dup-kid", alg="ed25519")
merged = dict(headers)
merged.update(sh.as_dict())
key = next(k for k in merged if k.lower() == "signature-input")
merged[key] = merged[key] + ", " + merged[key]  # duplicate the sig1 label

opts = VerifyOptions(
    now=time.time(),
    capability=VerifierCapability(
        supported=True, covers_content_digest="either",
        required_for=frozenset(), supported_for=frozenset({"create_media_buy"}),
    ),
    operation="create_media_buy",
    jwks_resolver=lambda keyid: jwk if keyid == "dup-kid" else None,
    expected_tag="adcp/request-signing/v1",
    expected_adcp_use="request-signing",
    allowed_algs=frozenset({"ed25519"}),
    signing_purpose="request-signing",
)
signer = verify_request_signature(
    method="POST", url=URL, headers=merged, body=BODY, options=opts)
print("ACCEPTED - signer keyid:", signer.key_id)
# prints: ACCEPTED - signer keyid: dup-kid
# expected: SignatureVerificationError(request_signature_header_malformed)

Expected

SignatureVerificationError with code request_signature_header_malformed, per the vector and the profile's step-6 strict-parse requirement.

We've added an application-side pre-check as defense in depth, so this isn't blocking us — filing so the SDK's parser matches the conformance vectors it ships with. Happy to contribute the fix if useful.

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