Repository navigation
fix(transcript): refuse a member that chunk framing runs through - #9
Merged
xgreenx merged 3 commits intoSep 8, 2026
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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\nbetween chunks, and that framing holds no quote, comma or brace — so every scan inranges.rsruns 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:compute_id_snippet_range_afterclaimed 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:
ProverResult::response_bodyis decoded by hyper while the commitment is over raw transcript bytes.The on-chain framing check passes anyway:
requireFramedCommitmentreads 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::http1speaks, and the request sendsConnection: close— both token endpoints chunk:github.com/login/oauth/access_tokenTransfer-Encoding: chunkedapi.x.com/2/oauth2/tokenTransfer-Encoding: chunkedapi.github.com/userContent-Lengthapi.x.com/2/users/meContent-LengthBoth 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_responsenow 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 giverequireFramedCommitmentno bearer to find.Verification
cargo +nightly fmt --all -- --checkcargo clippy --workspace --all-targets -- -D warningscargo 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
/userwith the same construction in TypeScript. Those endpoints sendContent-Lengthtoday, which is why it has not bitten there — a server-side choice that can change without notice. The TS mirror deserves the same check.