From 3583f5c52d44ea9581c10917b1b1507dbe625fb1 Mon Sep 17 00:00:00 2001 From: xgreenx Date: Wed, 9 Sep 2026 14:10:43 +0100 Subject: [PATCH 01/20] 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 6f9c3a5c2dc82eaa4282913ece2d9e771d7f720d Mon Sep 17 00:00:00 2001 From: xgreenx Date: Wed, 9 Sep 2026 14:28:41 +0100 Subject: [PATCH 02/20] 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 31680fae0fcbe6d79b4f3bc02bd0b0c6689cc9b2 Mon Sep 17 00:00:00 2001 From: xgreenx Date: Wed, 9 Sep 2026 15:50:11 +0100 Subject: [PATCH 03/20] 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 27aaf904dae0f394038f1673578b2a92fa95edc3 Mon Sep 17 00:00:00 2001 From: xgreenx Date: Wed, 9 Sep 2026 18:12:25 +0100 Subject: [PATCH 04/20] 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 ba4b280c28dd9d5ad9fdd9a23a7a2b1ca0d0f3f4 Mon Sep 17 00:00:00 2001 From: xgreenx Date: Wed, 9 Sep 2026 19:04:07 +0100 Subject: [PATCH 05/20] 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 d58930a470db639d7ad57d9944a4aeabb0176f44 Mon Sep 17 00:00:00 2001 From: xgreenx Date: Thu, 10 Sep 2026 10:04:10 +0100 Subject: [PATCH 06/20] 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 3acbf4a138953f758d7e3811ebabd6035420cbd1 Mon Sep 17 00:00:00 2001 From: xgreenx Date: Thu, 10 Sep 2026 10:07:17 +0100 Subject: [PATCH 07/20] 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 a76d547aa81445b753d04af096c8aa9905ccb5e7 Mon Sep 17 00:00:00 2001 From: xgreenx Date: Thu, 10 Sep 2026 14:01:20 +0100 Subject: [PATCH 08/20] 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 e74a9c75179b45dbe2cbfc15adba6f47982120af Mon Sep 17 00:00:00 2001 From: xgreenx Date: Thu, 10 Sep 2026 14:01:46 +0100 Subject: [PATCH 09/20] 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 070f0ea9cc892e509d028ea74d660ee95fd84330 Mon Sep 17 00:00:00 2001 From: xgreenx Date: Thu, 10 Sep 2026 14:40:24 +0100 Subject: [PATCH 10/20] 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 41cbe59ef855b23639e93b29fb3d0ffe7892a474 Mon Sep 17 00:00:00 2001 From: xgreenx Date: Thu, 10 Sep 2026 15:19:16 +0100 Subject: [PATCH 11/20] 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 59c66fe8c9d313fb7e79b8d0c4382f8f37fd53d9 Mon Sep 17 00:00:00 2001 From: xgreenx Date: Thu, 10 Sep 2026 16:36:39 +0100 Subject: [PATCH 12/20] 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 d8a66b72faaccd66275725ddc376ffd6aac95e80 Mon Sep 17 00:00:00 2001 From: xgreenx Date: Fri, 11 Sep 2026 01:47:11 +0100 Subject: [PATCH 13/20] 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 eeed0bca72ad4fd516c68bee4c7f6d90d9fd90a8 Mon Sep 17 00:00:00 2001 From: Wondertan Date: Sun, 13 Sep 2026 01:36:59 +0200 Subject: [PATCH 14/20] docs(specs): reconcile browser validation ownership Preserve downstream verification while assigning local parsing and correlation to Prover, keeping original requirement IDs and the framing prerequisite. Assisted-by: GPT-5 Signed-off-by: Wondertan --- specs/ceremony-common.md | 36 ++++-- specs/platform-ceremonies.md | 230 ++++++++++++++++++++++++----------- 2 files changed, 187 insertions(+), 79 deletions(-) diff --git a/specs/ceremony-common.md b/specs/ceremony-common.md index 458260d9..1215348b 100644 --- a/specs/ceremony-common.md +++ b/specs/ceremony-common.md @@ -229,6 +229,14 @@ identity-platform signing root, notary key, Platform Verifier, verifier governan browser supply chain, or Consumer Chain invalidates the properties that depend on it. +Browser result acceptance is not ledger verification. Prover performs canonical +parsing and local request/commitment consistency checks; Application validates +the delivered structure. Neither performs local notary-signature verification +or a separate Google nonce-versus-expected-digest comparison. Well-formed +mismatches or forgeries can survive those browser checks but still fail the +applicable downstream proof, digest-binding, trusted signing-key, or +notary-signature check before an authoritative effect. + - SP-BIND-01: Evidence produced by a ceremony discharges only for the Authorized Transaction Data committed in its Authorization Digest. Depends on @@ -237,15 +245,18 @@ on it. conformance tests (supporting, not proving) plus the collision resistance of SHA-256 and keccak256. - SP-CLIENT-01: - The Canonical Runtime rejects evidence issued to an OAuth client other than - the one fixed by its immutable ceremony profile. Depends on ASM-PROV-04, + The browser Prover rejects a parsed OAuth client identifier differing from + the one fixed by its immutable ceremony profile. This checks local consistency, + not the authenticity of an attestation's claimed identifier; ledger + verification authenticates that identifier independently. Depends on ASM-PROV-04, ASM-PROV-05, ASM-PROV-07, ASM-NOTARY-01, ASM-PROOF-01, and ASM-BROWSER-01. Evidence: checked invariant in the Canonical Runtime, plus conformance tests (supporting). - SP-DELIVERY-01: - An authorization response for one OAuth client reaches only an origin - registered to that client, so a site borrowing another deployment's client - cannot receive its evidence. Depends on ASM-PROV-01, ASM-BROWSER-01. + The Identity Platform delivers an OAuth client's initial authorization + response only to that client's registered redirect origin. Subsequent browser + release follows REQ-COMMON-30, so borrowing another deployment's client does + not authorize receipt of its response. Depends on ASM-PROV-01, ASM-BROWSER-01. Evidence: external audit of the registered redirect URI list, plus conformance tests (supporting). - SP-EXCHANGE-01: @@ -671,9 +682,11 @@ attestation to verify carries no value at all. The Deployment MUST register with each Identity Platform only redirect URIs whose origins it controls. - REQ-COMMON-30 (upholds SP-DELIVERY-01): - The Canonical Runtime MUST forward an authorization response only over a live - browser channel authenticated to an exact origin in the deployment-configured - allowed application-origin set. The set MAY contain more than one origin. + The Canonical Runtime MUST release an authorization response beyond Callback + only after authenticating the exact Application origin against the deployment + allowlist. The Canonical Runtime MUST carry the response only to the configured + Prover, preserving that authenticated origin restriction as defined by CCDP + (REQ-CCDP-03, REQ-CCDP-04). The set MAY contain more than one origin. - REQ-COMMON-31 (upholds SP-DELIVERY-01): The Canonical Runtime MUST ignore a forwarding target supplied in the redirect request. @@ -1337,7 +1350,9 @@ the constructions that role implements. - TEST-COMMON-14 (exercises REQ-COMMON-30, REQ-COMMON-31): Each of two configured application origins can complete its own authenticated live channel; an unlisted origin is rejected, and a redirect request carrying - a forwarding target cannot change either result. + a forwarding target cannot change either result. Callback privately carries + the return only to the configured Prover; that Prover authenticates the same + Application origin before credential use. - TEST-COMMON-15 (exercises REQ-COMMON-29): Every redirect URI registered against each production client resolves to an origin the deployment controls. Verification: audit of the platform client @@ -1363,7 +1378,8 @@ the constructions that role implements. moves no value; and a call whose native value differs from the quoted value is rejected at every hop. - TEST-COMMON-17 (exercises REQ-COMMON-33, REQ-COMMON-34B, REQ-COMMON-33A, REQ-COMMON-34, REQ-COMMON-34A, REQ-COMMON-34C, REQ-COMMON-34D, REQ-COMMON-34E): - An attestation carrying a foreign notary signature is rejected; a + At ledger verification, the trusted Notary Service rejects an attestation + carrying a foreign notary signature; a verification whose fee was not delivered is rejected; the charged fee is identical across differing attested content, authors, payers, and submitters; the current fee is readable before the Submission is submitted; and a diff --git a/specs/platform-ceremonies.md b/specs/platform-ceremonies.md index 3d431ead..4a721cda 100644 --- a/specs/platform-ceremonies.md +++ b/specs/platform-ceremonies.md @@ -10,8 +10,15 @@ exchange service, and platform-specific failure behavior. The [common ceremony rules](ceremony-common.md) own the Authorization Digest, serialization, PKCE, transcript extraction, client binding, and evidence time. The Consumer's protocol owns transaction dispatch and authorization. -The browser architecture owns browsing contexts, redirect transport, -interruption behavior, and application handoff. +[CCDP](ccdp.md) owns browsing contexts, redirect transport, and document +handoff. Its Application and browser participants together implement the +Canonical Runtime. Callback captures and clears the return; Prover owns complete +platform-return parsing, canonical evidence decoding, request bindings, and +commitment/opening correlation. Application structurally validates the delivered +identity and platform proof and wraps it with retained authorization fields; +it does not repeat Prover's evidence checks. Ledger verification means the +Proof Verifier, Platform Verifier, and Notary Service checks before Consumer +acceptance, not browser generation or a locally accepted result. Google returns a signed OIDC ID Token directly to the redirect fragment. X and GitHub use the OAuth authorization-code flow and notarized transcripts of @@ -234,16 +241,16 @@ operational guidance for obtaining a token whose signed claims satisfy reaches the local Redirect Runtime without introducing a confidential backend or bearer capability. - REQ-PLAT-12 (upholds SP-DELIVERY-01): - The Redirect Runtime MUST copy the bounded query and fragment into memory and - clear both before storage or network access. The Canonical Runtime MUST require + The Callback MUST copy the bounded query and fragment into memory and + clear both before storage or network access. The Prover MUST require an empty query and a fragment carrying exactly one `state` plus exactly one - `id_token` XOR `error`. The Canonical Runtime MUST reject duplicate, additional + `id_token` XOR `error`. The Prover MUST reject duplicate, additional authoritative, mixed-transport, or malformed fields and MUST ignore diagnostic fields. - REQ-PLAT-13 (upholds SP-DELIVERY-01): - The Canonical Runtime MUST match `state` to exactly one live local ceremony - and consume it once before accepting the ID Token. No server-side state or - prepare request participates in this lookup. + The Prover MUST match `state` to its bound live ceremony and accept that + return only once before using the ID Token. No server-side state, prepare + request, or application-wide ceremony lookup participates in this check. Conformance vector, for the Authorization Digest of [common §5](ceremony-common.md#5-authorization-digest): @@ -253,13 +260,19 @@ authorizationDigest = 0xb318fb559e16a179b853ed2853576cda16032d93b0839bb81a55135d Google nonce = sxj7VZ4WoXm4U-0oU1ds2hYDLZOwg5u4GlUTXTNMCvU ``` -### 3.2 Local token verification +### 3.2 Browser token validation - REQ-PLAT-14 (upholds SP-BIND-01): - The Canonical Runtime MUST reject an ID Token whose `nonce` differs from the - Authorization Digest it constructed. + The Prover MUST parse the token's `nonce` as canonical unpadded base64url + encoding of exactly 32 bytes and use those bytes as the candidate + Authorization Digest public input to the circuit. Neither the Prover nor + the Application performs a separate nonce-versus-expected-digest + comparison. The expected digest remains Application-side and is not an + additional Prover input. REQ-PLAT-16 and REQ-PLAT-18 bind the candidate to + the signed token; common REQ-COMMON-02 and REQ-COMMON-02A bind the proof to + the authorization that the Consumer is asked to accept. - REQ-PLAT-15: - The Canonical Runtime MUST reject a Google response carrying `code` or + The Prover MUST reject a Google response carrying `code` or `access_token`. Necessity: neither artifact belongs to this authentication-only profile. @@ -276,9 +289,11 @@ The Proving Circuit and Consumer enforce all of the following: below from that signed payload, not from a detached copy. The profile fixes RS256; the circuit performs no algorithm dispatch and does -not parse the protected header. A token signed under any other algorithm or -key simply fails the fixed verification relation. Algorithm-confusion attacks -require a verifier that dispatches on the header `alg`; none exists here. +not parse the protected header. A signature that does not satisfy this fixed +relation under the supplied modulus fails in circuit. A valid signature under +an untrusted modulus can satisfy the circuit but fails REQ-PLAT-23 downstream. +Algorithm-confusion attacks require a verifier that dispatches on the header +`alg`; none exists here. - REQ-PLAT-16A (upholds SP-CLIENT-01): The Proving Circuit MUST expose the exact RSA modulus used for REQ-PLAT-16 @@ -309,7 +324,10 @@ require a verifier that dispatches on the header `alg`; none exists here. The Proving Circuit MUST prove the signed `iss` equals `https://accounts.google.com`. - REQ-PLAT-18 (upholds SP-BIND-01): - The Proving Circuit MUST prove `nonce` equals the Authorization Digest. + The Proving Circuit MUST bind the signed `nonce` to the exact canonical + encoding of its 32-byte Authorization Digest public input. The Platform + Verifier MUST verify that proof against the digest recomputed under common + REQ-COMMON-02 and REQ-COMMON-02A, not a browser-supplied candidate digest. - REQ-PLAT-19 (upholds SP-CLIENT-01): The Proving Circuit MUST expose `SHA256` of the signed `aud` as the client-binding public input. @@ -374,19 +392,19 @@ Launch fixes X's `/2/oauth2/token` and `/2/users/me` sessions and GitHub's probe, failure, or retry select Browser MPC or switch transport within a launch ceremony. - REQ-PLAT-28 (upholds SP-DELIVERY-01): - The Canonical Runtime MUST require the X or GitHub authorization redirect to + The Prover MUST require the X or GitHub authorization redirect to carry an empty fragment and a query containing exactly one `state` plus - exactly one `code` XOR `error`. The Canonical Runtime MUST reject duplicate, + exactly one `code` XOR `error`. The Prover MUST reject duplicate, mixed-transport, additional authoritative, and malformed fields. The single accepted `code` is the code consumed at redirect ingress that REQ-PLAT-29 and REQ-PLAT-46 compare - against. + against. GitHub's required `iss` under REQ-PLAT-34A is a profile field, not + an additional authoritative field to reject. - REQ-PLAT-28A (upholds SP-DELIVERY-01): - The Canonical Runtime MUST match the redirect's `state` to exactly one live - local ceremony and consume it once before starting the token request. No - server-side state or prepare request participates in this lookup. - Necessity: the redirect is the only point where the ceremony that requested - the authorization can still be identified. + The Prover MUST match the redirect's `state` to its bound live ceremony and + accept that return only once before starting the token request. No + server-side state, prepare request, or application-wide ceremony lookup + participates in this check. Browser MPC is a deferred protocol alternative. It may remove the notary-to-platform path assumption and notary egress exposure, but it requires @@ -730,6 +748,34 @@ that session. It produces an attestation, not a proof. inherits previously granted scopes for the same OAuth application, so an omitted scope does not yield a known grant. +### 6.1a Authorization return + +GitHub returns the authorization response in the redirect query. In addition +to `state` and exactly one of `code` or `error`, the response carries `iss`, the +authorization-server issuer identifier defined by [RFC9207]. This profile pins +`https://github.com/login/oauth`, matching +[GitHub's authorization-server metadata](https://github.com/.well-known/oauth-authorization-server/login/oauth). +The expected value is a profile constant, not discovered from the response or +the configured redirect URI. + +- REQ-PLAT-34A (upholds SP-DELIVERY-01): + The Prover MUST accept a GitHub success, denial, or other OAuth error response + only when its query contains exactly one `iss` whose value, decoded once as + `application/x-www-form-urlencoded`, equals + `https://github.com/login/oauth` by exact string comparison. Necessity: + matching the response issuer to the selected authorization server prevents + OAuth authorization-server mix-ups before credentials are used. + +Missing, duplicate, malformed, or mismatched issuer values are browser +rejections before token exchange, not valid denials. Equivalent form-encoding +spellings are accepted, but URL normalization, case folding, trailing-slash +removal, and default-port removal do not repair a different decoded value. +This check precedes success/denial/error classification; a matching issuer +does not waive the state or remaining return checks. `iss` identifies the +authorization server, not the user or OAuth client, and is not itself signed +evidence. It adds no proof input or browser-protocol message field and does +not replace downstream verification. + ### 6.2 Token exchange `POST https://github.com/login/oauth/access_token`, media type @@ -925,11 +971,16 @@ which the exact tiling of common REQ-COMMON-35 makes derivable. the bearer and are what ties the circuit to the two verified attestations. - REQ-PLAT-44 (upholds SP-EXCHANGE-01): - The Canonical Runtime MUST verify the returned token-exchange attestation - locally against the GitHub profile's pinned notary key and attestation - format before using the bearer. Necessity: the browser checks what it got - back before spending a `/user` session on it; the Notary Service decision - the Consumer Chain relies on is separate. + The Prover MUST validate the returned token-exchange attestation's exact + structure, canonical encoding, profile authority/method/path, request + bindings, and commitment/opening correlation before using the bearer. + Neither the Prover nor the Application performs local notary-signature + verification; this browser path omits that optional defense-in-depth check + for every token and identity attestation, including X's browser sessions. + The Client checks only the delivered proof's structure. Signature-format + checks do not establish authenticity: downstream verification of the + original attested-data and signature bytes under the trusted Notary Service + remains mandatory under common REQ-COMMON-33 and REQ-COMMON-33A. - REQ-PLAT-45 (upholds SP-EXCHANGE-01): The GitHub Token Service MUST return an attestation carrying the configured notary's signature and revealing the token request's method and @@ -939,28 +990,31 @@ which the exact tiling of common REQ-COMMON-35 makes derivable. Necessity: the authority is never a revealed range, because the transcript carries it only in a prover-composed `Host` header. - REQ-PLAT-46 (upholds SP-EXCHANGE-01): - The Canonical Runtime MUST require the disclosed serialized `code` value to + The Prover MUST require the disclosed serialized `code` value to equal the canonical form serialization of the code it consumed at redirect ingress, byte for byte. - REQ-PLAT-47 (upholds SP-CLIENT-01): - The Canonical Runtime MUST require the disclosed `client_id` to equal its - configured client. Common REQ-COMMON-16B makes those bytes identical before - and after form serialization. + The Prover MUST require the disclosed `client_id` to equal the client + frozen by the Application. Common REQ-COMMON-16B makes those bytes + identical before and after form serialization. - REQ-PLAT-48 (upholds SP-BIND-01): - The Canonical Runtime MUST require the disclosed `code_verifier` to equal the - verifier it derived. Its base64url alphabet is byte-identical under form - serialization. + The Prover MUST require the disclosed `code_verifier` to equal the derived + verifier supplied by the Application; it does not rederive it. Its + base64url alphabet is byte-identical under form serialization. - REQ-PLAT-48A (upholds SP-EXCHANGE-01): - The Canonical Runtime MUST require the disclosed serialized `redirect_uri` - value to equal the canonical form serialization of its immutable - deployment-profile value. + The Prover MUST require the disclosed serialized `redirect_uri` value to + equal the canonical form serialization of the immutable value supplied by + the Application. - REQ-PLAT-49 (upholds SP-EXCHANGE-01): - The Canonical Runtime MUST require the returned bearer, under the returned + The Prover MUST require the returned bearer, under the returned `bearerOpening`, to open the bearer commitment of the token-exchange attestation. - REQ-PLAT-50 (upholds SP-EXCHANGE-01): - The Canonical Runtime MUST discard the response and start no `/user` request - when any check in REQ-PLAT-44 through REQ-PLAT-49 fails. + The Prover MUST discard the response and MUST NOT start `/user` when any + browser check in REQ-PLAT-44 through REQ-PLAT-49 fails. No response or + credential-bearing resume record is persisted. A structurally valid but + cryptographically invalid signature alone is not such an early failure; + downstream verification rejects it. Verifying only arbitrary byte substrings is insufficient: a prover that composes the request could otherwise witness one `code` or `code_verifier` @@ -1100,8 +1154,9 @@ Roles: Canonical Runtime, GitHub Token Service, Proving Circuit, Platform Verifier, Notary Service, Consumer. - TEST-PLAT-01 (exercises REQ-PLAT-10, REQ-PLAT-18): - The §3.1 nonce vector reproduces exactly, and a token carrying another nonce - is rejected. + The §3.1 nonce vector reproduces exactly. A proof bound to another token + nonce is rejected downstream when verified against this authorization's + recomputed digest; no separate browser digest comparison is required. - TEST-PLAT-02 (exercises REQ-PLAT-04, REQ-PLAT-05, REQ-PLAT-06, REQ-PLAT-07, REQ-PLAT-08): The §2.1 identifier vectors reproduce, and each listed malformed identifier is rejected. @@ -1111,22 +1166,30 @@ Platform Verifier, Notary Service, Consumer. fragment carrying duplicate `state`, both `id_token` and `error`, `code`, or `access_token` is rejected. - TEST-PLAT-04 (exercises REQ-PLAT-13, REQ-PLAT-14): - A fragment whose `state` has no unique live local ceremony, and an ID Token - whose `nonce` is not the constructed digest, are rejected. No backend state - lookup occurs. + The browser Prover rejects a return whose `state` does not match its bound + live ceremony, or has already been consumed, and rejects a missing, + malformed, padded, noncanonical, or non-32-byte nonce. No server-side state + lookup occurs. An otherwise valid signed token with a canonical nonce for + another digest is not rejected by an extra browser comparison: its circuit + inputs come from that nonce, and the Application only checks the delivery shape. + The resulting proof fails downstream verification against the requested + authorization's recomputed digest. The browser test adds no expected-digest + field to `ProveIdentity`. - TEST-PLAT-05 (exercises REQ-PLAT-15): A Google response carrying an authorization code or access token is rejected, and the deployment contains no Google exchange route or client secret. Verification: inspection of emitted artifacts. - TEST-PLAT-06 (exercises REQ-COMMON-19D, REQ-PLAT-16, REQ-PLAT-16A, REQ-PLAT-16B, REQ-PLAT-17, REQ-PLAT-19, REQ-PLAT-19A, REQ-PLAT-20, REQ-PLAT-21, REQ-PLAT-23): - A token with a foreign issuer, foreign audience, `email_verified: false`, a + The browser Prover rejects an audience differing from its frozen client. + A token with a foreign issuer, `email_verified: false`, a quoted or non-boolean `email_verified`, a quoted, negative, fractional, - exponent, leading-zero, or overflowing `exp`, or an untrusted signing - modulus is rejected in each case. A token signed - under any other algorithm or key fails the fixed verification relation. - Header, payload, signature, or public-output substitution is rejected. A - cryptographically valid proof under an inactive signing modulus passes - circuit verification but is rejected by the Platform Verifier. An Submission + exponent, leading-zero, or overflowing `exp` cannot satisfy the circuit. + A signature incompatible with the fixed RS256 relation or supplied modulus + likewise fails the circuit. Substitution of signed header/payload bytes or + signature invalidates that relation; public-output substitution fails proof + verification downstream. These are not separate browser proof-verification + requirements. A cryptographically valid proof under an inactive signing modulus passes + circuit verification but is rejected by the Platform Verifier. A submission whose supplied `aud` bytes do not hash to the audience public input is rejected, and an accepted one returns those exact bytes as the client identifier. @@ -1178,6 +1241,15 @@ Platform Verifier, Notary Service, Consumer. accepted one returns those exact bytes as the client identifier; and the configured secret contains neither `&` nor `=`. Verification: inspection of the configured credential for the secret rule. + +- TEST-PLAT-12A (exercises REQ-PLAT-28, REQ-PLAT-28A, REQ-PLAT-34A): + For otherwise valid GitHub success, `access_denied`, and other OAuth error + returns, the browser Prover accepts the exact issuer in literal or equivalent + form-encoded spelling. It rejects missing or duplicate `iss`, malformed + encoding, a foreign issuer, changed case, an added default port, or a trailing + slash before token exchange; an invalid issuer never resolves denied. + Correct `iss` with wrong state remains rejected. X acquires no GitHub issuer + prerequisite, and no proof or browser-protocol record gains an issuer field. - TEST-PLAT-13 (exercises REQ-PLAT-37, REQ-PLAT-38): The service receives the exact redirect authorization code and ceremony verifier. A successful result returns the bearer, opening, and attestation @@ -1190,12 +1262,20 @@ Platform Verifier, Notary Service, Consumer. 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. -- 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 - `redirect_uri`, or a bearer that does not - open the commitment under the returned `bearerOpening` is discarded in each - case, and no `/user` request starts. +- TEST-PLAT-15 (exercises REQ-PLAT-44, REQ-PLAT-45, REQ-PLAT-46, REQ-PLAT-47, REQ-PLAT-48, REQ-PLAT-48A, REQ-PLAT-49, REQ-PLAT-50): + The browser Prover discards a token-exchange attestation with malformed + structure or canonical encoding, incorrect correlation, a foreign endpoint, + a foreign `code`, a foreign client, a foreign `code_verifier`, a foreign + serialized `redirect_uri`, or a bearer that does not open the commitment + under the returned `bearerOpening`; `/user` never starts and no resume record + is written. Malformed signature length/encoding fails structurally. In a + separate case, a forged signature that is structurally valid and preserves + all checked bindings/correlations is not rejected solely by a browser + cryptographic check: neither Prover nor Application performs one. The trusted + Notary Service rejects it during ledger verification and the Consumer + accepts no authoritative effect. Exercise that distinction for both token + and identity attestations on X and GitHub; Application shape acceptance must not + be reported as attestation authenticity. - TEST-PLAT-15A (exercises REQ-PLAT-52, REQ-PLAT-52A, REQ-PLAT-52B): A GitHub proof whose bearer commitment public input differs from the commitment in either submitted attestation is rejected; substituting one @@ -1259,6 +1339,13 @@ Platform Verifier, Notary Service, Consumer. ## 9. Security Considerations +Browser validation does not authenticate attestation signatures or compare a +Google nonce against an independently supplied expected digest. A well-formed +forgery or mismatch can therefore survive browser checks. REQ-PLAT-14, +REQ-PLAT-18, and REQ-PLAT-44 preserve circuit and downstream verification; +TEST-PLAT-04 and TEST-PLAT-15 distinguish those later rejections from early +browser rejection. No ledger verification guarantee is weakened. + This document enforces SP-BIND-01, SP-CLIENT-01, SP-EXCHANGE-01, and SP-FRESH-01 for the launch platforms, under the assumptions of [common §3](ceremony-common.md#3-assumptions). @@ -1306,20 +1393,25 @@ The notary key is a trust root for X and GitHub evidence. Its compromise mints fresh evidence until the key is removed, and does not revoke authority already committed. -Google has no server-side token exchange. Its signed ID Token reaches the -redirect fragment, is cleared before -other work, and is bound to the local ceremony by `state`, signed `nonce`, and -signed `aud`. A deployment backend can withhold the static redirect document -but cannot substitute an ID Token through a server exchange that does not -exist. +Google has no server-side token exchange. Its fragment is not visible at +HTTP ingress, although the deployment controls Callback code. Its signed +ID Token reaches the redirect fragment and is cleared +before other work. The browser checks `state`, the configured audience, and +canonical nonce encoding; the circuit binds the signed nonce and claims, and +ledger verification binds that proof to the recomputed authorization digest +and trusted signing key. A deployment backend can withhold the static redirect +document but cannot substitute an ID Token through a server exchange that +does not exist. Google's JWKS rotation makes the trusted modulus set a liveness dependency -(REQ-PLAT-24): every Google ceremony fails closed while Google signs with an -untrusted modulus. +(REQ-PLAT-24): ledger verification rejects proofs while Google signs with an +untrusted modulus, even if browser proof generation completes. ## 10. References Normative: [RFC6749], [RFC7636], [RFC7515], [RFC7517], [RFC7518], [RFC7519], -[RFC8017], [OIDC], [RFC8446]. +[RFC8017], [OIDC], [RFC8446], [RFC9207]. Informative: [RFC9700], [TLSNotary-Proxy]. + +[RFC9207]: https://www.rfc-editor.org/rfc/rfc9207.html From 1c1a885b6e021c646a1db0bf1683b15c98b4613a Mon Sep 17 00:00:00 2001 From: Wondertan Date: Sun, 13 Sep 2026 01:36:59 +0200 Subject: [PATCH 15/20] docs(specs): extract CCDP and ceremony service contracts Keep the readable routes, messages, events and phases. Delegate transport to the popup specification and move implementation APIs, build tooling and qualification to the implementation PR. Assisted-by: GPT-5 Signed-off-by: Wondertan --- specs/ccdp-distribution.md | 415 ++++++++++++++++++ specs/ccdp.md | 838 +++++++++++++++++++++++++++++++++++++ specs/libid.md | 26 +- specs/oauth-bridge.md | 392 +++++++++++++++++ 4 files changed, 1668 insertions(+), 3 deletions(-) create mode 100644 specs/ccdp-distribution.md create mode 100644 specs/ccdp.md create mode 100644 specs/oauth-bridge.md diff --git a/specs/ccdp-distribution.md b/specs/ccdp-distribution.md new file mode 100644 index 00000000..42b8cb83 --- /dev/null +++ b/specs/ccdp-distribution.md @@ -0,0 +1,415 @@ +# CCDP Distribution + +This document defines the static browser resources and proving assets required +by [CCDP](ccdp.md#documents-and-routes). CCDP owns the protocol routes, +fragments, roles, navigations, and versions; this document owns their HTTP, +response, artifact-compatibility, and publication contract. Build scripts, +source-module APIs, dependency releases, and serving software are implementation +choices, not protocol requirements. + +The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", +"SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and +"OPTIONAL" in this document are to be interpreted as described in +BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all +capitals, as shown here. + +## Distribution boundary + +One CCDP Distribution is served from one canonical `ccdpOrigin` under the +[CCDP origin policy](ccdp.md#origin-policy): HTTPS, or HTTP on exact `localhost` +and `127.0.0.1` hosts. It contains: + +- every protocol resource for each supported CCDP version, including one + self-contained Callback artifact containing its supported implementations; and +- their bundled JavaScript, workers, WASM, circuits, and libID-owned assets. + +The resource graph distinguishes distributed assets from external assets. +Browsers prefetch and fetch external resources at their declared absolute +URLs; the static build does not download or mirror them. External asset availability and readable CORS remain release-qualified +dependencies rather than guarantees supplied by this host. + +The OAuth Bridge separately serves ceremony configuration, the registered +Callback document, and enabled confidential platform endpoints. It retrieves +the public Callback artifact server-side and inserts its deployment data before +serving it; this does not change the document's OAuth Bridge origin. Requests +to the OAuth Platform, OAuth Bridge, Notary Service, and public platform APIs are +protocol traffic rather than CCDP assets. + +The Distribution may be the canonical libID release or an operator-selected +replacement. Replacing it changes the code-supply-chain authority for Callback +and proof generation. + +One Distribution may serve any number of independently operated OAuth Bridges. +It does not enumerate or register them: each Bridge selects a `ccdpOrigin`, +which serves the same public resources to all of them. A Bridge advertises only +platform/version pairs present in its selected Distribution; no shared +deployment system is required. + +## HTTP contract + +The Distribution is static and request-invariant. It sets no cookies, serves no +unrelated same-origin application API, and performs no request-time compilation, +templating, source resolution, archive extraction, or remote asset fetch. + +### Protocol resources + +- REQ-DIST-01 (upholds SP-CCDP-01): The Distribution MUST serve the resources + with the request invariance, executable-source restrictions, and response + policies below. + +The Distribution exposes the exact versioned +[resources](ccdp.md#documents-and-routes) defined by CCDP. Their fragments, +roles, and execution contexts remain CCDP rules. + +Prefetch and Prover contain their clearing bootstrap and entry code directly, +with no browser-visible manifest or second entry-script request. They may load implementation-private immutable chunks. + +The aggregate [Callback artifact](#callback-artifact) is retrieved server-side +by OAuth Bridges; the contract below defines its configuration slot, embedded +startup, and the response they serve. + +Each supported path has one decoded representation and response policy. +`Accept-Encoding` may select only a Brotli or gzip transfer representation defined +below. Conditional caching may return `304 Not Modified`; otherwise query +values, request headers, `Origin`, `Referer`, cookies, and user agent cannot +select different bytes, policy, embedded configuration, or implementation. A +nonempty query may receive the same static resource, but its clearing bootstrap +rejects before protocol execution. Only `GET` and `HEAD` are defined. Unknown +paths and versions return an inert failure without fallback or redirect; other +methods execute no CCDP code. + +The not-found response is static HTML containing no script, style, link, form, +redirect, or protocol data. + +Versioned protocol resources and the aggregate Callback artifact use +`Cache-Control: no-cache` and an ETag so a path may receive compatible +implementation updates. A breaking protocol change publishes new versioned +routes and adds its implementation to the Callback artifact. The Bridge serves +its configured Callback response with `no-store`, independently of its own +upstream artifact cache. + +All protocol resources send their exact media type and +`X-Content-Type-Options: nosniff`. Top-level documents additionally send +`Referrer-Policy: no-referrer` and are not frameable. Document CSP begins with +`default-src 'none'`, `object-src 'none'`, `base-uri 'none'`, +`form-action 'none'`, and `frame-ancestors 'none'`; admits only the exact +build-generated entry code, resources, and network sources needed by that +document; and uses neither JavaScript `'unsafe-inline'` nor `'unsafe-eval'`. +Document-owned inline styles may use `style-src 'unsafe-inline'`; no caller +markup, executable code, or styling input is part of this contract. + +| Resource | Form | Additional response contract | +|---|---|---| +| [Callback artifact](#callback-artifact) | self-contained HTML template at `/ccdp/callback.html`, retrieved server-side by OAuth Bridges | `text/html; charset=utf-8`, `no-cache` and ETag, with exact executable hashes in CSP. No browser CORS permission is needed for this retrieval. The configured response follows [Served response](#served-response). | +| Prefetch | top-level non-isolated HTML | `Cross-Origin-Opener-Policy: unsafe-none` and no COEP. Script/worker sources remain same-origin; `connect-src` admits local assets and the pinned external asset origins. | +| Prover | top-level HTML | `Document-Isolation-Policy: isolate-and-require-corp`, `Cross-Origin-Opener-Policy: unsafe-none`, and no COEP. | +| Prover isolation fallback | top-level HTML at `/ccdp/v{CCDPVersion}/prover/fallback` | `Cross-Origin-Opener-Policy: same-origin` and `Cross-Origin-Embedder-Policy: require-corp`. Same Prover entrypoint, fragment contract, and non-isolation response rules. | +| Worker | module Service Worker JavaScript | `text/javascript; charset=utf-8` and `Service-Worker-Allowed: /`. Prefetch registers it with `scope: '/'`; it remains compatible with every live CCDP version and passes unrelated requests through unchanged. Code is same-origin; `connect-src` also admits the pinned Aztec CRS origins for asset caching. | + +The Distribution may publish smaller Brotli and gzip transfer representations. +It selects an available representation admitted by `Accept-Encoding` (including +quality values), otherwise the original. A compressed response keeps the original +media type and policy, declares its `Content-Encoding`, and varies on +`Accept-Encoding`. Decoding produces the exact original bytes. Native serving +software may supply validators and transfer framing; the protocol requires no +custom compression or ETag implementation. + +Both Prover responses close script and worker sources to the build-generated +same-origin graph and toolchain-required `blob:` workers. Every context that +fetches distributed assets, including Prefetch, Prover, the Service Worker, +and dedicated workers, admits `'self'` in `connect-src`; same-origin HTTP +assets must not be accidentally excluded by an HTTPS-only source list. + +Prover additionally admits `https: wss:` for declared external assets, +validated third-party OAuth Bridges, and secure notary WebSockets. Its local +Bridge HTTP sources are `http://localhost:* http://127.0.0.1:*`; its local +notary WS sources are `ws://localhost:* ws://127.0.0.1:*`. Both Prover responses +include these fixed sources for default and custom ports. Dedicated workers +include the corresponding sources where they perform Bridge or notary requests. +No resource admits a general `http:` or `ws:` source. Generated policy does +not add `upgrade-insecure-requests` or otherwise force local requests to TLS. +Script and worker loading remains same-origin under either permitted scheme; +these fetch exceptions admit no remote code. The Distribution embeds no +selected notary address, profile, or environment override; one byte-identical +response supports the same admitted origins in local and hosted deployments. +Selection changes no asset or cache key. This policy permits those network +schemes and explicit loopback hosts, not just the selected notary; application +code enforces destination selection. + +Every context fetching an external resource admits its declared request +origins, including fallback origins, without allowing external executable code. +Requests use noncredentialed readable CORS under both Prover responses, +including any declared Range requests; opaque responses and `no-cors` are +not substitutes. Availability and CORS policy remain external dependencies. + +Every context that compiles WASM, including dedicated proof and TLSNotary +workers, includes `script-src 'wasm-unsafe-eval'` alongside its code sources. +This permits WASM compilation, not JavaScript string evaluation. External execution worker scripts additionally carry +`Cross-Origin-Embedder-Policy: require-corp`. They have their own CSP; they do not rely on the document's CSP. +Blob workers inherit their creator's policy. Worker profiles admit only their +required script, asset, and protocol connections, and `worker-src` admits +same-origin or `blob:` children only for workers that spawn them. The Service +Worker only caches bytes and keeps ports: it needs no WASM compilation permission. + +Each request-invariant Prover response supports multiple platform profiles and +arbitrary OAuth Bridges satisfying the [origin policy](ccdp.md#origin-policy). +CSP cannot express a runtime-selected exact Bridge origin, so its HTTPS class +and fixed localhost HTTP sources are not per-Bridge compartmentalization. +Prover derives GitHub's fixed token route only from the +validated `redirectUri` frozen by the Application; no message supplies another +Bridge endpoint. Compromised Prover code can use every network class admitted +by the response. + +### Callback artifact + +- REQ-DIST-02 (upholds SP-CCDP-01): The Distribution MUST publish Callback + according to the insertion, browser-entry, and served-response contracts below; + the Bridge MUST validate and configure it according to that same contract. + +`GET /ccdp/callback.html` supplies a complete Callback document for +[OAuth Bridges](oauth-bridge.md#callback-document) to configure and serve at +their registered redirect URI. It executes on that Bridge's origin, without +a separate shell, HTTP redirect, or browser-side entry-script fetch. + +The artifact bundles the supported CCDP Callback implementations and their +dependencies. Its version-independent path lets the browser select a bundled +implementation from OAuth `state`, including Google fragment returns which +the Bridge cannot see. It contains no Bridge configuration and cannot accept +a connection until configured; a direct visit clears URL input and fails +locally on the missing deployment data. + +#### Configuration insertion + +The artifact contains the semantic equivalent of: + +```html + + + + + + libID + + +
+ + + + +``` + +The build produces exactly one configuration marker, in this non-executable +data block. The bridge substitutes serialized deployment data there, never +JavaScript source. Serialization escapes `<` as `\u003c` so data cannot terminate +the script element or introduce markup. Missing or repeated markers reject the +artifact. No callback request value participates in substitution. + +The inserted data is one unversioned JSON list, `[allowedOrigins, ccdpOrigin]`, +using the Bridge's [effective allowlist](oauth-bridge.md#deployment-configuration): + +```json +[ + ["https://app.example", "https://lib.id"], + "https://lib.id" +] +``` + +There is no version-keyed wrapper, input-declaration block, or Bridge-side +CCDP version list. Every bundled Callback implementation receives a deeply +frozen copy of the same list. The first two positions require a nonempty, +duplicate-free allowlist of canonical origins containing the configured CCDP +origin, and that origin itself. Both use the +[CCDP origin policy](ccdp.md#origin-policy), including its HTTP localhost +exception. These match the effective admission +set and public `CeremonyConfig` respectively. The list contains no secrets. +Neither URL input nor an upstream artifact supplies deployment values. + +Compatible evolution preserves existing positions, types, and meanings. New +optional trailing inputs may be defaulted when absent by newer implementations +and ignored by older ones. New CCDP versions using that compatible contract +require no Bridge change. A new required input or incompatible interpretation +instead requires an explicit input-contract version and corresponding Bridge +support; no such versioning is defined until needed. + +This is a data-insertion contract, not a UI template or renderer API. Callback +owns its code and presentation. Its dependencies are +bundled into this HTML rather than loaded relative to the bridge or fetched +from the Distribution by the browser. + +#### Browser entry + +URL clearing, version dispatch, and startup/failure UI are built and tested +with the bundled Callback implementations, not implemented by the Bridge. +A live document keeps the code and configuration it received. + +The embedded Callback code, before rendering, storage, error reporting, or any +network use: + +1. bounds and copies the raw query and fragment, then clears both with + `history.replaceState` while retaining the same path; +2. requires exactly one routing `state` and reads its `v.` prefix; +3. rejects a malformed version or one absent from its bundled implementations; +4. requires a JSON input list, validates the inputs used by the selected + implementation, and freezes the list and captured location; and +5. enters the selected Callback implementation once, without dynamic import. + +Oversized or malformed input is cleared and renders only fixed failure text. +A version absent from the bundle, including a retired version, displays a +package-owned message such as **This ceremony version is no longer supported. +Update the application and try again.** It establishes no connection, emits no +protocol message, and never substitutes another version. No retired transport +or abort-message implementation is retained for this screen. Applications need +no version-specific failure UI and receive no protocol notification of this +local failure; their ordinary cancellation/connection-failure handling remains. + +Missing or malformed required inputs likewise render fixed local +failure text without establishing a connection or emitting a protocol message. + +No platform credential is parsed here. The selected Callback +authenticates the Application against its configured allowlist before the +captured return can leave this document, then follows +[CCDP](ccdp.md#callback-get-redirecturi). + +#### Served response + +For one active artifact/configuration pair, HTML and headers are invariant +across requests. Nothing is derived from request `Origin`, `Referer`, query, +fragment, platform, or ceremony. The completed response uses: + +- `Cross-Origin-Opener-Policy: unsafe-none`, without COEP; +- `Content-Type: text/html; charset=utf-8`, `X-Content-Type-Options: nosniff`, + `Cache-Control: no-store`, and `Referrer-Policy: no-referrer`; +- CSP beginning with `default-src 'none'`, `object-src 'none'`, + `base-uri 'none'`, `form-action 'none'`, and `frame-ancestors 'none'`; +- `frame-src` admitting only the exact configured CCDP origin, retaining HTTP + and the configured port for an admitted localhost origin; +- `connect-src` admitting only fixed sources required by the configured popup + fallback; +- `style-src 'unsafe-inline'` for package-owned inline styles; and +- `script-src` containing only the build-generated hashes for the bundled + executable code, with no external script source, JavaScript + `'unsafe-inline'`, or `'unsafe-eval'`. + +The bridge combines the artifact's executable hashes with its own +deployment-specific policy, not an upstream policy permitting arbitrary +sources. Data substitution does not change executable bytes. Artifact and +matching policy update atomically; compatible UI changes require no manual +stylesheet hash, theme, or styling configuration. + +The Bridge accepts only a successful HTML artifact with the required unique +data slot and hash-only executable script policy. It performs substitution on +the decoded body and composes the final HTML and headers as one unit. Upstream +cache and transfer headers are not copied: the source artifact is revalidated, +while the configured browser response is non-cacheable. + +### Prover isolation + +- REQ-DIST-03 (upholds SP-CCDP-01): The Prover and its host MUST preserve the + isolation, fragment, and root-registration behavior described below. + +CCDP has one logical [Prover](ccdp.md#prover-get-prover). The primary response +requests Document Isolation Policy without severing the opener; an unisolated +arrival uses the same-origin fallback response through the +[popup transport's isolation replacement](popup-transport.md). +The two responses are not separate CCDP participants or phases. + +Both execute the same Prover implementation and fragment contract. They capture +and clear incoming fields before other work and preserve that capture through +replacement. Neither exposes readiness or executes proof work before isolation +and connection establishment succeed. If the fallback is still unisolated, +establishment fails; it does not loop or silently prove without shared memory. + +Both paths resolve the canonical root-scope Worker registration. A stale +`/ccdp/v1/` registration, even with the same script URL, is not that registration. +The host and participants uphold the popup transport's same-registration +continuity prerequisite. Successful DIP avoids replacement; fallback needs no +second window or extra user action. This mechanism does not repair an opener +already severed by the OAuth Platform; authenticated carrier fallback is a +separate popup-transport concern. + +### Proving assets + +- REQ-DIST-04: The Distribution MUST preserve the asset URL, byte, metadata, + and selected-profile resource contracts below. Necessity: Prefetch and Prover + must share compatible assets without runtime source negotiation. + +`GET /ccdp/assets/*` is the Distribution's static proving-resource namespace, +not a CCDP API or versioned protocol route. Locally served proving resources +other than the versioned protocol resources resolve there; Aztec CRS requests +retain their upstream URLs. CCDP +assigns no structure to the suffix: versioned code pins each exact path, while +protocol code neither enumerates nor parses the namespace. + +Each asset response: + +- has a canonical path with no query, fragment, mutable alias, or redirect; +- serves one immutable byte sequence with its exact media type and `nosniff`; +- uses `Cross-Origin-Resource-Policy: same-origin`; and +- uses `Cache-Control: public, max-age=31536000, immutable`. + +A release pins the resource graph for every supported platform ceremony version. +Requests, fragments, messages, and Application inputs cannot replace that graph. +Every local path referenced by published code exists; no browser-visible asset +catalog or request-time source resolution is required. External resources retain +their declared URLs. Prefetch and execution resolve the same selected-profile +resources, including shared resources, so their downloads and caches are reusable. + +### Publication and compatibility + +- REQ-DIST-05: The Publisher MUST activate a locally asset-complete release + and retain immutable paths as specified below. Necessity: compatible updates + must not invalidate live ceremonies or reusable cached bytes. + +Activation is asset-complete: every immutable resource referenced by an updated +protocol resource or Worker is retrievable with its final bytes and response +metadata before that update becomes reachable. +The external Aztec request set is qualified before promotion; CDN availability +cannot be made atomic with local deployment, and a later outage still fails +proving if no usable cache is present. + +An unchanged asset retains its URL across compatible releases. Changed bytes or +execution-relevant metadata receive a new immutable URL, and old URLs remain +available while any live ceremony, supported CCDP implementation, platform +profile, or compatibility window may reference them. Runtime content hashing is +not required; release-qualified, content-addressed, and build-generated +immutable paths all satisfy this contract. + +Asset revisions change `CCDPVersion` or `PlatformCeremonyVersion` only when +their observable protocol or proof semantics change. + +## Security Considerations + +This contract supports SP-CCDP-01 under ASM-CCDP-01 and ASM-CCDP-02. +The publisher controls executable browser code: headers and content-addressed +paths do not protect against a malicious publisher or compromised release. +CSP limits accidental source expansion, not the publisher's authority. +Request-invariant Prover policy deliberately permits classes of secure network +origins; runtime destination checks, not CSP, bind a ceremony to its Bridge and +notary. Local HTTP exceptions are confined to the popup origin policy. + +Callback deployment inputs are non-executable trusted configuration. Their +insertion cannot depend on OAuth ingress, change script bytes, or introduce +markup. The Bridge keeps that configured response separate from its upstream +artifact cache. Fragments do not reach this Distribution's HTTP service. + +Unreachable external resources can prevent proving despite an atomic local +deployment. Neither caching nor release qualification guarantees later CDN +availability. Common and platform specifications retain proof and trust-root +authority; this document selects no ledger verification keys. + +## Conformance + +Publishers, static hosts, and Bridge artifact consumers implement the roles +above. The package's build and deployment tests may qualify them with any +serving software that produces these observable responses. + +- TEST-DIST-01 (exercises REQ-DIST-01): + GET/HEAD serve invariant decoded bytes and policy; conditional/encoding responses preserve them; unknown paths are inert. Both isolation profiles support allowed local and external requests without admitting remote executable code. +- TEST-DIST-02 (exercises REQ-DIST-02): + Exactly one data marker is inserted safely; executable hashes remain valid; missing/duplicate slots fail. Query and fragment state select a supported bundled Callback without another script request; missing/retired versions fail locally. +- TEST-DIST-03 (exercises REQ-DIST-03): + Primary isolation or one replacement establishes the same participant; retained fragments survive. With both root and stale narrower registrations present, participants resolve root even if script URLs match. +- TEST-DIST-04 (exercises REQ-DIST-04): + Empty-cache Prefetch and execution use the same declared resource graph; shared resources are reusable, and ranged external responses remain readable under both isolation profiles. +- TEST-DIST-05 (exercises REQ-DIST-05): + Unchanged assets keep URLs, changed bytes get new URLs, both remain retrievable, and no updated document or Worker becomes reachable before all its local dependencies. External availability is qualified, not reported as atomic. diff --git a/specs/ccdp.md b/specs/ccdp.md new file mode 100644 index 00000000..b2a78dd4 --- /dev/null +++ b/specs/ccdp.md @@ -0,0 +1,838 @@ +# Ceremony Cross-Document Protocol (CCDP) + +This document defines the browser protocol across the Application and +the [documents](#documents-and-routes) it uses. It owns ceremony locations, +navigations, messages, ordering, and compatibility. Authorization, +platform-proof, and final-proof semantics are defined by the normative +[common ceremony](ceremony-common.md) and +[platform ceremony](platform-ceremonies.md) specifications. + +CCDP uses the authenticated logical connection defined by +[Popup transport](popup-transport.md). That specification owns authentication, +delivery, navigation, isolation fallback, and continuity; CCDP owns the protocol +carried over it. Connection authentication exposes the authenticated peer origin +to each participant, including after a fallback or replacement. Delivery remains +best effort across replacement: CCDP does not turn carrier readiness into a +delivery acknowledgement or replay messages lost during a transition. +[CCDP Distribution](ccdp-distribution.md) owns resource publication and response +policies; the [OAuth Bridge](oauth-bridge.md) owns callback ingress and its API. + +## Conventions and boundary + +The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", +"SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and +"OPTIONAL" in this document are to be interpreted as described in +BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all +capitals, as shown here. + +The actors, tables, and protocol prose below are the CCDP version-1 contract. +Examples and the sequence diagram illustrate it. Package APIs, runtime type +decoders, UI projections, tracing integrations, and proving algorithms are not +part of this contract. The common specification's Canonical Runtime spans the +Application and its browser participants; Callback is its Redirect Runtime and +Prover owns the platform-aware browser evidence checks. A Ledger Verifier means +the downstream verification path defined in the common specification, not a +browser component. + +- ASM-CCDP-01: Browser origin enforcement, authenticated popup transport, and + the configured Application, Bridge, and Distribution code execute correctly. + Compromised code on any of those trusted origins is outside the browser + credential-release guarantee; sharing an origin shares this failure domain. +- ASM-CCDP-02: The OAuth Platform returns the selected profile's response to + its registered redirect URI. Headers or navigation may sever the opener. + Progress then depends on an available authenticated fallback; CCDP does not + promise completion when no connection can be made. +- SP-CCDP-01 (depends on ASM-CCDP-01, ASM-CCDP-02): Against unrelated documents + and origins, only the authenticated ceremony and frozen profile may release + the captured return to Prover or initiate credential use. This does not + defend against a compromised Application, Bridge, or Distribution, or certify + user intent. Evidence: checked state/credential-flow invariants and supporting + conformance tests, not proof of cryptographic soundness. + +## Actors and origins + +An actor is an operator or external system. An origin is the exact +scheme/host/port authority used by browser security checks. A site is only the +browser's schemeful registrable-domain grouping: same-site actors may remain +cross-origin and do not gain authority over each other. +`Application` denotes both the actor and its top-level browser document when +the distinction is immaterial. + +| Actor | Browser authority | Responsibility | +|---|---|---| +| Application | application origin | hosts the application document, owns the operation and ceremony state, and drives the protocol | +| OAuth Bridge | OAuth bridge origin | publishes ceremony configuration, serves the complete Callback document obtained from the Distribution with bridge-owned inputs, owns OAuth registrations, and performs enabled confidential OAuth exchanges | +| CCDP Distribution | CCDP origin | contains the versioned [resources](#documents-and-routes) and proving assets used by any number of OAuth Bridges; it may be the canonical libID distribution or an operator-selected replacement | +| OAuth Platform | OAuth-platform origin set | hosts authorization/login documents and issues the OAuth return | + +The Application and OAuth Bridge may be operated together or independently; +the selected CCDP Distribution may be published by either party or another +one. Their origins may be same-origin, same-site, or cross-site. CCDP assumes +none of those relationships. Browser authority is always established against +an exact origin. A composition which continues the live popup connection after +CCDP has the additional requirements below. + +The OAuth redirect URI terminates on the bridge origin. The OAuth Bridge serves +CCDP's self-contained Callback artifact with its deployment inputs already +inserted; artifact retrieval happens server-side, independently of OAuth +requests. Callback captures and clears the OAuth return, then selects its +bundled CCDP implementation without another browser request. + +Multiple independently operated OAuth Bridges may select the same CCDP +Distribution through its `ccdpOrigin`. The Distribution keeps no Bridge +registry or reciprocal allowlist and exposes identical public CCDP resources +across that relationship. + +## Composition boundary + +CCDP does not define documents, messages, or policy outside the ceremony. A +composition may use the same popup and connection before or after CCDP, but +those steps remain outside this protocol. + +- REQ-CCDP-01: The Composition MUST obey the popup transport's continuity and + origin rules when reusing the connection beyond CCDP. Necessity: completion + does not transfer control to an unauthenticated document. + +Carrying the live connection beyond Prover requires the next document to use +the exact CCDP origin; same-site placement is insufficient. All code on that +origin shares one browser authority and must therefore be mutually trusted. + +## Documents and Routes + +**Resources** collectively means Prefetch, Callback, Prover, and +Worker. Authorization is an external document, not a CCDP resource. + +### Prefetch `GET /prefetch` + +| Property | Contract | +|---|---| +| Parameters |
Name#ceremonyId#platformId#ceremonyVersion
Valueslowercase UUIDv4exact identifier from the selected platform profileunsigned 16-bit platform ceremony version
| +| Location and context | CCDP origin; versioned, top-level, and non-isolated ceremony-popup document | +| Role | Starts the selected profile's fetches before the Application continues through [Prefetch to Authorization](#1-prefetch-to-authorization). It receives no authorization URL, OAuth return, or proof input. | + +### Authorization `GET platformAuthorizationUrl` + +| Property | Contract | +|---|---| +| Parameters | The complete frozen URL is opaque to CCDP. The selected platform ceremony version owns its parameters. | +| Location and context | Selected OAuth Platform; top-level ceremony-popup document | +| Role | Owns login and consent during [Authorization to Callback](#2-authorization-to-callback). No CCDP participant runs and no CCDP message or popup connection is exposed to this document. | +| External policy | Controlled entirely by the OAuth Platform. CCDP assumes nothing about its markup, scripts, headers, or origin transitions; it may sever the opener or browsing-context group. Callback reconnects without assuming direct window continuity. The selected platform ceremony version owns authorization request and return semantics. | + +### Callback `GET redirectUri` + +| Property | Contract | +|---|---| +| Location and context | OAuth Bridge origin at the fixed registered callback path `/auth/callback`; top-level, non-isolated document with complete bundled Callback code and bridge-owned deployment inputs | +| Role | Authenticates the Application during [Authorization to Callback](#2-authorization-to-callback), then privately carries the captured OAuth return in popup navigation to Prover during [Callback to Prover](#3-callback-to-prover). It installs no Service Worker, retains no state across navigation, and does not classify, prefetch, prove, verify, persist a checkpoint, or close the popup. | +| Failure and cleanup | Failure before connection acceptance is displayed locally and cannot release the return; observable failure after acceptance uses `Abort`. Terminal cleanup clears retained return bytes and releases listeners and references. | + +### Prover `GET /prover` + +| Property | Contract | +|---|---| +| Parameters |
Name#ceremonyId#applicationOrigin#oauthQuery#oauthFragment
Valueslowercase UUIDv4exact Application origin authenticated by Callbackcaptured OAuth query, including leading ? when nonemptycaptured OAuth fragment, including leading # when nonempty
| +| Location and context | CCDP origin; versioned, top-level ceremony-popup participant; cross-origin isolated before protocol readiness | +| Role | Accepts the logical Application connection during [Callback to Prover](#3-callback-to-prover), then validates the retained OAuth return under the Application-selected profile and runs [Prover execution](#4-prover-execution). The selected platform ceremony owns its proof and evidence semantics. | +| Outcome and cleanup | Local proof delivery does not assert Application acceptance. Prover clears transient proving inputs and execution resources without closing or navigating the popup. UI is an implementation-owned projection of events. | + +### Worker `GET /worker.js` + +| Property | Contract | +|---|---| +| Location and context | CCDP origin; same-origin module Service Worker whose response sets `Service-Worker-Allowed: /` and which Prefetch registers with `scope: '/'` | +| Role | Supports selected-profile fetches and the popup transport's same-origin continuity mechanism. It remains compatible with every live CCDP version and does not intercept unrelated origin traffic. | + +### Common + +#### Paths and versioning + +- REQ-CCDP-02: Each Participant MUST use the frozen locations, state encoding, and + compatible version selected below without silently selecting a newer version. + Necessity: independently deployed documents must execute one protocol. + +Prefetch, Prover, and Worker routes are relative to +`{ccdpOrigin}/ccdp/v{CCDPVersion}`. Callback executes at the frozen `redirectUri` +on the OAuth Bridge origin; the Distribution defines the public artifact the +bridge retrieves to serve it. Authorization is the external frozen +`platformAuthorizationUrl`, not a CCDP route. + +Before launch, the Application freezes the CCDP origin, redirect URI, platform +authorization URL, ceremony ID, platform ID, and platform ceremony version. +This document defines `CCDPVersion = 1`. The Application selects it in the +Prefetch path, carries the same version through OAuth `state`, and uses the +matching Prover path. Callback selects its bundled implementation from that +state; fragments and messages do not repeat the version. Google returns state +in the fragment, so the bridge cannot perform this selection at HTTP ingress. + +Compatible implementation changes keep the version. A breaking fragment +grammar, navigation order, message shape, direction, ordering, or validation +rule increments it, publishes new CCDP paths and Worker, and adds that version's +implementation to the self-contained Callback artifact. Old resources and +bundled Callback implementations remain available for live ceremonies and a +compatibility window. + +Once that window ends, a build may omit a retired Callback implementation. +Its version then takes Callback's local unsupported-version error path before +connection setup, rather than requiring an older transport or error protocol. +The popup owns that error display; it never falls forward to a different CCDP +version or reports this failure as OAuth denial. + +A later CCDP version substitutes its decimal version in the common path. The +registered callback URL stays fixed: its document includes a closed set of +supported implementations and enters the selected one directly. All browser +documents execute embedded entry code. Internal bundle names are not protocol +surface; obtaining Callback bytes from the Distribution does not change its +OAuth Bridge execution origin. + +The Prefetch and Prover paths select both CCDP version and document +role. + +Platform Ceremony Version independently versions one platform's authorization, +OAuth, proof, and output semantics. Popup connection controls and the OAuth +Bridge API are independently versioned as well. + +#### Popup and fragment model + +- REQ-CCDP-03 (upholds SP-CCDP-01): Each Participant MUST preserve, validate, clear, + and restrict the URL inputs according to this section before using them. + +The **ceremony popup** is a reusable browsing context, not an actor or +document. It sequentially contains Prefetch → Authorization → Callback → +Prover. Navigation creates a new JavaScript heap each time; no +participant relies on document-local state surviving it. These origins may all +be cross-site, and same-site placement grants no protocol authority. + +Internal fragments use URL-search-parameter encoding after `#`. Producers emit +each named field exactly once in the displayed order. Receivers require the +exact field set, reject duplicates, and otherwise do not depend on parameter +order. + +The Prefetch and Prover routes have no query. Their fragments are never sent +in HTTP requests and are copied and cleared before rendering, storage, or +network use. Prover's `oauthQuery` and `oauthFragment` are the sole internal +credential-bearing navigation fields. They preserve the original two URL +components separately, including empty values, with one outer +URL-search-parameter encoding layer; decoding that layer reproduces the +captured components without normalization or merging. The selected profile's +OAuth parser handles their contents later. + +Callback constructs this fragment locally for the frozen CCDP-origin Prover; +the Application receives neither the return nor the navigation target. +The Prover captures and clears it before use. Any internal isolation +replacement preserves the captured fragment and clears it again on arrival. +No participant deliberately includes the captured return in a request query, +connection notification, signaling record, Worker record, telemetry, or error. +Opaque dependency error text follows the [Abort boundary](#abort), which does +not promise automatic redaction. Proofs and other proving inputs never enter +navigation fragments. The OAuth-platform-mandated query on `redirectUri` +remains the sole credential-bearing HTTP-request URL. + +CCDP is connection-neutral. It defines which document runs at each location, +which participant initiates each navigation, what each message means, and their +order. Each recipient validates its permitted inbound messages and enforces +direction and state before acting. + +#### Origin policy + +- REQ-CCDP-04 (upholds SP-CCDP-01): The Callback and Prover MUST authenticate the + Application and carry its exact-origin restriction as specified below before + releasing the OAuth return or accepting proof work. + +Origins use the canonical-origin and loopback rules owned by +[Popup transport](popup-transport.md#6-origin-allowlists-and-binding). HTTPS is required except +for HTTP on the exact hosts `localhost` and `127.0.0.1`; ports are not fixed. +Different ports, schemes, or those two hostnames remain different origins. +URL-bearing fields retain their own path/query/fragment contract. The exception +does not relax OAuth-platform TLS, external-resource policy, or browser secure +context and isolation requirements. + +One CCDP Distribution serves Applications admitted by any number of independent +OAuth Bridges without a Distribution-wide allowlist. Prefetch uses +`allowedApplicationOrigins: '*'` for public asset fetching and authenticates +the exact Application peer. The Application exact-authenticates the configured +CCDP origin. + +Callback exact-authenticates the Application against its containing OAuth +Bridge's explicit deployment allowlist before navigating with the captured +return to the configured CCDP origin. It sets `applicationOrigin` in the Prover +fragment from that connection's authenticated peer origin, never from OAuth +parameters, request headers, or an Application-supplied value. Prover requires +that field to satisfy the canonical origin rule above and accepts only +`allowedApplicationOrigins: [applicationOrigin]`. Its connection authenticates +the peer against that exact origin before readiness or proof requests, including +after an isolation replacement or fallback-carrier selection. Missing or invalid +origin input, an unavailable authenticated peer origin, or an origin mismatch +fails locally before protocol readiness; it never falls back to open admission. + +This is defense in depth against the opener navigating to another origin +between Callback authentication and Prover's fresh handshake: a retained window +reference alone does not preserve its document's origin. The fragment carries +Callback's restriction, not proof of the peer's origin; connection authentication +still establishes that. No additional handshake or configuration fetch is needed. +This check does not protect against compromised code on an already trusted origin +or change downstream proof verification. + +The public Callback artifact contains no Bridge policy; the serving Bridge +inserts its trusted configuration. Server-side artifact retrieval does not +replace Callback's credential-release check. Asset caching and popup-connection +construction are outside CCDP. + +## Messages + +- REQ-CCDP-05 (upholds SP-CCDP-01): The Recipient MUST enforce the message + shapes, permitted directions, state guards, and cardinalities in this section. + +The following table is the complete CCDP version-1 message set. + +| Message | Direction | Accepted after | Cardinality and effect | +|---|---|---|---| +| [`ProveIdentity`](#proveidentity) | Application → Prover | `Event(prover, started)` | exactly once; selects the profile for OAuth validation and proof execution | +| [`IdentityProof`](#identityproof) | Prover → Application | `ProveIdentity` and valid OAuth acceptance | at most once; ends the Prover run | +| [`Cancel`](#cancel) | Application → Callback or Prover; Prover → Application | active connection for Application cancellation; `ProveIdentity` and valid OAuth denial for Prover cancellation | at most once; ends the run without a technical error | +| [`Abort`](#abort) | Prefetch, Callback, or Prover → Application | connection acceptance | at most once; reports technical failure and ends the run | +| [`Event`](#event) | Prefetch, Callback, or Prover → Application | connection acceptance and the event's documented emission point | core occurrences follow the [event catalog](#core-events); additional observations do not advance the protocol | + +Every recipient requires a plain record with the exact fields, types, and bounds +defined below. Unknown fields, coercion, normalization, defaults, and +unrecognized discriminators are invalid. Messages outside the listed direction, +predecessor, and cardinality are invalid. Cancellation, proof delivery, and +abort make later messages inert even when they race in transit. + +### ProveIdentity + +```ts +interface ProveIdentity { + type: 'prove-identity' + platformId: string + platformCeremonyVersion: number + clientId: string + redirectUri: string + codeVerifier: string | null + notaryAddress: string | null +} +``` + +`platformId` and `platformCeremonyVersion` are the exact supported profile +selected at launch and must match the active Prover. The message is valid only +after `Event(prover, started)`. The remaining fields are the frozen client +identifier and redirect, derived code verifier, and resolved notary address. +`redirectUri` is the canonical OAuth Bridge origin with the fixed +`/auth/callback` path and no +query or fragment. The Application derives it before OAuth; public bridge +configuration carries no redirect field. The OAuth return is already retained +by Prover and is not repeated in the message. +Starting Prover initiates OAuth validation; it does not assert acceptance or +mean that proof generation has already begun. + +`notaryAddress` follows the [origin policy](#origin-policy) for a platform that +uses notarization; it is null for Google. The local HTTP exception needs no +client option or environment override. A remote HTTP address is rejected, +never upgraded or used as a downgrade fallback. +The Application selects and freezes it before OAuth. Prover validates it before +credential use and uses it unchanged for all sessions, including the GitHub +token request. It neither selects defaults nor accepts a separate profile, +ledger identifier, hash, or testnet flag. The address changes network routing, +not the proof statement or trusted signing keys. + +The Application origin is trusted for this transient input because it already +supplies the operation being authorized. It retains the authorization nonce; +only the derived code verifier crosses this boundary. The message contains no +authorization digest, operation field, separate OAuth state, Job revision, +composition state, connector, or carrier kind. + +For GitHub, Prover derives the fixed OAuth Bridge token route from the origin of +`redirectUri`; no second bridge origin or endpoint field is carried. Prover +exact-validates the CCDP record and selected platform/version before credential +use. That profile parses the retained query/fragment pair, enforcing exact +transport, fields, client/redirect checks applicable to the response, and +success/denial grammar. It matches OAuth `state` to +`v.` using the versioned resource and the ID of the +authenticated logical connection, not a second caller-selected expected state. +The return is consumed once; no second request or replacement response can +restart the run. + +### IdentityProof + +```ts +interface IdentityProof { + type: 'identity-proof' + identity: { + platformId: string + oauthClientId: string + userId: string + userName: string + } + proof: unknown +} +``` + +`identity` is a separate, exact-shaped record of prover-extracted strings: +platform identifier, OAuth client identifier, user identifier, and user name +(the signed email for Google). The selected platform validator checks their +encodings and the platform/client binding to `ProveIdentity`. +`proof` is the exact value defined by that platform ceremony version, without +a nested identity copy. CCDP treats the proof as opaque; adding a platform does +not change this message. Neither browser endpoint cryptographically verifies +the delivered result; identity is non-authoritative until ledger verification. + +### Cancel + +```ts +interface Cancel { + type: 'cancel' +} +``` + +`Cancel` is a parameterless, bidirectional terminal message: + +- Application → Callback or Prover stops reachable work after explicit + cancellation or retirement of Application authority. +- Prover → Application reports only a valid, ceremony-bound OAuth-platform + denial discovered while validating `ProveIdentity`. The Application + resolves `{ status: 'denied' }`. Prover sends it before token exchange, + proof execution, or proving-operation events, never as a substitute for a + failure. + +Malformed, mismatched, or otherwise invalid OAuth returns use +`Abort`, not cancellation. An Application which has already canceled +ignores a racing denial or proof. Cancellation has no acknowledgement; +recipients clear reachable input but do not close or navigate the popup. + +### Abort + +```ts +interface Abort { + type: 'abort' + event: string + message: string +} +``` + +`Abort` reports an observable technical failure after connection +acceptance from whichever of Prefetch, Callback, or Prover is active. `event` +is the nonempty, bounded, code-owned name of the failing core or +implementation-defined operation; it is not a UI +stage and need not have an earlier notification when failure preceded emission. +`message` is bounded, opaque display text. Producers may preserve a caught +error's message or a thrown string, removing control characters and bounding +its length. They do not serialize exception objects, stacks, nested causes, or +arbitrary objects. There is no required code, reason enum, code-to-text mapping, +or automatic credential-redaction guarantee: dependency error text may contain +sensitive details. Recipients render it as text, never markup or control data, +and exclude it from telemetry exports. The Application rejects the live ceremony. + +Failure before connection acceptance has no CCDP path. Its display text may be +rendered locally; an undeliverable report records a fixed local diagnostic, +not the opaque error text. Reporting failure changes neither cleanup nor the +ceremony outcome. + +### Event + +- REQ-CCDP-06: Each Participant MUST emit and interpret the core events, extension + boundary, and timestamps defined below. Necessity: readiness gates and + observations must have the same meaning at independently deployed endpoints. + +One event stream carries protocol readiness, operation timing, and additional +platform observations. The same observations can drive UI or tracing; they do +not require separate wire protocols. `Event(name, phase)` below abbreviates +this record, not a distinct message type: + +| Field | Contract | +|---|---| +| `type` | Exactly `event` | +| `event` | Nonempty core or implementation-defined operation name | +| `phase` | `started` or `finished` for an operation; omitted for a single-shot observation | +| `timestamp` | Finite, nonnegative occurrence time in milliseconds on the browser's epoch-relative performance timeline | +| `operationId` | Optional, nonempty instrumentation-only identifier pairing repeated concurrent instances of the same operation | +| `attributes` | Optional bounded record of event-defined scalar measurements or facts: strings, finite numbers, or booleans | + +Optional fields are absent when unused, not null. Event names, attribute names, +string values, and record sizes are bounded by the implementation. Names and +attribute meanings are code-owned, not supplied by OAuth returns or callers. +No event contains a UI stage, display label, progress percentage, overall +ceremony status, or error text. Technical failure uses `Abort`. + +#### Core events + +The catalog includes Application-local observations so a complete timeline has +one vocabulary. A **local** occurrence is not sent over CCDP. All other listed +occurrences are required `Event` messages from the indicated document when +their conditions are reached. Interrupted operations need not finish, and an +inapplicable operation emits nothing. + +| Event | Start | Finish or observation | +|---|---|---| +| `prefetch-dispatch` | Application, locally before the first Prefetch navigation | Prefetch, after authenticating the connection, registering the Worker, and dispatching selected-profile fetches. Permits Authorization navigation; downloads need not be complete. | +| `authorization` | Application, locally when initiating Authorization navigation | Callback, after capturing the OAuth return and authenticating the Application, before navigating to Prover. Includes the return and connection setup; asserts neither approval nor pure user-consent duration. | +| `prover` | Prover, after isolated connection readiness and installation of its CCDP handlers. Permits `ProveIdentity`; does not assert OAuth acceptance or ZK execution. | Application, locally after structurally accepting `IdentityProof` and assembling its result. Prover never sends this finish over CCDP. | +| `prover-fallback` | — | Prover, once after an isolation replacement, with the replacement navigation's start timestamp and no `phase`. See [Fallback timing](#fallback-timing). | +| `token-fetch` | Prover, when starting to obtain a usable access token | Prover, when that token is available, without waiting for its final attestation | +| `token-attestation` | Prover, when starting work to obtain the token attestation | Prover, when the complete attestation passes its required structural, request-binding, and commitment/opening checks | +| `identity-fetch` | Prover, when starting the platform identity request | Prover, when its response has been received and parsed | +| `identity-attestation` | Prover, when starting work to obtain the identity attestation | Prover, when the complete attestation passes its required structural, request-binding, and commitment/opening checks | +| `zk-proof-preparation` | Prover, when starting input and proving-backend preparation | Prover, when both inputs and backend are ready | +| `zk-proof-generation` | Prover, when starting witness execution | Prover, when the ZK proof has been generated | + +`prover-fallback` is the only single-shot core event. Each core operation has +one start and, on success, one finish per ceremony; these occurrences omit +`operationId`. X uses all six proving operations. GitHub omits `token-fetch`: +its `token-attestation` covers the complete Bridge request returning both the +token and its attestation. Its identity operations match X's. Google uses only +the two ZK operations. + +The six proving operations start only after `ProveIdentity` and valid OAuth +acceptance. They may overlap according to the selected profile's dependencies. +Backend and input preparation need not wait for final attestations; proof +delivery still waits for all required evidence. Events describe logical work, +not a required scheduling algorithm or mutually exclusive execution intervals. +No event adds browser cryptographic verification of proofs or attestations. + +`prefetch-dispatch.finished` and `prover.started` are protocol gates, each +accepted exactly once from its designated document at the matching phase. +`authorization.finished` is emitted once before Callback departs, but does not +require acknowledgement or introduce another gate before Prover readiness. +Additional attributes, operation IDs, or extension events cannot satisfy, +duplicate, or bypass a gate. + +#### Extensions and observation + +Implementations and platform-version modules may add operation pairs or +single-shot events without changing the message shape or core meanings. +Extensions cannot reuse a core name for a different operation. Receivers +validate the envelope and may ignore unknown extension names or attributes; +they never treat those as readiness, cancellation, or success. Core names with +wrong phase, sender, order, or cardinality are invalid, not extensions. + +Repeated overlapping extension operations use `operationId` to pair starts and +finishes; the identifier has no routing or authorization role and must not +reuse ceremony IDs, OAuth state, credentials, or identity values. Observations +and attributes contain no OAuth parameters, URLs or origins, identity data, +proofs, witnesses, attestations, or raw exceptions. + +The producing document may expose the same event locally before forwarding it +to Application. Local observers require no Application roundtrip. Required +events are emitted regardless of subscriptions; internal protocol handling +precedes observer filtering, and observer/exporter failure cannot interrupt it. +Application may merge local and remote events and derive UI stages, terminal +status, or tracing spans. Such projections and telemetry export policy are +outside CCDP, not additional messages or authorities. + +#### Timing + +Record the occurrence time as `performance.timeOrigin + performance.now()` +when an operation starts or finishes. Preserve that timestamp across forwarding +and delayed delivery; receipt time is not operation time. Independently running +operations may overlap, and retrospective observations may arrive after later +timestamps. Protocol ordering follows authenticated state and messages, never +timestamp sorting. + +Start/finish differences measure operation intervals. Do not sum overlapping +intervals as total elapsed time, fabricate a finish after context loss, or turn +missing observations into zero-duration work. Resource observations distinguish +requests, shared-flight joiners, and actual network retrievals; a joiner or +cache hit is not another download. The event model adds no separate collector, +network export, or measurement acknowledgement. + +#### Fallback timing + +The isolation replacement occurs during connection establishment, before +ordinary CCDP delivery is available. The fallback document therefore records +its [navigation time origin](https://www.w3.org/TR/hr-time-3/#sec-time-origin) +as the occurrence timestamp of `prover-fallback`. Once its connection is ready, +it sends that observation retrospectively, before `Event(prover, started)`. +The successful non-replacement path emits no `prover-fallback`. + +`prover.started.timestamp - prover-fallback.timestamp` measures replacement +navigation, document loading, and work up to Prover readiness. It excludes +source-document work before navigation, such as preserving a port, and is not +the exact additional cost against a hypothetical successful DIP path. This +requires no stored timestamp, pre-authentication message, extra handshake, or +new CCDP phase. If connection establishment fails, the observation stays local; +it does not invent readiness or a completed interval. + +## Security Considerations + +CCDP upholds SP-CCDP-01 under ASM-CCDP-01 and ASM-CCDP-02. Authentication +protects against other origins, not malicious scripts already served by a +trusted origin. Callback's carried `applicationOrigin` is a restriction that +Prover authenticates against, not independently authenticated evidence. + +A compromised Application can supply an operation the user did not intend. +A compromised Bridge or Distribution can replace browser code and observe or +withhold credentials. Browser isolation enables proving; it does not remove +those code-supply-chain trusts. OAuth state binds a live ceremony, not human +understanding of consent. + +CCDP supplies no durable OAuth/proof recovery or guaranteed cancellation of +already dispatched work. Background suspension, lost connections, and document +replacement can prevent progress. A missing event or closed popup is never +success or valid denial. Error text is deliberately useful for local debugging +and is not guaranteed to be credential-free; the Abort and Event contracts +separate it from exported observations. + +The selected platform and common ceremony rules own proof soundness, identity +extraction, replay, freshness, and trust-root lifecycle. Browser structural checks +do not authenticate notary signatures or verify generated proofs. Omitting +those early checks delays some forgery/mismatch rejection to the Ledger +Verifier; it changes neither its checks nor accepted proof statements. + +## Conformance + +Application, Prefetch, Callback, and Prover implementations conform to their +roles together with the resource policies and popup transport they use. Required +events remain required even when no UI or tracing subscriber is installed. +Conformance tests support the browser guarantees; they do not prove the +cryptographic properties delegated to the common and platform specifications. + +- TEST-CCDP-01 (exercises REQ-CCDP-01, REQ-CCDP-08): + Proof delivery leaves the popup available for a same-origin composition; terminal traffic cannot restart CCDP. +- TEST-CCDP-02 (exercises REQ-CCDP-02): + Frozen version/path/state agree; unsupported or retired Callback versions fail locally rather than falling forward. +- TEST-CCDP-03 (exercises REQ-CCDP-03): + Separate query/fragment bytes survive private navigation and isolation replacement, are cleared before use, and never appear in Application/control/signaling records. Duplicate or malformed fields fail. +- TEST-CCDP-04 (exercises REQ-CCDP-04): + Public Prefetch authenticates its exact peer; Callback rejects an unlisted Application; Prover rejects a different origin, including one occupying the same retained window after navigation. Canonical HTTP loopback works at arbitrary ports. +- TEST-CCDP-05 (exercises REQ-CCDP-05): + Malformed, duplicated, wrong-direction, out-of-state, and post-terminal records cause no authorized action. Proof payloads are structurally checked under the selected platform version. +- TEST-CCDP-06 (exercises REQ-CCDP-06): + Only the designated core occurrences open gates; extensions cannot do so. Overlap, occurrence timestamps, and retrospective fallback timing are preserved. +- TEST-CCDP-07 (exercises REQ-CCDP-07): + The four phases preserve navigation ownership and credential privacy; approval enters execution, bound denial exits before proving, and malformed returns abort. +- TEST-CCDP-08 (exercises REQ-CCDP-08): + Cancel/Abort/IdentityProof races settle once; errors are text-only, excluded from exported events, and undeliverable failures have a fixed local diagnostic. + +## Protocol + +- REQ-CCDP-07 (upholds SP-CCDP-01): Each Participant MUST follow the phase guards, + navigation ownership, and invariants in this section. + +The protocol advances one named ceremony popup through +[Prefetch](#prefetch-get-prefetch), +[Authorization](#authorization-get-platformauthorizationurl), +[Callback](#callback-get-redirecturi), and +[Prover](#prover-get-prover). Those route sections own each participant's +inputs, context, and role; [Messages](#messages) owns the records crossing the +popup connection. The phases below own their sequencing, entry conditions, and +exit conditions. Navigation retires the source document, and no later message +can reactivate an earlier phase. + +### Invariants + +- One live ceremony owns one authenticated popup connection. Connection + ownership supplies message correlation and its private version; loaded + resources supply the CCDP version. CCDP messages repeat neither. +- Each participant accepts only exact records permitted by its direction, + current state, and cardinality. Unknown message types, malformed, replayed, + out-of-order, wrong-direction, and post-terminal values change no state. + Valid event extensions may be observed but never advance the protocol. +- Browser-observed exact origins establish authority. Same-site placement, + navigation history, request headers, and message fields do not substitute for + connection authentication. +- Documents use only the frozen locations and fragments defined here. A CCDP + message never selects an origin, implementation, or navigation destination. +- Raw OAuth returns pass only from the cleared Callback capture to Prover's + private fragment, including any isolation replacement. Every arrival clears + its URL before use; participants do not deliberately copy the return into an + intermediate store, notification, or diagnostic. Opaque error text has the + separate [Abort boundary](#abort). The platform-mandated callback query is the + sole HTTP-request ingress exception. +- Callback carries the return onward only after authenticating the Application. + Prover validates it against the authenticated ceremony and selected profile + before any credential-bearing request. Application receives only protocol + outcomes and the final proof, whose evidence may contain profile-required + disclosed fields. Authorization receives no CCDP message or connection. +- Events report only their defined conditions. Required readiness events permit + the next protocol action, but neither they nor other observations, carrier + state, navigation, popup closure, or unvalidated proof delivery constitute + ceremony success. Operation completion is not ceremony completion. +- The Application owns terminal popup lifetime. No CCDP document closes the + popup. +- Cancellation and context-loss cleanup are best effort. CCDP has no durable + checkpoint, ceremony recovery, or migration to another popup connection. + +### Phases + +#### 1. Prefetch to Authorization + +The protocol enters this phase on user activation. The Application records +`prefetch-dispatch.started` locally, then initiates one named popup's first +navigation to [Prefetch](#prefetch-get-prefetch) and +establishes its connection there. A scripted opener may first reserve the +popup at `about:blank`; if that fails, the same activation's real anchor +navigates it directly to Prefetch. + +Prefetch clears and validates its fragment, accepts the connection, registers +the Worker, and dispatches the selected profile's fetches. It then sends +[`Event(prefetch-dispatch, finished)`](#event). Only after accepting that event +from Prefetch, the Application records `authorization.started` locally and +navigates the retained popup to +[Authorization](#authorization-get-platformauthorizationurl) at the frozen +`platformAuthorizationUrl`. The Application owns this transition because it +alone retains that URL; neither the URL nor a navigation command crosses the +carrier. Authorization is not a participating document, so the navigation +retires the Prefetch carrier while leaving the Application endpoint available +for Callback. + +Worker registration, activation, or selected-profile dispatch failure after +connection acceptance sends `Abort` for `prefetch-dispatch` instead of its +finish event; Application rejects without navigating to Authorization. Download +failure after successful dispatch remains an asset-cache concern and uses the normal +cold-fetch path, not a late Prefetch abort. Failures before connection acceptance +are reported locally and release no protocol message. + +#### 2. Authorization to Callback + +This phase begins when the Application initiates navigation to +[Authorization](#authorization-get-platformauthorizationurl); CCDP cannot +observe when the platform page loads. The OAuth Platform owns the popup and +initiates browser navigation to the frozen `redirectUri` after approval or +denial; neither CCDP endpoint +initiates that transition. The Bridge serves the complete +[Callback](#callback-get-redirecturi), which captures and clears the return and +enters its bundled CCDP implementation selected by `state`. The +[OAuth Bridge contract](oauth-bridge.md#callback-document) exclusively defines +ingress. + +Callback accepts the Application connection using the ceremony ID extracted +from the captured `state`. This authenticates the Application against the +Bridge's deployment allowlist before the return can leave Callback. It sends +`Event(authorization, finished)` before navigating onward. This reports return +and connection readiness, not permission granted, and carries no OAuth-return +data. Callback need not wait for an acknowledgement or Application scheduling +before the Prover transition. + +#### 3. Callback to Prover + +The popup-side [Callback](#callback-get-redirecturi) endpoint asks its connection +to navigate to the frozen [Prover](#prover-get-prover) location, supplying the +ceremony ID, authenticated Application origin, and captured query/fragment as +that route's structured fragment. +Callback owns this transition to keep the return private from Application and +because the OAuth Platform may have severed Application's direct popup handle. + +Prover captures and clears the fragment, then accepts the same logical +Application connection restricted to the carried origin under the +[origin policy](#origin-policy). It sends [`Event(prover, started)`](#event) only after +cross-origin isolation is established and its CCDP handlers are installed. +Connection establishment and any internal isolation transition are below CCDP: +neither introduces another participant, message type, or phase. On the +replacement path, the connected Prover first reports `prover-fallback` as +specified in [Fallback timing](#fallback-timing). The captured parameters +survive that transition without passing through Application. + +Application accepts one `Event(prover, started)` and sends one +[`ProveIdentity`](#proveidentity) using its frozen configuration and code +verifier. It does not receive or parse the OAuth return. On receiving +`ProveIdentity`, the selected platform/version validates the retained return +before credential use. A valid denial sends +[`Cancel`](#cancel); malformed or mismatched input sends +[`Abort`](#abort). Both end the run in this phase, as does +Application cancellation. Only valid OAuth acceptance enters Phase 4. + +#### 4. Prover execution + +This phase begins only after [Prover](#prover-get-prover) has validated and +accepted the OAuth return in Phase 3. It performs the selected profile's token +exchange, notarization, and proof-generation steps as applicable. It sends the +applicable [core operation events](#core-events) and may add implementation or +platform events. Each operation's `finished` reports only that operation; +events from overlapping operations are not forced into a global order. +After all required proof and evidence work completes, Prover sends one +[`IdentityProof`](#identityproof), unless it sends +[`Abort`](#abort) or receives +[`Cancel`](#cancel). The first terminal outcome—proof +delivery, abort, or cancellation—ends the phase; later messages have no effect. + +### Terminal outcomes + +- REQ-CCDP-08 (upholds SP-CCDP-01): Each Participant MUST treat the first valid + terminal outcome as final and perform the cleanup described below without + closing the popup or fabricating a successful operation finish. + +Terminal processing begins when the Application cancels an active Callback +or Prover; Prover reports valid OAuth denial; an active document reports an +abort; or Prover delivers a proof. These outcomes are mutually terminal even +when they race in transit. +Cancellation has no acknowledgement. Prover's valid denial resolves denied; +Application cancellation retains its local canceled outcome. An observable +abort rejects the live ceremony; a failure before connection acceptance is +reported locally. Application structurally validates the delivered identity and +selected platform/version proof and assembles its result before recording +`prover.finished` locally. Neither endpoint adds local cryptographic proof or +attestation verification; ledger verification remains authoritative. + +Early failure, denial, and cancellation do not fabricate `prover.finished`; +late traffic cannot reactivate the ceremony. Application-local event/status APIs +are outside this protocol. + +CCDP initiates no further navigation: the Application composition alone decides +whether to retain, navigate, or close the popup because any subsequent flow is +outside CCDP. Terminal cleanup follows the [invariants](#invariants). + +### Sequence (informative) + +The popup lifeline is one browsing context whose current document is replaced +at every navigation; it does not imply shared document state. + +```mermaid +sequenceDiagram + participant A as Application + participant P as Ceremony popup + + Note over A,P: Phase 1 - Prefetch to Authorization + Note over A: Local prefetch-dispatch.started + A->>P: Navigate to Prefetch + P->>P: Prefetch accepts connection + P->>P: Prefetch registers Worker and dispatches selected-profile fetches + break Prefetch setup fails + P-->>A: Abort + end + P-->>A: Event(prefetch-dispatch, finished) + Note over A: Local authorization.started + A->>P: Navigate away to Authorization + + Note over A,P: Phase 2 - Authorization to Callback + Note over P: User completes login and consent in Authorization + P->>P: OAuth Platform redirects to redirectUri + P->>P: Callback starts and selects its bundled version + P->>P: Callback accepts authenticated connection + break Callback fails after connection acceptance + P-->>A: Abort + end + P-->>A: Event(authorization, finished) + + Note over A,P: Phase 3 - Callback to Prover + P->>P: Callback navigates to Prover with private return fragment + P->>P: Prover accepts connection with isolation established + opt Isolation replacement occurred + P-->>A: Event(prover-fallback), original navigation timestamp + end + P-->>A: Event(prover, started) + break Application cancels + A-->>P: Cancel + end + A-->>P: ProveIdentity + + P->>P: Validate retained OAuth return + break Valid OAuth denial + P-->>A: Cancel + end + break Invalid OAuth return + P-->>A: Abort + end + Note over A,P: Phase 4 - Prover execution after OAuth acceptance + loop Applicable operations, possibly overlapping + P-->>A: Event(operation, started or finished) + end + break Prover fails + P-->>A: Abort + end + P-->>A: IdentityProof + Note over A: Validate structure and assemble result + Note over A: Local prover.finished, completed +``` + +Terminal exits are shown without their cleanup details, which follow +[Terminal outcomes](#terminal-outcomes) and the [message contracts](#messages). +Carrier mechanics and proof-generation internals are omitted. diff --git a/specs/libid.md b/specs/libid.md index 38c9f68a..817cd1db 100644 --- a/specs/libid.md +++ b/specs/libid.md @@ -22,6 +22,19 @@ specifications. protocols cite it instead of restating opener, isolation, and continuity mechanics. +## Browser ceremony and services + +- [Ceremony Cross-Document Protocol](ccdp.md) defines its documents, routes, + private navigation inputs, messages, events, and phases over popup transport. +- [OAuth Bridge](oauth-bridge.md) defines public configuration, callback ingress, + and the confidential GitHub token-exchange HTTP contract. +- [CCDP Distribution](ccdp-distribution.md) defines static resource responses, + Callback configuration insertion, isolation policies, and compatible publication. + +These chapters are normative browser/service boundaries. TypeScript APIs, build +tooling, UI projections, dependency pins, and qualification evidence belong to +the implementation documentation, not this specification. + ## System model and specification ownership libID turns an identity-platform authorization into a proof that a Consumer @@ -52,8 +65,10 @@ Authorized Transaction Data, and client identifier, and the Consumer decides what that transaction means. [Common §5.1](ceremony-common.md#51-verification-path) owns this path. -The application operator controls its frontend, redirect deployment, OAuth -clients, and GitHub Token Service, but is not trusted to choose identity fields, +The Application, OAuth Bridge, and CCDP Distribution may have different operators. +They control their frontend, redirect deployment, OAuth registrations, token +service, and distributed browser code respectively. Those deployments are +trusted for the local browser ceremony, but not to choose authoritative identity fields, change the proof-bound operation, or widen proof validity. The identity platform controls the authenticated account response. The notary authenticates X/GitHub transcripts and their creation times. Verifier governance selects accepted @@ -64,6 +79,8 @@ authenticates the Transaction Author and supplies its Chain ID and Block Time. |---|---|---|---| | User | chooses an account and authorizes an operation | human intent | parsing or cryptographic verification | | Application operator | configures clients and deployment assets; starts or withholds work | deployment availability and declared configuration | identity fields, proof target, or proof validity | +| OAuth Bridge operator | holds OAuth client registrations and any confidential secret; configures and serves Callback; exchanges GitHub codes | correct Callback configuration, credential handling, and availability | ledger identity, digest, notary-key, or validity decisions | +| CCDP Distribution publisher | supplies browser code, proving assets, and response policies to multiple Bridges | correct code and asset supply under ASM-CCDP-01 | authority to change ledger verification rules | | Identity-platform operator | authenticates accounts and issues signed or TLS-authenticated responses | the `ASM-PROV-*` behavior the selected profile cites | the proof-bound transaction or Transaction Author | | Notary operator | operates the X/GitHub attestation key and observes sessions | `ASM-NOTARY-01` | user intent or transaction authorization | | Verifier governance administrator | activates verifier artifacts, trust roots, parameters, and the Supported Version Set | correct authority lifecycle | user consent | @@ -91,7 +108,10 @@ root and verifier. | Chain ID, Transaction Author, Block Time, and transaction-data encoding | consumer protocol Chain Profile | | Platform endpoints, fields, trust roots, and proof projections | [Identity-platform ceremonies](platform-ceremonies.md) | | Popup origin allowlists, message model, delivery, navigation, closure, and continuity guarantees | [Popup transport](popup-transport.md) | -| Redirect transport, interruption behavior, and UI control flow | browser architecture | +| Ceremony documents, routes, private fragments, messages, events, and phase transitions | [CCDP](ccdp.md) | +| Public ceremony configuration, callback ingress, GitHub token HTTP API | [OAuth Bridge](oauth-bridge.md) | +| Static response policies, aggregate Callback artifact, immutable asset publication | [CCDP Distribution](ccdp-distribution.md) | +| Package APIs, UI projections, build tooling, and qualification evidence | implementation documentation (non-normative) | | Transaction dispatch and author authentication | Consumer protocol | | Verification dispatch, replay recording, trust roots, and version governance | [Common ceremony rules](ceremony-common.md) | diff --git a/specs/oauth-bridge.md b/specs/oauth-bridge.md new file mode 100644 index 00000000..ccd767fa --- /dev/null +++ b/specs/oauth-bridge.md @@ -0,0 +1,392 @@ +# OAuth Bridge Server + +This document defines the HTTP and deployment contract for the OAuth bridge +used by CCDP. The bridge publishes platform +configuration, serves the OAuth callback document, and performs the one +confidential platform exchange required by GitHub. + +[CCDP](ccdp.md) owns the browser flow. The +[common](ceremony-common.md) and [platform](platform-ceremonies.md) ceremony rules +own authorization, platform-return, token-exchange, and proof semantics. +This document owns only the Bridge's public HTTP and deployment contract. + +The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", +"SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and +"OPTIONAL" in this document are to be interpreted as described in +BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all +capitals, as shown here. + +## Boundary + +The OAuth bridge owns: + +- OAuth application registrations, public client IDs, and confidential client + credentials; +- the public ceremony configuration; +- callback ingress, response policy, and deployment inputs; and +- GitHub's confidential token exchange and token attestation when GitHub is + enabled. + +For Callback, the bridge is a configuration-inserting, cached proxy to the +[CCDP Distribution](ccdp-distribution.md#callback-artifact). It neither +implements the document nor requires a TypeScript build. It serves no other +CCDP resources and stores no ceremony state. Google and X require no +confidential bridge route. + +The bridge and Distribution may be cross-site and independently operated; +both are code-supply-chain trust boundaries. The Distribution requires no +Bridge registration and receives no application allowlists. + +## Deployment configuration + +- REQ-BRIDGE-01 (upholds SP-CCDP-01): The Bridge MUST derive its fixed redirect + URI, effective origin allowlist, and Callback inputs from the deployment + configuration below, not incoming requests. + +One bridge deployment has these inputs. Every origin follows the +[CCDP origin policy](ccdp.md#origin-policy), including HTTP on exact +`localhost` and `127.0.0.1` hosts: + +| Input | Contract | +|---|---| +| Bridge origin | Canonical origin used by every bridge route and the derived OAuth redirect URI | +| `allowedAppOrigins` | Nonempty, duplicate-free set of canonical application origins admitted by the bridge | +| CCDP origin | One canonical origin selected by the operator; defaults to `https://lib.id` when omitted | +| Platform profiles | Public OAuth client ID and supported ceremony versions for each enabled platform | +| Callback inputs | One unversioned list `[allowedOrigins, ccdpOrigin]` derived from the values above, plus deployment-policy sources required by the [artifact contract](ccdp-distribution.md#configuration-insertion); no separate input configuration or CCDP version list | +| GitHub settings | Client secret, token endpoint settings, and the notary's native MPC-TLS listener port when GitHub is enabled; the notary host remains request-selected | + +Every enabled platform's OAuth registration uses +`{bridgeOrigin}/auth/callback` as its `redirect_uri`. The path is fixed, not a +deployment option. The Bridge uses this same derived URL for its confidential +token exchange; no separate redirect URI is configured. + +`allowedAppOrigins` has no protocol maximum. A duplicate or invalid member is a +deployment error rather than something the bridge normalizes. After resolving +the default or configured `ccdpOrigin`, the bridge derives one effective set: +`allowedOrigins = allowedAppOrigins ∪ {ccdpOrigin}`. Adding an already-listed +CCDP origin does not duplicate it. When `ccdpOrigin` is omitted, this adds +`https://lib.id`; when overridden, only the replacement is added automatically. +`https://lib.id` then remains allowed only if explicitly listed. + +The effective set governs configuration GET, callback connection authentication, +and GitHub preflight/POST admission. It is embedded into Callback, not separately +configured, and never inferred from a request's `Origin`, `Referer`, query, +fragment, or body. + +The CCDP origin is likewise deployment data. It is returned to the +application in public configuration and embedded into the callback document so +Callback can navigate the popup to Prover. The bridge also resolves the fixed +Callback artifact path against it; no separate Callback artifact URL is +configured. Omitting it selects the canonical `https://lib.id` +Distribution. + +An all-HTTP local setup can use Application `http://localhost:3000`, Bridge +`http://localhost:3001`, and CCDP `http://localhost:8787`. The Bridge configures +the Application in `allowedAppOrigins`, adds the CCDP origin to `allowedOrigins`, +serves Callback at `http://localhost:3001/auth/callback`, and retrieves +`http://localhost:8787/ccdp/callback.html` without requiring a local certificate. +Callback receives that same HTTP allowlist and CCDP origin; the client, +Callback, Prover, and their popup connections must not reject them solely for +using HTTP. Notary selection remains with the Application, not this config. +Platform acceptance of the registered redirect URI is a separate prerequisite. + +The Bridge injects the same input list regardless of CCDP version. New versions +with compatible inputs work on artifact refresh without a Bridge rebuild or +configuration change. Callback owns browser version selection; the Bridge +neither enumerates versions nor reads input declarations from the artifact. +Input-contract versioning and Bridge awareness are introduced only if that +contract actually becomes incompatible, not for an ordinary CCDP version bump. + +One platform configuration generates both the public profile entries and the +OAuth registrations used by the callback. The bridge advertises only +platform/version pairs supported by its selected CCDP Distribution. Selecting +a shared Distribution requires no reciprocal configuration; the effective +allowlist automatically admits its origin at this Bridge's GitHub endpoint. + +## Route surface + +- REQ-BRIDGE-02 (upholds SP-CCDP-01): The Bridge MUST enforce the route, + method, and origin-admission rules below before performing route work. + +The bridge exposes only: + +| Method | Route | Availability | Purpose | Origin enforcement | +|---|---|---|---|---| +| `GET` | `/api/v1/ceremony/config` | always | public platform and CCDP configuration | exact `Origin` in `allowedOrigins`; absent `Origin` accepted only by the same-origin rule below | +| `GET` | `/auth/callback` | always | complete OAuth Callback document | none at HTTP ingress; callback authenticates its popup connection after clearing its input | +| `OPTIONS`, `POST` | `/api/v1/ceremony/github-token` | only when GitHub is enabled | confidential GitHub token exchange and token attestation | exact request `Origin` in `allowedOrigins`, checked on every request; exact noncredentialed CORS | + +Top-level navigation may omit `Origin`, and an OAuth-platform callback may +identify the platform rather than the application. `Referer` is never an +authority input. The callback document is therefore public and +request-invariant; its browser protocol authenticates the application after it +loads. + +No separate Callback script, prover, proving-asset, preparation, continuation, +polling, status, result, cancellation, browser TLS bridge, or proof-recovery +route exists on the OAuth bridge. Unsupported methods fail without route work. +Except for the OAuth-platform-mandated callback query and the GitHub JSON +request, bridge routes accept no query or request body. + +The `v1` in `/api/v1/ceremony/...` versions the bridge's JSON API. +`/auth/callback` is a browser protocol document; its embedded code +selects `CCDPVersion` from OAuth `state`. +`PlatformCeremonyVersion` independently versions one platform ceremony. There +is no request-time version negotiation. + +## Public configuration + +- REQ-BRIDGE-03: The Bridge and Application MUST produce and validate the + configuration record and response policy below. Necessity: each ceremony + must freeze one mutually supported profile and Distribution. + +`GET /api/v1/ceremony/config` returns `application/json` with this exact record: + +```ts +interface PlatformConfig { + clientId: string + ceremonyVersions: readonly number[] // unsigned 16-bit integers +} + +interface CeremonyConfig { + ccdpOrigin: string + platforms: Readonly> +} +``` + +The response rules are: + +- `PlatformCeremonyVersion` is an unsigned 16-bit integer. +- `ccdpOrigin` is the configured canonical origin under the + [origin policy](ccdp.md#origin-policy), with no credentials, path, query, or + fragment. The Application accepts the localhost HTTP exception for this field and + the configured Bridge origin. +- Each platform entry has one public client ID and a nonempty, duplicate-free + list of supported ceremony versions. List order has no meaning. +- Unknown fields, malformed URLs, and unsupported numeric representations are + invalid. A platform absent from the client's closed local catalog is ignored; + known entries remain exact-validated before use. +- The record contains no redirect URI, secret, allowlist, artifact URL, CSP source, + notary setting, platform display metadata, or application-specific value. + +When present, `Origin` must exactly match an `allowedOrigins` member. A +successful cross-origin response sets that exact origin in +`Access-Control-Allow-Origin`, permits no credentials, and never uses `*`. +A same-origin browser GET may omit `Origin`: accept that case only when +`Sec-Fetch-Site` is `same-origin` and the Bridge's configured public origin is +itself in `allowedOrigins`. It needs no CORS response header. Do not infer +admission from `Referer`, the request host, or absent Fetch Metadata; an explicit +invalid, `null`, or unlisted `Origin` always fails. + +Both cases use `Content-Type: application/json`, `Cache-Control: no-store`, +`Vary: Origin, Sec-Fetch-Site`, and `X-Content-Type-Options: nosniff`. Rejected +requests return no configuration. These browser admission checks do not make +the public record a secret from non-browser clients. Request values do not +alter the response record. + +The Application fetches and validates this record without credentials. It derives `redirectUri` as +`new URL('/auth/callback', oauthBridge).href` from its validated canonical +OAuth Bridge origin, not from the response. It freezes the selected client ID, +derived redirect URI, CCDP origin, and mutually supported platform ceremony +version in each live ceremony. CCDP browser [resources](ccdp.md#documents-and-routes) +never fetch bridge configuration; server-side Callback artifact retrieval is +separate. + +## Callback document + +- REQ-BRIDGE-04 (upholds SP-CCDP-01): The Bridge MUST retrieve, configure, and + serve Callback as specified below without forwarding OAuth ingress data to + the Distribution. + +`GET /auth/callback` serves Callback on the bridge origin, without +an HTTP redirect. Its [artifact contract](ccdp-distribution.md#callback-artifact) +owns the HTML, configuration slot, response policy, browser startup, version +selection, and failure UI. The bridge only: + +- retrieves `{ccdpOrigin}/ccdp/callback.html` at startup and revalidates it + independently of callback requests, rejecting upstream redirects; +- sends no callback query, OAuth return, incoming request headers, cookies, or + credentials upstream; the configured source never depends on a request; +- validates and inserts its unversioned input list using the artifact contract, + then publishes the completed HTML and matching response headers atomically; +- serves the cached result until a valid replacement is ready; a failed + refresh retains the last valid result, or returns an inert unavailable + response when none exists; and +- suppresses or redacts callback query strings throughout its ingress proxies, + access logs, traces, analytics, metrics labels, and errors. + +Compatible artifact updates require no bridge rebuild. The bridge neither +parses OAuth state nor selects a CCDP implementation, generates browser code, +or handles protocol errors. Google fragments never reach this server; +platform-mandated callback queries are not forwarded to the Distribution. + +## GitHub token endpoint + +- REQ-BRIDGE-05 (upholds SP-CCDP-01): The Bridge MUST enforce the exact token + request, destination-admission, and response contract below; the Prover MUST + validate its response before dependent identity notarization. + +When GitHub is enabled, `POST /api/v1/ceremony/github-token` performs the +confidential OAuth token exchange and token MPC-TLS session synchronously. It +retains no state. The prover derives this fixed route from the origin of the +Application's frozen `redirectUri` in `ProveIdentity`; the prover document +does not embed it. + +The ceremony's browser caller is Prover on `ccdpOrigin`, but this route uses the +same `allowedOrigins` rule as configuration and Callback: configured application +origins are also admitted. The requested `notaryAddress` grants no caller +admission. + +The bridge API version implements GitHub ceremony version `1` only. The request +carries no ceremony-version field, and configuration must not advertise a +GitHub version requiring different token-service semantics. Such a change +requires a new bridge API version. + +```ts +interface TokenRequest { + code: string + codeVerifier: string + notaryAddress: string +} + +interface TokenResponse { + accessToken: string + tokenAttestation: { + attestedData: string // canonical unpadded base64url + signature: string // canonical unpadded base64url + } + bearerOpening: string // canonical unpadded base64url +} +``` + +Both records are UTF-8 JSON objects. Member order and insignificant whitespace +have no meaning. Duplicate, missing, additional, or wrongly typed members are +invalid. The versioned route carries no redundant schema field. + +`code` is nonempty printable ASCII without whitespace or control bytes and at +most 1,024 bytes. `codeVerifier` matches `[A-Za-z0-9_-]{43}`. The bridge does +not normalize either value. + +`notaryAddress` follows the canonical origin rules in +[`ProveIdentity`](ccdp.md#proveidentity), including its localhost HTTP exception; +credentials, paths, queries, and fragments remain forbidden. Prover forwards the address frozen by Application and uses that same address +for identity notarization. Both sessions select the same Notary Service, but +use different transports: + +- The Bridge uses native MPC-TLS over a TCP connection to the supplied host and + the configured MPC listener port. It opens the pinned GitHub platform socket + itself and performs the confidential token exchange jointly with the notary. +- The browser uses Proxy mode over the selected service's `/notarize-proxy` + WebSocket for `/user`, preserving the supplied origin's scheme and effective + port under the browser adapter's HTTPS-to-WSS or local HTTP-to-WS mapping. + +The origin's scheme and HTTP port describe the browser-facing service, not the +native MPC listener. The Bridge does not derive a Proxy WebSocket or interpret +that HTTP port as its MPC port. Listener configuration selects only the native +port, not a replacement notary host or ledger profile. Failure never switches +notary or mode. Callback configuration and public `CeremonyConfig` carry no +notary selection. + +This server-side destination is request-controlled. Origin/CORS checks do not +authenticate non-browser callers or replace egress controls. The Bridge must +prevent access to private/internal destinations, including through DNS +resolution, unless its operator explicitly permits the development destination. +The localhost HTTP exception removes the TLS requirement, not these egress +checks. Loopback is the Bridge's machine, which may differ from the browser's; +a development setup must make the intended notary reachable by both. Canonical +origin syntax alone is insufficient; redirects remain forbidden. + +`accessToken` is nonempty printable ASCII without whitespace or control bytes +and at most 128 bytes, matching GitHub v1's bearer circuit. Both Bridge and +Prover enforce this bound before dependent identity notarization; an oversized +token fails rather than being truncated. `attestedData` decodes to a nonempty +byte-exact record of at most 2 MiB; `signature` decodes to exactly 65 bytes; and `bearerOpening` +decodes to exactly 16 bytes. Every byte string uses canonical unpadded base64url. +The encoded response body is at most 3 MiB. + +The returned fields are one correlated result: the uniquely framed bearer +commitment in `tokenAttestation.attestedData` equals +`SHA256(accessToken || bearerOpening)`. The bridge preserves signed attestation +bytes exactly. Before its dependent identity notarization, the prover validates +the response encoding, correlation, and open request bindings. Neither Prover +nor Application performs local notary-signature verification. A structurally valid +forged signature is not detected by these checks alone; downstream verification +of the original bytes against trusted notary keys remains authoritative and +mandatory. + +The endpoint contract is: + +- the query is empty and the request media type is exactly `application/json`; +- every preflight and POST carries one valid `Origin` exactly matching an + `allowedOrigins` member; missing, `null`, malformed, multiple, or unlisted origins + reject before notary resolution, connection, or token exchange. Successful + preflight never substitutes for checking the actual POST; +- successful preflight admits only `POST` and `Content-Type`, uses no + credentials, and returns no ceremony data; +- malformed UTF-8, JSON, or fields fail before token exchange; +- client ID, client secret, and the GitHub TLS/request target come only from + bridge configuration; `redirect_uri` is derived from the configured Bridge + origin and fixed `/auth/callback` path. Only the notary destination comes from + the validated request above; +- redirects are rejected and request duration and response size are bounded; +- success is status `200` with exact noncredentialed CORS, + `Content-Type: application/json`, `Cache-Control: no-store`, and one bounded + `TokenResponse`; +- credentials and OAuth-platform-return values never enter logs, traces, analytics, + metrics labels, or error bodies; and +- failure returns no partial credential, attestation, or caller-selected + diagnostic content. + +Timeout, duplicate request, restart, or response loss leaves no bridge record. +There is no action ID, polling route, progress stream, or response recovery; a +lost response requires a fresh ceremony. + +## Compatibility + +- REQ-BRIDGE-06: The Bridge MUST preserve the public API version's semantics + when refreshing compatible Callback artifacts. Necessity: CCDP version + selection must not silently change the confidential exchange contract. + +A breaking JSON request or response changes the bridge API version. CCDP, +platform ceremony, prover release, and popup connection versions remain +independent; the [CCDP version rules](ccdp.md#paths-and-versioning) do not +version the Bridge's API. + +## Security Considerations + +The Bridge participates in SP-CCDP-01 under ASM-CCDP-01 and ASM-CCDP-02. +Its operator supplies the OAuth registrations, confidential client secret, and +Callback deployment policy. Its code and deployment are trusted for correct +browser code delivery and credential handling, not for ledger acceptance. + +CORS and Origin checks protect browser admission, not non-browser +authentication. The request-selected notary address therefore retains the +egress restrictions above. A notary URL is a routing choice, never authority to +select a trusted attestation key. Confidential token exchange is the irreversible +boundary: denial, timeout, or response loss cannot unspend an OAuth code. + +Structural and commitment checks do not authenticate notary signatures. +A forged but structurally consistent result can reach proof generation; the +Ledger Verifier still authenticates the original signed bytes against its trusted +keys. Common and platform ceremony rules own that verification and all replay, +freshness, and proof-soundness properties. + +## Conformance + +A Bridge implements configuration and Callback; its GitHub route is required +only when that platform is enabled. These tests cover the HTTP boundary, not +cryptographic soundness. + +- TEST-BRIDGE-01 (exercises REQ-BRIDGE-01): + Default Distribution origin is added once; a replacement is added instead. Invalid/duplicate configured origins fail, and HTTP loopback works at any port. +- TEST-BRIDGE-02 (exercises REQ-BRIDGE-02): + Disallowed and missing origins reject according to each route's rules before dependency work; explicit bad Origin cannot use the same-origin GET exception. +- TEST-BRIDGE-03 (exercises REQ-BRIDGE-03): + Exact public config has no redirect field, secrets, or notary selection; malformed known profiles reject and unknown platforms are ignored. +- TEST-BRIDGE-04 (exercises REQ-BRIDGE-04): + Callback queries/cookies/headers never reach the artifact request; failed refresh preserves the last valid HTML/policy pair, or serves inert unavailability. +- TEST-BRIDGE-05 (exercises REQ-BRIDGE-05): + Actual POST origin is checked after preflight; malformed JSON, oversize token, bad canonical bytes, forbidden notary destination, and mismatched opening reject. Native MPC uses the requested host and configured listener port, not Proxy transport. +- TEST-BRIDGE-06 (exercises REQ-BRIDGE-06): + Compatible bundled Callback updates need no Bridge rebuild; unsupported browser versions fail locally and do not change the Bridge API version. From 0fd7f0a91acf560d0f5b2bef70c7a75744a87150 Mon Sep 17 00:00:00 2001 From: Wondertan Date: Sun, 13 Sep 2026 02:39:16 +0200 Subject: [PATCH 16/20] docs(ccdp): separate denial from local cancellation Report valid OAuth denial with one-way Denied. Application cancellation retires the local run; popup navigation and closure stay composition-owned. Assisted-by: GPT-5 Signed-off-by: Wondertan --- specs/ccdp.md | 74 ++++++++++++++++++++++++--------------------------- 1 file changed, 35 insertions(+), 39 deletions(-) diff --git a/specs/ccdp.md b/specs/ccdp.md index b2a78dd4..5a61659c 100644 --- a/specs/ccdp.md +++ b/specs/ccdp.md @@ -289,15 +289,16 @@ The following table is the complete CCDP version-1 message set. |---|---|---|---| | [`ProveIdentity`](#proveidentity) | Application → Prover | `Event(prover, started)` | exactly once; selects the profile for OAuth validation and proof execution | | [`IdentityProof`](#identityproof) | Prover → Application | `ProveIdentity` and valid OAuth acceptance | at most once; ends the Prover run | -| [`Cancel`](#cancel) | Application → Callback or Prover; Prover → Application | active connection for Application cancellation; `ProveIdentity` and valid OAuth denial for Prover cancellation | at most once; ends the run without a technical error | +| [`Denied`](#denied) | Prover → Application | `ProveIdentity` and valid OAuth denial | at most once; reports platform denial without a technical error | | [`Abort`](#abort) | Prefetch, Callback, or Prover → Application | connection acceptance | at most once; reports technical failure and ends the run | | [`Event`](#event) | Prefetch, Callback, or Prover → Application | connection acceptance and the event's documented emission point | core occurrences follow the [event catalog](#core-events); additional observations do not advance the protocol | Every recipient requires a plain record with the exact fields, types, and bounds defined below. Unknown fields, coercion, normalization, defaults, and unrecognized discriminators are invalid. Messages outside the listed direction, -predecessor, and cardinality are invalid. Cancellation, proof delivery, and -abort make later messages inert even when they race in transit. +predecessor, and cardinality are invalid. Denial, proof delivery, abort, and +Application-local cancellation make later messages inert even when they race +in transit. ### ProveIdentity @@ -376,28 +377,23 @@ a nested identity copy. CCDP treats the proof as opaque; adding a platform does not change this message. Neither browser endpoint cryptographically verifies the delivered result; identity is non-authoritative until ledger verification. -### Cancel +### Denied ```ts -interface Cancel { - type: 'cancel' +interface Denied { + type: 'denied' } ``` -`Cancel` is a parameterless, bidirectional terminal message: +`Denied` reports only a valid, ceremony-bound OAuth-platform denial discovered +by Prover while validating `ProveIdentity`. The Application resolves +`{ status: 'denied' }`. Prover sends it before token exchange, proof execution, +or proving-operation events, never as a substitute for a failure. -- Application → Callback or Prover stops reachable work after explicit - cancellation or retirement of Application authority. -- Prover → Application reports only a valid, ceremony-bound OAuth-platform - denial discovered while validating `ProveIdentity`. The Application - resolves `{ status: 'denied' }`. Prover sends it before token exchange, - proof execution, or proving-operation events, never as a substitute for a - failure. - -Malformed, mismatched, or otherwise invalid OAuth returns use -`Abort`, not cancellation. An Application which has already canceled -ignores a racing denial or proof. Cancellation has no acknowledgement; -recipients clear reachable input but do not close or navigate the popup. +Malformed, mismatched, or otherwise invalid OAuth returns use `Abort`, not +`Denied`. Denial has no acknowledgement and does not close or navigate the +popup. Application cancellation is local, not a CCDP message; see +[Terminal outcomes](#terminal-outcomes). ### Abort @@ -594,13 +590,13 @@ cryptographic properties delegated to the common and platform specifications. - TEST-CCDP-04 (exercises REQ-CCDP-04): Public Prefetch authenticates its exact peer; Callback rejects an unlisted Application; Prover rejects a different origin, including one occupying the same retained window after navigation. Canonical HTTP loopback works at arbitrary ports. - TEST-CCDP-05 (exercises REQ-CCDP-05): - Malformed, duplicated, wrong-direction, out-of-state, and post-terminal records cause no authorized action. Proof payloads are structurally checked under the selected platform version. + Malformed, duplicated, wrong-direction, out-of-state, and post-terminal records cause no authorized action. Legacy `cancel` records and Application-sent `Denied` are invalid. Proof payloads are structurally checked under the selected platform version. - TEST-CCDP-06 (exercises REQ-CCDP-06): Only the designated core occurrences open gates; extensions cannot do so. Overlap, occurrence timestamps, and retrospective fallback timing are preserved. - TEST-CCDP-07 (exercises REQ-CCDP-07): The four phases preserve navigation ownership and credential privacy; approval enters execution, bound denial exits before proving, and malformed returns abort. - TEST-CCDP-08 (exercises REQ-CCDP-08): - Cancel/Abort/IdentityProof races settle once; errors are text-only, excluded from exported events, and undeliverable failures have a fixed local diagnostic. + Denied/Abort/IdentityProof and local-cancellation races settle once. Local cancellation sends no CCDP message; subsequent composition-owned navigation or closure cannot let late traffic revive the run. Errors are text-only, excluded from exported events, and undeliverable failures have a fixed local diagnostic. ## Protocol @@ -726,9 +722,9 @@ Application accepts one `Event(prover, started)` and sends one verifier. It does not receive or parse the OAuth return. On receiving `ProveIdentity`, the selected platform/version validates the retained return before credential use. A valid denial sends -[`Cancel`](#cancel); malformed or mismatched input sends +[`Denied`](#denied); malformed or mismatched input sends [`Abort`](#abort). Both end the run in this phase, as does -Application cancellation. Only valid OAuth acceptance enters Phase 4. +context loss. Only valid OAuth acceptance enters Phase 4. #### 4. Prover execution @@ -739,10 +735,9 @@ applicable [core operation events](#core-events) and may add implementation or platform events. Each operation's `finished` reports only that operation; events from overlapping operations are not forced into a global order. After all required proof and evidence work completes, Prover sends one -[`IdentityProof`](#identityproof), unless it sends -[`Abort`](#abort) or receives -[`Cancel`](#cancel). The first terminal outcome—proof -delivery, abort, or cancellation—ends the phase; later messages have no effect. +[`IdentityProof`](#identityproof), unless failure or context loss ends its work. +Observable failure sends [`Abort`](#abort). Prover accepts no second proof +request; Application-local cancellation makes any later delivery inert. ### Terminal outcomes @@ -750,18 +745,22 @@ delivery, abort, or cancellation—ends the phase; later messages have no effect terminal outcome as final and perform the cleanup described below without closing the popup or fabricating a successful operation finish. -Terminal processing begins when the Application cancels an active Callback -or Prover; Prover reports valid OAuth denial; an active document reports an -abort; or Prover delivers a proof. These outcomes are mutually terminal even -when they race in transit. -Cancellation has no acknowledgement. Prover's valid denial resolves denied; -Application cancellation retains its local canceled outcome. An observable -abort rejects the live ceremony; a failure before connection acceptance is -reported locally. Application structurally validates the delivered identity and +Prover's `Denied` reports valid OAuth denial; an active document's `Abort` +reports failure; and `IdentityProof` delivers a proof. These outcomes are +mutually terminal even when they race in transit. Denial resolves denied; +an observable abort rejects the live ceremony. A failure before connection +acceptance is reported locally. Application structurally validates the delivered identity and selected platform/version proof and assembles its result before recording `prover.finished` locally. Neither endpoint adds local cryptographic proof or attestation verification; ledger verification remains authoritative. +The Application may cancel locally at any point, settling its run and ignoring +late events or results before the composition navigates or closes the popup +through popup transport. It sends no CCDP message and waits for no +acknowledgement. Navigation or closure retires the current document; local +cancellation alone does not stop remote proving. Neither path guarantees +cancellation of already-dispatched server work. + Early failure, denial, and cancellation do not fabricate `prover.finished`; late traffic cannot reactivate the ceremony. Application-local event/status APIs are outside this protocol. @@ -809,14 +808,11 @@ sequenceDiagram P-->>A: Event(prover-fallback), original navigation timestamp end P-->>A: Event(prover, started) - break Application cancels - A-->>P: Cancel - end A-->>P: ProveIdentity P->>P: Validate retained OAuth return break Valid OAuth denial - P-->>A: Cancel + P-->>A: Denied end break Invalid OAuth return P-->>A: Abort From 01d5fb0eb729f36f618998f0e3cc648a63abe9e8 Mon Sep 17 00:00:00 2001 From: Wondertan Date: Sun, 13 Sep 2026 15:48:12 +0200 Subject: [PATCH 17/20] docs(ccdp): group event instrumentation fields Define Event as an interface like the other messages. Nest operationId and attributes under optional instrumentation and preserve their validation and protocol boundaries. Assisted-by: GPT-5 Signed-off-by: Wondertan --- specs/ccdp.md | 47 +++++++++++++++++++++++++++++++---------------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/specs/ccdp.md b/specs/ccdp.md index 5a61659c..b31ef694 100644 --- a/specs/ccdp.md +++ b/specs/ccdp.md @@ -425,6 +425,19 @@ ceremony outcome. ### Event +```ts +interface Event { + type: 'event' + event: string + phase?: 'started' | 'finished' + timestamp: number + instrumentation?: { + operationId?: string + attributes?: Record + } +} +``` + - REQ-CCDP-06: Each Participant MUST emit and interpret the core events, extension boundary, and timestamps defined below. Necessity: readiness gates and observations must have the same meaning at independently deployed endpoints. @@ -432,16 +445,18 @@ ceremony outcome. One event stream carries protocol readiness, operation timing, and additional platform observations. The same observations can drive UI or tracing; they do not require separate wire protocols. `Event(name, phase)` below abbreviates -this record, not a distinct message type: +this record, not a distinct message type. -| Field | Contract | -|---|---| -| `type` | Exactly `event` | -| `event` | Nonempty core or implementation-defined operation name | -| `phase` | `started` or `finished` for an operation; omitted for a single-shot observation | -| `timestamp` | Finite, nonnegative occurrence time in milliseconds on the browser's epoch-relative performance timeline | -| `operationId` | Optional, nonempty instrumentation-only identifier pairing repeated concurrent instances of the same operation | -| `attributes` | Optional bounded record of event-defined scalar measurements or facts: strings, finite numbers, or booleans | +`event` is a nonempty core or implementation-defined operation name. `phase` +marks an operation's start or finish and is omitted for a single-shot +observation. `timestamp` is a finite, nonnegative +occurrence time in milliseconds on the browser's epoch-relative performance +timeline. + +`instrumentation` is an optional plain record containing only the optional +fields shown above. Its `operationId` is a nonempty identifier pairing repeated +concurrent instances of the same operation. Its `attributes` is a bounded plain +record of event-defined scalar measurements or facts; numeric values are finite. Optional fields are absent when unused, not null. Event names, attribute names, string values, and record sizes are bounded by the implementation. Names and @@ -472,7 +487,8 @@ inapplicable operation emits nothing. `prover-fallback` is the only single-shot core event. Each core operation has one start and, on success, one finish per ceremony; these occurrences omit -`operationId`. X uses all six proving operations. GitHub omits `token-fetch`: +`instrumentation.operationId`. X uses all six proving operations. GitHub omits +`token-fetch`: its `token-attestation` covers the complete Bridge request returning both the token and its attestation. Its identity operations match X's. Google uses only the two ZK operations. @@ -488,8 +504,7 @@ No event adds browser cryptographic verification of proofs or attestations. accepted exactly once from its designated document at the matching phase. `authorization.finished` is emitted once before Callback departs, but does not require acknowledgement or introduce another gate before Prover readiness. -Additional attributes, operation IDs, or extension events cannot satisfy, -duplicate, or bypass a gate. +Instrumentation or extension events cannot satisfy, duplicate, or bypass a gate. #### Extensions and observation @@ -500,8 +515,8 @@ validate the envelope and may ignore unknown extension names or attributes; they never treat those as readiness, cancellation, or success. Core names with wrong phase, sender, order, or cardinality are invalid, not extensions. -Repeated overlapping extension operations use `operationId` to pair starts and -finishes; the identifier has no routing or authorization role and must not +Repeated overlapping extension operations use `instrumentation.operationId` +to pair starts and finishes; the identifier has no routing or authorization role and must not reuse ceremony IDs, OAuth state, credentials, or identity values. Observations and attributes contain no OAuth parameters, URLs or origins, identity data, proofs, witnesses, attestations, or raw exceptions. @@ -591,8 +606,8 @@ cryptographic properties delegated to the common and platform specifications. Public Prefetch authenticates its exact peer; Callback rejects an unlisted Application; Prover rejects a different origin, including one occupying the same retained window after navigation. Canonical HTTP loopback works at arbitrary ports. - TEST-CCDP-05 (exercises REQ-CCDP-05): Malformed, duplicated, wrong-direction, out-of-state, and post-terminal records cause no authorized action. Legacy `cancel` records and Application-sent `Denied` are invalid. Proof payloads are structurally checked under the selected platform version. -- TEST-CCDP-06 (exercises REQ-CCDP-06): - Only the designated core occurrences open gates; extensions cannot do so. Overlap, occurrence timestamps, and retrospective fallback timing are preserved. +- TEST-CCDP-06 (exercises REQ-CCDP-05, REQ-CCDP-06): + Only the designated core occurrences open gates; extensions cannot do so. Omitted instrumentation and either or both nested fields are accepted when valid; null, unknown instrumentation members, nonfinite attribute numbers, and top-level operationId/attributes are rejected. Overlap, occurrence timestamps, and retrospective fallback timing are preserved. - TEST-CCDP-07 (exercises REQ-CCDP-07): The four phases preserve navigation ownership and credential privacy; approval enters execution, bound denial exits before proving, and malformed returns abort. - TEST-CCDP-08 (exercises REQ-CCDP-08): From d266091d8638d375cb0a809e6b081e414c1218d8 Mon Sep 17 00:00:00 2001 From: Wondertan Date: Sun, 13 Sep 2026 16:04:30 +0200 Subject: [PATCH 18/20] docs(ccdp): clarify denial and failure message names Rename Denied to UserDenied and Abort to CeremonyFailed, including wire discriminators, message links, conformance cases, and sequence labels. Preserve outcome semantics and local cancellation. Assisted-by: GPT-5 Signed-off-by: Wondertan --- specs/ccdp-distribution.md | 2 +- specs/ccdp.md | 73 +++++++++++++++++++------------------- 2 files changed, 38 insertions(+), 37 deletions(-) diff --git a/specs/ccdp-distribution.md b/specs/ccdp-distribution.md index 42b8cb83..2c4b2a2a 100644 --- a/specs/ccdp-distribution.md +++ b/specs/ccdp-distribution.md @@ -258,7 +258,7 @@ A version absent from the bundle, including a retired version, displays a package-owned message such as **This ceremony version is no longer supported. Update the application and try again.** It establishes no connection, emits no protocol message, and never substitutes another version. No retired transport -or abort-message implementation is retained for this screen. Applications need +or failure-message implementation is retained for this screen. Applications need no version-specific failure UI and receive no protocol notification of this local failure; their ordinary cancellation/connection-failure handling remains. diff --git a/specs/ccdp.md b/specs/ccdp.md index b31ef694..b0911845 100644 --- a/specs/ccdp.md +++ b/specs/ccdp.md @@ -125,7 +125,7 @@ Worker. Authorization is an external document, not a CCDP resource. |---|---| | Location and context | OAuth Bridge origin at the fixed registered callback path `/auth/callback`; top-level, non-isolated document with complete bundled Callback code and bridge-owned deployment inputs | | Role | Authenticates the Application during [Authorization to Callback](#2-authorization-to-callback), then privately carries the captured OAuth return in popup navigation to Prover during [Callback to Prover](#3-callback-to-prover). It installs no Service Worker, retains no state across navigation, and does not classify, prefetch, prove, verify, persist a checkpoint, or close the popup. | -| Failure and cleanup | Failure before connection acceptance is displayed locally and cannot release the return; observable failure after acceptance uses `Abort`. Terminal cleanup clears retained return bytes and releases listeners and references. | +| Failure and cleanup | Failure before connection acceptance is displayed locally and cannot release the return; observable failure after acceptance uses `CeremonyFailed`. Terminal cleanup clears retained return bytes and releases listeners and references. | ### Prover `GET /prover` @@ -223,8 +223,9 @@ The Prover captures and clears it before use. Any internal isolation replacement preserves the captured fragment and clears it again on arrival. No participant deliberately includes the captured return in a request query, connection notification, signaling record, Worker record, telemetry, or error. -Opaque dependency error text follows the [Abort boundary](#abort), which does -not promise automatic redaction. Proofs and other proving inputs never enter +Opaque dependency error text follows the +[CeremonyFailed boundary](#ceremonyfailed), which does not promise automatic +redaction. Proofs and other proving inputs never enter navigation fragments. The OAuth-platform-mandated query on `redirectUri` remains the sole credential-bearing HTTP-request URL. @@ -289,8 +290,8 @@ The following table is the complete CCDP version-1 message set. |---|---|---|---| | [`ProveIdentity`](#proveidentity) | Application → Prover | `Event(prover, started)` | exactly once; selects the profile for OAuth validation and proof execution | | [`IdentityProof`](#identityproof) | Prover → Application | `ProveIdentity` and valid OAuth acceptance | at most once; ends the Prover run | -| [`Denied`](#denied) | Prover → Application | `ProveIdentity` and valid OAuth denial | at most once; reports platform denial without a technical error | -| [`Abort`](#abort) | Prefetch, Callback, or Prover → Application | connection acceptance | at most once; reports technical failure and ends the run | +| [`UserDenied`](#userdenied) | Prover → Application | `ProveIdentity` and valid OAuth denial | at most once; reports platform denial without a technical error | +| [`CeremonyFailed`](#ceremonyfailed) | Prefetch, Callback, or Prover → Application | connection acceptance | at most once; reports technical failure and ends the run | | [`Event`](#event) | Prefetch, Callback, or Prover → Application | connection acceptance and the event's documented emission point | core occurrences follow the [event catalog](#core-events); additional observations do not advance the protocol | Every recipient requires a plain record with the exact fields, types, and bounds @@ -377,35 +378,35 @@ a nested identity copy. CCDP treats the proof as opaque; adding a platform does not change this message. Neither browser endpoint cryptographically verifies the delivered result; identity is non-authoritative until ledger verification. -### Denied +### UserDenied ```ts -interface Denied { - type: 'denied' +interface UserDenied { + type: 'user-denied' } ``` -`Denied` reports only a valid, ceremony-bound OAuth-platform denial discovered +`UserDenied` reports only a valid, ceremony-bound OAuth-platform denial discovered by Prover while validating `ProveIdentity`. The Application resolves `{ status: 'denied' }`. Prover sends it before token exchange, proof execution, or proving-operation events, never as a substitute for a failure. -Malformed, mismatched, or otherwise invalid OAuth returns use `Abort`, not -`Denied`. Denial has no acknowledgement and does not close or navigate the +Malformed, mismatched, or otherwise invalid OAuth returns use `CeremonyFailed`, not +`UserDenied`. Denial has no acknowledgement and does not close or navigate the popup. Application cancellation is local, not a CCDP message; see [Terminal outcomes](#terminal-outcomes). -### Abort +### CeremonyFailed ```ts -interface Abort { - type: 'abort' +interface CeremonyFailed { + type: 'ceremony-failed' event: string message: string } ``` -`Abort` reports an observable technical failure after connection +`CeremonyFailed` reports an observable technical failure after connection acceptance from whichever of Prefetch, Callback, or Prover is active. `event` is the nonempty, bounded, code-owned name of the failing core or implementation-defined operation; it is not a UI @@ -462,7 +463,7 @@ Optional fields are absent when unused, not null. Event names, attribute names, string values, and record sizes are bounded by the implementation. Names and attribute meanings are code-owned, not supplied by OAuth returns or callers. No event contains a UI stage, display label, progress percentage, overall -ceremony status, or error text. Technical failure uses `Abort`. +ceremony status, or error text. Technical failure uses `CeremonyFailed`. #### Core events @@ -579,8 +580,8 @@ CCDP supplies no durable OAuth/proof recovery or guaranteed cancellation of already dispatched work. Background suspension, lost connections, and document replacement can prevent progress. A missing event or closed popup is never success or valid denial. Error text is deliberately useful for local debugging -and is not guaranteed to be credential-free; the Abort and Event contracts -separate it from exported observations. +and is not guaranteed to be credential-free; the `CeremonyFailed` and `Event` +contracts separate it from exported observations. The selected platform and common ceremony rules own proof soundness, identity extraction, replay, freshness, and trust-root lifecycle. Browser structural checks @@ -605,13 +606,13 @@ cryptographic properties delegated to the common and platform specifications. - TEST-CCDP-04 (exercises REQ-CCDP-04): Public Prefetch authenticates its exact peer; Callback rejects an unlisted Application; Prover rejects a different origin, including one occupying the same retained window after navigation. Canonical HTTP loopback works at arbitrary ports. - TEST-CCDP-05 (exercises REQ-CCDP-05): - Malformed, duplicated, wrong-direction, out-of-state, and post-terminal records cause no authorized action. Legacy `cancel` records and Application-sent `Denied` are invalid. Proof payloads are structurally checked under the selected platform version. + Malformed, duplicated, wrong-direction, out-of-state, and post-terminal records cause no authorized action. Legacy `cancel`, `denied`, and `abort` records and Application-sent `UserDenied` are invalid. Proof payloads are structurally checked under the selected platform version. - TEST-CCDP-06 (exercises REQ-CCDP-05, REQ-CCDP-06): Only the designated core occurrences open gates; extensions cannot do so. Omitted instrumentation and either or both nested fields are accepted when valid; null, unknown instrumentation members, nonfinite attribute numbers, and top-level operationId/attributes are rejected. Overlap, occurrence timestamps, and retrospective fallback timing are preserved. - TEST-CCDP-07 (exercises REQ-CCDP-07): The four phases preserve navigation ownership and credential privacy; approval enters execution, bound denial exits before proving, and malformed returns abort. - TEST-CCDP-08 (exercises REQ-CCDP-08): - Denied/Abort/IdentityProof and local-cancellation races settle once. Local cancellation sends no CCDP message; subsequent composition-owned navigation or closure cannot let late traffic revive the run. Errors are text-only, excluded from exported events, and undeliverable failures have a fixed local diagnostic. + UserDenied/CeremonyFailed/IdentityProof and local-cancellation races settle once. Local cancellation sends no CCDP message; subsequent composition-owned navigation or closure cannot let late traffic revive the run. Errors are text-only, excluded from exported events, and undeliverable failures have a fixed local diagnostic. ## Protocol @@ -646,8 +647,8 @@ can reactivate an earlier phase. private fragment, including any isolation replacement. Every arrival clears its URL before use; participants do not deliberately copy the return into an intermediate store, notification, or diagnostic. Opaque error text has the - separate [Abort boundary](#abort). The platform-mandated callback query is the - sole HTTP-request ingress exception. + separate [CeremonyFailed boundary](#ceremonyfailed). The platform-mandated + callback query is the sole HTTP-request ingress exception. - Callback carries the return onward only after authenticating the Application. Prover validates it against the authenticated ceremony and selected profile before any credential-bearing request. Application receives only protocol @@ -686,10 +687,10 @@ retires the Prefetch carrier while leaving the Application endpoint available for Callback. Worker registration, activation, or selected-profile dispatch failure after -connection acceptance sends `Abort` for `prefetch-dispatch` instead of its +connection acceptance sends `CeremonyFailed` for `prefetch-dispatch` instead of its finish event; Application rejects without navigating to Authorization. Download failure after successful dispatch remains an asset-cache concern and uses the normal -cold-fetch path, not a late Prefetch abort. Failures before connection acceptance +cold-fetch path, not a late Prefetch failure. Failures before connection acceptance are reported locally and release no protocol message. #### 2. Authorization to Callback @@ -737,8 +738,8 @@ Application accepts one `Event(prover, started)` and sends one verifier. It does not receive or parse the OAuth return. On receiving `ProveIdentity`, the selected platform/version validates the retained return before credential use. A valid denial sends -[`Denied`](#denied); malformed or mismatched input sends -[`Abort`](#abort). Both end the run in this phase, as does +[`UserDenied`](#userdenied); malformed or mismatched input sends +[`CeremonyFailed`](#ceremonyfailed). Both end the run in this phase, as does context loss. Only valid OAuth acceptance enters Phase 4. #### 4. Prover execution @@ -751,8 +752,8 @@ platform events. Each operation's `finished` reports only that operation; events from overlapping operations are not forced into a global order. After all required proof and evidence work completes, Prover sends one [`IdentityProof`](#identityproof), unless failure or context loss ends its work. -Observable failure sends [`Abort`](#abort). Prover accepts no second proof -request; Application-local cancellation makes any later delivery inert. +Observable failure sends [`CeremonyFailed`](#ceremonyfailed). Prover accepts no +second proof request; Application-local cancellation makes any later delivery inert. ### Terminal outcomes @@ -760,10 +761,10 @@ request; Application-local cancellation makes any later delivery inert. terminal outcome as final and perform the cleanup described below without closing the popup or fabricating a successful operation finish. -Prover's `Denied` reports valid OAuth denial; an active document's `Abort` -reports failure; and `IdentityProof` delivers a proof. These outcomes are +Prover's `UserDenied` reports valid OAuth denial; an active document's +`CeremonyFailed` reports failure; and `IdentityProof` delivers a proof. These outcomes are mutually terminal even when they race in transit. Denial resolves denied; -an observable abort rejects the live ceremony. A failure before connection +an observable failure rejects the live ceremony. A failure before connection acceptance is reported locally. Application structurally validates the delivered identity and selected platform/version proof and assembles its result before recording `prover.finished` locally. Neither endpoint adds local cryptographic proof or @@ -800,7 +801,7 @@ sequenceDiagram P->>P: Prefetch accepts connection P->>P: Prefetch registers Worker and dispatches selected-profile fetches break Prefetch setup fails - P-->>A: Abort + P-->>A: CeremonyFailed end P-->>A: Event(prefetch-dispatch, finished) Note over A: Local authorization.started @@ -812,7 +813,7 @@ sequenceDiagram P->>P: Callback starts and selects its bundled version P->>P: Callback accepts authenticated connection break Callback fails after connection acceptance - P-->>A: Abort + P-->>A: CeremonyFailed end P-->>A: Event(authorization, finished) @@ -827,17 +828,17 @@ sequenceDiagram P->>P: Validate retained OAuth return break Valid OAuth denial - P-->>A: Denied + P-->>A: UserDenied end break Invalid OAuth return - P-->>A: Abort + P-->>A: CeremonyFailed end Note over A,P: Phase 4 - Prover execution after OAuth acceptance loop Applicable operations, possibly overlapping P-->>A: Event(operation, started or finished) end break Prover fails - P-->>A: Abort + P-->>A: CeremonyFailed end P-->>A: IdentityProof Note over A: Validate structure and assemble result From 596ceb390ccfab375a6cf82409fd68f846973e01 Mon Sep 17 00:00:00 2001 From: Wondertan Date: Sun, 13 Sep 2026 18:00:14 +0200 Subject: [PATCH 19/20] docs(ccdp): allow notary inputs across platform profiles Accept a resolved notary address for any platform without requiring notarization. Keep nullable wire input and let the selected profile require an address only when needed. Assisted-by: GPT-5 Signed-off-by: Wondertan --- specs/ccdp.md | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/specs/ccdp.md b/specs/ccdp.md index b0911845..c0d8f688 100644 --- a/specs/ccdp.md +++ b/specs/ccdp.md @@ -327,15 +327,18 @@ by Prover and is not repeated in the message. Starting Prover initiates OAuth validation; it does not assert acceptance or mean that proof generation has already begun. -`notaryAddress` follows the [origin policy](#origin-policy) for a platform that -uses notarization; it is null for Google. The local HTTP exception needs no -client option or environment override. A remote HTTP address is rejected, -never upgraded or used as a downgrade fallback. -The Application selects and freezes it before OAuth. Prover validates it before -credential use and uses it unchanged for all sessions, including the GitHub -token request. It neither selects defaults nor accepts a separate profile, -ledger identifier, hash, or testnet flag. The address changes network routing, -not the proof statement or trusted signing keys. +`notaryAddress` may be supplied for any platform; a non-null value follows the +[origin policy](#origin-policy). The Application can pass its resolved address +uniformly without knowing which platforms use notarization. The selected +platform ignores it when unused and requires a non-null address before starting +work that needs notarization. The local HTTP exception needs no client option or +environment override. A remote HTTP address is rejected, never upgraded or used +as a downgrade fallback. +The Application selects and freezes a supplied address before OAuth. Prover +validates it before credential use and, when needed, uses it unchanged for all +sessions, including the GitHub token request. It neither selects defaults nor +accepts a separate profile, ledger identifier, hash, or testnet flag. The address +changes network routing, not the proof statement or trusted signing keys. The Application origin is trusted for this transient input because it already supplies the operation being authorized. It retains the authorization nonce; @@ -607,6 +610,9 @@ cryptographic properties delegated to the common and platform specifications. Public Prefetch authenticates its exact peer; Callback rejects an unlisted Application; Prover rejects a different origin, including one occupying the same retained window after navigation. Canonical HTTP loopback works at arbitrary ports. - TEST-CCDP-05 (exercises REQ-CCDP-05): Malformed, duplicated, wrong-direction, out-of-state, and post-terminal records cause no authorized action. Legacy `cancel`, `denied`, and `abort` records and Application-sent `UserDenied` are invalid. Proof payloads are structurally checked under the selected platform version. + A valid notary address is accepted for any platform, including one that does + not notarize; null is accepted when unused but rejected before work requiring + notarization. Malformed non-null addresses are rejected under the origin policy. - TEST-CCDP-06 (exercises REQ-CCDP-05, REQ-CCDP-06): Only the designated core occurrences open gates; extensions cannot do so. Omitted instrumentation and either or both nested fields are accepted when valid; null, unknown instrumentation members, nonfinite attribute numbers, and top-level operationId/attributes are rejected. Overlap, occurrence timestamps, and retrospective fallback timing are preserved. - TEST-CCDP-07 (exercises REQ-CCDP-07): From 5b46008f632656fd0ecffecd0cb32fdbbf7a05b4 Mon Sep 17 00:00:00 2001 From: Wondertan Date: Sun, 13 Sep 2026 18:24:51 +0200 Subject: [PATCH 20/20] docs(specs): reconcile CCDP with existing contracts Align browser identity ownership, GitHub routing and admission, and transport failure and closure semantics. Distinguish ledger-local verifier versions from ceremony versions without changing the browser flow or ledger verification. Assisted-by: GPT-5 Signed-off-by: Wondertan --- specs/ccdp.md | 51 +++++++++++++++++++-------- specs/libid.md | 3 +- specs/oauth-bridge.md | 3 +- specs/platform-ceremonies.md | 68 +++++++++++++++++++++++------------- 4 files changed, 85 insertions(+), 40 deletions(-) diff --git a/specs/ccdp.md b/specs/ccdp.md index c0d8f688..1aaa307d 100644 --- a/specs/ccdp.md +++ b/specs/ccdp.md @@ -32,7 +32,8 @@ part of this contract. The common specification's Canonical Runtime spans the Application and its browser participants; Callback is its Redirect Runtime and Prover owns the platform-aware browser evidence checks. A Ledger Verifier means the downstream verification path defined in the common specification, not a -browser component. +browser component. CCDP ends at browser proof delivery; construction and +submission of a ledger-specific Submission remain composition-owned. - ASM-CCDP-01: Browser origin enforcement, authenticated popup transport, and the configured Application, Bridge, and Distribution code execute correctly. @@ -297,9 +298,12 @@ The following table is the complete CCDP version-1 message set. Every recipient requires a plain record with the exact fields, types, and bounds defined below. Unknown fields, coercion, normalization, defaults, and unrecognized discriminators are invalid. Messages outside the listed direction, -predecessor, and cardinality are invalid. Denial, proof delivery, abort, and -Application-local cancellation make later messages inert even when they race -in transit. +predecessor, and cardinality are invalid. Unknown discriminators and decoder +rejections fail the logical connection under REQ-POPUP-MSG-04; they are not +silently ignored. Handler state guards authorize no action for an invalid +sequence. Denial, proof delivery, failure, and Application-local cancellation +make later valid CCDP messages inert even when they race in transit; transport +validation still applies. ### ProveIdentity @@ -315,9 +319,10 @@ interface ProveIdentity { } ``` -`platformId` and `platformCeremonyVersion` are the exact supported profile -selected at launch and must match the active Prover. The message is valid only -after `Event(prover, started)`. The remaining fields are the frozen client +The Application sends the exact `platformId` and `platformCeremonyVersion` +selected at launch. Prover requires that pair to be supported by its loaded +implementation; this message selects its profile. The message is valid only after +`Event(prover, started)`. The remaining fields are the frozen client identifier and redirect, derived code verifier, and resolved notary address. `redirectUri` is the canonical OAuth Bridge origin with the fixed `/auth/callback` path and no @@ -380,6 +385,9 @@ encodings and the platform/client binding to `ProveIdentity`. a nested identity copy. CCDP treats the proof as opaque; adding a platform does not change this message. Neither browser endpoint cryptographically verifies the delivered result; identity is non-authoritative until ledger verification. +This browser delivery is not the common specification's ledger-specific +Submission. The composition adds its retained authorization and dispatch inputs +without changing the delivered evidence. ### UserDenied @@ -541,6 +549,8 @@ and delayed delivery; receipt time is not operation time. Independently running operations may overlap, and retrospective observations may arrive after later timestamps. Protocol ordering follows authenticated state and messages, never timestamp sorting. +These browser observations are not the authenticated evidence timestamps of +common §10 and never supply proof validity or metadata ordering. Start/finish differences measure operation intervals. Do not sum overlapping intervals as total elapsed time, fabricate a finish after context loss, or turn @@ -610,6 +620,9 @@ cryptographic properties delegated to the common and platform specifications. Public Prefetch authenticates its exact peer; Callback rejects an unlisted Application; Prover rejects a different origin, including one occupying the same retained window after navigation. Canonical HTTP loopback works at arbitrary ports. - TEST-CCDP-05 (exercises REQ-CCDP-05): Malformed, duplicated, wrong-direction, out-of-state, and post-terminal records cause no authorized action. Legacy `cancel`, `denied`, and `abort` records and Application-sent `UserDenied` are invalid. Proof payloads are structurally checked under the selected platform version. + An unknown discriminator or rejected decoder fails the logical connection; + a handler's state guard permits no invalid transition. Late valid CCDP + messages cannot change a settled ceremony outcome. A valid notary address is accepted for any platform, including one that does not notarize; null is accepted when unused but rejected before work requiring notarization. Malformed non-null addresses are rejected under the origin policy. @@ -619,6 +632,8 @@ cryptographic properties delegated to the common and platform specifications. The four phases preserve navigation ownership and credential privacy; approval enters execution, bound denial exits before proving, and malformed returns abort. - TEST-CCDP-08 (exercises REQ-CCDP-08): UserDenied/CeremonyFailed/IdentityProof and local-cancellation races settle once. Local cancellation sends no CCDP message; subsequent composition-owned navigation or closure cannot let late traffic revive the run. Errors are text-only, excluded from exported events, and undeliverable failures have a fixed local diagnostic. + Outcome cleanup leaves the popup connection available, and its authenticated + closure control still closes an isolated popup after the ceremony settles. ## Protocol @@ -641,14 +656,17 @@ can reactivate an earlier phase. ownership supplies message correlation and its private version; loaded resources supply the CCDP version. CCDP messages repeat neither. - Each participant accepts only exact records permitted by its direction, - current state, and cardinality. Unknown message types, malformed, replayed, - out-of-order, wrong-direction, and post-terminal values change no state. + current state, and cardinality. Invalid or post-terminal traffic authorizes + no ceremony action; malformed or unregistered records still fail transport + under REQ-POPUP-MSG-04. Valid event extensions may be observed but never advance the protocol. - Browser-observed exact origins establish authority. Same-site placement, navigation history, request headers, and message fields do not substitute for connection authentication. -- Documents use only the frozen locations and fragments defined here. A CCDP - message never selects an origin, implementation, or navigation destination. +- Documents use only the frozen locations and fragments defined here. Messages + select no document implementation or popup navigation destination. + `ProveIdentity` carries the frozen platform selection and service-routing + inputs used by that platform, not a navigation command. - Raw OAuth returns pass only from the cleared Callback capture to Prover's private fragment, including any isolation replacement. Every arrival clears its URL before use; participants do not deliberately copy the return into an @@ -664,8 +682,9 @@ can reactivate an earlier phase. the next protocol action, but neither they nor other observations, carrier state, navigation, popup closure, or unvalidated proof delivery constitute ceremony success. Operation completion is not ceremony completion. -- The Application owns terminal popup lifetime. No CCDP document closes the - popup. +- The Application owns terminal popup lifetime. CCDP outcomes do not initiate + popup closure; documents still honor the popup transport's authenticated + closure control. - Cancellation and context-loss cleanup are best effort. CCDP has no durable checkpoint, ceremony recovery, or migration to another popup connection. @@ -765,7 +784,11 @@ second proof request; Application-local cancellation makes any later delivery in - REQ-CCDP-08 (upholds SP-CCDP-01): Each Participant MUST treat the first valid terminal outcome as final and perform the cleanup described below without - closing the popup or fabricating a successful operation finish. + initiating popup closure or fabricating a successful operation finish. + +Outcome cleanup releases ceremony resources, not the composition-owned popup +connection. That connection remains available for navigation or closure under +the popup transport contract, including after isolation. Prover's `UserDenied` reports valid OAuth denial; an active document's `CeremonyFailed` reports failure; and `IdentityProof` delivers a proof. These outcomes are diff --git a/specs/libid.md b/specs/libid.md index 817cd1db..5f78060e 100644 --- a/specs/libid.md +++ b/specs/libid.md @@ -56,7 +56,8 @@ User -> Identity Platform -> Canonical Runtime -> Proving Circuit -> Consumer The Consumer never verifies evidence itself. It calls the Proof Verifier, which selects the Platform Verifier registered for the named identity platform -and Platform Ceremony Version, which in turn obtains +and ledger-local Verifier Version. Several Verifier Versions may implement the +same Platform Ceremony Version. The selected Platform Verifier obtains attestation authenticity from the Notary Service once for each attestation that profile carries. Google carries none, so its path reaches no Notary Service and pays no fee; X and GitHub carry two each. The result travels diff --git a/specs/oauth-bridge.md b/specs/oauth-bridge.md index ccd767fa..ea80c55b 100644 --- a/specs/oauth-bridge.md +++ b/specs/oauth-bridge.md @@ -298,7 +298,8 @@ a development setup must make the intended notary reachable by both. Canonical origin syntax alone is insufficient; redirects remain forbidden. `accessToken` is nonempty printable ASCII without whitespace or control bytes -and at most 128 bytes, matching GitHub v1's bearer circuit. Both Bridge and +and at most 128 bytes. This is the Bridge API's acceptance bound, not a change +to the platform profile's circuit bound. Both Bridge and Prover enforce this bound before dependent identity notarization; an oversized token fails rather than being truncated. `attestedData` decodes to a nonempty byte-exact record of at most 2 MiB; `signature` decodes to exactly 65 bytes; and `bearerOpening` diff --git a/specs/platform-ceremonies.md b/specs/platform-ceremonies.md index 4a721cda..f121d54d 100644 --- a/specs/platform-ceremonies.md +++ b/specs/platform-ceremonies.md @@ -74,19 +74,25 @@ REQ-COMMON-15A. contains every fixed route it requires. Necessity: cross-component interoperability between the Canonical Runtime build and server deployment. - REQ-PLAT-03 (upholds SP-CLIENT-01): - The Canonical Runtime MUST derive the local identity fields exclusively from - the Platform Profile's canonical source in the exact Submission it - returns. + The Prover MUST derive the local identity fields exclusively from the + Platform Profile's canonical sources in the evidence it returns. Those fields are not an authority decision; only the Consumer's - acceptance of that exact Submission is. For X and GitHub, the Canonical Runtime MUST parse the exact revealed identity-response bytes that the + acceptance of the resulting Submission is. For X and GitHub, the Prover + MUST parse the exact revealed identity-response bytes that the Platform Verifier extracts, using the same canonical extraction and - normalization rules. The Canonical Runtime MUST reject a detached proof - output, sidecar value, or caller value that supplies or overrides `userId`, - handle, or `metadataObservedAt`. + normalization rules. The Prover MUST take `metadataObservedAt` from the + profile's evidence-time source in §2.2, not from a detached identity value. + The Prover MUST reject a caller-supplied or detached value used as an + alternative source for `userId`, handle, or `metadataObservedAt`. This is a data-source invariant, not a browser-flow requirement. It defines the identity fields returned to callers and used by any composition-owned UI; -it does not create a ceremony-owned confirmation page. +it does not create a ceremony-owned confirmation page. CCDP's separate +`IdentityProof.identity` record transports Prover's derivation alongside the +proof; it is not another evidence source. Application validates that delivery's +structure and selected platform/client binding without repeating the evidence +extraction. Common REQ-COMMON-19E still binds the derivation to the exact evidence +used in the resulting Submission. ### 2.1 Canonical platform user identifiers @@ -824,7 +830,7 @@ revealed `client_id` something other than the credential GitHub authenticated. ### 6.3 GitHub token service boundary -The Deployment exposes one GitHub Token Service to its isolated prover. This +The Deployment exposes one GitHub Token Service used by its isolated prover. This specification defines the semantic call, not its endpoint or wire encoding: ```text @@ -842,6 +848,9 @@ These identifiers name protocol values, not serialized field names. The browser and deployment specifications own endpoint naming, transport framing, serialization, parsing bounds, caller authentication, and cache policy. Those choices MUST preserve the semantic interface and security requirements below. +The notary destination is routing input owned by the +[OAuth Bridge contract](oauth-bridge.md#github-token-endpoint), not another +proof input or a caller-selected trust root. - REQ-PLAT-37: The Canonical Runtime MUST invoke the GitHub Token Service with the exact @@ -870,20 +879,25 @@ choices MUST preserve the semantic interface and security requirements below. and the commitment together reveal the committed bearer, so a published opening publishes the credential its commitment exists to hide. - REQ-PLAT-41 (upholds SP-EXCHANGE-01): - The GitHub Token Service MUST use only its compiled client identifier, client - secret, redirect URI, token endpoint, and notary configuration. The GitHub + The GitHub Token Service MUST use its deployment-configured client + identifier, client secret, redirect URI, and token endpoint. The GitHub Token Service MUST NOT accept a caller-selected action, job, client, redirect, - endpoint, return URL, or operation. + platform endpoint, return URL, or operation. The GitHub Token Service MUST + use the request-selected notary destination under the validation and egress + rules owned by REQ-BRIDGE-05. That address selects routing, not the notary + keys accepted during ledger verification. - REQ-PLAT-42: The GitHub Token Service MUST persist no code, verifier, bearer, proof, result, or progress state. The GitHub Token Service MUST expose no polling or result route. Necessity: the service holds ceremony credentials, so retention creates a compromise target with no protocol purpose. - REQ-PLAT-43: - The deployment transport MUST make the GitHub Token Service callable only by - its authenticated isolated prover boundary, not by an application frontend - or unrelated origin. The browser and deployment specifications define the - concrete enforcement mechanism. + The GitHub Token Service MUST enforce the deployment-origin admission rules + owned by REQ-BRIDGE-02 and REQ-BRIDGE-05. The effective allowlist admits the + configured Application origins and the selected CCDP origin; isolation is + not an HTTP caller-authentication mechanism. Origin admission does not + authenticate non-browser callers or replace notary-destination egress checks. + Necessity: the service's browser admission must agree with its HTTP contract. - REQ-PLAT-43B: The GitHub Token Service MUST reject redirects. Necessity: a followed redirect would notarize a session other than the pinned token endpoint. @@ -983,7 +997,7 @@ which the exact tiling of common REQ-COMMON-35 makes derivable. remains mandatory under common REQ-COMMON-33 and REQ-COMMON-33A. - REQ-PLAT-45 (upholds SP-EXCHANGE-01): The GitHub Token Service MUST return an attestation carrying the - configured notary's signature and revealing the token request's method and + selected notary's signature and revealing the token request's method and path. The Platform Verifier MUST compare those two revealed values with the GitHub profile. The Platform Verifier MUST compare the authority that attestation authenticates with the same profile, per common REQ-COMMON-21A. @@ -1256,10 +1270,14 @@ Platform Verifier, Notary Service, Consumer. 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): - 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 + A request selecting a platform endpoint, client, or return URL is rejected; + a valid request-selected notary destination is used without changing those + deployment fields or ledger trust roots. Invalid or forbidden notary + destinations fail the Bridge's routing/egress checks. No state survives the + call. Both an admitted Application origin and the selected CCDP origin pass + browser admission; an unlisted or invalid Origin is refused before exchange. + A redirected token exchange is rejected; an attestation + revealing a range outside the rows marked `yes`, 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. - TEST-PLAT-15 (exercises REQ-PLAT-44, REQ-PLAT-45, REQ-PLAT-46, REQ-PLAT-47, REQ-PLAT-48, REQ-PLAT-48A, REQ-PLAT-49, REQ-PLAT-50): @@ -1302,9 +1320,11 @@ Platform Verifier, Notary Service, Consumer. acceptance of that exact Submission makes the claim authoritative. - TEST-PLAT-17A (exercises REQ-PLAT-03, REQ-PLAT-31A, REQ-PLAT-51A): Pair authenticated X or GitHub identity-response bytes for account B with a - detached `userId`, handle, or metadata value for account A. The Canonical Runtime - rejects the extra representation; without it, the Canonical Runtime and the - Platform Verifier both derive account B byte for byte. Replacing the proof, + detached `userId`, handle, or metadata value for account A offered as an + extraction input. Prover rejects that alternative source; without it, Prover + and the Platform Verifier both derive account B byte for byte. Prover may + deliver its derived identity separately through CCDP, and Application accepts + its valid structure without repeating evidence extraction. Replacing the proof, attestation, platform, or version after deriving the local identity fields discards them and requires rederivation from the replacement Submission. - TEST-PLAT-18 (exercises REQ-PLAT-25, REQ-PLAT-26, REQ-PLAT-27, REQ-PLAT-28, REQ-PLAT-28A):