From e84ae70d4d67c5eef379f616d92483e8500f3b55 Mon Sep 17 00:00:00 2001 From: xgreenx Date: Wed, 9 Sep 2026 14:10:43 +0100 Subject: [PATCH 01/15] docs(specs): the token request reveals its headers, and the verifier checks them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The token request's headers were hidden. §5.2 left the request direction's headers out of its table, and §6.4 said so outright -- REQ-PLAT-43D bounded the disclosure to "the seven rows marked `yes`", and the prose named the `Host` header as one "this table hides". That leaves a region of the request the Platform Verifier cannot read, in the one direction it has to reason about. It reads `grant_type`, `client_id` and `code_verifier` out of the body with a form-encoding reading -- and common REQ-COMMON-21B fixes the media type precisely because it "selects the platform's request parser". A media type the verifier cannot see is a value the profile pins and nothing checks, and the platform may have parsed those bytes into fields other than the ones read. So the request line and every request header are revealed, and REQ-PLAT-56A has the verifier compare them against a fixed list, byte for byte and in order. Both halves are needed: revealing without comparing would leave the bytes public AND unconstrained, which is neither private nor checked. Both profiles' requests are now enumerated the way §5.3 and §6.5 already enumerate the identity request's headers, which is the precedent this follows. Nothing here is user data. The request is composed byte for byte by the implementation and driven over a raw MPC-TLS session, not the browser's HTTP stack, so there is no cookie jar and no ambient credential: the four headers are profile constants. Revealing them discloses nothing and leaves the sent direction with no region a verifier cannot read -- and concealment would not have protected a user in any case, since the implementation is what would have put anything private there. The response direction is unchanged and still hides its status line and headers: those are the platform's own bytes and nothing reads them. That asymmetry is now stated rather than left to be inferred. The `Host` argument survives and had to. A revealed `Host` header is still not the authority -- it is prover-composed text -- so the authority still reaches the verifier as the authenticated TLS server identity, and the header is compared against the profile like every other. `client_secret` is untouched: still committed, still ordered last by REQ-COMMON-22 so the revealed run stays contiguous. The linter reports what it reported before this change: 3 errors, 9 warnings, all pre-existing. Assisted-by: Claude Opus 5 Signed-off-by: xgreenx --- specs/platform-ceremonies.md | 55 ++++++++++++++++++++++++++++-------- 1 file changed, 43 insertions(+), 12 deletions(-) diff --git a/specs/platform-ceremonies.md b/specs/platform-ceremonies.md index 6d8f41ac..377792d5 100644 --- a/specs/platform-ceremonies.md +++ b/specs/platform-ceremonies.md @@ -452,7 +452,7 @@ attestation format: | Range | Revealed | Why | |---|---|---| -| request method and path | yes | the Platform Verifier compares them with its profile constants | +| the request line and every request header | yes | the Platform Verifier compares the method and path with its profile constants, and the header set with the fixed list below. The media type is among them, and it is the header that selects the parser the platform applied to the body rows beneath this one (common REQ-COMMON-21B) | | endpoint authority | not a range | the Notary Service authenticated the TLS server identity, and the Platform Verifier compares the attested authority against its pinned constant per common REQ-COMMON-21A | | `grant_type` | yes | constant `authorization_code`; the Platform Verifier compares it byte for byte per REQ-PLAT-56 | | `client_id` | yes | the Platform Verifier reads and returns it | @@ -462,7 +462,7 @@ attestation format: | attestation timestamp | not a range | the attestation's own signed creation time, which derives the authenticated validity ceiling per §2.2 | | `"access_token":"` and the closing quote immediately around the bearer value | yes | anchor the committed bearer range as that field's value, per common REQ-COMMON-18A | | bearer range | committed | a blinded commitment, opened only in circuit | -| everything else | no | headers, `scope`, `token_type`, other response fields | +| everything else | no | the response status line and headers, `scope`, `token_type`, other response fields | Neither the authority nor the attestation timestamp is a transcript range. The authority reaches the Platform Verifier as @@ -505,6 +505,26 @@ dependency. Platform Verifier enforces the disclosure: an attestation hiding either range does not match the profile layout of common REQ-COMMON-17A and REQ-COMMON-18A and fails verification. + +The request carries exactly four headers, in this order: `host: api.x.com`, +`content-type: application/x-www-form-urlencoded`, `accept: application/json`, +and `connection: close`. + +- REQ-PLAT-56A (upholds SP-EXCHANGE-01): + The Implementation MUST reveal the token request's request line and every + one of its headers. The Platform Verifier MUST compare the revealed headers + against the profile's fixed list, byte for byte and in order. The Platform + Verifier MUST reject a token attestation carrying any other header set. + Necessity: the verifier reads `grant_type`, `client_id` and `code_verifier` + out of the body with a form-encoding reading, and common REQ-COMMON-21B + fixes the media type precisely because it "selects the platform's request + parser" -- so a media type the verifier cannot see is a value the profile + pins and nothing checks, and the platform could have parsed those bytes into + fields other than the ones read. Revealing without comparing closes nothing: + the bytes would be public and unconstrained. Nothing in this request is user + data -- the four headers are constants of the profile -- so revealing them + discloses nothing and leaves the sent direction with no region a verifier + cannot read. - REQ-PLAT-56 (upholds SP-EXCHANGE-01): The Platform Verifier MUST reject an X token attestation whose revealed `grant_type` differs from the exact ASCII bytes `authorization_code`. @@ -816,10 +836,9 @@ Submission and every published artifact. | bearer range | committed | a blinded commitment, opened only in circuit to link this attestation to `/user` | | attestation timestamp | not a range | the attestation's own signed creation time, which derives the authenticated validity ceiling per §2.2 | | token endpoint authority | not a range | the Notary Service authenticated the TLS server identity, and the Platform Verifier compares the attested authority against its pinned constant per common REQ-COMMON-21A | -| token request method | yes | the Platform Verifier checks its profile method | -| token request path | yes | the Platform Verifier checks its profile path | +| the request line and every request header | yes | the Platform Verifier checks its profile method and path, and compares the header set with the fixed list below, for the reason REQ-PLAT-56A gives | | `client_secret` | no | never revealed, per REQ-PLAT-35A | -| everything else | no | headers, status line, `scope`, `token_type`, other response fields | +| everything else | no | the response status line and headers, `scope`, `token_type`, other response fields | Every unrevealed range stays behind the pinned attestation format's range commitment. The delimiter row is what anchors the committed bearer range in @@ -829,16 +848,28 @@ authority nor the attestation timestamp is a transcript range at all. The authority reaches the Platform Verifier as the TLS server identity the Notary Service authenticated under common REQ-COMMON-21, carried in the attested data, because the -transcript holds the authority only in a `Host` header this table hides and a -revealed `Host` header is prover-composed text that says nothing about which -server answered. The timestamp is the signed creation time of the attested +transcript holds the authority only in a `Host` header, and that header is +prover-composed text that says nothing about which server answered. Revealing +it, as REQ-PLAT-56A now requires, does not make it the authority: it is +compared against the profile's fixed list like every other header, while the +authority continues to reach the verifier as the authenticated TLS server +identity. The timestamp is the signed creation time of the attested data itself, which is why common REQ-COMMON-25 can forbid inferring it from a -response header. Revealing more would widen exposure without adding a check. +response header. Revealing more than this would widen exposure without adding +a check -- which is why the request headers are revealed and the response's +are not: the request's are profile constants a verifier compares, and the +response's are the platform's own bytes that nothing reads. + +The exchange request carries exactly four headers, in this order: +`host: github.com`, `content-type: application/x-www-form-urlencoded`, +`accept: application/json`, and `connection: close`. - REQ-PLAT-43D (upholds SP-EXCHANGE-01): - The GitHub Token Service MUST reveal no range outside the seven rows - marked `yes` above. The GitHub Token Service MUST commit the bearer range - rather than reveal it. + The GitHub Token Service MUST reveal no range outside the rows marked `yes` + above. The GitHub Token Service MUST commit the bearer range rather than + reveal it. The GitHub Token Service MUST commit `client_secret` rather than + reveal it, which REQ-COMMON-22 orders last so the revealed run stays + contiguous. REQ-PLAT-56A applies to this request too. - REQ-PLAT-58 (upholds SP-EXCHANGE-01): The GitHub Token Service MUST reveal the `"access_token":"` delimiter bytes immediately preceding that committed range and the closing quote byte From d936f49c89d52945fe54b18d7e1b126cd0a5709c Mon Sep 17 00:00:00 2001 From: xgreenx Date: Wed, 9 Sep 2026 14:28:41 +0100 Subject: [PATCH 02/15] docs(specs): the token request's Content-Length, and the line endings around it The header list in the previous commit was wrong, and wrong in the way that would have been found by a failing launch rather than by review: it enumerated four headers and required the verifier to reject any other set, but a request carrying a body carries a `Content-Length`. hyper emits one for every known-size body -- `set_length` calls `set_content_length` -- and libid sets it nowhere, so the GitHub Token Service's own exchange would have produced a head of five headers and been refused by its own profile. As written the rule rejected every genuine attestation. `Content-Length` cannot be a profile constant: its value is the body's byte count. So REQ-PLAT-56B pins the value against something the verifying side can derive rather than against a literal -- the signed transcript length of the sent direction less the head is the body, whether the body is revealed whole as in X's request or revealed up to a committed suffix as in GitHub's, because common REQ-COMMON-35 makes the direction tile exactly. That is not bookkeeping. The verifier takes the body to be everything after the sole CRLFCRLF; the platform takes it to be `Content-Length` bytes. Where those disagree the fields the verifier reads are not the fields the platform parsed, which is the same divergence REQ-PLAT-56's `grant_type` check exists to stop, reached by a different route. `Transfer-Encoding` overrides `Content-Length` outright and so is refused rather than described. REQ-PLAT-56C carries the line-ending discipline over from the identity request. Common REQ-COMMON-39 already refuses a bare line feed and an obsolete line fold there, because a parser that accepts either ends the head somewhere the verifier does not -- and the token head, which no requirement had ever covered, is the same head with the same parsers reading it. REQ-PLAT-56A now also says the match is exhaustive rather than a presence test. "These five appear" is satisfied by a request carrying a sixth. Found by an audit of the previous commit, which was written from a reading of the request that no HTTP client produces. Assisted-by: Claude Opus 5 Signed-off-by: xgreenx --- specs/platform-ceremonies.md | 55 +++++++++++++++++++++++++++++------- 1 file changed, 45 insertions(+), 10 deletions(-) diff --git a/specs/platform-ceremonies.md b/specs/platform-ceremonies.md index 377792d5..a79f865d 100644 --- a/specs/platform-ceremonies.md +++ b/specs/platform-ceremonies.md @@ -506,25 +506,55 @@ dependency. range does not match the profile layout of common REQ-COMMON-17A and REQ-COMMON-18A and fails verification. -The request carries exactly four headers, in this order: `host: api.x.com`, +The request carries exactly five headers, in this order: `host: api.x.com`, `content-type: application/x-www-form-urlencoded`, `accept: application/json`, -and `connection: close`. +`connection: close`, and `content-length`, whose value is the body's byte +count and therefore the one header value the profile cannot fix. - REQ-PLAT-56A (upholds SP-EXCHANGE-01): The Implementation MUST reveal the token request's request line and every one of its headers. The Platform Verifier MUST compare the revealed headers against the profile's fixed list, byte for byte and in order. The Platform - Verifier MUST reject a token attestation carrying any other header set. + Verifier MUST reject a token attestation whose head carries any header + outside that list, a second copy of any of them, or them in another order. Necessity: the verifier reads `grant_type`, `client_id` and `code_verifier` out of the body with a form-encoding reading, and common REQ-COMMON-21B fixes the media type precisely because it "selects the platform's request parser" -- so a media type the verifier cannot see is a value the profile pins and nothing checks, and the platform could have parsed those bytes into fields other than the ones read. Revealing without comparing closes nothing: - the bytes would be public and unconstrained. Nothing in this request is user - data -- the four headers are constants of the profile -- so revealing them - discloses nothing and leaves the sent direction with no region a verifier - cannot read. + the bytes would be public and unconstrained. The match is exhaustive rather + than a presence test, because a request satisfying "these five appear" may + still carry a sixth the platform acts on. Nothing here is user data -- the + request is composed byte for byte by the Implementation over a raw notarized + session rather than by a browser's HTTP stack, so no cookie or ambient + credential can reach it -- and revealing leaves the sent direction with no + region a verifier cannot read. +- REQ-PLAT-56B (upholds SP-EXCHANGE-01): + The Platform Verifier MUST reject a token attestation whose revealed + `content-length` value is not the exact decimal byte count of the request + body it frames. The Platform Verifier MUST reject a token attestation whose + head carries a `transfer-encoding` header. Necessity: the verifier takes the + body to be everything after the sole `\r\n\r\n`, while the platform takes + the body to be `content-length` bytes, and where those two disagree the + fields the verifier reads are not the fields the platform parsed -- a short + `content-length` leaves the remainder outside the request the platform + answered. `transfer-encoding` overrides `content-length` entirely and so + removes the framing this requirement pins. The body byte count is derivable + on the verifying side without trusting the value: the signed transcript + length of the sent direction, less the head, is the body, whether that body + is revealed whole as in X's request or revealed up to a committed suffix as + in GitHub's. +- REQ-PLAT-56C (upholds SP-EXCHANGE-01): + The Platform Verifier MUST reject a token attestation whose revealed head + contains a line feed not preceded by a carriage return, or a line beginning + with a space or horizontal tab. Necessity: the head ends at the sole + `\r\n\r\n` for the verifier, but an HTTP parser accepting a bare line feed + or an obsolete line fold ends it elsewhere, which moves the platform's + head-body boundary away from the verifier's and turns bytes the verifier + read as a header into bytes the platform parsed as the body. Common + REQ-COMMON-39 already requires this of the identity request; the token + request needs it for the same reason and did not have it. - REQ-PLAT-56 (upholds SP-EXCHANGE-01): The Platform Verifier MUST reject an X token attestation whose revealed `grant_type` differs from the exact ASCII bytes `authorization_code`. @@ -860,16 +890,21 @@ a check -- which is why the request headers are revealed and the response's are not: the request's are profile constants a verifier compares, and the response's are the platform's own bytes that nothing reads. -The exchange request carries exactly four headers, in this order: +The exchange request carries exactly five headers, in this order: `host: github.com`, `content-type: application/x-www-form-urlencoded`, -`accept: application/json`, and `connection: close`. +`accept: application/json`, `connection: close`, and `content-length`. Its +body includes the committed `client_secret`, so the byte count REQ-PLAT-56B +compares against spans the revealed prefix and that commitment together -- +which the exact tiling of common REQ-COMMON-35 makes derivable without +revealing the secret. - REQ-PLAT-43D (upholds SP-EXCHANGE-01): The GitHub Token Service MUST reveal no range outside the rows marked `yes` above. The GitHub Token Service MUST commit the bearer range rather than reveal it. The GitHub Token Service MUST commit `client_secret` rather than reveal it, which REQ-COMMON-22 orders last so the revealed run stays - contiguous. REQ-PLAT-56A applies to this request too. + contiguous. REQ-PLAT-56A, REQ-PLAT-56B and REQ-PLAT-56C apply to this + request too. - REQ-PLAT-58 (upholds SP-EXCHANGE-01): The GitHub Token Service MUST reveal the `"access_token":"` delimiter bytes immediately preceding that committed range and the closing quote byte From 53717a4f29d5c78e7ceab9221e8676cd195529a6 Mon Sep 17 00:00:00 2001 From: xgreenx Date: Wed, 9 Sep 2026 15:50:11 +0100 Subject: [PATCH 03/15] docs(specs): the header set, not its order Order carries no meaning here. Field order is insignificant in HTTP except for repeated names, which REQ-PLAT-56A forbids outright, and the `transfer-encoding`/`content-length` precedence REQ-PLAT-56B settles is by presence rather than position. Nothing a reordering does is not already refused by requiring the exact set. What fixing it would cost is real. It binds every prover to the header order its HTTP library happens to emit: the browser reaches the wire through tlsn's wasm prover, whose `HttpRequest` carries headers in a `HashMap`, so the order is whatever that iteration gives on the day. Pinning it also pins `content-length` last, which is not a promise anyone made -- it is where hyper's `set_length` inserts it, and a patch release could move it. So the set is fixed and the order is not, and the prose says so once rather than three times. Assisted-by: Claude Opus 5 Signed-off-by: xgreenx --- specs/platform-ceremonies.md | 77 ++++++++++++------------------------ 1 file changed, 26 insertions(+), 51 deletions(-) diff --git a/specs/platform-ceremonies.md b/specs/platform-ceremonies.md index a79f865d..4a65c29b 100644 --- a/specs/platform-ceremonies.md +++ b/specs/platform-ceremonies.md @@ -452,7 +452,7 @@ attestation format: | Range | Revealed | Why | |---|---|---| -| the request line and every request header | yes | the Platform Verifier compares the method and path with its profile constants, and the header set with the fixed list below. The media type is among them, and it is the header that selects the parser the platform applied to the body rows beneath this one (common REQ-COMMON-21B) | +| the request line and every request header | yes | the Platform Verifier compares the method and path with its profile constants and the header set with the list below, the media type among them: it selects the parser the platform applied to the body rows beneath this one (common REQ-COMMON-21B) | | endpoint authority | not a range | the Notary Service authenticated the TLS server identity, and the Platform Verifier compares the attested authority against its pinned constant per common REQ-COMMON-21A | | `grant_type` | yes | constant `authorization_code`; the Platform Verifier compares it byte for byte per REQ-PLAT-56 | | `client_id` | yes | the Platform Verifier reads and returns it | @@ -506,55 +506,33 @@ dependency. range does not match the profile layout of common REQ-COMMON-17A and REQ-COMMON-18A and fails verification. -The request carries exactly five headers, in this order: `host: api.x.com`, +The request carries these five headers, in any order: `host: api.x.com`, `content-type: application/x-www-form-urlencoded`, `accept: application/json`, -`connection: close`, and `content-length`, whose value is the body's byte -count and therefore the one header value the profile cannot fix. +`connection: close`, and `content-length`, whose value is the body's own count. - REQ-PLAT-56A (upholds SP-EXCHANGE-01): The Implementation MUST reveal the token request's request line and every - one of its headers. The Platform Verifier MUST compare the revealed headers - against the profile's fixed list, byte for byte and in order. The Platform - Verifier MUST reject a token attestation whose head carries any header - outside that list, a second copy of any of them, or them in another order. - Necessity: the verifier reads `grant_type`, `client_id` and `code_verifier` - out of the body with a form-encoding reading, and common REQ-COMMON-21B - fixes the media type precisely because it "selects the platform's request - parser" -- so a media type the verifier cannot see is a value the profile - pins and nothing checks, and the platform could have parsed those bytes into - fields other than the ones read. Revealing without comparing closes nothing: - the bytes would be public and unconstrained. The match is exhaustive rather - than a presence test, because a request satisfying "these five appear" may - still carry a sixth the platform acts on. Nothing here is user data -- the - request is composed byte for byte by the Implementation over a raw notarized - session rather than by a browser's HTTP stack, so no cookie or ambient - credential can reach it -- and revealing leaves the sent direction with no - region a verifier cannot read. + header. The Platform Verifier MUST reject a head carrying a header outside + that list, one of them twice, or a listed header with another value. + Necessity: the verifier reads the body with a form-encoding reading, and + common REQ-COMMON-21B fixes the media type because it "selects the platform's + request parser" -- a media type nothing compares is a pin in name only. + Order is left free because it changes nothing the platform does with the + request, and fixing it would bind every prover to the header order its HTTP + library happens to emit. - REQ-PLAT-56B (upholds SP-EXCHANGE-01): - The Platform Verifier MUST reject a token attestation whose revealed - `content-length` value is not the exact decimal byte count of the request - body it frames. The Platform Verifier MUST reject a token attestation whose - head carries a `transfer-encoding` header. Necessity: the verifier takes the - body to be everything after the sole `\r\n\r\n`, while the platform takes - the body to be `content-length` bytes, and where those two disagree the - fields the verifier reads are not the fields the platform parsed -- a short - `content-length` leaves the remainder outside the request the platform - answered. `transfer-encoding` overrides `content-length` entirely and so - removes the framing this requirement pins. The body byte count is derivable - on the verifying side without trusting the value: the signed transcript - length of the sent direction, less the head, is the body, whether that body - is revealed whole as in X's request or revealed up to a committed suffix as - in GitHub's. + The Platform Verifier MUST reject a `content-length` other than the decimal + count of the body it frames. The Platform Verifier MUST reject a + `transfer-encoding` header. Necessity: the verifier takes the body to be what + follows the head while the platform takes it to be `content-length` bytes, so + where the two disagree the fields read are not the fields parsed; + `transfer-encoding` removes that framing outright. - REQ-PLAT-56C (upholds SP-EXCHANGE-01): - The Platform Verifier MUST reject a token attestation whose revealed head - contains a line feed not preceded by a carriage return, or a line beginning - with a space or horizontal tab. Necessity: the head ends at the sole - `\r\n\r\n` for the verifier, but an HTTP parser accepting a bare line feed - or an obsolete line fold ends it elsewhere, which moves the platform's - head-body boundary away from the verifier's and turns bytes the verifier - read as a header into bytes the platform parsed as the body. Common - REQ-COMMON-39 already requires this of the identity request; the token - request needs it for the same reason and did not have it. + The Platform Verifier MUST reject a head carrying a line feed not preceded by + a carriage return, or a line beginning with a space or a tab. Necessity: a + parser accepting either ends the head somewhere this one does not, moving + bytes between head and body. Common REQ-COMMON-39 asks the same of the + identity request. - REQ-PLAT-56 (upholds SP-EXCHANGE-01): The Platform Verifier MUST reject an X token attestation whose revealed `grant_type` differs from the exact ASCII bytes `authorization_code`. @@ -890,13 +868,10 @@ a check -- which is why the request headers are revealed and the response's are not: the request's are profile constants a verifier compares, and the response's are the platform's own bytes that nothing reads. -The exchange request carries exactly five headers, in this order: -`host: github.com`, `content-type: application/x-www-form-urlencoded`, -`accept: application/json`, `connection: close`, and `content-length`. Its -body includes the committed `client_secret`, so the byte count REQ-PLAT-56B -compares against spans the revealed prefix and that commitment together -- -which the exact tiling of common REQ-COMMON-35 makes derivable without -revealing the secret. +The exchange request carries those same five headers, in any order, with +`host: github.com`. Its body includes the committed `client_secret`, so the +count REQ-PLAT-56B compares spans the revealed prefix and that commitment, +which the exact tiling of common REQ-COMMON-35 makes derivable. - REQ-PLAT-43D (upholds SP-EXCHANGE-01): The GitHub Token Service MUST reveal no range outside the rows marked `yes` From a35a9d78d743f69ccbbe67d360013ab1bbda264d Mon Sep 17 00:00:00 2001 From: xgreenx Date: Wed, 9 Sep 2026 18:12:25 +0100 Subject: [PATCH 04/15] docs(specs): the identity requests carry their headers in any order too Sections 5.3 and 6.5 said "in this order" of headers no verifier orders: the identity request is held to coverage, one line-anchored authorization and the bearer framing, and common section 6 already says header order carries no proof semantics unless a profile commits it. The token request's headers were freed of order in this branch; these two now say the same. Assisted-by: Claude Opus 5 Signed-off-by: xgreenx --- specs/platform-ceremonies.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specs/platform-ceremonies.md b/specs/platform-ceremonies.md index 4a65c29b..e0dff9ed 100644 --- a/specs/platform-ceremonies.md +++ b/specs/platform-ceremonies.md @@ -564,7 +564,7 @@ The request carries these five headers, in any order: `host: api.x.com`, ### 5.3 Identity request `GET https://api.x.com/2/users/me` with no query. The request carries -exactly four headers, in this order: `authorization: Bearer `, +exactly four headers, in any order: `authorization: Bearer `, `accept: application/json`, `host: api.x.com`, and `connection: close`. Per common §9, the identity session reveals exactly these request ranges; @@ -947,7 +947,7 @@ keeping the client secret from the browser. ### 6.5 Identity request `GET https://api.github.com/user` with no query. The request carries -exactly five headers, in this order: +exactly five headers, in any order: `authorization: Bearer `, `accept: application/vnd.github+json`, `x-github-api-version: 2022-11-28`, `host: api.github.com`, and `connection: close`. From ae33a29e454198212215764c167eb10f624d926a Mon Sep 17 00:00:00 2001 From: xgreenx Date: Wed, 9 Sep 2026 19:04:07 +0100 Subject: [PATCH 05/15] docs(specs): the token request is one revealed range, its fields are rows The tables listed each body field as its own revealed range. The verifiers, the prover and the attested record carry the request as one range -- the record cannot hold adjacent ranges apart, it merges them before signing -- so a browser planning one range per field would see its count change under it. Say so, once per platform. Assisted-by: Claude Opus 5 Signed-off-by: xgreenx --- specs/platform-ceremonies.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/specs/platform-ceremonies.md b/specs/platform-ceremonies.md index e0dff9ed..d4de0647 100644 --- a/specs/platform-ceremonies.md +++ b/specs/platform-ceremonies.md @@ -446,6 +446,11 @@ sessions. to link two attestations, so it needs a bound and a charset; the circuit verifies no other property of the token response. +The request is one revealed range: the request line, every header and the +body. The rows below name what the Platform Verifier reads out of it, not +separate ranges; the attested record carries adjacent revealed ranges as one, +so a plan of one range per field would not survive signing. + Per common §9, the token session reveals exactly these ranges; every other byte stays behind a charset-constrained range commitment of the pinned attestation format: @@ -827,6 +832,9 @@ commitment, so the browser never receives the secret. The attestation is verified by the compatible Notary Service selected for the GitHub profile, exactly as the `/user` attestation is. +The request is one revealed range up to the committed `client_secret`, which +REQ-COMMON-22 orders last; the rows below name what is read out of it. + The token-exchange attestation reveals exactly the ranges needed to bind it to the local ceremony and to the later `/user` attestation. The separately returned `accessToken` and the `bearerOpening` of REQ-PLAT-54 are the only From 03c79cda543b19eeba638f3c4862fb0f8774646d Mon Sep 17 00:00:00 2001 From: xgreenx Date: Thu, 10 Sep 2026 10:04:10 +0100 Subject: [PATCH 06/15] docs(specs): GitHub's identity request needs a user-agent api.github.com refuses any request without one: 403, with a body that says so, where the same request with one gets the 401 an absent bearer earns. Section 6.5 listed five headers and left it out, so a prover built from the text alone could never reach the identity read. The browser draft already sends one. X's endpoint and both token endpoints do not care, checked the same way, so the token request's pinned set is unchanged. The value is the runtime's to choose: nothing verifies it, and GitHub requires only that it exist. Assisted-by: Claude Opus 5 Signed-off-by: xgreenx --- specs/platform-ceremonies.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/specs/platform-ceremonies.md b/specs/platform-ceremonies.md index d4de0647..a2a9c3e8 100644 --- a/specs/platform-ceremonies.md +++ b/specs/platform-ceremonies.md @@ -955,10 +955,11 @@ keeping the client secret from the browser. ### 6.5 Identity request `GET https://api.github.com/user` with no query. The request carries -exactly five headers, in any order: +exactly six headers, in any order: `authorization: Bearer `, `accept: application/vnd.github+json`, -`x-github-api-version: 2022-11-28`, `host: api.github.com`, and -`connection: close`. +`x-github-api-version: 2022-11-28`, `host: api.github.com`, +`connection: close`, and a `user-agent` of the Canonical Runtime's choosing, +which GitHub requires of every API request and answers `403` without. Per common §9, the identity session reveals exactly these request ranges; the bearer value is the only committed request range, and every other From 5bbd838c81d4a47849104cf0f965ad985b2e5b98 Mon Sep 17 00:00:00 2001 From: xgreenx Date: Thu, 10 Sep 2026 10:07:17 +0100 Subject: [PATCH 07/15] docs(specs): the identity request may carry headers nothing compares Both identity requests said "exactly N headers". No verifier holds them to that: the contract compares the request line and the authorization line, and the prover's layout finds the same line and nothing else. A count nothing checks is a rule nobody can rely on and a prover can only fail to meet, as GitHub's user-agent just showed. Say which lines are compared and leave the rest to the runtime. The token request is unchanged: there the verifier does hold the set. Assisted-by: Claude Opus 5 Signed-off-by: xgreenx --- specs/platform-ceremonies.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/specs/platform-ceremonies.md b/specs/platform-ceremonies.md index a2a9c3e8..0ef88db1 100644 --- a/specs/platform-ceremonies.md +++ b/specs/platform-ceremonies.md @@ -568,9 +568,11 @@ The request carries these five headers, in any order: `host: api.x.com`, ### 5.3 Identity request -`GET https://api.x.com/2/users/me` with no query. The request carries -exactly four headers, in any order: `authorization: Bearer `, -`accept: application/json`, `host: api.x.com`, and `connection: close`. +`GET https://api.x.com/2/users/me` with no query. The request carries these +four headers, in any order, and may carry others: `authorization: Bearer +`, `accept: application/json`, `host: api.x.com`, and +`connection: close`. The Platform Verifier compares the request line and the +`authorization` line and no other header. Per common §9, the identity session reveals exactly these request ranges; the bearer value is the only committed request range, and every other @@ -954,12 +956,14 @@ keeping the client secret from the browser. ### 6.5 Identity request -`GET https://api.github.com/user` with no query. The request carries -exactly six headers, in any order: +`GET https://api.github.com/user` with no query. The request carries these +six headers, in any order, and may carry others: `authorization: Bearer `, `accept: application/vnd.github+json`, `x-github-api-version: 2022-11-28`, `host: api.github.com`, `connection: close`, and a `user-agent` of the Canonical Runtime's choosing, -which GitHub requires of every API request and answers `403` without. +which GitHub requires of every API request and answers `403` without. The +Platform Verifier compares the request line and the `authorization` line and +no other header. Per common §9, the identity session reveals exactly these request ranges; the bearer value is the only committed request range, and every other From a64931cff41b3ae48b93e767f02b35bbe81cffd0 Mon Sep 17 00:00:00 2001 From: xgreenx Date: Thu, 10 Sep 2026 14:01:20 +0100 Subject: [PATCH 08/15] docs(specs): the token request's head is held to a subset, not a set REQ-PLAT-56A had the verifier reject any header outside the profile's list. A header outside it changes only what the platform answers, and a wrong answer is a response the verifier cannot read, not one it can be fooled by; the rule bound every prover to one HTTP library's habits for nothing. It now requires `host` and the media type, forbids the five names that change what the platform does with the request in a way no revealed byte shows, and ignores the rest. `transfer-encoding` moves from 56B to that list, beside `content-encoding`. Assisted-by: Claude Opus 5 Signed-off-by: xgreenx --- specs/platform-ceremonies.md | 52 +++++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/specs/platform-ceremonies.md b/specs/platform-ceremonies.md index 0ef88db1..4520b635 100644 --- a/specs/platform-ceremonies.md +++ b/specs/platform-ceremonies.md @@ -457,7 +457,7 @@ attestation format: | Range | Revealed | Why | |---|---|---| -| the request line and every request header | yes | the Platform Verifier compares the method and path with its profile constants and the header set with the list below, the media type among them: it selects the parser the platform applied to the body rows beneath this one (common REQ-COMMON-21B) | +| the request line and every request header | yes | the Platform Verifier compares the method and path with its profile constants, requires `host` and the media type that selects the parser the platform applied to the body rows beneath this one (common REQ-COMMON-21B), and refuses the headers REQ-PLAT-56A forbids | | endpoint authority | not a range | the Notary Service authenticated the TLS server identity, and the Platform Verifier compares the attested authority against its pinned constant per common REQ-COMMON-21A | | `grant_type` | yes | constant `authorization_code`; the Platform Verifier compares it byte for byte per REQ-PLAT-56 | | `client_id` | yes | the Platform Verifier reads and returns it | @@ -511,27 +511,35 @@ dependency. range does not match the profile layout of common REQ-COMMON-17A and REQ-COMMON-18A and fails verification. -The request carries these five headers, in any order: `host: api.x.com`, -`content-type: application/x-www-form-urlencoded`, `accept: application/json`, -`connection: close`, and `content-length`, whose value is the body's own count. +The request carries `host: api.x.com`, +`content-type: application/x-www-form-urlencoded`, and a `content-length` of the +body's own count. The Canonical Runtime also sends `accept: application/json` +and `connection: close`, which nothing verifies, and may send any other header +REQ-PLAT-56A does not forbid. - REQ-PLAT-56A (upholds SP-EXCHANGE-01): The Implementation MUST reveal the token request's request line and every - header. The Platform Verifier MUST reject a head carrying a header outside - that list, one of them twice, or a listed header with another value. - Necessity: the verifier reads the body with a form-encoding reading, and - common REQ-COMMON-21B fixes the media type because it "selects the platform's - request parser" -- a media type nothing compares is a pin in name only. - Order is left free because it changes nothing the platform does with the - request, and fixing it would bind every prover to the header order its HTTP - library happens to emit. + header. The Platform Verifier MUST reject a head without exactly one `host` + naming the pinned authority and exactly one `content-type` whose value is + `application/x-www-form-urlencoded`, comparing names lowercased and values + exactly. The Platform Verifier MUST reject a head carrying `authorization`, + `content-encoding`, `transfer-encoding`, `cookie` or + `x-http-method-override` under any spelling of the name, and MUST ignore + every other header. Necessity: common REQ-COMMON-21B fixes the media type + because it "selects the platform's request parser", and a media type nothing + compares is a pin in name only. The forbidden headers change what the + platform does with the request in a way no revealed byte shows: which client + it authenticates, which bytes it parses, which method it runs. Any other + header changes only what the platform answers, and a wrong answer is a + response the verifier cannot read rather than one it can be fooled by, so + requiring its absence would bind every prover to one HTTP library's habits + for nothing. - REQ-PLAT-56B (upholds SP-EXCHANGE-01): - The Platform Verifier MUST reject a `content-length` other than the decimal - count of the body it frames. The Platform Verifier MUST reject a - `transfer-encoding` header. Necessity: the verifier takes the body to be what - follows the head while the platform takes it to be `content-length` bytes, so - where the two disagree the fields read are not the fields parsed; - `transfer-encoding` removes that framing outright. + The Platform Verifier MUST reject a head without exactly one + `content-length`, or with one other than the decimal count of the body it + frames. Necessity: the verifier takes the body to be what follows the head + while the platform takes it to be `content-length` bytes, so where the two + disagree the fields read are not the fields parsed. - REQ-PLAT-56C (upholds SP-EXCHANGE-01): The Platform Verifier MUST reject a head carrying a line feed not preceded by a carriage return, or a line beginning with a space or a tab. Necessity: a @@ -854,7 +862,7 @@ Submission and every published artifact. | bearer range | committed | a blinded commitment, opened only in circuit to link this attestation to `/user` | | attestation timestamp | not a range | the attestation's own signed creation time, which derives the authenticated validity ceiling per §2.2 | | token endpoint authority | not a range | the Notary Service authenticated the TLS server identity, and the Platform Verifier compares the attested authority against its pinned constant per common REQ-COMMON-21A | -| the request line and every request header | yes | the Platform Verifier checks its profile method and path, and compares the header set with the fixed list below, for the reason REQ-PLAT-56A gives | +| the request line and every request header | yes | the Platform Verifier compares the method and path with its profile constants, requires `host` and the media type that selects the parser the platform applied to the body rows beneath this one (common REQ-COMMON-21B), and refuses the headers REQ-PLAT-56A forbids | | `client_secret` | no | never revealed, per REQ-PLAT-35A | | everything else | no | the response status line and headers, `scope`, `token_type`, other response fields | @@ -878,8 +886,10 @@ a check -- which is why the request headers are revealed and the response's are not: the request's are profile constants a verifier compares, and the response's are the platform's own bytes that nothing reads. -The exchange request carries those same five headers, in any order, with -`host: github.com`. Its body includes the committed `client_secret`, so the +The exchange request carries `host: github.com` and the same media type under +the same REQ-PLAT-56A; the GitHub Token Service also sends +`accept: application/json` and `connection: close`, which nothing verifies. +Its body includes the committed `client_secret`, so the count REQ-PLAT-56B compares spans the revealed prefix and that commitment, which the exact tiling of common REQ-COMMON-35 makes derivable. From 770b2fa0481f9af6fad0b50bb4d333e1cc1d12c6 Mon Sep 17 00:00:00 2001 From: xgreenx Date: Thu, 10 Sep 2026 14:01:46 +0100 Subject: [PATCH 09/15] docs(specs): one keyword per sentence in REQ-PLAT-56A The linter says so, and it is right: two MUSTs in one sentence read as one rule with a clause. Assisted-by: Claude Opus 5 Signed-off-by: xgreenx --- specs/platform-ceremonies.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specs/platform-ceremonies.md b/specs/platform-ceremonies.md index 4520b635..8a25b4c7 100644 --- a/specs/platform-ceremonies.md +++ b/specs/platform-ceremonies.md @@ -524,8 +524,8 @@ REQ-PLAT-56A does not forbid. `application/x-www-form-urlencoded`, comparing names lowercased and values exactly. The Platform Verifier MUST reject a head carrying `authorization`, `content-encoding`, `transfer-encoding`, `cookie` or - `x-http-method-override` under any spelling of the name, and MUST ignore - every other header. Necessity: common REQ-COMMON-21B fixes the media type + `x-http-method-override` under any spelling of the name. The Platform + Verifier MUST ignore every other header. Necessity: common REQ-COMMON-21B fixes the media type because it "selects the platform's request parser", and a media type nothing compares is a pin in name only. The forbidden headers change what the platform does with the request in a way no revealed byte shows: which client From 2780ff2e217098ef89a16e28ed308df1dc6a9278 Mon Sep 17 00:00:00 2001 From: xgreenx Date: Thu, 10 Sep 2026 14:40:24 +0100 Subject: [PATCH 10/15] docs(specs): say how the token head is normalized before comparison REQ-PLAT-56A said values are compared exactly; the verifier removes the optional whitespace around them first, and lowercases names with their whitespace removed, which is the normalization REQ-COMMON-39 already gives the identity request. A verifier built from the text alone would have refused `content-type:application/...`, which the contract accepts. 56C now also names a line with no colon, which the contract refuses. Assisted-by: Claude Opus 5 Signed-off-by: xgreenx --- specs/platform-ceremonies.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/specs/platform-ceremonies.md b/specs/platform-ceremonies.md index 8a25b4c7..46f3ad22 100644 --- a/specs/platform-ceremonies.md +++ b/specs/platform-ceremonies.md @@ -521,8 +521,10 @@ REQ-PLAT-56A does not forbid. The Implementation MUST reveal the token request's request line and every header. The Platform Verifier MUST reject a head without exactly one `host` naming the pinned authority and exactly one `content-type` whose value is - `application/x-www-form-urlencoded`, comparing names lowercased and values - exactly. The Platform Verifier MUST reject a head carrying `authorization`, + `application/x-www-form-urlencoded`, comparing names lowercased with every + space and tab removed, as common REQ-COMMON-39 normalizes, and values + exactly once the optional whitespace around them is removed. The Platform + Verifier MUST reject a head carrying `authorization`, `content-encoding`, `transfer-encoding`, `cookie` or `x-http-method-override` under any spelling of the name. The Platform Verifier MUST ignore every other header. Necessity: common REQ-COMMON-21B fixes the media type @@ -542,9 +544,11 @@ REQ-PLAT-56A does not forbid. disagree the fields read are not the fields parsed. - REQ-PLAT-56C (upholds SP-EXCHANGE-01): The Platform Verifier MUST reject a head carrying a line feed not preceded by - a carriage return, or a line beginning with a space or a tab. Necessity: a - parser accepting either ends the head somewhere this one does not, moving - bytes between head and body. Common REQ-COMMON-39 asks the same of the + a carriage return, a line beginning with a space or a tab, or a line with no + colon. Necessity: a parser accepting a bare line feed or a fold ends the head + somewhere this one does not, moving bytes between head and body, and a line + no colon splits is not a header field, so a parser that tolerates one reads a + head this one cannot. Common REQ-COMMON-39 asks the first two of the identity request. - REQ-PLAT-56 (upholds SP-EXCHANGE-01): The Platform Verifier MUST reject an X token attestation whose revealed From 40340a6c801e9f53233b0caf2f2c14f6c8ba622d Mon Sep 17 00:00:00 2001 From: xgreenx Date: Thu, 10 Sep 2026 15:19:16 +0100 Subject: [PATCH 11/15] docs(specs): count every authorization header, and forbid on both requests REQ-COMMON-39 counted `authorization:bearer` lines, so a second header under Basic or a platform's token scheme was never counted and the Identity Platform answered for whichever credential it honoured; the committed bearer is the one thing the cross-bind fixes. The needle is now `authorization:` under any scheme. REQ-COMMON-39A states the line-ending rule the verifier already applies to the identity request -- no bare line feed, no fold -- which nothing in this file said, and adds the bare carriage return. REQ-COMMON-39B forbids on the identity request the names that change what the platform does with it, `cookie` above all, with `_` read as `-`; the token request's REQ-PLAT-56A now refers to that one list plus `authorization`, and 56C names the bare carriage return and points at 39A. Assisted-by: Claude Opus 5 Signed-off-by: xgreenx --- specs/ceremony-common.md | 43 +++++++++++++++++++++++++++--------- specs/platform-ceremonies.md | 27 +++++++++++----------- 2 files changed, 47 insertions(+), 23 deletions(-) diff --git a/specs/ceremony-common.md b/specs/ceremony-common.md index 1f38058f..1b2e5e98 100644 --- a/specs/ceremony-common.md +++ b/specs/ceremony-common.md @@ -942,16 +942,39 @@ and no `authorization` needle to count. removing every space and horizontal tab. The Platform Verifier MUST leave carriage-return and line-feed bytes in place. The Platform Verifier MUST require exactly one occurrence of the normalized, - line-anchored credential header needle `\r\nauthorization:bearer` across - all revealed request bytes, counting the region before the committed - range and the region after it together. Necessity: HTTP field names and - the auth-scheme token are case-insensitive and the colon admits optional - whitespace, so a literal search over raw bytes is evadable; removing only - bytes absent from the needle can create a spurious match, an over-reject - which is safe, but can never hide a real one; and keeping CR and LF is - what makes the needle count header lines rather than any substring, so a - second genuine `authorization` header is rejected whatever the Identity - Platform would have done with it. + line-anchored credential header needle `\r\nauthorization:` across + all revealed request bytes, whatever auth scheme follows it, counting the + region before the committed range and the region after it together. + Necessity: HTTP field names are case-insensitive and the colon admits + optional whitespace, so a literal search over raw bytes is evadable; + removing only bytes absent from the needle can create a spurious match, an + over-reject which is safe, but can never hide a real one; keeping CR and LF + is what makes the needle count header lines rather than any substring; and + counting under any scheme is what rejects a second `authorization` header + whatever it carries. A count of `bearer` lines alone leaves a second header + under Basic or a platform's own token scheme uncounted, and the Identity + Platform answering for whichever credential it honoured, which is the + committed bearer or someone else's. +- REQ-COMMON-39A (upholds SP-EXCHANGE-01): + For that same identity-session request, the Platform Verifier MUST reject + revealed request bytes carrying a line feed not preceded by a carriage + return, a carriage return not followed by a line feed, or a line beginning + with a space or a horizontal tab. Necessity: the count of REQ-COMMON-39 + reads header lines, and each of the three is a byte some parser reads as a + line boundary this one does not, so a second header could sit where the + count sees none. +- REQ-COMMON-39B (upholds SP-EXCHANGE-01): + For that same identity-session request, the Platform Verifier MUST reject a + revealed header line whose name, normalized as REQ-COMMON-39 normalizes + and with `_` read as `-`, is `cookie`, `content-encoding`, + `transfer-encoding`, `x-http-method-override`, `x-http-method` or + `x-method-override`. Necessity: each changes what the Identity Platform + does with the request in a way no revealed byte shows. `cookie` is the case + that matters: another credential a platform might honour over the + committed bearer, and that bearer is the one thing the cross-bind to the + token exchange fixes. The underscore folds because a CGI-style stack reads + `content_encoding` as `content-encoding`. `authorization` is not on this + list only because REQ-COMMON-39 already holds it to one line. - REQ-COMMON-40 (upholds SP-EXCHANGE-01): For that same identity-session request, the Platform Verifier MUST require the raw transcript bytes immediately before the committed range to be diff --git a/specs/platform-ceremonies.md b/specs/platform-ceremonies.md index 46f3ad22..b5e1bf94 100644 --- a/specs/platform-ceremonies.md +++ b/specs/platform-ceremonies.md @@ -522,16 +522,16 @@ REQ-PLAT-56A does not forbid. header. The Platform Verifier MUST reject a head without exactly one `host` naming the pinned authority and exactly one `content-type` whose value is `application/x-www-form-urlencoded`, comparing names lowercased with every - space and tab removed, as common REQ-COMMON-39 normalizes, and values - exactly once the optional whitespace around them is removed. The Platform - Verifier MUST reject a head carrying `authorization`, - `content-encoding`, `transfer-encoding`, `cookie` or - `x-http-method-override` under any spelling of the name. The Platform - Verifier MUST ignore every other header. Necessity: common REQ-COMMON-21B fixes the media type + space and tab removed and `_` read as `-`, as common REQ-COMMON-39B + normalizes, and values exactly once the optional whitespace around them is + removed. The Platform Verifier MUST reject a head carrying `authorization` + or any name common REQ-COMMON-39B forbids, under any spelling of the name. + The Platform Verifier MUST ignore every other header. Necessity: common REQ-COMMON-21B fixes the media type because it "selects the platform's request parser", and a media type nothing compares is a pin in name only. The forbidden headers change what the platform does with the request in a way no revealed byte shows: which client - it authenticates, which bytes it parses, which method it runs. Any other + it authenticates, which session it answers for, which bytes it parses, which + method it runs. Any other header changes only what the platform answers, and a wrong answer is a response the verifier cannot read rather than one it can be fooled by, so requiring its absence would bind every prover to one HTTP library's habits @@ -544,12 +544,13 @@ REQ-PLAT-56A does not forbid. disagree the fields read are not the fields parsed. - REQ-PLAT-56C (upholds SP-EXCHANGE-01): The Platform Verifier MUST reject a head carrying a line feed not preceded by - a carriage return, a line beginning with a space or a tab, or a line with no - colon. Necessity: a parser accepting a bare line feed or a fold ends the head - somewhere this one does not, moving bytes between head and body, and a line - no colon splits is not a header field, so a parser that tolerates one reads a - head this one cannot. Common REQ-COMMON-39 asks the first two of the - identity request. + a carriage return, a carriage return not followed by a line feed, a line + beginning with a space or a tab, or a line with no colon. Necessity: a + parser accepting a bare line feed, a bare carriage return or a fold ends the + head somewhere this one does not, moving bytes between head and body, and a + line no colon splits is not a header field, so a parser that tolerates one + reads a head this one cannot. Common REQ-COMMON-39A asks the first three of + the identity request. - REQ-PLAT-56 (upholds SP-EXCHANGE-01): The Platform Verifier MUST reject an X token attestation whose revealed `grant_type` differs from the exact ASCII bytes `authorization_code`. From 449de5515d10c8a02da81bf23d96a92b3f5a0b49 Mon Sep 17 00:00:00 2001 From: xgreenx Date: Thu, 10 Sep 2026 16:36:39 +0100 Subject: [PATCH 12/15] docs(specs): the head boundary is one, and the length has one spelling The verifier requires exactly one empty line in the revealed token request and a declared length with no leading zero; REQ-PLAT-56B said neither. A second empty line is a second place a parser could end the head, and a second spelling of the count is a second thing to compare one spelling of. Assisted-by: Claude Opus 5 Signed-off-by: xgreenx --- specs/platform-ceremonies.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/specs/platform-ceremonies.md b/specs/platform-ceremonies.md index b5e1bf94..de1717a7 100644 --- a/specs/platform-ceremonies.md +++ b/specs/platform-ceremonies.md @@ -537,11 +537,15 @@ REQ-PLAT-56A does not forbid. requiring its absence would bind every prover to one HTTP library's habits for nothing. - REQ-PLAT-56B (upholds SP-EXCHANGE-01): - The Platform Verifier MUST reject a head without exactly one - `content-length`, or with one other than the decimal count of the body it - frames. Necessity: the verifier takes the body to be what follows the head - while the platform takes it to be `content-length` bytes, so where the two - disagree the fields read are not the fields parsed. + The Platform Verifier MUST reject a token request whose revealed bytes carry + other than exactly one empty line, the one that ends the head. The Platform + Verifier MUST reject a head without exactly one `content-length`, or with + one other than the decimal count of the body it frames, written without a + leading zero. Necessity: the verifier takes the body to be what follows the + head while the platform takes it to be `content-length` bytes, so where the + two disagree the fields read are not the fields parsed; a second empty line + is a second place a parser could end the head, and a second spelling of the + count is a second thing to compare one spelling of. - REQ-PLAT-56C (upholds SP-EXCHANGE-01): The Platform Verifier MUST reject a head carrying a line feed not preceded by a carriage return, a carriage return not followed by a line feed, a line From 860075a4bf288dc7fee20866ed3536dc260f4574 Mon Sep 17 00:00:00 2001 From: xgreenx Date: Fri, 11 Sep 2026 01:47:11 +0100 Subject: [PATCH 13/15] docs(specs): the reader removes JSON whitespace before it matches GitHub pretty-prints `/user` for the media type the profile pins, so the compact delimiters this specification spells matched nothing it serves. REQ-COMMON-19F fixes what libid-org/libid-contracts#37 does: the Platform Verifier removes the JSON whitespace touching a structural byte and matches, counts and reads over what is left; the Implementation reveals a member as the wire carried it, whitespace inside, and never commits that whitespace with a bearer; the compact spellings name the member after removal. TEST-COMMON-10A lists the vectors. REQ-PLAT-51 judges GitHub's terminator after that removal and REQ-PLAT-60 keeps the whitespace in the reveal. Carries the vectors of libid-org/libid#32 in the form the merged verifier implements. Co-authored-by: Wondertan Co-Authored-By: Claude Fable 5.1 Signed-off-by: xgreenx --- specs/ceremony-common.md | 29 +++++++++++++++++++++++++++++ specs/platform-ceremonies.md | 9 ++++++--- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/specs/ceremony-common.md b/specs/ceremony-common.md index 1b2e5e98..458260d9 100644 --- a/specs/ceremony-common.md +++ b/specs/ceremony-common.md @@ -1060,6 +1060,26 @@ and no `authorization` needle to count. more than one position. Necessity: an authenticated response value the account holder influences, such as a display name, can embed a lookalike field. +- REQ-COMMON-19F (upholds SP-BIND-01, SP-EXCHANGE-01): + The Platform Verifier reading a JSON field from revealed attestation bytes + MUST first remove every JSON whitespace byte (`0x20`, `0x09`, `0x0a`, + `0x0d`) that touches a structural byte (`:`, `,`, `{`, `}`, `[`, `]`) on + either side, and no other byte. The Platform Verifier MUST match the + field's delimiter, count its positions under REQ-COMMON-19A, read its + value, and judge its terminator over the bytes that removal leaves. The + Implementation MUST reveal a member as the transcript carries it, its JSON + whitespace inside the revealed range at its offsets. The Implementation + MUST NOT commit that whitespace with a bearer. Every compact delimiter this + specification spells, such as `"login":"` or `"access_token":"`, names the + member that removal leaves, not the bytes a platform must serve. The + Proving Circuit is outside this rule: REQ-COMMON-19 and REQ-COMMON-19D fix + what it asserts at the offset the prover supplies. Necessity: a platform + may pretty-print the response it serves for the media type a profile pins, + and GitHub does for `/user`. Removing whitespace only where it touches a + structural byte leaves every reader one exact template and makes a member + in any spelling the same member, so a second copy spelled with spaces is + still the duplicate REQ-COMMON-19A rejects, while `123 456` still does not + read as `123456`. - REQ-COMMON-20 (upholds SP-EXCHANGE-01): The Proving Circuit MUST constrain every variable value it opens or extracts to the charset the profile states, including values that are never @@ -1290,6 +1310,15 @@ the constructions that role implements. format, or required security properties is invalid. A destination chain cannot support it without selecting a compatible Notary Service. A profile whose Attestation Count is zero remains valid without either. +- TEST-COMMON-10A (exercises REQ-COMMON-19F, REQ-COMMON-19A): + A revealed member spelled with each JSON whitespace byte, alone and as a + run, between its name and its colon, between its colon and its value, and + between its integer and its terminator, reads as the compact member, and + its bytes are revealed at their transcript offsets; a second copy of the + field spelled with whitespace is rejected as a duplicate; a byte JSON does + not call whitespace, such as `0x0b`, in any of those positions is rejected; + an integer with whitespace between its digits is rejected; and a member + whose whitespace an HTTP chunk boundary splits is not built as a layout. - TEST-COMMON-11 (exercises REQ-COMMON-21, REQ-COMMON-21A, REQ-COMMON-21B, REQ-COMMON-21C): The Platform Verifier rejects an authenticated foreign authority, method, or path. The request constructor refuses a media type or `redirect_uri` diff --git a/specs/platform-ceremonies.md b/specs/platform-ceremonies.md index de1717a7..3d431ead 100644 --- a/specs/platform-ceremonies.md +++ b/specs/platform-ceremonies.md @@ -1017,8 +1017,10 @@ REQ-COMMON-18A requires. The Implementation MUST reveal the full `"id":` delimiter, its integer token, and the structural byte after it, together with the full `"login":"` delimiter, its value, and its closing quote, in the `/user` - response. The Implementation MUST redact every other response byte behind - a range commitment. Necessity: REQ-PLAT-51 reads both fields out of + response. The Implementation MUST keep the JSON whitespace GitHub puts + inside either member in the revealed range, per common REQ-COMMON-19F. The + Implementation MUST redact every other response byte behind a range + commitment. Necessity: REQ-PLAT-51 reads both fields out of revealed response bytes, and a session revealing no response range at all leaves it nothing to read. - REQ-PLAT-51 (upholds SP-BIND-01): @@ -1028,7 +1030,8 @@ REQ-COMMON-18A requires. REQ-COMMON-19A. The Platform Verifier MUST reject a noncanonical `id` encoding. The GitHub profile fixes the structural byte following the `id` integer token, which common REQ-COMMON-19D leaves to the profile, as - `,` or `}` and no other byte. The Platform Verifier MUST reject any other + `,` or `}` and no other byte, judged after the removal common + REQ-COMMON-19F fixes. The Platform Verifier MUST reject any other following byte. Necessity: the terminator is what proves the revealed digits are the whole number rather than a prefix of a longer one, and JSON member order does not guarantee which of the two closes it. From 5afdf08ec4e9442de70540b5fb0f6febf49e79e4 Mon Sep 17 00:00:00 2001 From: xgreenx Date: Mon, 14 Sep 2026 10:18:31 +0100 Subject: [PATCH 14/15] docs(specs): the reader's range boundary, maximal runs, and tests for every header rule Answers Wondertan's review of 2026-09-11. REQ-COMMON-19F now says what the verifier does at a range boundary: a member is read inside one revealed range, the delimiter is counted over the concatenation of the direction's revealed ranges after the same removal, and no value is ever read from that concatenation. Removal is stated per maximal run of JSON whitespace bounded by a structural byte, which is what `normalizeJsonBytes` removes; "every byte that touches" read as the ends of a run. TEST-COMMON-10A gains the three range negatives: a field assembled from two ranges, a delimiter a boundary cuts in two, and prefix whitespace pushed into the committed range. REQ-COMMON-39A/39B and REQ-PLAT-56A/56B/56C had no test entry, five E10 findings: TEST-COMMON-18 carries the identity-request vectors, TEST-PLAT-09C the token-head vectors, and TEST-PLAT-14 runs them on the exchange with the count held to the revealed prefix plus the committed secret. The lint's E10 count goes from five to none; the rest is main's. Prose that still described the first draft: REQ-PLAT-56A ignores every other header except the `content-length` REQ-PLAT-56B holds; the X section's `Host` is revealed and held to the authority, not hidden; the GitHub rationale no longer says "fixed list"; both identity-request sections say the verifier also refuses the names REQ-COMMON-39B forbids. Co-Authored-By: Claude Fable 5.1 Signed-off-by: xgreenx --- specs/ceremony-common.md | 53 ++++++++++++++++++++++++++---------- specs/platform-ceremonies.md | 51 +++++++++++++++++++++++----------- 2 files changed, 74 insertions(+), 30 deletions(-) diff --git a/specs/ceremony-common.md b/specs/ceremony-common.md index 458260d9..29cd4cef 100644 --- a/specs/ceremony-common.md +++ b/specs/ceremony-common.md @@ -1062,11 +1062,16 @@ and no `authorization` needle to count. field. - REQ-COMMON-19F (upholds SP-BIND-01, SP-EXCHANGE-01): The Platform Verifier reading a JSON field from revealed attestation bytes - MUST first remove every JSON whitespace byte (`0x20`, `0x09`, `0x0a`, - `0x0d`) that touches a structural byte (`:`, `,`, `{`, `}`, `[`, `]`) on - either side, and no other byte. The Platform Verifier MUST match the - field's delimiter, count its positions under REQ-COMMON-19A, read its - value, and judge its terminator over the bytes that removal leaves. The + MUST first remove each maximal run of JSON whitespace bytes (`0x20`, + `0x09`, `0x0a`, `0x0d`) whose immediately preceding or immediately + following byte is a structural byte (`:`, `,`, `{`, `}`, `[`, `]`), and no + other byte. The Platform Verifier MUST match the field's delimiter, read + its value, and judge its terminator inside one revealed range, over the + bytes that removal leaves of that range. The Platform Verifier MUST count + the delimiter's positions under REQ-COMMON-19A over the concatenation of + every revealed range of that direction, in transcript order, after the same + removal, so that a delimiter a range boundary splits is still counted. The + Platform Verifier MUST NOT read a value from that concatenation. The Implementation MUST reveal a member as the transcript carries it, its JSON whitespace inside the revealed range at its offsets. The Implementation MUST NOT commit that whitespace with a bearer. Every compact delimiter this @@ -1075,11 +1080,16 @@ and no `authorization` needle to count. Proving Circuit is outside this rule: REQ-COMMON-19 and REQ-COMMON-19D fix what it asserts at the offset the prover supplies. Necessity: a platform may pretty-print the response it serves for the media type a profile pins, - and GitHub does for `/user`. Removing whitespace only where it touches a - structural byte leaves every reader one exact template and makes a member - in any spelling the same member, so a second copy spelled with spaces is - still the duplicate REQ-COMMON-19A rejects, while `123 456` still does not - read as `123456`. + and GitHub does for `/user`. Removing a run only where a structural byte + bounds it leaves every reader one exact template and makes a member in any + spelling the same member, so a second copy spelled with spaces is still the + duplicate REQ-COMMON-19A rejects, while `123 456` still does not read as + `123456`. Reading and counting want opposite things: a read that crossed a + range boundary would let a prover assemble, from fragments the notary + signed at unrelated offsets, a document that never crossed the wire, and a + count that stopped at one range would miss a second delimiter the prover + cut a boundary through. The concatenation can only over-count, which fails + closed. - REQ-COMMON-20 (upholds SP-EXCHANGE-01): The Proving Circuit MUST constrain every variable value it opens or extracts to the charset the profile states, including values that are never @@ -1317,8 +1327,14 @@ the constructions that role implements. its bytes are revealed at their transcript offsets; a second copy of the field spelled with whitespace is rejected as a duplicate; a byte JSON does not call whitespace, such as `0x0b`, in any of those positions is rejected; - an integer with whitespace between its digits is rejected; and a member - whose whitespace an HTTP chunk boundary splits is not built as a layout. + an integer with whitespace between its digits is rejected; a member whose + whitespace an HTTP chunk boundary splits is not built as a layout; a field + assembled from two revealed ranges, the member's opening in one and its + value's tail in another, is rejected as absent, however the ranges are + ordered; a second copy of the delimiter cut in two by a range boundary is + rejected as a duplicate; and a bearer prefix whose whitespace is pushed + into the committed range, so that the revealed prefix ends before the + value's opening quote, is rejected. - TEST-COMMON-11 (exercises REQ-COMMON-21, REQ-COMMON-21A, REQ-COMMON-21B, REQ-COMMON-21C): The Platform Verifier rejects an authenticated foreign authority, method, or path. The request constructor refuses a media type or `redirect_uri` @@ -1369,7 +1385,7 @@ the constructions that role implements. submitters; the current fee is readable before the Submission is submitted; and a verification whose native value differs from the current fee is rejected. -- TEST-COMMON-18 (exercises REQ-COMMON-35, REQ-COMMON-36, REQ-COMMON-39, REQ-COMMON-40, REQ-COMMON-43): +- TEST-COMMON-18 (exercises REQ-COMMON-35, REQ-COMMON-36, REQ-COMMON-39, REQ-COMMON-39A, REQ-COMMON-39B, REQ-COMMON-40, REQ-COMMON-43): An identity attestation whose ranges do not sum to the signed request transcript length, or whose ranges leave a gap or an overlap, is rejected; an attestation carrying no signed total transcript length for @@ -1380,7 +1396,16 @@ the constructions that role implements. occurrence inside another header's value is not counted, because the needle is line-anchored; and an attestation whose committed range is not immediately preceded by `\r\nauthorization: Bearer ` and immediately - followed by `\r\n` in the raw bytes is rejected. Every case above runs on + followed by `\r\n` in the raw bytes is rejected. A second `authorization` + header under another scheme, such as `Basic`, is rejected for a duplicate + needle occurrence; a request carrying a header the profile does not list, + such as `accept-encoding`, passes; a header whose name normalizes to + `cookie`, `content-encoding`, `transfer-encoding`, + `x-http-method-override`, `x-http-method` or `x-method-override` -- in + another letter case, with `_` for `-`, or padded before the colon -- is + rejected; and revealed request bytes carrying a bare line feed, a bare + carriage return, or a line beginning with a space or a horizontal tab are + rejected. Every case above runs on an identity-session attestation. A GitHub token-exchange attestation, whose only committed credential is the `client_secret` in its form body, passes verification with no coverage, needle, or framing check applied to that diff --git a/specs/platform-ceremonies.md b/specs/platform-ceremonies.md index 3d431ead..a981808c 100644 --- a/specs/platform-ceremonies.md +++ b/specs/platform-ceremonies.md @@ -473,10 +473,10 @@ Neither the authority nor the attestation timestamp is a transcript range. The authority reaches the Platform Verifier as the TLS server identity the Notary Service authenticated under common REQ-COMMON-21, carried in the attested data: the transcript -holds the authority only in a `Host` header this table hides, and a revealed -`Host` header is prover-composed text that says nothing about which server -answered. The timestamp is the signed creation time of the attested data -itself, which is why common REQ-COMMON-25 can forbid inferring it from a +holds the authority only in a `host` header, which the first row reveals and +REQ-PLAT-56A holds to the pinned authority, and that header is +prover-composed text that says nothing about which server answered. The +timestamp is the signed creation time of the attested data itself, which is why common REQ-COMMON-25 can forbid inferring it from a response header. The two delimiter reveals are what anchor the committed range in the received direction, which would otherwise reveal no byte at all and leave that range indistinguishable from a `refresh_token` value. @@ -526,7 +526,8 @@ REQ-PLAT-56A does not forbid. normalizes, and values exactly once the optional whitespace around them is removed. The Platform Verifier MUST reject a head carrying `authorization` or any name common REQ-COMMON-39B forbids, under any spelling of the name. - The Platform Verifier MUST ignore every other header. Necessity: common REQ-COMMON-21B fixes the media type + The Platform Verifier MUST ignore every other header, `content-length` + excepted, which REQ-PLAT-56B holds to the body it frames. Necessity: common REQ-COMMON-21B fixes the media type because it "selects the platform's request parser", and a media type nothing compares is a pin in name only. The forbidden headers change what the platform does with the request in a way no revealed byte shows: which client @@ -589,7 +590,8 @@ REQ-PLAT-56A does not forbid. four headers, in any order, and may carry others: `authorization: Bearer `, `accept: application/json`, `host: api.x.com`, and `connection: close`. The Platform Verifier compares the request line and the -`authorization` line and no other header. +`authorization` line, refuses the names common REQ-COMMON-39B forbids, and +compares no other header. Per common §9, the identity session reveals exactly these request ranges; the bearer value is the only committed request range, and every other @@ -885,8 +887,8 @@ as the TLS server identity the Notary Service authenticated under common REQ-COMMON-21, carried in the attested data, because the transcript holds the authority only in a `Host` header, and that header is prover-composed text that says nothing about which server answered. Revealing -it, as REQ-PLAT-56A now requires, does not make it the authority: it is -compared against the profile's fixed list like every other header, while the +it, as REQ-PLAT-56A now requires, does not make it the authority: it is one +of the two headers the verifier holds to a pinned value, while the authority continues to reach the verifier as the authenticated TLS server identity. The timestamp is the signed creation time of the attested data itself, which is why common REQ-COMMON-25 can forbid inferring it from a @@ -981,8 +983,9 @@ six headers, in any order, and may carry others: `x-github-api-version: 2022-11-28`, `host: api.github.com`, `connection: close`, and a `user-agent` of the Canonical Runtime's choosing, which GitHub requires of every API request and answers `403` without. The -Platform Verifier compares the request line and the `authorization` line and -no other header. +Platform Verifier compares the request line and the `authorization` line, +refuses the names common REQ-COMMON-39B forbids, and compares no other +header. Per common §9, the identity session reveals exactly these request ranges; the bearer value is the only committed request range, and every other @@ -1151,7 +1154,7 @@ Platform Verifier, Notary Service, Consumer. - TEST-PLAT-09B (exercises REQ-PLAT-30A, REQ-PLAT-32A): An X transcript that reveals plaintext `access_token` bytes in either session, or omits the bearer hash commitment, is rejected. -- TEST-PLAT-09C (exercises REQ-PLAT-29C, REQ-PLAT-56): +- TEST-PLAT-09C (exercises REQ-PLAT-29C, REQ-PLAT-56, REQ-PLAT-56A, REQ-PLAT-56B, REQ-PLAT-56C): The Platform Verifier rejects an X attestation that hides the `grant_type` or `redirect_uri` range, and the Canonical Runtime rejects a revealed value differing from the canonical form serialization of its deployment profile. @@ -1159,7 +1162,18 @@ Platform Verifier, Notary Service, Consumer. as literal unencoded bytes. The Platform Verifier rejects an attestation whose revealed `grant_type` is `refresh_token`, and one whose `grant_type` differs from `authorization_code` in any byte, even when every - other revealed range and the proof itself check out. + other revealed range and the proof itself check out. The Platform Verifier + accepts a token request whose headers arrive in another order, or carry a + header the profile does not name, and one whose required names are spelled + in another letter case, with `_` for `-`, or padded before the colon; it + rejects a head missing `host` or `content-type`, carrying either twice, or + carrying `authorization`, `cookie`, `content-encoding`, `transfer-encoding` + or a method-override name under any spelling; it rejects a head with no + empty line or a second one, with no `content-length` or two, or with a + count that is not the body's length, is not decimal digits, or carries a + leading zero, and accepts the count wherever it sits in the head; and it + rejects a head carrying a bare line feed, a bare carriage return, an + obsolete line fold, or a line with no colon. - TEST-PLAT-10 (exercises REQ-PLAT-30, REQ-PLAT-31, REQ-PLAT-32, REQ-PLAT-36, REQ-PLAT-51, REQ-PLAT-52): An opened bearer range that is empty, over 4096 bytes, or outside printable ASCII fails to prove; a revealed identity response missing `id` or the @@ -1183,13 +1197,18 @@ Platform Verifier, Notary Service, Consumer. verifier. A successful result returns the bearer, opening, and attestation from one notarized session; substitution, a mixed-session tuple, and a partial result on failure are rejected. -- TEST-PLAT-14 (exercises REQ-PLAT-41, REQ-PLAT-42, REQ-PLAT-43, REQ-PLAT-43B, REQ-PLAT-43D, REQ-PLAT-43E): +- TEST-PLAT-14 (exercises REQ-PLAT-41, REQ-PLAT-42, REQ-PLAT-43, REQ-PLAT-43B, REQ-PLAT-43D, REQ-PLAT-43E, REQ-PLAT-56A, REQ-PLAT-56B, REQ-PLAT-56C): A request selecting an endpoint, client, or return URL is rejected; no state survives the call; a caller outside the authenticated isolated-prover boundary is refused; a redirected token exchange is rejected; an attestation - revealing a range outside the seven marked rows, or revealing the bearer - range instead of committing it, is rejected; and no proof exposes the bearer - or a value from which it can be recovered. + revealing a range outside the rows marked `yes`, revealing the bearer + range instead of committing it, or hiding the request line, is rejected; + and no proof exposes the bearer or a value from which it can be recovered. + The head vectors of TEST-PLAT-09C run on the exchange request too: an + unlisted header passes, a forbidden or a missing required name is rejected, + and the `content-length` count is held to the revealed body prefix and the + committed `client_secret` together, so a count that stops at the revealed + bytes is rejected. - TEST-PLAT-15 (exercises REQ-PLAT-44, REQ-PLAT-45, REQ-PLAT-47, REQ-PLAT-48, REQ-PLAT-48A, REQ-PLAT-49, REQ-PLAT-50): A token-exchange attestation with a bad notary signature, a foreign endpoint, a foreign client, a foreign `code_verifier`, a foreign serialized From 607322addafbbd2ef680338f74f43541cba68adb Mon Sep 17 00:00:00 2001 From: Wondertan Date: Fri, 18 Sep 2026 18:23:23 +0200 Subject: [PATCH 15/15] docs(specs): limit colon checks to header field lines Exclude the separately checked request line and terminating empty line, and cover a valid colon-free request line in conformance. Assisted-by: GPT-5 Signed-off-by: Wondertan --- specs/platform-ceremonies.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/specs/platform-ceremonies.md b/specs/platform-ceremonies.md index a981808c..f853b0c6 100644 --- a/specs/platform-ceremonies.md +++ b/specs/platform-ceremonies.md @@ -550,7 +550,9 @@ REQ-PLAT-56A does not forbid. - REQ-PLAT-56C (upholds SP-EXCHANGE-01): The Platform Verifier MUST reject a head carrying a line feed not preceded by a carriage return, a carriage return not followed by a line feed, a line - beginning with a space or a tab, or a line with no colon. Necessity: a + beginning with a space or a tab, or a header field line with no colon. + The colon requirement excludes the separately validated request line and the + empty line terminating the head. Necessity: a parser accepting a bare line feed, a bare carriage return or a fold ends the head somewhere this one does not, moving bytes between head and body, and a line no colon splits is not a header field, so a parser that tolerates one @@ -1173,7 +1175,8 @@ Platform Verifier, Notary Service, Consumer. count that is not the body's length, is not decimal digits, or carries a leading zero, and accepts the count wherever it sits in the head; and it rejects a head carrying a bare line feed, a bare carriage return, an - obsolete line fold, or a line with no colon. + obsolete line fold, or a header field line with no colon. An otherwise valid + request with a colon-free request line and its terminating empty line passes. - TEST-PLAT-10 (exercises REQ-PLAT-30, REQ-PLAT-31, REQ-PLAT-32, REQ-PLAT-36, REQ-PLAT-51, REQ-PLAT-52): An opened bearer range that is empty, over 4096 bytes, or outside printable ASCII fails to prove; a revealed identity response missing `id` or the