Skip to content

Commit cbcd4c3

Browse files
test(signing): vendor the complete AdCP 3.1.8 request-signing vector set and pin it against drift (#980)
* test(signing): vendor the complete 3.1.1 request-signing vector set and pin it The vendored request-signing conformance vectors were an incomplete copy of the spec set, and the loaders could not tell. Both graders globbed the vector directory and asserted only that the result was non-empty (test_verifier_vectors.py, test_canonicalization.py), so any non-empty subset passed and a missing vector was indistinguishable from a complete set. Against AdCP 3.1.1 -- the version src/adcp/ADCP_VERSION already targets -- the vendored copy was missing: - 4 of 12 positive vectors (009/010 percent-encoding edges, 011/012 IPv6 authority) - 8 of 28 negative vectors (021-028) - canonicalization.json entirely, despite the vendored README documenting it and devoting a section to it Three further vectors (016, 017, 020) had silently drifted from their upstream contents. 017 is the notable one: it had been vendored at a revision that revoked test-ed25519-2026, the primary signing key, rather than the dedicated test-revoked-2026 keypair the spec now uses -- and keys.json had never been refreshed to carry that key. Changes: - Vendor all 12 missing files plus canonicalization.json, and re-sync the three drifted vectors, keys.json and README.md. The tree is now byte-identical to dist/compliance/3.1.1/test-vectors/request-signing/. - Add tests/conformance/signing/vectors.py: one shared loader whose parametrization is gated on a completeness pin, replacing the per-module glob-and-assert-non-empty helpers. - Add vector_manifest.json, pinning the SHA-256 of every vendored file. It fails on a missing file, an unexpected extra file, and on content drift. A filename-only manifest would not have caught what happened to 016/017/020, so the pin is over content, not names. - Add test_vector_completeness.py, which also ties the pin to get_adcp_spec_version(), so bumping the targeted spec version without re-vendoring fails loudly instead of leaving the SDK graded against a stale contract. - Wire canonicalization.json into test_canonicalization.py. It earns its own grader: it exercises canonicalize_target_uri/canonicalize_authority over 31 cases including 6 malformed-authority rejections that no signed vector covers at all. The newly graded vectors surface 12 real conformance failures, marked strict-xfail with the gap each one tracks so a fix XPASSes and forces the entry to be retired. They fall into two groups: - The verifier has no step-1 strict-parse stage, so malformed or ambiguous input flows on to a later check and is rejected under the wrong error code (negative 021, 022, 023, 026). Conformance grades the code byte-for-byte, 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. adcp.signing ships canonicalize_host(), but canonical.py never calls it. Each group is filed separately; this change deliberately adds no production behavior, only the vectors and the guard that keeps them complete. * test(signing): cite tracking issues in the strict-xfail ledgers * test(signing): avoid private _pytest import in vector parametrization * test(signing): note why _write_manifest replaces files instead of mutating * test(signing): assert the reject code, not just the refusal The reject branch asserted a bare ValueError -- but urlsplit raises exactly that for `https://[::1/p`, so `malformed-ipv6-missing-closing-bracket` passed on someone else's exception while five sibling cases sat in the xfail ledger. Six reject cases ship in the data; only five were recorded as gaps. No case asserted `expected_error_code` at all, so the vector set could not detect its own incompleteness -- the defect this PR exists to close. Capture the exception and assert the code the vector ships. All six reject cases now xfail together under #978 and retire together when the typed error lands. * test(signing): re-pin the vendored vectors at AdCP 3.1.8 Review follow-up. main moved to 3.1.8 (#992) while this branch pinned 3.1.1, so test_manifest_pins_the_spec_version_the_sdk_targets failed once the two met. Only spec_version and source change: the 3.1.1 and 3.1.8 dist/compliance/.../test-vectors/request-signing trees are byte-identical, and regenerating the manifest reproduces all 43 file hashes unchanged. That is the pin doing its job -- it forced an explicit re-vendor decision on a spec bump rather than letting the old set ride silently. Note for the next bump: `python -m tests.conformance.signing.vectors --write` regenerates `files` only and carries spec_version/source forward, so those two fields need editing before the regen, not by it. Refs #975.
1 parent b5c6f16 commit cbcd4c3

23 files changed

Lines changed: 1050 additions & 29 deletions

tests/conformance/signing/test_canonicalization.py

Lines changed: 83 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,26 +10,67 @@
1010

1111
import json
1212
from pathlib import Path
13+
from typing import Any
1314

1415
import pytest
1516

1617
from adcp.signing.canonical import (
1718
build_signature_base,
19+
canonicalize_authority,
20+
canonicalize_target_uri,
1821
parse_signature_input_header,
1922
)
23+
from tests.conformance.signing.vectors import (
24+
VECTORS_DIR,
25+
load_canonicalization_cases,
26+
load_vector_set,
27+
)
2028

21-
VECTORS_DIR = Path(__file__).parent.parent / "vectors" / "request-signing"
29+
# Cases from canonicalization.json that the SDK does not yet satisfy, mapped to
30+
# the issue tracking each gap. Marked strict so a fix XPASSes and forces the
31+
# entry to be retired instead of lingering.
32+
KNOWN_CANONICALIZATION_GAPS: dict[str, str] = {
33+
# No UTS-46 A-label conversion on the signing path. adcp.signing has a
34+
# canonicalize_host() helper, but canonical.py does not call it, so an IDN
35+
# authority signs and verifies under a non-ASCII host.
36+
"idn-to-punycode": "#977: IDN host is not converted to a Punycode A-label",
37+
"idn-mixed-case-to-punycode": "#977: IDN host is not converted to a Punycode A-label",
38+
# urlunsplit() cannot distinguish "no query" from "empty query", so the
39+
# trailing '?' is dropped and signer/verifier can disagree on the base.
40+
"trailing-empty-query-preserved": "#979: trailing '?' with an empty query is dropped",
41+
# Malformed authorities are canonicalized rather than rejected; the spec's
42+
# request_target_uri_malformed code is not implemented at all.
43+
"malformed-port-without-host": "#978: authority with a port but no host is accepted",
44+
"malformed-userinfo-without-host": "#978: authority with userinfo but no host is accepted",
45+
"malformed-empty-authority": "#978: empty authority is accepted",
46+
"malformed-bare-ipv6": "#978: unbracketed IPv6 literal is accepted",
47+
"malformed-ipv6-zone-identifier": "#978: RFC 6874 zone identifier is accepted",
48+
# This one is refused today, but by urlsplit() rather than by us: a bare
49+
# ValueError carrying no code. It belongs in the ledger with its five
50+
# siblings so all six reject cases retire together under #978.
51+
"malformed-ipv6-missing-closing-bracket": (
52+
"#978: refused by urlsplit with a bare ValueError that carries no error code"
53+
),
54+
}
2255

2356

2457
def _vectors_with_expected_base() -> list[tuple[str, Path]]:
25-
all_vectors = sorted((VECTORS_DIR / "positive").glob("*.json")) + sorted(
26-
(VECTORS_DIR / "negative").glob("*.json")
27-
)
28-
result = [
29-
(p.name, p) for p in all_vectors if "expected_signature_base" in json.loads(p.read_text())
58+
all_vectors = load_vector_set("positive") + load_vector_set("negative")
59+
return [
60+
(name, path)
61+
for name, path in all_vectors
62+
if "expected_signature_base" in json.loads(path.read_text())
3063
]
31-
assert result, f"no vectors with expected_signature_base under {VECTORS_DIR}"
32-
return result
64+
65+
66+
def _canonicalization_params() -> list[Any]:
67+
params = []
68+
for name, case in load_canonicalization_cases():
69+
marks = []
70+
if name in KNOWN_CANONICALIZATION_GAPS:
71+
marks.append(pytest.mark.xfail(strict=True, reason=KNOWN_CANONICALIZATION_GAPS[name]))
72+
params.append(pytest.param(name, case, marks=marks))
73+
return params
3374

3475

3576
@pytest.mark.parametrize(
@@ -57,6 +98,40 @@ def test_signature_base_matches_expected(name: str, path: Path) -> None:
5798
)
5899

59100

101+
@pytest.mark.parametrize(("name", "case"), _canonicalization_params())
102+
def test_canonicalization_case(name: str, case: dict[str, Any]) -> None:
103+
"""Grade `canonicalization.json` -- pure URL canonicalization, no crypto.
104+
105+
The per-vector `expected_signature_base` path only exercises the URL shapes
106+
that happen to appear in a signed vector. This fixture is the exhaustive
107+
set: IDN, IPv6, userinfo, empty-query and the six malformed-authority
108+
rejections, several of which no signed vector covers at all.
109+
"""
110+
url = case["input_url"]
111+
112+
if case.get("reject"):
113+
# Assert the code the vector ships, not merely that something raised.
114+
# `https://[::1/p` is refused inside urlsplit() with a bare ValueError
115+
# carrying no code, so a bare `pytest.raises(ValueError)` passes for the
116+
# wrong reason and grades nothing -- the refusal has to be ours, and it
117+
# has to name which rule fired.
118+
with pytest.raises(ValueError) as excinfo:
119+
canonicalize_target_uri(url)
120+
actual_code = getattr(excinfo.value, "code", None)
121+
assert actual_code == case["expected_error_code"], (
122+
f"{name}: expected error code {case['expected_error_code']!r}, got "
123+
f"{actual_code!r} from {type(excinfo.value).__name__} ({case['rule']})"
124+
)
125+
return
126+
127+
assert (
128+
canonicalize_target_uri(url) == case["expected_target_uri"]
129+
), f"{name}: @target-uri mismatch for {url!r} ({case['rule']})"
130+
assert (
131+
canonicalize_authority(url) == case["expected_authority"]
132+
), f"{name}: @authority mismatch for {url!r} ({case['rule']})"
133+
134+
60135
def test_multi_label_signature_input_selects_sig1() -> None:
61136
"""Per spec, verifiers process exactly sig1 and ignore additional labels."""
62137
vector = json.loads(
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
"""Guard the vendored request-signing vector set against silent shrinkage.
2+
3+
``test_verifier_vectors`` and ``test_canonicalization`` grade whatever vectors
4+
happen to be on disk. That makes them blind in one specific direction: a vector
5+
the spec defines but the SDK never vendored is a rule nobody grades, and no
6+
amount of green in those modules can tell you it is absent.
7+
8+
These tests supply the missing direction by pinning the vendored tree to a
9+
manifest -- filenames *and* content hashes -- for the AdCP release named in
10+
``VECTOR_SET_SPEC_VERSION``.
11+
"""
12+
13+
from __future__ import annotations
14+
15+
import json
16+
17+
import pytest
18+
19+
from adcp import get_adcp_spec_version
20+
from tests.conformance.signing.vectors import (
21+
VECTOR_MANIFEST,
22+
VECTOR_SET_SPEC_VERSION,
23+
VECTORS_DIR,
24+
load_canonicalization_cases,
25+
manifest_ids,
26+
on_disk_files,
27+
)
28+
29+
30+
def test_vendored_tree_matches_manifest_exactly() -> None:
31+
"""Every vendored file is present, unmodified, and nothing extra is present."""
32+
on_disk = on_disk_files()
33+
34+
missing = sorted(set(VECTOR_MANIFEST) - set(on_disk))
35+
unexpected = sorted(set(on_disk) - set(VECTOR_MANIFEST))
36+
drifted = sorted(
37+
name
38+
for name, digest in VECTOR_MANIFEST.items()
39+
if name in on_disk and on_disk[name] != digest
40+
)
41+
42+
assert not missing, (
43+
f"vendored vectors missing from disk: {missing}\n"
44+
f"Re-vendor from the AdCP {VECTOR_SET_SPEC_VERSION} spec repo "
45+
f"(dist/compliance/{VECTOR_SET_SPEC_VERSION}/test-vectors/request-signing/)."
46+
)
47+
assert not unexpected, (
48+
f"files present under {VECTORS_DIR} that the manifest does not pin: {unexpected}\n"
49+
"Vectors are vendored verbatim from the spec; add the file upstream first, "
50+
"then re-vendor and regenerate the manifest."
51+
)
52+
assert not drifted, (
53+
f"vendored vectors differ from their pinned content: {drifted}\n"
54+
"Either the local copy was edited (revert it -- vectors are vendored verbatim) "
55+
"or the spec changed and the SDK must be re-graded before re-pinning."
56+
)
57+
58+
59+
def test_manifest_pins_the_spec_version_the_sdk_targets() -> None:
60+
"""A spec bump must re-vendor the vectors, not silently keep the old set."""
61+
assert get_adcp_spec_version() == VECTOR_SET_SPEC_VERSION, (
62+
f"SDK targets AdCP {get_adcp_spec_version()} but the vendored request-signing "
63+
f"vectors are pinned at {VECTOR_SET_SPEC_VERSION}. Re-vendor the vector tree "
64+
f"from the new spec release and regenerate the pin with "
65+
f"`python -m tests.conformance.signing.vectors --write`."
66+
)
67+
68+
69+
@pytest.mark.parametrize(("subdir", "expected_count"), [("positive", 12), ("negative", 28)])
70+
def test_vector_counts_match_spec(subdir: str, expected_count: int) -> None:
71+
"""Counts are stated literally so a shrunken set is obvious in the diff."""
72+
ids = manifest_ids(subdir)
73+
assert len(ids) == expected_count, (
74+
f"AdCP {VECTOR_SET_SPEC_VERSION} defines {expected_count} {subdir} vectors, "
75+
f"manifest pins {len(ids)}"
76+
)
77+
78+
79+
@pytest.mark.parametrize("subdir", ["positive", "negative"])
80+
def test_vector_numbering_is_contiguous(subdir: str) -> None:
81+
"""Vectors are numbered 001..NNN; a gap means one was dropped."""
82+
numbers = sorted(int(name.split("-", 1)[0]) for name in manifest_ids(subdir))
83+
assert numbers == list(
84+
range(1, len(numbers) + 1)
85+
), f"{subdir} vector numbering has a gap: {numbers}"
86+
87+
88+
def test_canonicalization_fixture_is_vendored_and_populated() -> None:
89+
"""The standalone canonicalization fixture ships and carries both case kinds."""
90+
cases = load_canonicalization_cases()
91+
assert len(cases) == 31, (
92+
f"AdCP {VECTOR_SET_SPEC_VERSION} canonicalization.json defines 31 cases, "
93+
f"found {len(cases)}"
94+
)
95+
assert sum(1 for _, case in cases if case.get("reject")) == 6
96+
assert sum(1 for _, case in cases if not case.get("reject")) == 25
97+
98+
99+
def test_every_key_referenced_by_a_vector_exists_in_keys_json() -> None:
100+
"""``jwks_ref`` resolution is silent on a missing kid; catch it here instead."""
101+
available = {k["kid"] for k in json.loads((VECTORS_DIR / "keys.json").read_text())["keys"]}
102+
for name in VECTOR_MANIFEST:
103+
if not name.startswith(("positive/", "negative/")):
104+
continue
105+
vector = json.loads((VECTORS_DIR / name).read_text())
106+
for kid in vector.get("jwks_ref", []):
107+
assert kid in available, f"{name} references kid {kid!r} absent from keys.json"

tests/conformance/signing/test_verifier_vectors.py

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import json
1111
from pathlib import Path
12+
from typing import Any
1213
from urllib.parse import urlsplit
1314

1415
import pytest
@@ -21,10 +22,36 @@
2122
VerifyOptions,
2223
verify_request_signature,
2324
)
25+
from tests.conformance.signing.vectors import VECTORS_DIR, load_vector_set
2426

25-
VECTORS_DIR = Path(__file__).parent.parent / "vectors" / "request-signing"
2627
KEYS_BY_KID = {k["kid"]: k for k in json.loads((VECTORS_DIR / "keys.json").read_text())["keys"]}
2728

29+
# Negative vectors the verifier does not yet satisfy, each mapped to the gap it
30+
# tracks. Every one is the same shape: the verifier has no step-1 strict-parse
31+
# stage, so malformed or ambiguous input flows on to a later check and is
32+
# rejected under the wrong error code. Conformance grades the code
33+
# byte-for-byte, so "rejected anyway" is not a pass.
34+
#
35+
# Marked strict: when the verifier is fixed the vector XPASSes and this module
36+
# goes red, which is the signal to delete the entry rather than leave a stale
37+
# exemption behind.
38+
KNOWN_VERIFIER_GAPS: dict[str, str] = {
39+
"021-duplicate-signature-input-label.json": (
40+
"#976: duplicate Signature-Input dictionary key is silently resolved instead of "
41+
"rejected (RFC 8941 §3.2 / covered-component smuggling)"
42+
),
43+
"022-multi-valued-content-type.json": (
44+
"#976: multi-valued Content-Type on a covered non-list field is not rejected at parse"
45+
),
46+
"023-multi-valued-content-digest.json": (
47+
"#976: multi-valued Content-Digest (duplicate RFC 9530 algorithm) is not rejected at parse"
48+
),
49+
"026-non-ascii-host.json": (
50+
"#977: raw IDN U-label in the authority is not rejected; @target-uri canonicalization "
51+
"applies no UTS-46 A-label conversion"
52+
),
53+
}
54+
2855

2956
def _operation_from_url(url: str) -> str:
3057
path = urlsplit(url).path
@@ -73,16 +100,21 @@ def _build_options(vector: dict) -> tuple[VerifyOptions, InMemoryReplayStore]:
73100
return options, replay_store
74101

75102

76-
def _load(subdir: str) -> list[tuple[str, Path]]:
77-
result = [(p.name, p) for p in sorted((VECTORS_DIR / subdir).glob("*.json"))]
78-
assert result, f"no vectors under {VECTORS_DIR / subdir}"
79-
return result
103+
_vector_id = lambda v: v if isinstance(v, str) else v.name # noqa: E731
80104

81105

82-
_vector_id = lambda v: v if isinstance(v, str) else v.name # noqa: E731
106+
def _negative_params() -> list[Any]:
107+
"""Negative vectors, with known verifier gaps marked strict-xfail."""
108+
params = []
109+
for name, path in load_vector_set("negative"):
110+
marks = []
111+
if name in KNOWN_VERIFIER_GAPS:
112+
marks.append(pytest.mark.xfail(strict=True, reason=KNOWN_VERIFIER_GAPS[name]))
113+
params.append(pytest.param(name, path, marks=marks))
114+
return params
83115

84116

85-
@pytest.mark.parametrize(("name", "path"), _load("positive"), ids=_vector_id)
117+
@pytest.mark.parametrize(("name", "path"), load_vector_set("positive"), ids=_vector_id)
86118
def test_positive_vector(name: str, path: Path) -> None:
87119
vector = json.loads(path.read_text())
88120
options, _ = _build_options(vector)
@@ -97,7 +129,7 @@ def test_positive_vector(name: str, path: Path) -> None:
97129
assert signer.label == vector["expected_outcome"].get("verified_label", "sig1")
98130

99131

100-
@pytest.mark.parametrize(("name", "path"), _load("negative"), ids=_vector_id)
132+
@pytest.mark.parametrize(("name", "path"), _negative_params(), ids=_vector_id)
101133
def test_negative_vector(name: str, path: Path) -> None:
102134
vector = json.loads(path.read_text())
103135
options, _ = _build_options(vector)
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
{
2+
"$comment": "Completeness pin for the vendored AdCP request-signing conformance vectors. Generated by `python -m tests.conformance.signing.vectors --write`; do not hand-edit. Graded by tests/conformance/signing/test_vector_completeness.py.",
3+
"spec_version": "3.1.8",
4+
"source": "adcontextprotocol/adcp @ v3.1.8 -- dist/compliance/3.1.8/test-vectors/request-signing/",
5+
"files": {
6+
"README.md": "74977dcb44eef69aee2b45e78e9fc111625e101d7e8646ed9f44b521aee396e8",
7+
"canonicalization.json": "83a15584bebb0ff006ffd99a4080f5a922ea56e9e590d516ee87becaaf15ad1b",
8+
"keys.json": "67b4f32f72ad5aa0463b8f4d407f18fe3c22c0c52655ba47a5f2de89ae974edf",
9+
"negative/001-no-signature-header.json": "94e148f6855026304a04cc34fcd6639ce1e874b7190422f1acbd79545d1ba60d",
10+
"negative/002-wrong-tag.json": "55640f52000dd5d9f9c158ed9417c20802489991ccd42d59b652ccb30fc706b7",
11+
"negative/003-expired-signature.json": "457332ba1626d23a77cac47eb1d59c319eb64af0b048edc9de87224418910ce0",
12+
"negative/004-window-too-long.json": "ebc849fc39789fb71fbf16ff73928f53eff351a5351769fdc3bd7496c0eb5f2f",
13+
"negative/005-alg-not-allowed.json": "3c635f2c3300c6d9856ee6ea6231147cdc6489353b891a5cce16d9bcfc02bfec",
14+
"negative/006-missing-covered-component.json": "5bc102d6518f1def3cc3a43599edefc391eb922dc517e209a56979eb73560a78",
15+
"negative/007-missing-content-digest.json": "7898dc9599722af3cd487af8461c15fafc00165ae425b949b159251cbb4b310e",
16+
"negative/008-unknown-keyid.json": "0694be78535561e2028ddee44813a5a48f77c9b2341900023aff63070eabaded",
17+
"negative/009-key-ops-missing-verify.json": "9eceb605d6c8be1dfffedbb9a0233c3551a2fb7d2034fcbac30b9316dead26ea",
18+
"negative/010-content-digest-mismatch.json": "3bbaefd9a894f6769417705c6daa48c125c1385a65c8ba0610f15555a3b8c396",
19+
"negative/011-malformed-header.json": "0ca8c66e02bd1eb011212de32db4c92af5885d585c18221bf8592cfb7b58bc78",
20+
"negative/012-missing-expires-param.json": "573407764dc539aaaeac0d0b0df1392c1ddb7c9ef315633e47955b617f505c15",
21+
"negative/013-expires-le-created.json": "7cf14332588f4b5d677265087aadeebf953bda5592131224adf72d38e9dba0a6",
22+
"negative/014-missing-nonce-param.json": "19bfcbd02be903a27515ec4af9a1fffd66e8f100ddd6624e2e4500368d8acd3d",
23+
"negative/015-signature-invalid.json": "c835f3452dbab92b055a446e2ca38ce9faf2defaf90eeaecbbf6878dd33f231b",
24+
"negative/016-replayed-nonce.json": "c490b4e2f121d07cbdfc06942b7586328bc5576db3ba78572cece651d67dc717",
25+
"negative/017-key-revoked.json": "c8c535a02b71becf73388894ca0aebd687974e347ab93817b88a030920a202f0",
26+
"negative/018-digest-covered-when-forbidden.json": "45e14505f97e09f1435a1ff892ea6c70583397ee5b495c67fbc2336efb48a548",
27+
"negative/019-signature-without-signature-input.json": "dfc90dcca95d84124f24d7e26abf5f7fe1c8aaebb22e126e135f65260c8efe72",
28+
"negative/020-rate-abuse.json": "2db09f91eb20e0c0443f98266c4cba8c06a419fd8908ff5521f48985bc65f213",
29+
"negative/021-duplicate-signature-input-label.json": "bb57d930f888c6e87cd7dc4865accffde923115289fe1e96f0577de20c2899ac",
30+
"negative/022-multi-valued-content-type.json": "0b09cf0fccd80a288819583f4cd8b27bc124a42c19e893d9ecbcb272194f3529",
31+
"negative/023-multi-valued-content-digest.json": "ed3d211ec5f089a8e024fd4a401cb8a287c549e35156006c4110024651690e71",
32+
"negative/024-unquoted-string-param.json": "a59f698851905fecc163bf7ddae4e1ad3e15477610af4ea0812d9db95a96f9db",
33+
"negative/025-jwk-alg-crv-mismatch.json": "10aca9a60485c61930e2aedd25444653612c14ea5eed908b40b9402e95c220a8",
34+
"negative/026-non-ascii-host.json": "2fb7b813edd4d0dbe102063e611a2147cf8dceb0c3ff62135dc1a9afd7444661",
35+
"negative/027-webhook-registration-authentication-unsigned.json": "8d9a7276fa6ad16ee59343bbdce9e5c081318cdb73aaacfe70a65eef304db5c5",
36+
"negative/028-unsigned-protocol-method-required.json": "d3f5dbe447bef275a9eb88bebd30eed0f8ddfb85addffb83b827db046573c5ac",
37+
"positive/001-basic-post.json": "a592b78de5877f03d852443921760191c2f2c1fa9d8ce23aaacbb1e2c1157158",
38+
"positive/002-post-with-content-digest.json": "7dad1aecf1ea91b5e8fc434989ad2b1b14058e406e91e9aa3a9d05be31942f2d",
39+
"positive/003-es256-post.json": "24972773bacc4445998f5ab1347caaaf2c13f924f5eac89ae8c775e5467aa4c1",
40+
"positive/004-multiple-signature-labels.json": "c9894a9ee26724c46534d7265696282a566e84ff203156d83e28d40eadebb1cc",
41+
"positive/005-default-port-stripped.json": "33f8c9c25b274dccb8a88f23cf08dc11903a001201cdec94ad0644c804287763",
42+
"positive/006-dot-segment-path.json": "153ff35e8b7c89f09099bbe3cdf7cbc48d6db677fbb7c731cd40d1b462ca3383",
43+
"positive/007-query-byte-preserved.json": "83649a92c3488742a49d47e2dabcd8570b3e94caab8951708f934c3b043dc270",
44+
"positive/008-percent-encoded-path.json": "98a4a8ce2b6f4a40564422737c7104a80a85675b85e99ef709e8285aaa5029d0",
45+
"positive/009-percent-encoded-unreserved-decoded.json": "674b99f0e53536e10b7cd8fb046df07219fc79f2a799731cf37a183c0ec57262",
46+
"positive/010-percent-encoded-slash-preserved.json": "669fa85495e494bbcfa9f361925e109beba76667d5d4b37022d98867882bc93b",
47+
"positive/011-ipv6-authority.json": "39a0fd4aba2b81ce9d9687c375b7d25e7727b87282832c41151b96b6c5675308",
48+
"positive/012-ipv6-authority-default-port-stripped.json": "fab9dcef74c2b849b47a0c4a0c0669205300aa9b77d6bced5bbf54e9a40551ba"
49+
}
50+
}

0 commit comments

Comments
 (0)