Summary
The vendored AdCP request-signing conformance vectors under tests/conformance/vectors/request-signing/ are an incomplete copy of the spec set, and the tests that grade against them are structurally incapable of noticing.
Both graders load vectors by globbing and then assert only that the result is non-empty:
tests/conformance/signing/test_verifier_vectors.py:77-78 — _load() does glob("*.json") then assert result, f"no vectors under ..."
tests/conformance/signing/test_canonicalization.py:24-31 — same shape for vectors carrying expected_signature_base
Any non-empty subset satisfies that assertion. A missing vector is indistinguishable from a complete set, so the suite reports green on a partial copy — which is exactly what has been happening.
The gap
Measured against AdCP 3.1.1, the version src/adcp/ADCP_VERSION already targets (spec repo dist/compliance/3.1.1/test-vectors/request-signing/):
|
spec 3.1.1 |
vendored |
missing |
positive/ |
12 |
8 |
009-percent-encoded-unreserved-decoded, 010-percent-encoded-slash-preserved, 011-ipv6-authority, 012-ipv6-authority-default-port-stripped |
negative/ |
28 |
20 |
021-duplicate-signature-input-label, 022-multi-valued-content-type, 023-multi-valued-content-digest, 024-unquoted-string-param, 025-jwk-alg-crv-mismatch, 026-non-ascii-host, 027-webhook-registration-authentication-unsigned, 028-unsigned-protocol-method-required |
canonicalization.json |
ships |
absent |
the standalone signing-base fixture |
canonicalization.json is the conspicuous one: the vendored README.md documents it in the file-layout tree and devotes a full section to it, describing it as "the fastest way to surface cross-implementation divergence" and recommending SDKs "run canonicalization.json on every commit". The file was never copied in.
Three further vectors had silently drifted from their upstream contents: 016-replayed-nonce, 017-key-revoked, 020-rate-abuse. 017 is the consequential one — it was vendored at a revision that revokes test-ed25519-2026, the primary signing key, rather than the dedicated test-revoked-2026 keypair the spec now uses. keys.json had never been refreshed to carry that key either.
Why it matters
Signature-base construction is the known-hard part of RFC 9421 interop, and the missing vectors sit exactly on its edges: 009/010 are percent-encoding, 011/012 are IPv6 authority, 026 is a non-ASCII host, 028 is the protocol_methods_required_for enforcement path.
A verifier can pass every currently-vendored vector and still reject validly-signed requests from a conformant counterparty. And because the SDK owns canonicalization on behalf of its consumers, these gaps are untested in every downstream implementation too, not just in this suite — this was found while adopting adcp.signing in a seller implementation that delegates canonicalization entirely to the SDK.
Fix
PR #980 vendors the complete 3.1.1 set and, more importantly, replaces the non-empty assertion with a completeness pin so the set cannot silently drift again at the next spec bump. Adding the 12 files without that guard would only reset the clock.
The pin is over content hashes, not filenames — a filename-only manifest would not have caught the 016/017/020 drift. It is tied to get_adcp_spec_version(), so bumping the targeted spec version without re-vendoring fails loudly.
Grading the new vectors turns up 12 real conformance failures
They are filed separately and marked strict-xfail in the PR rather than fixed there, since they are production behavior changes in a security-critical verifier:
- The verifier has no step-1 strict-parse stage, so malformed/ambiguous input flows through to a later check and is rejected under the wrong error code (negative
021, 022, 023, 026). Conformance requires byte-for-byte error codes, so "rejected anyway" is not a pass.
@target-uri canonicalization applies no UTS-46 A-label conversion, drops a trailing empty query, and accepts five malformed authority shapes the spec requires be rejected.
Summary
The vendored AdCP request-signing conformance vectors under
tests/conformance/vectors/request-signing/are an incomplete copy of the spec set, and the tests that grade against them are structurally incapable of noticing.Both graders load vectors by globbing and then assert only that the result is non-empty:
tests/conformance/signing/test_verifier_vectors.py:77-78—_load()doesglob("*.json")thenassert result, f"no vectors under ..."tests/conformance/signing/test_canonicalization.py:24-31— same shape for vectors carryingexpected_signature_baseAny non-empty subset satisfies that assertion. A missing vector is indistinguishable from a complete set, so the suite reports green on a partial copy — which is exactly what has been happening.
The gap
Measured against AdCP 3.1.1, the version
src/adcp/ADCP_VERSIONalready targets (spec repodist/compliance/3.1.1/test-vectors/request-signing/):positive/009-percent-encoded-unreserved-decoded,010-percent-encoded-slash-preserved,011-ipv6-authority,012-ipv6-authority-default-port-strippednegative/021-duplicate-signature-input-label,022-multi-valued-content-type,023-multi-valued-content-digest,024-unquoted-string-param,025-jwk-alg-crv-mismatch,026-non-ascii-host,027-webhook-registration-authentication-unsigned,028-unsigned-protocol-method-requiredcanonicalization.jsoncanonicalization.jsonis the conspicuous one: the vendoredREADME.mddocuments it in the file-layout tree and devotes a full section to it, describing it as "the fastest way to surface cross-implementation divergence" and recommending SDKs "runcanonicalization.jsonon every commit". The file was never copied in.Three further vectors had silently drifted from their upstream contents:
016-replayed-nonce,017-key-revoked,020-rate-abuse.017is the consequential one — it was vendored at a revision that revokestest-ed25519-2026, the primary signing key, rather than the dedicatedtest-revoked-2026keypair the spec now uses.keys.jsonhad never been refreshed to carry that key either.Why it matters
Signature-base construction is the known-hard part of RFC 9421 interop, and the missing vectors sit exactly on its edges:
009/010are percent-encoding,011/012are IPv6 authority,026is a non-ASCII host,028is theprotocol_methods_required_forenforcement path.A verifier can pass every currently-vendored vector and still reject validly-signed requests from a conformant counterparty. And because the SDK owns canonicalization on behalf of its consumers, these gaps are untested in every downstream implementation too, not just in this suite — this was found while adopting
adcp.signingin a seller implementation that delegates canonicalization entirely to the SDK.Fix
PR #980 vendors the complete 3.1.1 set and, more importantly, replaces the non-empty assertion with a completeness pin so the set cannot silently drift again at the next spec bump. Adding the 12 files without that guard would only reset the clock.
The pin is over content hashes, not filenames — a filename-only manifest would not have caught the
016/017/020drift. It is tied toget_adcp_spec_version(), so bumping the targeted spec version without re-vendoring fails loudly.Grading the new vectors turns up 12 real conformance failures
They are filed separately and marked strict-xfail in the PR rather than fixed there, since they are production behavior changes in a security-critical verifier:
021,022,023,026). Conformance requires byte-for-byte error codes, so "rejected anyway" is not a pass.@target-uricanonicalization applies no UTS-46 A-label conversion, drops a trailing empty query, and accepts five malformed authority shapes the spec requires be rejected.