Skip to content

fix(transcript): refuse a member that chunk framing runs through - #9

Merged
xgreenx merged 3 commits into
feat/ceremony-constructionsfrom
fix/reveal-layouts-follow-the-reader
Sep 8, 2026
Merged

xgreenx merged 3 commits into
feat/ceremony-constructionsfrom
fix/reveal-layouts-follow-the-reader

Conversation

@xgreenx

@xgreenx xgreenx commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Stacked on #2 (feat/ceremony-constructions). The commit GitHub never attached to #8 — that PR merged carrying only its first commit, so this is the other half, re-cut on the current base.

The measurement

A chunked body carries \r\n<size>\r\n between chunks, and that framing holds no quote, comma or brace — so every scan in ranges.rs runs straight through it. A member split across a boundary is found in the decoded body and in the raw one, and the raw range silently spans the framing:

RAW:  ...19\r\n{"access_token":"ghu_AAAA\r\n1c\r\nBBBB","token_type":"bearer"}...
compute_field_snippet_range -> "access_token":"ghu_AAAA\r\n1c\r\nBBBB"

compute_id_snippet_range_after claimed the opposite in a comment — "a snippet split across a chunk boundary won't be found contiguously here and fails closed — never mis-resolves" — which holds only when the split lands inside the needle. A split inside the value is the case that occurs, and nothing caught it.

Why it matters, and why it is live

What that range selects is not the member:

  • revealed, it puts framing inside the handle a Platform Verifier reads;
  • committed, it puts framing inside the bearer a Proving Circuit opens — against the clean value, because ProverResult::response_body is decoded by hyper while the commitment is over raw transcript bytes.

The on-chain framing check passes anyway: requireFramedCommitment reads the delimiters either side of the commitment, not its contents. So it fails at REQ-PLAT-52, three components from the cause.

This is not hypothetical. Over HTTP/1.1 — which is what hyper::client::conn::http1 speaks, and the request sends Connection: close — both token endpoints chunk:

endpoint HTTP/1.1 framing
github.com/login/oauth/access_token Transfer-Encoding: chunked
api.x.com/2/oauth2/token Transfer-Encoding: chunked
api.github.com/user Content-Length
api.x.com/2/users/me Content-Length

Both answer HTTP/2 to a modern client, so this is invisible unless you force 1.1. A small JSON body usually arrives as a single chunk, which is the bad shape: passes tests, passes staging, fails intermittently when a response is flushed mid-body.

The fix

Re-framing cannot repair a spanning range — a commitment covers one contiguous run and this member is two — so all three finders compare the raw bytes against the decoded ones and refuse when they differ. The session fails where the reason is a decodable body rather than an unopenable commitment.

token_response now goes through that shared reader instead of scanning the raw transcript with its own copy of the template. This is the session the deployment itself runs (platform-ceremonies §6: a deployment-owned token exchange, a browser-owned /user), so it is the one that most needed the check and had none. The reuse also scopes the search to the response body — the old scan started at byte zero, so a response header carrying the delimiter answered before the body's own member. An empty bearer is refused too: it would leave the two delimiters adjacent, commit nothing, and give requireFramedCommitment no bearer to find.

Verification

  • cargo +nightly fmt --all -- --check
  • cargo clippy --workspace --all-targets -- -D warnings
  • cargo test --workspace — 102 passed (6 new)

Tests cover the straddling case for each of the three finders and for token_response, plus a chunked body whose member sits in one piece and still resolves: the rule is contiguity, not the absence of chunking.

Follow-up, not in this PR

The browser runs /user with the same construction in TypeScript. Those endpoints send Content-Length today, which is why it has not bitten there — a server-side choice that can change without notice. The TS mirror deserves the same check.

A chunked body carries `\r\n<size>\r\n` between chunks, and that framing holds
no quote, comma or brace -- so every scan here runs straight through it. A
member split across a boundary is therefore found in the decoded body AND in
the raw one, and the raw range silently spans the framing:

    RAW:  ...19\r\n{"access_token":"ghu_AAAA\r\n1c\r\nBBBB","token_type":...
    compute_field_snippet_range -> "access_token":"ghu_AAAA\r\n1c\r\nBBBB"

`compute_id_snippet_range_after` claimed the opposite in a comment -- "a
snippet split across a chunk boundary won't be found contiguously here and
fails closed" -- which holds only when the split lands inside the NEEDLE. A
split inside the value is what actually happens, and nothing caught it.

What the spanning range selects is not the member. Revealed, it puts framing
inside the handle a Platform Verifier reads. Committed, it puts framing inside
the bearer a Proving Circuit opens -- against the clean value hyper handed the
caller, because `ProverResult::response_body` is decoded and the commitment is
over raw transcript bytes. Re-framing cannot repair either: a commitment covers
one contiguous run and this member is two. So all three finders now compare the
raw bytes against the decoded ones and refuse when they differ, which fails the
session where the reason is a decodable body rather than an unopenable
commitment three components later.

`token_response` goes through that shared reader instead of scanning the raw
transcript with a copy of the template. This is the session the deployment
itself runs -- the GitHub token exchange, per platform-ceremonies section 6 --
so it is the one that most needed the check, and it had none. The reuse also
scopes the search to the response BODY: the old scan started at byte zero, so a
response header carrying the delimiter answered before the body's own member.

An empty bearer is refused too. It would leave the two revealed delimiters
adjacent and commit nothing, and a response direction with no commitment is one
`requireFramedCommitment` finds no bearer in.

Six tests, including the straddling case for each finder and a chunked body
whose member sits in one piece, which still resolves -- the rule is contiguity,
not the absence of chunking.

Assisted-by: Claude Opus 5
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
…the scan

`token_response` spelled `access_token` twice -- once as the argument to the
reader, once inside the literal it measured the delimiter length from. Change
one and the other disagrees silently, moving the revealed boundary into the
bearer or past it. That is the same class of defect this branch exists to
close, so it should not be introduced by the fix for it.

`compute_json_member` now returns the member AND its value, both from the scan
that located them, and `compute_field_snippet_range` is that with the value
dropped. The caller revealing two delimiters and committing what sits between
them takes both boundaries from the finder and restates no template.

The field stays a constant rather than becoming a parameter. `access_token` is
RFC 6749 section 5.1, not a platform's choice, which is why the contract pins
`ACCESS_TOKEN_PREFIX` on `TlsNotaryVerifierBase` for every profile instead of
on each Platform Verifier. What IS a platform choice is a per-profile virtual
there and a parameter here: the committed body credential of `token_request`,
the field names of `identity_response`. The comment says so, because the
asymmetry looks like an oversight until you know where the contract put it.

Assisted-by: Claude Opus 5
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
`compute_json_member` returns two ranges and `token_response` trusts their
relationship without restating it, so the relationship is what these check:
the value sits inside the member, and what the member holds either side of it
is exactly `"field":"` and the closing quote. A boundary that drifts fails here
rather than as a committed bearer with a quote in it.

Also pinned: a value carrying `:`, `,` or `}` still ends at its quote and is
committed whole -- a scan stopping at a structural byte would commit a prefix
of the bearer and REVEAL the rest, which is the failure worth a test of its
own; an empty value is FOUND with an empty range, because whether that is
usable belongs to the caller, and `token_response` refuses it for its own
reason; and `compute_field_snippet_range` still equals the member range, so the
two cannot drift.

Assisted-by: Claude Opus 5
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
@xgreenx
xgreenx marked this pull request as ready for review September 8, 2026 18:09
@xgreenx
xgreenx merged commit cc40b0c into feat/ceremony-constructions Sep 8, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant