From 91d351f46de98b06d4124a598e0277cbfd8b9f25 Mon Sep 17 00:00:00 2001 From: wshallwshall Date: Mon, 12 Oct 2026 12:39:43 -0500 Subject: [PATCH 1/2] api, file: SVGZ inflate gets a trial pass; a disabled File byte cap warns (BACKLOG #1129) The SVGZ download path bounded its inflate only while keeping output, so markup over the 32 MiB bound was refused after holding up to it. _gunzip_bounded now walks members through the shared bounded_inflate loop. The head sniff and the size check keep no output; a decompressor wrapper notes the first byte past leading noise, so the not-markup clearance is unchanged. Only an admitted body is inflated again. Member cap and size bound unchanged. A File inbound with decompress set and max_decompressed_bytes off (0, None, "0") logs one warning at build, naming the connection and the setting, never a value. --- ...vgz-trial-and-file-cap-warning.security.md | 11 ++ docs/DANGEROUS-FUNCTIONALITY.md | 1 + messagefoundry/api/svg_sanitize.py | 140 ++++++++++++++---- messagefoundry/parsing/_bounded_inflate.py | 9 +- messagefoundry/transports/file.py | 9 ++ tests/test_svg_sanitize.py | 60 ++++++++ tests/test_transports.py | 38 +++++ 7 files changed, 235 insertions(+), 33 deletions(-) create mode 100644 changelog.d/1129-svgz-trial-and-file-cap-warning.security.md diff --git a/changelog.d/1129-svgz-trial-and-file-cap-warning.security.md b/changelog.d/1129-svgz-trial-and-file-cap-warning.security.md new file mode 100644 index 0000000000..5bb3d56cc4 --- /dev/null +++ b/changelog.d/1129-svgz-trial-and-file-cap-warning.security.md @@ -0,0 +1,11 @@ +- **An SVGZ attachment is checked by trial passes before the pass that keeps it.** The download + route inflates a gzip attachment to vet any SVG inside. That inflate used to bound its output + only as it kept it, so markup over the 32 MiB bound was refused after up to 32 MiB was held. Now + the head sniff and the size check keep no output, through the shared bounded inflate loop the + compression codec uses. Markup over the bound is refused before any of it is held, and only an + admitted body is inflated again to be sanitized. The member cap and the size bound are + unchanged. (`BACKLOG #1129`) +- **A File inbound with `decompress=` set warns when `max_decompressed_bytes` is off.** Turning the + decompressed-byte ceiling off with `0` or `None` is still allowed, but the connection now logs one + warning when it is built, naming the connection and the setting. Off without `decompress=` changes + nothing and logs nothing. (`BACKLOG #1129`) diff --git a/docs/DANGEROUS-FUNCTIONALITY.md b/docs/DANGEROUS-FUNCTIONALITY.md index aa4fbc1649..cffc6e2241 100644 --- a/docs/DANGEROUS-FUNCTIONALITY.md +++ b/docs/DANGEROUS-FUNCTIONALITY.md @@ -652,6 +652,7 @@ or compression library: in memory. A trial pass that keeps no output checks the byte ceiling, and for gzip the member cap, before the real decompression starts (BACKLOG #1129). The File connector's ceiling, `max_decompressed_bytes`, defaults to 64 MiB; setting it to `0` turns it off. + A connection with `decompress=` set and that ceiling off logs one warning when it is built. Its gzip member cap, `max_decompressed_members`, defaults to 1024 and cannot be turned off. A zip is refused whole for too many members, duplicate or overlapped members, or bytes before or after the archive. Each member's name must be a safe relative path, and its bytes must match the diff --git a/messagefoundry/api/svg_sanitize.py b/messagefoundry/api/svg_sanitize.py index 0baba21462..69117160fd 100644 --- a/messagefoundry/api/svg_sanitize.py +++ b/messagefoundry/api/svg_sanitize.py @@ -24,6 +24,10 @@ change the parts that are not SVG, and those are verbatim clinical content. - A gzip body, which is what an SVGZ file is, is inflated under the same size bound and vetted like plain bytes. A sanitized SVG inside is served gzipped again (BACKLOG #2391). +- Trial passes that keep no output check the gzip's size bound, its member cap and its soundness + before the one inflate that keeps the document runs (BACKLOG #1129). They use the shared loop in + ``parsing/_bounded_inflate.py``, as the compression codec's trial pass does, so a body over the + bound is refused without holding any of its output. """ from __future__ import annotations @@ -31,6 +35,8 @@ import gzip import re import zlib +from collections.abc import Buffer +from dataclasses import dataclass from typing import Final from xml.etree.ElementTree import ( # nosec B405 -- types only; every parse goes through defusedxml Element, @@ -41,6 +47,7 @@ from messagefoundry._vendor.defusedxml.common import DefusedXmlException from messagefoundry._vendor.defusedxml.ElementTree import DefusedXMLParser from messagefoundry._vendor.defusedxml.ElementTree import fromstring as _xml_fromstring +from messagefoundry.parsing._bounded_inflate import InflateCeilingExceeded, bounded_inflate __all__ = ["SvgRejected", "may_be_svg", "sanitize_if_svg", "sanitize_svg"] @@ -62,11 +69,13 @@ _ELEMENT_NAME_RE: Final = re.compile(rb"[^\s/>]*") #: The two leading bytes of every gzip member (RFC 1952). An SVGZ file is a gzipped SVG. _GZIP_MAGIC: Final = b"\x1f\x8b" -#: The most gzip members one body may hold. Each member after the first costs a copy of what remains, -#: so an unbounded count of tiny members would cost quadratic time. +#: The most gzip members one body may hold. A member can inflate to nothing, so a body of many tiny +#: members passes any byte bound while each still costs a header parse and a new decompressor. _MAX_GZIP_MEMBERS: Final = 16 #: How much of a gzip body is inflated first, to clear one that is plainly not markup cheaply. _GZIP_SNIFF_BYTES: Final = 64 * 1024 +#: Any byte that is not NUL. Only NUL padding may follow the last gzip member. +_NOT_NUL_RE: Final = re.compile(rb"[^\x00]") #: Traces of SVG in markup the parser could not read through: an ``svg`` start tag under any prefix #: and in any case, the SVG namespace name, or a namespace name spelled with a reference, which can #: hide it. Possessive quantifiers, and name classes that exclude ``<`` and ``:``, keep the scan @@ -391,24 +400,88 @@ def _contains_svg(body: bytes) -> bool: return _SVG_TAG_RE.search(_without_nul(body)) is not None -def _gunzip_bounded(body: bytes, limit: int) -> tuple[bytes, bool]: - """Inflate the gzip ``body``, keeping at most ``limit + 1`` bytes. +class _FirstContentWatch: + """A gzip decompressor for the shared inflate loop that notes the first byte past leading noise. - Returns the bytes inflated and whether they are the whole document; they are not only when the - output passes ``limit``. The bound is enforced while inflating, so a small body that inflates to - gigabytes costs no more than ``limit`` bytes of work. + A trial pass keeps no output, so this is how it still learns whether the document starts as + markup. The walk makes one per member and passes in the ``first`` the members before it found, + since a reader joins the members into one document.""" + + __slots__ = ("first", "inner") + + def __init__(self, first: bytes | None) -> None: + self.first = first + self.inner = zlib.decompressobj(wbits=31) + + def _see(self, piece: bytes) -> bytes: + if self.first is None: + match = _FIRST_CONTENT_BYTE_RE.search(piece) + if match is not None: + self.first = match.group() + return piece + + def decompress(self, data: Buffer, max_length: int = 0, /) -> bytes: + return self._see(self.inner.decompress(data, max_length)) + + def flush(self) -> bytes: + return self._see(self.inner.flush()) + + @property + def eof(self) -> bool: + return self.inner.eof + + @property + def unconsumed_tail(self) -> bytes: + return self.inner.unconsumed_tail + + @property + def unused_data(self) -> bytes: + return self.inner.unused_data + + +@dataclass(frozen=True, slots=True) +class _Gunzipped: + """What one walk of a gzip body found. + + ``output`` is the document, and is empty unless the walk kept its output and the body inflated + whole. ``whole`` is false only when the output passed the walk's limit. ``first`` is the first + byte past leading noise among the bytes inflated, or ``None`` when there was none.""" + + output: bytes + whole: bool + first: bytes | None + + +def _gunzip_bounded(body: bytes, limit: int, *, keep_output: bool) -> _Gunzipped: + """Walk every member of the gzip ``body`` through the shared inflate loop, up to ``limit`` bytes. + + The bound is enforced while inflating, so a small body that inflates to gigabytes costs no more + than ``limit`` bytes of work. With ``keep_output`` false this is a trial pass: it holds none of + the output, and refuses and reports exactly as a pass that keeps it would. Raises :class:`SvgRejected` for a gzip that is not sound before the bound is reached: a corrupt header or block, a CRC or length check that fails, a member cut short, more than :data:`_MAX_GZIP_MEMBERS` members, or anything but NUL padding after the last member. zlib keeps none of a call's output when the call raises, so there is no partial document to judge, and a reader that skips the trailer check could still show one. A corrupt body is never served.""" - out = bytearray() - rest = body + view = memoryview(body) + first: bytes | None = None + parts: list[bytes] = [] + produced = 0 + position = 0 for _ in range(_MAX_GZIP_MEMBERS): - inflater = zlib.decompressobj(wbits=31) + watch = _FirstContentWatch(first) try: - out += inflater.decompress(rest, limit + 1 - len(out)) + result = bounded_inflate( + view[position:], + watch, + max_output_bytes=limit - produced, + trailing="stop", + keep_output=keep_output, + exact_ceiling=True, + ) + except InflateCeilingExceeded: + return _Gunzipped(b"", False, watch.first) except zlib.error: # The refusal is raised after the except ends, so the zlib error stays off its chain, # as in sanitize_svg (BACKLOG #1796). @@ -417,14 +490,15 @@ def _gunzip_bounded(body: bytes, limit: int) -> tuple[bytes, bool]: corrupt = False if corrupt: raise SvgRejected("gzip body is corrupt") - if len(out) > limit: - return bytes(out), False - if not inflater.eof: + first = watch.first + if not result.eof: raise SvgRejected("gzip body is cut short") - rest = inflater.unused_data - if not rest.strip(b"\x00"): - return bytes(out), True - if not rest.startswith(_GZIP_MAGIC): + parts.append(result.output) + produced += result.produced + position += result.end + if _NOT_NUL_RE.search(view, position) is None: + return _Gunzipped(b"".join(parts), True, first) + if bytes(view[position : position + 2]) != _GZIP_MAGIC: raise SvgRejected("gzip body has bytes after its last member") raise SvgRejected(f"gzip body has more than {_MAX_GZIP_MEMBERS} members") @@ -451,22 +525,30 @@ def _vet_gzip(label: str | None, body: bytes) -> bytes: The head is cleared without reading the rest of the body, so damage past the head is not seen. That cannot hide an SVG from a browser: no browser renders a document as SVG when its first byte past that noise is not ``<``. A reader that autodetects EBCDIC could, which is the same - assumption :func:`may_be_svg` makes for plain bytes.""" - head, head_whole = _gunzip_bounded(body, _GZIP_SNIFF_BYTES) - first = _FIRST_CONTENT_BYTE_RE.search(head) - if first is not None and first.group() != b"<": + assumption :func:`may_be_svg` makes for plain bytes. + + The head and the size check are trial passes that keep no output (BACKLOG #1129). Only a body + they admit as whole markup is inflated again, and that pass is the one that keeps the document. + So markup over the bound is refused before any of it is held.""" + head = _gunzip_bounded(body, _GZIP_SNIFF_BYTES, keep_output=False) + if head.first not in (None, b"<"): return body - # A body that inflated whole within the head is not inflated a second time. - inner, whole = (head, True) if head_whole else _gunzip_bounded(body, _MAX_SVG_BYTES) - first = _FIRST_CONTENT_BYTE_RE.search(inner) - if first is not None and first.group() != b"<": + # A body that inflated whole within the head needs no second trial pass. + trial = head if head.whole else _gunzip_bounded(body, _MAX_SVG_BYTES, keep_output=False) + if trial.first not in (None, b"<"): return body - if first is None and whole: - return body # empty, or only whitespace to its end: there is nothing to show - if not whole: + if not trial.whole: # Markup that passes the bound cannot be vetted, and a reader may still show the part that # did inflate. raise SvgRejected("gzip markup is larger than the bound") + if trial.first is None: + return body # empty, or only whitespace to its end: there is nothing to show + kept = _gunzip_bounded(body, _MAX_SVG_BYTES, keep_output=True) + if not kept.whole: + # The trial pass admitted this body, and zlib is deterministic, so this does not happen. It + # refuses rather than vet an empty document, which would serve the body as stored. + raise SvgRejected("gzip markup is larger than the bound") + inner = kept.output vetted = _vet_markup(label, inner) if vetted is inner: return body diff --git a/messagefoundry/parsing/_bounded_inflate.py b/messagefoundry/parsing/_bounded_inflate.py index 579028c754..751c5d2664 100644 --- a/messagefoundry/parsing/_bounded_inflate.py +++ b/messagefoundry/parsing/_bounded_inflate.py @@ -6,10 +6,11 @@ :func:`messagefoundry.parsing.compression.deflate_decompress` (zlib-wrapped), :func:`messagefoundry.parsing.compression.gzip_decompress` (one gzip member per call; BACKLOG #1129) and :func:`messagefoundry.parsing.dicom._inflate.bounded_inflate_or_error` (raw DEFLATE, -discards the output). The codec's trial pass discards it too (BACKLOG #1129). They used to carry -copies of this loop, so a fix could reach one and -not the other. The end-of-stream hang (BACKLOG #1964) was fixed in one copy first for exactly that -reason. +discards the output). The codec's trial pass discards it too (BACKLOG #1129). So do the SVGZ trial +passes in :mod:`messagefoundry.api.svg_sanitize`, which walk a gzip attachment's members before +the pass that keeps the document. The codec and the DICOM guard used to carry copies of this loop, +so a fix could reach one and not the other. The end-of-stream hang (BACKLOG #1964) was fixed in one +copy first for exactly that reason. The loop keeps these guarantees for every caller: diff --git a/messagefoundry/transports/file.py b/messagefoundry/transports/file.py index 4589ef28bf..9a34122e98 100644 --- a/messagefoundry/transports/file.py +++ b/messagefoundry/transports/file.py @@ -731,6 +731,15 @@ def __init__(self, config: Source) -> None: knob="max_decompressed_bytes", transport="file source", ) + if self.decompress is not None and self.max_decompressed_bytes is None: + # Turning the bomb guard off is documented, so it is allowed, but it is said once at build + # so it is never silent (BACKLOG #1129). Off without decompress changes nothing, so that + # case says nothing. The line names the setting, never its value. + logger.warning( + "file source %s: max_decompressed_bytes is off, so a compressed drop inflates with " + "no byte ceiling; set a positive value to bound it", + config.name or self.directory, + ) # Caps the gzip members in one drop (BACKLOG #1129). Only consulted when `decompress` is set, # but read at build either way, like `max_decompressed_bytes`, so a typo fails early. self.max_decompressed_members: int = _member_cap(s.get("max_decompressed_members")) diff --git a/tests/test_svg_sanitize.py b/tests/test_svg_sanitize.py index ced40e6eb4..71a88b8f9c 100644 --- a/tests/test_svg_sanitize.py +++ b/tests/test_svg_sanitize.py @@ -15,6 +15,7 @@ import re import time import zlib +from typing import Any from xml.etree.ElementTree import Element import pytest @@ -28,6 +29,7 @@ sanitize_if_svg, sanitize_svg, ) +from messagefoundry.parsing._bounded_inflate import bounded_inflate from tests.test_xml_parser_consistency import HOSTILE _DRAWING = ( @@ -315,6 +317,64 @@ def test_a_gzip_bomb_is_bounded() -> None: assert time.perf_counter() - started < 10 +def _spy_on_inflate(monkeypatch: pytest.MonkeyPatch) -> list[tuple[bool, int | None]]: + """Record ``(keep_output, max_output_bytes)`` for each call the sanitizer makes to the shared + inflate loop, in order, and let the call through.""" + calls: list[tuple[bool, int | None]] = [] + + def spy(*args: Any, **kwargs: Any) -> Any: + calls.append((kwargs["keep_output"], kwargs["max_output_bytes"])) + return bounded_inflate(*args, **kwargs) + + monkeypatch.setattr(svg_sanitize, "bounded_inflate", spy) + return calls + + +def test_svgz_over_the_bound_is_refused_before_any_output_is_kept( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """BACKLOG #1129. A trial pass that keeps nothing refuses markup over the bound, so the pass + that keeps the document never runs for it. The ceiling it ran under is the bound.""" + calls = _spy_on_inflate(monkeypatch) + with pytest.raises(SvgRejected, match="larger than the bound"): + sanitize_if_svg("image/svg+xml", _gz(b"" + b" " * (40 * 1024 * 1024))) + assert calls, "the sanitizer no longer inflates through the shared loop" + assert not any(keep for keep, _ in calls) + assert calls[-1] == (False, svg_sanitize._MAX_SVG_BYTES) + + +def test_svgz_at_the_bound_renders_and_one_over_is_refused(monkeypatch: pytest.MonkeyPatch) -> None: + """The control for the refusal above: an SVGZ that inflates to exactly the bound is still + sanitized and served, after a trial pass at the bound. The bounds are shrunk so the document is + cheap to sanitize; the head stays smaller than the bound, as it is in production.""" + monkeypatch.setattr(svg_sanitize, "_GZIP_SNIFF_BYTES", 1024) + monkeypatch.setattr(svg_sanitize, "_MAX_SVG_BYTES", 4096) + head, tail = b'', b"" + at_bound = head + b" " * (4096 - len(head) - len(tail)) + tail + assert len(at_bound) == 4096 + calls = _spy_on_inflate(monkeypatch) + out = sanitize_if_svg(None, _gz(at_bound)) + assert b" None: + """The trial pass keeps no output, yet still sees the first byte past leading noise. A body that + passes the bound but starts, past that noise, with a byte other than ``<`` is not markup and is + served as stored, as it was before the trial pass.""" + monkeypatch.setattr(svg_sanitize, "_GZIP_SNIFF_BYTES", 1024) + monkeypatch.setattr(svg_sanitize, "_MAX_SVG_BYTES", 4096) + body = _gz(b" " * 2048 + b"%PDF-1.4 synthetic" + b"A" * 8192) + assert sanitize_if_svg("application/gzip", body) is body + + # --- BACKLOG #2391: SVG below a root that is not svg ----------------------------------------------- _XHTML = b'' diff --git a/tests/test_transports.py b/tests/test_transports.py index 4599ff28fd..41ecba95ae 100644 --- a/tests/test_transports.py +++ b/tests/test_transports.py @@ -1749,6 +1749,44 @@ def test_file_source_member_cap_refuses_zero_and_bad_values(bad: object) -> None FileSource(Source(type=ConnectorType.FILE, settings=settings)) +def _decompressed_cap_warnings(caplog: pytest.LogCaptureFixture) -> list[logging.LogRecord]: + return [ + r + for r in caplog.records + if r.levelno == logging.WARNING and "max_decompressed_bytes is off" in r.getMessage() + ] + + +@pytest.mark.parametrize("off", [0, None, "0"], ids=["zero", "none", "zero-text"]) +def test_file_source_warns_once_when_the_decompressed_byte_cap_is_off( + off: object, caplog: pytest.LogCaptureFixture +) -> None: # #1129 + # Off is documented and allowed, but never silent. The line names the connection and the setting. + settings = {"directory": "x", "decompress": "gzip", "max_decompressed_bytes": off} + with caplog.at_level(logging.WARNING, logger="messagefoundry.transports.file"): + src = FileSource(Source(type=ConnectorType.FILE, name="IB_DEMO_ADT", settings=settings)) + assert src.max_decompressed_bytes is None + [record] = _decompressed_cap_warnings(caplog) + assert "IB_DEMO_ADT" in record.getMessage() + + +@pytest.mark.parametrize( + "settings", + [ + pytest.param({"decompress": "gzip", "max_decompressed_bytes": 1024}, id="positive-cap"), + pytest.param({"decompress": "gzip"}, id="default-cap"), + pytest.param({"max_decompressed_bytes": 0}, id="off-without-decompress"), + ], +) +def test_file_source_does_not_warn_while_the_decompressed_byte_cap_holds( + settings: dict[str, object], caplog: pytest.LogCaptureFixture +) -> None: # #1129 + # Without decompress the cap is never read, so turning it off changes nothing to warn about. + with caplog.at_level(logging.WARNING, logger="messagefoundry.transports.file"): + FileSource(Source(type=ConnectorType.FILE, settings={"directory": "x", **settings})) + assert _decompressed_cap_warnings(caplog) == [] + + async def test_file_source_quarantines_corrupt_archive(tmp_path: Path) -> None: # A file that isn't a valid gzip stream is quarantined to .error, never crashes the poller. inbox = tmp_path / "in" From 01a2e64d6ab4add670d611aa26282fb9066b5743 Mon Sep 17 00:00:00 2001 From: wshallwshall Date: Mon, 12 Oct 2026 12:48:17 -0500 Subject: [PATCH 2/2] docs: the ECH sidecar contract states request framing (BACKLOG #1125) Point 4 of the sidecar contract: keep each request's Content-Length, refuse ambiguous framing, drop the partner connection after a misframed reply. It links to the DEPLOYMENT.md front-proxy section and the CONFIGURATION.md forward-proxy rule rather than restating them. Proposed PR title: SVGZ inflate gets a trial pass, a disabled File byte cap warns, and the ECH sidecar contract states framing (BACKLOG #1129, #1125) Banner #1129: limb fixed, SVGZ trial pass and File cap warning. Banner #1125: limb fixed, ECH sidecar framing clause. --- changelog.d/1125-ech-sidecar-framing.security.md | 5 +++++ samples/ech-sidecar/README.md | 14 ++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 changelog.d/1125-ech-sidecar-framing.security.md diff --git a/changelog.d/1125-ech-sidecar-framing.security.md b/changelog.d/1125-ech-sidecar-framing.security.md new file mode 100644 index 0000000000..a317da7d19 --- /dev/null +++ b/changelog.d/1125-ech-sidecar-framing.security.md @@ -0,0 +1,5 @@ +- **The ECH egress sidecar contract states a framing rule.** A conforming sidecar reads and re-sends + every request and reply, so it must not re-frame them. It keeps each request's `Content-Length`, + refuses ambiguous framing rather than cleaning it up, and drops its connection to the partner after + a misframed reply. [samples/ech-sidecar/README.md](../samples/ech-sidecar/README.md) links to the + front-proxy rules rather than restating them. (`BACKLOG #1125`) diff --git a/samples/ech-sidecar/README.md b/samples/ech-sidecar/README.md index 10922afcb6..a1ee70f0c0 100644 --- a/samples/ech-sidecar/README.md +++ b/samples/ech-sidecar/README.md @@ -48,18 +48,28 @@ A conforming sidecar is a **loopback TLS-terminating re-originator**, not a tunn either — solving both halves). 3. Be **fail-closed**: if ECH cannot be negotiated (destination publishes no `ECHConfig`), fail rather than silently completing a non-ECH handshake. +4. **Do not re-frame what it carries** (BACKLOG #1125). The sidecar reads and re-sends every request + and reply, so it is a second HTTP parser on the hop. Keep each engine request's `Content-Length`, + and never re-send its body chunked. Refuse ambiguous framing rather than clean it up: both + `Content-Length` and `Transfer-Encoding`, a duplicate or conflicting `Content-Length`, a folded + header line (obs-fold), or a bare LF or bare CR in a head. After a reply it could not read to its + framed end, drop that connection to the partner. The shapes and the reasons are in + [DEPLOYMENT.md](../../docs/DEPLOYMENT.md#a-proxy-in-front-of-the-api-must-refuse-the-request-shapes-the-engine-refuses), + and the outbound form of the rule is in + [CONFIGURATION.md](../../docs/CONFIGURATION.md#a-forward-proxy-must-not-re-frame-what-it-carries). Candidate implementations (operator-supplied, downloaded beside the engine like OpenBao for the store sidecar): **sing-box** (Go `crypto/tls` client ECH since 1.23) configured as a TLS-terminating proxy, or a purpose-built ~500-line Go re-originator. A generic CONNECT-tunneling proxy is **not** conforming. **A reference implementation exists in history, not in the tree.** A 312-line stdlib-only Go -re-originator satisfying all three points above was written here and, on 2026-08-10, retired from the +re-originator satisfying points 1 to 3 above was written here and, on 2026-08-10, retired from the repository (BACKLOG #1011): nothing built, tested, linted or version-pinned it, and it shipped in no sdist or wheel. Recover it with `git show 62fd628d:tools/ech-sidecar/main.go` (its own README, at `62fd628d:tools/ech-sidecar/README.md`, carries the recorded manual proof against `crypto.cloudflare.com` — a one-off observation, never re-run by any automated check). Treat it as a -worked example of the contract, not as supported software. +worked example of the contract, not as supported software. It predates point 4, and nobody has +checked it against that point. ## Wire it in MessageFoundry