From bc2e68a87b0883fe718e66c42ec0e5cffed3bd04 Mon Sep 17 00:00:00 2001 From: xgreenx Date: Mon, 17 Aug 2026 15:42:36 +0100 Subject: [PATCH 1/5] docs(specs): thin circuits, permissionless clients, observedAt validity rule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses PR #3 review 4952164865. The unifying principle: circuits check the exact fields the profile needs — JSON pattern matches at prover-supplied offsets, never whole-template equality — and open blinded hash commitments for everything hidden; on-chain contracts verify revealed attestation bytes and public inputs. This is the architecture the shipped dyaka circuits (jwt_email, dyaka-noir-token) and verifiers already implement. - common §9 rewritten: field checks are '"field":"value"' pattern asserts at private-input offsets, with charset exclusion of the closing delimiter and bounds inside the authenticated payload (REQ-COMMON-19/19B); hidden ranges are blinded commitments the circuit opens (REQ-COMMON-18); layout tiling with revealed anchors (REQ-COMMON-18A); unique-match guard for extraction from revealed attestation bytes (REQ-COMMON-19A); no deployment value is a compiled circuit constant (REQ-COMMON-21A); ASM-PROV-06 covers duplicate-free platform responses. - Client admission is permissionless: no on-chain client set, any OAuth application can produce evidence; the client id stays a public input a contract MAY read (REQ-COMMON-17C). The authorization is that the claim's target sends the transaction. - One validity rule: proofValidUntil = metadataObservedAt + MAX_PROOF_AGE, with MAX_PROOF_AGE contract-configurable (default 1h for X/GitHub; Google's observedAt is the signed exp, default 0). The Claim Digest carries no expiration. metadataObservedAt doubles as the monotone replay watermark (REQ-COMMON-25A). - Google: §3.1 authorization table demoted to operational guidance; RS256 stays in circuit with the modulus exposed for the on-chain JWKS root check (REQ-PLAT-16A); claims asserted at supplied offsets, no iat (REQ-PLAT-21). - X: explicit §5.2 disclosure table; client id range revealed and public (REQ-PLAT-29A/29B); bearer revealed only as its SHA-256 commitment in both sessions (REQ-PLAT-30A/32A). - GitHub: token endpoint checked over the revealed request prefix (REQ-PLAT-45); token_type/scope asserted in circuit against committed ranges without disclosure. Co-Authored-By: Claude Opus 4.8 --- specs/ceremony-common.md | 136 +++++++++++++++++++++++------------ specs/libid.md | 4 +- specs/platform-ceremonies.md | 128 +++++++++++++++++++++++++-------- 3 files changed, 192 insertions(+), 76 deletions(-) diff --git a/specs/ceremony-common.md b/specs/ceremony-common.md index 2483070b..9733fea2 100644 --- a/specs/ceremony-common.md +++ b/specs/ceremony-common.md @@ -61,6 +61,9 @@ Canonical Runtime: The immutable browser release that constructs claim - ASM-PROV-05: Google signs ID Tokens with a key published at its JWKS endpoint, and includes the requested `nonce` verbatim. +- ASM-PROV-06: + An Identity Platform emits a well-formed authenticated response in which + each authoritative field appears exactly once at the top level. - ASM-NOTARY-01: The configured notary key signs only transcripts it observed. - ASM-BROWSER-01: @@ -190,13 +193,14 @@ Google as the OIDC `nonce`, X and GitHub through the PKCE construction in §7. The Implementation MUST serialize each listed parameter tuple with the WHATWG `application/x-www-form-urlencoded` serializer, taking UTF-8 input, encoding space as `+`, and using uppercase hexadecimal percent escapes. Necessity: - byte-exact request reproduction across implementations, without which a - notarized transcript cannot be matched against a template. + byte-exact request reproduction across implementations, without which the + fixed range layout of §9 does not hold. This is a runtime serialization + rule; no circuit re-verifies it. - REQ-COMMON-08: The Implementation MUST emit each field listed in the platform profile exactly once, in the listed order. The Implementation MUST emit no other field. Necessity: cross-component interoperability of the transcript - template. + layout. - REQ-COMMON-09 (upholds SP-BIND-01): The Implementation MUST NOT accept a caller-supplied parameter into a serialized authorization or token request. @@ -275,11 +279,11 @@ code_challenge = YdTXrtdSCJvd5UpsW2wS13-XwSe7kJ5OE7Ex6J_AWks ## 8. Client binding -The OAuth client that issued the evidence is a public proof input, and the +The OAuth client that issued the evidence is a public proof input. The Canonical Runtime compares it against the exact client fixed by the immutable -ceremony profile before constructing a verified claim. Different application -deployments may use different OAuth clients without changing Registry or -verifier admission. +ceremony profile before constructing a verified claim. Client admission is +permissionless: any OAuth application can produce acceptable evidence, and no +on-chain registration of clients exists. | Identity platform | Authenticated source of the client identifier | |---|---| @@ -295,23 +299,31 @@ verifier admission. byte for byte from the client fixed by the selected immutable ceremony profile. - REQ-COMMON-17C (upholds SP-CLIENT-01): - The Consuming Contract MUST NOT accept or reject evidence based on an - application OAuth client identifier. Necessity: client selection and - authentication are local ceremony policy, not Registry admission. + The Verifier MUST NOT require the exposed client identifier to belong to a + registered set. A Consuming Contract MAY read the exposed client identifier + for its own semantics. Necessity: client selection is permissionless + application policy; the on-chain authorization is that the claim's target + sends the transaction. Redirect origin, frontend origin, and application authorization remain browser-local and produce no on-chain effect. ## 9. Notarized-transcript extraction -A proof over a TLSNotary transcript authenticates bytes, not fields. These -rules apply wherever a party that composes a request also proves over it. +A proof authenticates bytes, not fields. The Proving Circuit is responsible +for checking the fields the profile needs — and only those fields, never the +whole template. A field check is a JSON pattern match at a witnessed offset: +the full `"field":"` delimiter, the value, and the closing structural byte. +Because an authenticated JSON document carries each authoritative field once +(ASM-PROV-06), per-field pattern checks give the same guarantee that +whole-template equality would, at a fraction of the constraints. Hidden +ranges stay behind blinded hash commitments the circuit opens; the circuit +links transcripts through those commitments and binds the Claim Digest. Disclosure and verification are two separate layers. The Platform Profile -fixes a minimal set of revealed ranges; every other byte in a profile-listed -structured region is redacted, yet still proven against that region's template -as a private witness. Separately, the proof exposes a minimal set of public -inputs, which never includes a credential. +fixes a minimal set of revealed ranges; every other byte stays behind a +blinded hash commitment. Separately, the proof exposes a minimal set of +public inputs, which never includes a credential. - REQ-COMMON-17A (upholds SP-CLIENT-01): The Platform Profile MUST list the exact ranges a notarized session reveals. @@ -319,18 +331,37 @@ inputs, which never includes a credential. The Implementation MUST redact every byte outside the ranges its profile lists. - REQ-COMMON-18 (upholds SP-EXCHANGE-01): - The Proving Circuit MUST take each complete profile-listed structured request - region as a private witness. For a launch token request, that region is the - complete form body. The Proving Circuit MUST assert equality with the - region's template, allowing a bounded hole only where the profile lists a - variable value. + The attestation MUST commit every hidden range with a blinded hash + commitment. The Proving Circuit MUST open the commitment of each hidden + range whose value the profile checks. +- REQ-COMMON-18A (upholds SP-EXCHANGE-01): + The verifier of an attestation MUST check that the revealed ranges and + hidden-range commitments tile the transcript in the exact layout the + profile fixes, with each hidden range bounded by revealed anchor bytes. - REQ-COMMON-19 (upholds SP-EXCHANGE-01): - The Proving Circuit MUST locate a field by its full delimiter in the - template. The Proving Circuit MUST NOT locate a field by searching for a bare - field name. + A Proving Circuit extracting a field MUST receive the field's offset as a + private input supplied by the prover; the circuit performs no search. The + Proving Circuit MUST assert the full `"field":"` delimiter at that offset, + the value bytes, and the closing delimiter. The Proving Circuit MUST + assert that the byte after the match is a structural byte the profile + fixes. +- REQ-COMMON-19B (upholds SP-EXCHANGE-01): + The Proving Circuit MUST constrain the extracted value's charset to exclude + the closing delimiter. The Proving Circuit MUST assert the whole match lies + inside the authenticated payload length, not in prover-controlled padding. + Necessity: without the charset bound a longer witnessed value extends the + match into the neighboring field; without the bounds check a pattern can be + planted in zero-padding. +- REQ-COMMON-19A (upholds SP-EXCHANGE-01): + A Consuming Contract extracting a field from revealed attestation bytes + MUST reject a transcript in which the field's full delimiter matches at + 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-20 (upholds SP-EXCHANGE-01): - The Proving Circuit MUST constrain every variable value to the charset the - profile states, including values that are never disclosed. + The Proving Circuit MUST constrain every variable value it opens or + extracts to the charset the profile states, including values that are never + disclosed. Endpoint proof inputs use these canonical byte strings: authority is the lowercase ASCII TLS server DNS name with no trailing dot, method is the exact @@ -357,6 +388,11 @@ platform endpoint constants into each circuit without weakening the binding. contract input. Necessity: media type selects the platform's request parser, while redirect URI is application delivery configuration rather than on-chain identity authority. +- REQ-COMMON-21C (upholds SP-CLIENT-01): + The Proving Circuit MUST NOT embed a deployment-configured value, including + a client identifier, client secret, or `redirect_uri`, as a compiled + constant. Necessity: a compiled deployment value fragments the verifying + key per deployment. - REQ-COMMON-22 (upholds SP-EXCHANGE-01): The Platform Profile MUST order any redacted credential last in the request body. @@ -364,9 +400,10 @@ platform endpoint constants into each circuit without weakening the binding. The Proving Circuit MUST NOT expose a client secret, or any value derived from one, as a public proof input. -An undisclosed range still reaches the platform. Ordering it last, and -constraining its charset so it cannot contain a field delimiter, prevents a -prover hiding a second copy of a field behind it. +An undisclosed range still reaches the platform. Ordering it last, +constraining its charset so it cannot contain a field delimiter, and tiling +the layout with revealed anchors prevents a prover hiding a second copy of a +field behind it. ## 10. Evidence time @@ -381,11 +418,16 @@ prover hiding a second copy of a field behind it. The Implementation MUST take `metadataObservedAt` from the platform-profile value. The Implementation MUST NOT infer it from an HTTP `Date` header or a local clock. +- REQ-COMMON-25A (upholds SP-FRESH-01): + The Consuming Contract MUST reject evidence whose `metadataObservedAt` + precedes the watermark stored for the binding it updates. Necessity: an + older proof replayed after a handle change would roll the binding back. - REQ-COMMON-26 (upholds SP-FRESH-01): - The Consuming Contract MUST derive `proofValidUntil` from the profile's - authenticated platform time and the current protocol parameters. The + The Consuming Contract MUST derive `proofValidUntil` as + `metadataObservedAt` plus the profile's current protocol parameter. The Consuming Contract MUST reject a submission where - `block.timestamp >= proofValidUntil`. + `block.timestamp >= proofValidUntil`. The parameter is Registry + configuration; a transaction caller cannot supply it. - REQ-COMMON-27 (upholds SP-FRESH-01): The Consuming Contract MUST NOT accept a caller-supplied validity bound. - REQ-COMMON-28 (upholds SP-FRESH-01): @@ -420,21 +462,27 @@ the constructions that role implements. No ceremony artifact, log, or public input contains `pkceNonce`. Verification: inspection of the emitted artifacts. - TEST-COMMON-09 (exercises REQ-COMMON-16, REQ-COMMON-17, REQ-COMMON-17C, REQ-COMMON-22A): - The Canonical Runtime rejects a proof carrying a client identifier other than - its immutable profile's client, while two application deployments using - different clients remain acceptable to the same Consuming Contract. -- TEST-COMMON-10 (exercises REQ-COMMON-17A, REQ-COMMON-17B, REQ-COMMON-18, REQ-COMMON-19, REQ-COMMON-20, REQ-COMMON-22): + The Canonical Runtime rejects a proof carrying a client identifier other + than its immutable profile's client, while a proof carrying a client + identifier registered nowhere remains acceptable to the Verifier. +- TEST-COMMON-10 (exercises REQ-COMMON-17A, REQ-COMMON-17B, REQ-COMMON-18, REQ-COMMON-18A, REQ-COMMON-19, REQ-COMMON-19A, REQ-COMMON-19B, REQ-COMMON-20, REQ-COMMON-22): A transcript carrying a second copy of a templated field, placed inside or - after an undisclosed range, is rejected. -- TEST-COMMON-11 (exercises REQ-COMMON-21, REQ-COMMON-21A, REQ-COMMON-21B): - The Consuming Contract rejects an authenticated foreign authority, method, or - path. The request constructor refuses a media type or `redirect_uri` - differing from its immutable deployment profile. + after an undisclosed range, is rejected; a transcript whose extraction + delimiter matches at two positions in the revealed bytes is rejected; a + witnessed value containing the closing delimiter, or a pattern placed in + padding past the payload length, fails to prove; and a transcript whose + ranges do not tile the profile layout is rejected. +- TEST-COMMON-11 (exercises REQ-COMMON-21, REQ-COMMON-21A, REQ-COMMON-21B, REQ-COMMON-21C): + The Consuming Contract rejects an authenticated foreign authority, method, + or path. The request constructor refuses a media type or `redirect_uri` + differing from its immutable deployment profile. One verifying key serves + two deployments configured with different clients and redirect URIs. - TEST-COMMON-12 (exercises REQ-COMMON-23, REQ-COMMON-24, REQ-COMMON-25): A fractional, negative, overflowing, or textual timestamp is rejected. -- TEST-COMMON-13 (exercises REQ-COMMON-26, REQ-COMMON-27, REQ-COMMON-28): - A submission at or after `proofValidUntil` is rejected, and a caller-supplied - validity bound has no effect. +- TEST-COMMON-13 (exercises REQ-COMMON-25A, REQ-COMMON-26, REQ-COMMON-27, REQ-COMMON-28): + A submission at or after `proofValidUntil` is rejected, a caller-supplied + validity bound has no effect, and evidence whose `metadataObservedAt` + precedes the stored watermark is rejected. - TEST-COMMON-14 (exercises REQ-COMMON-30, REQ-COMMON-31): A redirect request carrying a forwarding target forwards to the compiled application origin instead. diff --git a/specs/libid.md b/specs/libid.md index 7d275c52..e13adfb1 100644 --- a/specs/libid.md +++ b/specs/libid.md @@ -33,8 +33,8 @@ ceilings. | Parameter | Launch value | Use | |---|---:|---| -| `proofLifetime[x]` | 600 | maximum age of the earlier X token/identity attestation | -| `proofLifetime[github]` | 600 | maximum age of the earlier GitHub token/identity attestation | +| `proofLifetime[x]` | 3600 | maximum age of the earlier X token/identity attestation | +| `proofLifetime[github]` | 3600 | maximum age of the earlier GitHub token/identity attestation | | `googleClockSkewGrace` | 300 | grace added to the signed Google ID-Token expiry | | `maxFutureAttestationSkew` | 300 | maximum X/GitHub attestation lead over chain time | diff --git a/specs/platform-ceremonies.md b/specs/platform-ceremonies.md index d4b7584a..b1c2de73 100644 --- a/specs/platform-ceremonies.md +++ b/specs/platform-ceremonies.md @@ -82,14 +82,26 @@ replace the immutable `userId`. ### 2.2 Metadata ordering and validity ceilings -| Identity platform | `metadataObservedAt` | `proofValidUntil` — first invalid timestamp | +One rule bounds every proof's validity: `proofValidUntil = +metadataObservedAt` plus a per-platform protocol parameter, where +`metadataObservedAt` is the platform-signed time below and the parameter is +Registry-configurable state, never a caller parameter. + +| Identity platform | `metadataObservedAt` | Validity parameter | |---|---|---| -| Google | signed ID-Token `iat` | `min(signed exp + googleClockSkewGrace, trustedUntil)` | -| X | signed `meAttest.timestamp` | `min(tokenAttest.timestamp, meAttest.timestamp) + proofLifetime[x]` | -| GitHub | signed `userAttest.timestamp` | `min(tokenAttest.timestamp, userAttest.timestamp) + proofLifetime[github]` | +| Google | signed ID-Token `exp` | `googleClockSkewGrace` | +| X | `min(tokenAttest.timestamp, meAttest.timestamp)` | `proofLifetime[x]` | +| GitHub | `min(tokenAttest.timestamp, userAttest.timestamp)` | `proofLifetime[github]` | For X and GitHub, "timestamp" is the signed TLSNotary attestation creation -time. The named values are current [protocol parameters](libid.md#protocol-parameters). +time. The named values are current +[protocol parameters](libid.md#protocol-parameters). Google's `exp` already +encodes issuance plus Google's own token lifetime, so its parameter is only +clock-skew grace. The Claim Digest carries no expiration; validity derives +only from authenticated platform time plus the current parameter. +`metadataObservedAt` is also the monotone replay watermark of common +REQ-COMMON-25A: a binding never regresses to evidence observed earlier than +what it already holds. - REQ-PLAT-09 (upholds SP-FRESH-01): The Consuming Contract MUST reject an X or GitHub attestation timestamp more @@ -116,6 +128,11 @@ Identity evidence is the signed ID Token delivered in the redirect fragment. | 6 | `state` | immutable one-use OAuth state | | 7 | `nonce` | `BASE64URL_NOPAD(bytes32(claimDigest))` | +The authorization request is plain browser navigation and is never +notarized; no proof semantics attach to any field above. The table is +operational guidance for obtaining a token whose signed claims satisfy +§3.2–§3.3. The signed ID Token is the only Google evidence. + - REQ-PLAT-10 (upholds SP-BIND-01): The Canonical Runtime MUST set `nonce` to the base64url encoding of the 32 Claim Digest bytes, not to hexadecimal text. @@ -161,6 +178,12 @@ The Proving Circuit and consuming contract enforce all of the following: - REQ-PLAT-16 (upholds SP-CLIENT-01): The Proving Circuit MUST prove Google's signature over the ID Token under a key whose modulus the registry lists as trusted. +- REQ-PLAT-16A (upholds SP-CLIENT-01): + The Proving Circuit MUST expose the signing modulus, or a + collision-resistant digest of it, as a public proof input. Necessity: the + signature is proven in circuit because verifying it on chain would publish + the signed token and disclose its claims; the Consuming Contract checks + only the exposed modulus against the trusted set of REQ-PLAT-23. - REQ-PLAT-17 (upholds SP-BIND-01): The Proving Circuit MUST prove the signed `iss` equals `https://accounts.google.com`. @@ -168,16 +191,20 @@ The Proving Circuit and consuming contract enforce all of the following: The Proving Circuit MUST prove `nonce` equals the Claim Digest. - REQ-PLAT-19 (upholds SP-CLIENT-01): The Proving Circuit MUST expose the signed `aud` as the client-binding public - input. + input. Admission stays permissionless per common REQ-COMMON-17C; a Consuming + Contract MAY read the exposed `aud`. - REQ-PLAT-20: The Proving Circuit MUST prove `email_verified` is the boolean `true`. Necessity: an unverified email would let one account assert another party's address as its handle. - REQ-PLAT-21 (upholds SP-BIND-01): - The Proving Circuit MUST require exactly one top-level value of the required - type for `iss`, `sub`, `aud`, `iat`, `exp`, `nonce`, `email`, and - `email_verified`. The Proving Circuit MUST reject duplicate keys, type - substitution, and nested lookalikes. + The prover supplies the offset of each checked claim as a private input. + The Proving Circuit MUST assert the JSON pattern of `iss`, `sub`, `aud`, + `exp`, `nonce`, `email`, and `email_verified` at the supplied offsets per + common REQ-COMMON-19 and REQ-COMMON-19B. The Proving Circuit MUST + constrain each extracted value to its required type and charset. + Duplicate-free top-level structure is the issuer's behavior under + ASM-PROV-06; the circuit performs no search and no duplicate scan. - REQ-PLAT-22 (upholds SP-FRESH-01): The Consuming Contract MUST reject a proof whose signed `exp` places `proofValidUntil` at or before `block.timestamp`. @@ -265,18 +292,46 @@ sessions. The Proving Circuit MUST require exactly one nonempty printable-ASCII top-level `access_token` string of at most 4096 bytes in the token response. +Per common §9, the token session reveals exactly these ranges; every other +byte stays behind a blinded, charset-constrained hash commitment: + +| Range | Revealed | Why | +|---|---|---| +| endpoint authority, method, path | yes | exposed as public proof inputs per common REQ-COMMON-21 | +| `client_id` | yes | exposed as a public proof input | +| `code` | yes | compared to the code consumed at redirect ingress | +| `SHA256(ASCII(access_token))` | yes | hash commitment linking the token and identity transcripts | +| attestation timestamp | yes | derives the authenticated validity ceiling | +| everything else | no | headers, `code_verifier`, `scope`, `token_type`, other response fields | + +- REQ-PLAT-29A (upholds SP-CLIENT-01): + The Proving Circuit MUST reveal the `client_id` range of the token request. +- REQ-PLAT-29B (upholds SP-CLIENT-01): + The Proving Circuit MUST expose that revealed `client_id` as a public proof + input. +- REQ-PLAT-30A (upholds SP-EXCHANGE-01): + The notarized token session MUST reveal the returned `access_token` only as + its `SHA256(ASCII(access_token))` hash commitment. The plaintext token bytes + MUST stay redacted. + ### 5.3 Identity request `GET https://api.x.com/2/users/me` with no query, `Authorization: Bearer `, `Accept: application/json`. - REQ-PLAT-31 (upholds SP-BIND-01): - The Proving Circuit MUST require one top-level `data` object carrying exactly - one string `id` and one string `username`. The Proving Circuit MUST reject - duplicate, nested-lookalike, differently typed, and out-of-object fields. + The Consuming Contract MUST extract `id` and `username` from the revealed + response bytes by their full `"field":"` delimiters, rejecting a transcript + in which either delimiter matches at more than one position, per common + REQ-COMMON-19A. Necessity: the response carries account-holder-influenced + text, such as the display name, that can embed a lookalike field. - REQ-PLAT-32 (upholds SP-EXCHANGE-01): The Proving Circuit MUST assert the same bearer commitment across the token transcript and the identity transcript. +- REQ-PLAT-32A (upholds SP-EXCHANGE-01): + The notarized identity session MUST reveal the `Authorization` bearer value + only as the same `SHA256(ASCII(access_token))` hash commitment. The + plaintext token bytes MUST stay redacted. Public proof inputs are the Claim Digest, the client identifier, both attestation timestamps, the identity fields, and the authenticated authority, @@ -417,10 +472,11 @@ interface TokenProofResponseV1 { ### 6.4 Disclosure and verification The Token-Proof Service, which knows the client secret and complete exchange -transcript, produces `tokenProof`. Its circuit verifies the TLSNotary -attestation and applies REQ-COMMON-18 to the complete form body and response. -The browser never receives the client secret or an unverifiable selectively -disclosed transcript. +transcript, produces `tokenProof`. Its circuit opens the hidden-range +commitments of common REQ-COMMON-18, and the verifier checks the revealed +ranges and layout per common REQ-COMMON-18A and REQ-COMMON-21A. The browser +never receives the client secret or an unverifiable selectively disclosed +transcript. The token proof exposes exactly the public outputs needed to bind it to the local ceremony and the later `/user` transcript. The separately returned @@ -440,10 +496,11 @@ local ceremony and the later `/user` transcript. The separately returned | `client_secret` | no | never revealed, per REQ-PLAT-35A | | everything else | no | headers, status line, `scope`, `token_type`, other response fields | -The circuit proves every unexposed form-body and response byte against the -profile template. It also proves the exact response grammar, `token_type`, and -scope rule from REQ-PLAT-36. Revealing those bytes would add no check and would -widen exposure. +Every unexposed range stays behind a blinded hash commitment, bounded by +revealed anchor bytes and charset-constrained per common REQ-COMMON-20. The +`token_type` and scope rule of REQ-PLAT-36 are asserted in circuit against +their committed ranges without disclosure. Revealing those bytes would add no +check and would widen exposure. - REQ-PLAT-43D (upholds SP-EXCHANGE-01): The Token Proof MUST expose no public output outside the nine rows marked `yes` @@ -476,11 +533,14 @@ widen exposure. The Canonical Runtime MUST discard the response and start neither `/user` nor a resume record when any check in REQ-PLAT-44 through REQ-PLAT-49 fails. -Verifying only selected attestation ranges is insufficient: a prover that -composes the request could otherwise hide a second `code` or `code_verifier` -in an unverified range and let GitHub honor that copy. The server-produced -token proof is the concrete artifact that enforces REQ-COMMON-18 while keeping -the client secret from the browser. +Verifying only an arbitrary selection of ranges is insufficient: a prover +that composes the request could otherwise hide a second `code` or +`code_verifier` in an unaccounted range and let GitHub honor that copy. The +layout tiling of common REQ-COMMON-18A closes this: every transcript byte is +either revealed or inside a charset-constrained committed range, so no +unaccounted range exists. The server-produced token proof is the concrete +artifact that carries these commitments while keeping the client secret from +the browser. ### 6.5 Identity request @@ -489,9 +549,11 @@ the client secret from the browser. `X-GitHub-Api-Version: 2022-11-28`. - REQ-PLAT-51 (upholds SP-BIND-01): - The Proving Circuit MUST require exactly one top-level `id` JSON integer and - one top-level `login` string. The Proving Circuit MUST reject duplicate, - nested-lookalike, differently typed, and noncanonical values. + The Consuming Contract MUST extract `id` and `login` from the revealed + response bytes by their full field delimiters, rejecting a transcript in + which either delimiter matches at more than one position, per common + REQ-COMMON-19A. The Consuming Contract MUST reject a noncanonical `id` + encoding. - REQ-PLAT-52 (upholds SP-EXCHANGE-01): The Final Identity Circuit MUST verify the `github/v1` token proof and assert the same bearer commitment across that proof and the identity transcript. The @@ -546,7 +608,7 @@ contract. 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-PLAT-16, REQ-PLAT-17, REQ-PLAT-19, REQ-PLAT-20, REQ-PLAT-21, REQ-PLAT-23): +- TEST-PLAT-06 (exercises REQ-PLAT-16, REQ-PLAT-16A, REQ-PLAT-17, REQ-PLAT-19, REQ-PLAT-20, REQ-PLAT-21, REQ-PLAT-23): A token with a foreign issuer, foreign audience, `email_verified: false`, a duplicated top-level claim, or an untrusted signing modulus is rejected in each case. @@ -559,6 +621,12 @@ contract. - TEST-PLAT-09 (exercises REQ-PLAT-29, REQ-PLAT-46): A transcript whose disclosed `code` differs from the code consumed at redirect ingress is rejected on X and on GitHub. +- TEST-PLAT-09A (exercises REQ-PLAT-29A, REQ-PLAT-29B): + An X proof that does not reveal the token request's `client_id`, or does not + expose it as a public input, is rejected. +- TEST-PLAT-09B (exercises REQ-PLAT-30A, REQ-PLAT-32A): + An X transcript that reveals plaintext `access_token` bytes in either + session, or omits the bearer hash commitment, is rejected. - TEST-PLAT-10 (exercises REQ-PLAT-30, REQ-PLAT-31, REQ-PLAT-32, REQ-PLAT-36, REQ-PLAT-51, REQ-PLAT-52): A response missing the required field, carrying a duplicate, or carrying a differently typed value is rejected; GitHub rejects a response whose From 9e06a134d83ee47ec7d49e27f668fd6a09575f11 Mon Sep 17 00:00:00 2001 From: Wondertan Date: Mon, 17 Aug 2026 16:58:55 +0100 Subject: [PATCH 2/5] docs(ceremony): separate validity from metadata time Use token-exchange time for X and GitHub proof validity, identity time for mutable metadata ordering, and Google's signed expiry for both roles. Assisted-by: GPT-5 Signed-off-by: Wondertan --- specs/ceremony-common.md | 31 +++++++++---------- specs/libid.md | 17 +++++----- specs/platform-ceremonies.md | 60 +++++++++++++++++++++--------------- 3 files changed, 58 insertions(+), 50 deletions(-) diff --git a/specs/ceremony-common.md b/specs/ceremony-common.md index 9733fea2..ed4c6e48 100644 --- a/specs/ceremony-common.md +++ b/specs/ceremony-common.md @@ -74,9 +74,9 @@ Canonical Runtime: The immutable browser release that constructs claim - SP-BIND-01: Evidence produced by a ceremony discharges only for the Call Data committed - in its Claim Digest. Depends on ASM-PROV-02, ASM-PROV-05, ASM-CHAIN-01. - Evidence: conformance tests (supporting, not proving) plus the collision - resistance of SHA-256 and keccak256. + in its Claim Digest. Depends on ASM-PROV-02, ASM-PROV-05, ASM-PROV-06, + ASM-CHAIN-01. Evidence: 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, @@ -300,7 +300,7 @@ on-chain registration of clients exists. profile. - REQ-COMMON-17C (upholds SP-CLIENT-01): The Verifier MUST NOT require the exposed client identifier to belong to a - registered set. A Consuming Contract MAY read the exposed client identifier + registered set. The Consuming Contract MAY read the exposed client identifier for its own semantics. Necessity: client selection is permissionless application policy; the on-chain authorization is that the claim's target sends the transaction. @@ -331,15 +331,15 @@ public inputs, which never includes a credential. The Implementation MUST redact every byte outside the ranges its profile lists. - REQ-COMMON-18 (upholds SP-EXCHANGE-01): - The attestation MUST commit every hidden range with a blinded hash - commitment. The Proving Circuit MUST open the commitment of each hidden + The Implementation MUST commit every hidden attestation range with a blinded + hash commitment. The Proving Circuit MUST open the commitment of each hidden range whose value the profile checks. - REQ-COMMON-18A (upholds SP-EXCHANGE-01): - The verifier of an attestation MUST check that the revealed ranges and - hidden-range commitments tile the transcript in the exact layout the - profile fixes, with each hidden range bounded by revealed anchor bytes. + The Verifier MUST check that the revealed ranges and hidden-range commitments + tile the transcript in the exact layout the profile fixes, with each hidden + range bounded by revealed anchor bytes. - REQ-COMMON-19 (upholds SP-EXCHANGE-01): - A Proving Circuit extracting a field MUST receive the field's offset as a + The Proving Circuit extracting a field MUST receive the field's offset as a private input supplied by the prover; the circuit performs no search. The Proving Circuit MUST assert the full `"field":"` delimiter at that offset, the value bytes, and the closing delimiter. The Proving Circuit MUST @@ -353,7 +353,7 @@ public inputs, which never includes a credential. match into the neighboring field; without the bounds check a pattern can be planted in zero-padding. - REQ-COMMON-19A (upholds SP-EXCHANGE-01): - A Consuming Contract extracting a field from revealed attestation bytes + The Consuming Contract extracting a field from revealed attestation bytes MUST reject a transcript in which the field's full delimiter matches at more than one position. Necessity: an authenticated response value the account holder influences, such as a display name, can embed a lookalike @@ -423,11 +423,10 @@ field behind it. precedes the watermark stored for the binding it updates. Necessity: an older proof replayed after a handle change would roll the binding back. - REQ-COMMON-26 (upholds SP-FRESH-01): - The Consuming Contract MUST derive `proofValidUntil` as - `metadataObservedAt` plus the profile's current protocol parameter. The - Consuming Contract MUST reject a submission where - `block.timestamp >= proofValidUntil`. The parameter is Registry - configuration; a transaction caller cannot supply it. + The Consuming Contract MUST derive `proofValidUntil` from the platform + profile's authenticated validity input and any current protocol parameter + that profile names. The Consuming Contract MUST reject a submission where + `block.timestamp >= proofValidUntil`. - REQ-COMMON-27 (upholds SP-FRESH-01): The Consuming Contract MUST NOT accept a caller-supplied validity bound. - REQ-COMMON-28 (upholds SP-FRESH-01): diff --git a/specs/libid.md b/specs/libid.md index e13adfb1..d51bbf34 100644 --- a/specs/libid.md +++ b/specs/libid.md @@ -28,14 +28,12 @@ The Registry Governance Process may update a supported parameter and emits its key, previous value, and new value. The Consuming Contract reads the current value when it verifies a proof; browser reads are advisory only. Lowering a parameter may reject an outstanding proof, while raising one may extend an -outstanding proof only within its authenticated platform and trust-root -ceilings. +outstanding X/GitHub proof. Current trust-root membership remains required. | Parameter | Launch value | Use | |---|---:|---| -| `proofLifetime[x]` | 3600 | maximum age of the earlier X token/identity attestation | -| `proofLifetime[github]` | 3600 | maximum age of the earlier GitHub token/identity attestation | -| `googleClockSkewGrace` | 300 | grace added to the signed Google ID-Token expiry | +| `proofLifetime[x]` | 3600 | maximum age of the X token-exchange attestation | +| `proofLifetime[github]` | 3600 | maximum age of the GitHub token-exchange attestation | | `maxFutureAttestationSkew` | 300 | maximum X/GitHub attestation lead over chain time | - REQ-PARAM-01: @@ -56,10 +54,11 @@ ceilings. ## Security Considerations -Registry governance can shorten or widen the acceptance window within the -authenticated provider and trust-root ceilings. The linked chapters define the -remaining assumptions, security properties, requirements, and -platform-specific security considerations. +Registry governance can shorten or widen the X/GitHub acceptance window. Every +proof still requires a currently active trust root, and Google remains bounded +by its signed expiry. The linked chapters define the remaining assumptions, +security properties, requirements, and platform-specific security +considerations. ## References diff --git a/specs/platform-ceremonies.md b/specs/platform-ceremonies.md index b1c2de73..8e5ac9d6 100644 --- a/specs/platform-ceremonies.md +++ b/specs/platform-ceremonies.md @@ -82,30 +82,36 @@ replace the immutable `userId`. ### 2.2 Metadata ordering and validity ceilings -One rule bounds every proof's validity: `proofValidUntil = -metadataObservedAt` plus a per-platform protocol parameter, where -`metadataObservedAt` is the platform-signed time below and the parameter is -Registry-configurable state, never a caller parameter. +Proof validity and mutable-metadata ordering use the authenticated times below. -| Identity platform | `metadataObservedAt` | Validity parameter | +| Identity platform | `metadataObservedAt` | `proofValidUntil` | |---|---|---| -| Google | signed ID-Token `exp` | `googleClockSkewGrace` | -| X | `min(tokenAttest.timestamp, meAttest.timestamp)` | `proofLifetime[x]` | -| GitHub | `min(tokenAttest.timestamp, userAttest.timestamp)` | `proofLifetime[github]` | +| Google | signed ID-Token `exp` | signed ID-Token `exp` | +| X | signed `meAttest.timestamp` | `tokenAttest.timestamp + proofLifetime[x]` | +| GitHub | signed `userAttest.timestamp` | `tokenAttest.timestamp + proofLifetime[github]` | For X and GitHub, "timestamp" is the signed TLSNotary attestation creation -time. The named values are current -[protocol parameters](libid.md#protocol-parameters). Google's `exp` already -encodes issuance plus Google's own token lifetime, so its parameter is only -clock-skew grace. The Claim Digest carries no expiration; validity derives -only from authenticated platform time plus the current parameter. -`metadataObservedAt` is also the monotone replay watermark of common -REQ-COMMON-25A: a binding never regresses to evidence observed earlier than -what it already holds. +time. The token attestation is the one-time PKCE and Claim-Digest binding, so +it alone anchors proof validity. The identity attestation opens the same bearer +and its timestamp orders the mutable handle it observed; it does not refresh +the authorization. The named lifetimes are current +[protocol parameters](libid.md#protocol-parameters). + +Google's signed `exp` already supplies the accepted one-hour ordering and +validity value. A Google proof also requires its signing modulus to remain in +the Registry's active set. The Claim Digest carries no expiration. +`metadataObservedAt` is the monotone replay watermark of common +REQ-COMMON-25A. A binding never regresses to evidence ordered earlier than what +it already holds. - REQ-PLAT-09 (upholds SP-FRESH-01): The Consuming Contract MUST reject an X or GitHub attestation timestamp more than `maxFutureAttestationSkew` ahead of `block.timestamp`. +- REQ-PLAT-09A (upholds SP-FRESH-01): + The Consuming Contract MUST derive `metadataObservedAt` and + `proofValidUntil` from the exact sources in the table above. An X or GitHub + identity attestation does not authorize an extension: the Consuming Contract + MUST NOT use it to extend `proofValidUntil`. ## 3. Google OIDC ceremony @@ -191,7 +197,7 @@ The Proving Circuit and consuming contract enforce all of the following: The Proving Circuit MUST prove `nonce` equals the Claim Digest. - REQ-PLAT-19 (upholds SP-CLIENT-01): The Proving Circuit MUST expose the signed `aud` as the client-binding public - input. Admission stays permissionless per common REQ-COMMON-17C; a Consuming + input. Admission stays permissionless per common REQ-COMMON-17C; the Consuming Contract MAY read the exposed `aud`. - REQ-PLAT-20: The Proving Circuit MUST prove `email_verified` is the boolean `true`. @@ -310,9 +316,9 @@ byte stays behind a blinded, charset-constrained hash commitment: The Proving Circuit MUST expose that revealed `client_id` as a public proof input. - REQ-PLAT-30A (upholds SP-EXCHANGE-01): - The notarized token session MUST reveal the returned `access_token` only as - its `SHA256(ASCII(access_token))` hash commitment. The plaintext token bytes - MUST stay redacted. + The Implementation MUST reveal the returned `access_token` from the notarized + token session only as its `SHA256(ASCII(access_token))` hash commitment. The + Implementation MUST keep the plaintext token bytes redacted. ### 5.3 Identity request @@ -329,9 +335,10 @@ byte stays behind a blinded, charset-constrained hash commitment: The Proving Circuit MUST assert the same bearer commitment across the token transcript and the identity transcript. - REQ-PLAT-32A (upholds SP-EXCHANGE-01): - The notarized identity session MUST reveal the `Authorization` bearer value - only as the same `SHA256(ASCII(access_token))` hash commitment. The - plaintext token bytes MUST stay redacted. + The Implementation MUST reveal the `Authorization` bearer value from the + notarized identity session only as the same + `SHA256(ASCII(access_token))` hash commitment. The Implementation MUST keep + the plaintext token bytes redacted. Public proof inputs are the Claim Digest, the client identifier, both attestation timestamps, the identity fields, and the authenticated authority, @@ -612,9 +619,12 @@ contract. A token with a foreign issuer, foreign audience, `email_verified: false`, a duplicated top-level claim, or an untrusted signing modulus is rejected in each case. -- TEST-PLAT-07 (exercises REQ-PLAT-22, REQ-PLAT-09): +- TEST-PLAT-07 (exercises REQ-PLAT-22, REQ-PLAT-09, REQ-PLAT-09A): A proof at or after `proofValidUntil`, and an attestation timestamp more than - `maxFutureAttestationSkew` ahead of `block.timestamp`, are rejected. + `maxFutureAttestationSkew` ahead of `block.timestamp`, are rejected. A later + X or GitHub identity attestation advances `metadataObservedAt` without + extending the token-attestation-derived `proofValidUntil`; Google uses its + signed `exp` for both values. - TEST-PLAT-08 (exercises REQ-PLAT-24): The trusted modulus set contains every modulus currently published at Google's JWKS endpoint. From e03dd1336e96a1eba9edd1344b32dc58872f4086 Mon Sep 17 00:00:00 2001 From: Wondertan Date: Mon, 17 Aug 2026 17:21:28 +0100 Subject: [PATCH 3/5] docs(ceremony): state token parser assumption Assisted-by: GPT-5 Signed-off-by: Wondertan --- specs/ceremony-common.md | 73 ++++++++++++++++++++++++------------ specs/platform-ceremonies.md | 29 ++++++++++---- 2 files changed, 71 insertions(+), 31 deletions(-) diff --git a/specs/ceremony-common.md b/specs/ceremony-common.md index ed4c6e48..c1100260 100644 --- a/specs/ceremony-common.md +++ b/specs/ceremony-common.md @@ -64,6 +64,15 @@ Canonical Runtime: The immutable browser release that constructs claim - ASM-PROV-06: An Identity Platform emits a well-formed authenticated response in which each authoritative field appears exactly once at the top level. +- ASM-PROV-07: + At the exact token endpoint and method fixed by a Platform Profile, the + Identity Platform accepts token redemption only under the profile's media + type and rejects a form body containing more than one decoded occurrence of + any profile-listed field. Necessity: launch circuits deliberately avoid + proving the complete form grammar; without this parser property a prover + could witness one `code` or `code_verifier` while the platform consumes + another. Evidence: recurring integration probes against each production + endpoint. - ASM-NOTARY-01: The configured notary key signs only transcripts it observed. - ASM-BROWSER-01: @@ -75,13 +84,14 @@ Canonical Runtime: The immutable browser release that constructs claim - SP-BIND-01: Evidence produced by a ceremony discharges only for the Call Data committed in its Claim Digest. Depends on ASM-PROV-02, ASM-PROV-05, ASM-PROV-06, - ASM-CHAIN-01. Evidence: conformance tests (supporting, not proving) plus the - collision resistance of SHA-256 and keccak256. + ASM-PROV-07, ASM-CHAIN-01. Evidence: 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, - ASM-PROV-05, ASM-NOTARY-01, and ASM-BROWSER-01. Evidence: checked invariant - in the Canonical Runtime, plus conformance tests (supporting). + ASM-PROV-05, ASM-PROV-07, ASM-NOTARY-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 @@ -90,8 +100,8 @@ Canonical Runtime: The immutable browser release that constructs claim conformance tests (supporting). - SP-EXCHANGE-01: An attested token exchange redeems the authorization code produced by this - ceremony and no other. Depends on ASM-PROV-03, ASM-NOTARY-01, ASM-BROWSER-01. - Evidence: conformance tests (supporting, not proving). + ceremony and no other. Depends on ASM-PROV-03, ASM-PROV-07, ASM-NOTARY-01, + ASM-BROWSER-01. Evidence: conformance tests (supporting, not proving). - SP-FRESH-01: Evidence older than its authenticated ceiling is rejected. Depends on ASM-CHAIN-01, ASM-NOTARY-01, ASM-PROV-05. Evidence: checked invariant in the @@ -220,6 +230,11 @@ Google as the OIDC `nonce`, X and GitHub through the PKCE construction in §7. - REQ-COMMON-31 (upholds SP-DELIVERY-01): The Canonical Runtime MUST ignore a forwarding target supplied in the redirect request. +- REQ-COMMON-32 (upholds SP-BIND-01, SP-EXCHANGE-01): + The Deployment MUST run recurring integration probes establishing + ASM-PROV-07 for every production form-encoded token endpoint. The Deployment + MUST make the affected Platform Profile ineligible for new ceremonies when + a probe fails. The HTTPS authority, method, path, parameter tuple, and authenticated request and response values carry proof semantics. Header casing and order do @@ -312,13 +327,15 @@ browser-local and produce no on-chain effect. A proof authenticates bytes, not fields. The Proving Circuit is responsible for checking the fields the profile needs — and only those fields, never the -whole template. A field check is a JSON pattern match at a witnessed offset: +whole template. A JSON field check is a pattern match at a witnessed offset: the full `"field":"` delimiter, the value, and the closing structural byte. -Because an authenticated JSON document carries each authoritative field once -(ASM-PROV-06), per-field pattern checks give the same guarantee that -whole-template equality would, at a fraction of the constraints. Hidden -ranges stay behind blinded hash commitments the circuit opens; the circuit -links transcripts through those commitments and binds the Claim Digest. +A form-field check likewise asserts a field boundary, the exact ASCII name and +`=`, the value, and the next `&` or body end. Because the authenticated parser +outputs satisfy ASM-PROV-06 and ASM-PROV-07, these local checks provide the +required field meaning without the impractical proving cost of a complete JSON +or form parser. Hidden ranges stay behind blinded hash commitments the circuit +opens; the circuit links transcripts through those commitments and binds the +Claim Digest. Disclosure and verification are two separate layers. The Platform Profile fixes a minimal set of revealed ranges; every other byte stays behind a @@ -352,6 +369,13 @@ public inputs, which never includes a credential. Necessity: without the charset bound a longer witnessed value extends the match into the neighboring field; without the bounds check a pattern can be planted in zero-padding. +- REQ-COMMON-19C (upholds SP-BIND-01, SP-EXCHANGE-01): + The Proving Circuit extracting a field from an + `application/x-www-form-urlencoded` request MUST assert that the match begins + at byte zero or immediately after `&`, followed by the exact ASCII field + name, `=`, the charset-constrained value, and then `&` or the authenticated + body end. The circuit does not scan the rest of the body for duplicates; + that property is ASM-PROV-07. - REQ-COMMON-19A (upholds SP-EXCHANGE-01): The Consuming Contract extracting a field from revealed attestation bytes MUST reject a transcript in which the field's full delimiter matches at @@ -400,10 +424,11 @@ platform endpoint constants into each circuit without weakening the binding. The Proving Circuit MUST NOT expose a client secret, or any value derived from one, as a public proof input. -An undisclosed range still reaches the platform. Ordering it last, -constraining its charset so it cannot contain a field delimiter, and tiling -the layout with revealed anchors prevents a prover hiding a second copy of a -field behind it. +An undisclosed range still reaches the platform. Ordering a credential last +and constraining its charset prevents that credential from injecting a form +delimiter. Range tiling proves that no transcript bytes are omitted, but does +not prove the complete grammar of hidden bytes or exclude a second form field; +request soundness additionally depends on ASM-PROV-07. ## 10. Evidence time @@ -464,13 +489,15 @@ the constructions that role implements. The Canonical Runtime rejects a proof carrying a client identifier other than its immutable profile's client, while a proof carrying a client identifier registered nowhere remains acceptable to the Verifier. -- TEST-COMMON-10 (exercises REQ-COMMON-17A, REQ-COMMON-17B, REQ-COMMON-18, REQ-COMMON-18A, REQ-COMMON-19, REQ-COMMON-19A, REQ-COMMON-19B, REQ-COMMON-20, REQ-COMMON-22): - A transcript carrying a second copy of a templated field, placed inside or - after an undisclosed range, is rejected; a transcript whose extraction - delimiter matches at two positions in the revealed bytes is rejected; a - witnessed value containing the closing delimiter, or a pattern placed in - padding past the payload length, fails to prove; and a transcript whose - ranges do not tile the profile layout is rejected. +- TEST-COMMON-10 (exercises REQ-COMMON-17A, REQ-COMMON-17B, REQ-COMMON-18, REQ-COMMON-18A, REQ-COMMON-19, REQ-COMMON-19A, REQ-COMMON-19B, REQ-COMMON-19C, REQ-COMMON-20, REQ-COMMON-22): + An authenticated JSON response carrying a second copy of a templated field, + placed inside or after an undisclosed range, is rejected; a transcript whose + extraction delimiter matches at two positions in the revealed bytes is + rejected; a witnessed value containing the closing delimiter, or a pattern + placed in padding past the payload length, fails to prove; a form-field match + not bounded by the body start or `&` and the next `&` or body end fails to + prove; and a transcript whose ranges do not tile the profile layout is + rejected. - TEST-COMMON-11 (exercises REQ-COMMON-21, REQ-COMMON-21A, REQ-COMMON-21B, REQ-COMMON-21C): The Consuming Contract 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 8e5ac9d6..a6a21139 100644 --- a/specs/platform-ceremonies.md +++ b/specs/platform-ceremonies.md @@ -540,14 +540,14 @@ check and would widen exposure. The Canonical Runtime MUST discard the response and start neither `/user` nor a resume record when any check in REQ-PLAT-44 through REQ-PLAT-49 fails. -Verifying only an arbitrary selection of ranges is insufficient: a prover -that composes the request could otherwise hide a second `code` or -`code_verifier` in an unaccounted range and let GitHub honor that copy. The -layout tiling of common REQ-COMMON-18A closes this: every transcript byte is -either revealed or inside a charset-constrained committed range, so no -unaccounted range exists. The server-produced token proof is the concrete -artifact that carries these commitments while keeping the client secret from -the browser. +Verifying only arbitrary byte substrings is insufficient: a prover that +composes the request could otherwise witness one `code` or `code_verifier` +while GitHub consumes a duplicate. The local form-field matcher and layout +tiling prove where the checked values occur and that no transcript bytes are +omitted. Deliberately, they do not pay the impractical circuit cost of proving +the complete form grammar; uniqueness of decoded request fields is the +endpoint-parser assumption ASM-PROV-07. The server-produced token proof carries +the commitments while keeping the client secret from the browser. ### 6.5 Identity request @@ -667,6 +667,12 @@ contract. Launch uses Proxy mode, rejects application or request selection of Browser MPC, uses no application-controlled platform egress, and carries no partial transcript state into a retry. +- TEST-PLAT-19 (exercises REQ-COMMON-32; supports ASM-PROV-07): + Recurring integration probes send each profile-listed X and GitHub token + request field twice, in both orders and using both literal and percent-encoded + equivalent field names, and send the otherwise valid request under alternate + media types. The production endpoint rejects every probe and issues no + bearer. ## 9. Security Considerations @@ -694,6 +700,13 @@ platform behavior rather than a proven property. The Implementation claiming conformance MUST run a recurring check that each platform still rejects a mismatched `code_verifier`. +X and GitHub request-field uniqueness likewise rests on their fixed token +endpoints' decoded-form behavior (ASM-PROV-07), rather than a complete grammar +proof. The authenticated authority, method, and path plus the endpoint's +refusal of other media types scope that assumption to the behavior exercised +by TEST-PLAT-19. If an endpoint begins accepting any duplicate profile field, +its profile is ineligible until a new proof construction closes the ambiguity. + A malicious Token-Proof Service cannot rebind a ceremony to other call data, because the Claim Digest fixes it before the platform is contacted. It can withhold, and it can attempt to substitute a token obtained under a From 608e3d9d484930597d9ccb17924c05ec43bd687d Mon Sep 17 00:00:00 2001 From: Wondertan Date: Mon, 17 Aug 2026 17:48:26 +0100 Subject: [PATCH 4/5] docs(ceremony): unify verified identity source Assisted-by: GPT-5 Signed-off-by: Wondertan --- specs/platform-ceremonies.md | 37 ++++++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/specs/platform-ceremonies.md b/specs/platform-ceremonies.md index a6a21139..0fdec294 100644 --- a/specs/platform-ceremonies.md +++ b/specs/platform-ceremonies.md @@ -36,8 +36,17 @@ Each platform ceremony has an independently versioned immutable profile: contains every fixed route it requires. Necessity: cross-component interoperability between runtime and deployment. - REQ-PLAT-03 (upholds SP-CLIENT-01): - The Canonical Runtime MUST construct the verified claim exclusively from - locally verified proof public inputs. + The Canonical Runtime MUST construct the verified claim exclusively from the + Platform Profile's canonical authenticated source after locally verifying + the proof. For X and GitHub, the Canonical Runtime MUST parse the exact + revealed identity-response bytes that the Consuming Contract parses, 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`. + +This is a data-source invariant, not a browser-flow requirement. It defines the +claim returned to callers and used by any composition-owned UI; it does not +create a ceremony-owned confirmation page. ### 2.1 Canonical platform user identifiers @@ -331,6 +340,11 @@ byte stays behind a blinded, charset-constrained hash commitment: in which either delimiter matches at more than one position, per common REQ-COMMON-19A. Necessity: the response carries account-holder-influenced text, such as the display name, that can embed a lookalike field. +- REQ-PLAT-31A (upholds SP-BIND-01): + The Canonical Runtime MUST derive the X `userId` and normalized handle from + those same revealed `id` and `username` bytes. The Final Identity Circuit + MUST NOT expose a second independently supplied representation of either + identity field. - REQ-PLAT-32 (upholds SP-EXCHANGE-01): The Proving Circuit MUST assert the same bearer commitment across the token transcript and the identity transcript. @@ -341,10 +355,11 @@ byte stays behind a blinded, charset-constrained hash commitment: the plaintext token bytes redacted. Public proof inputs are the Claim Digest, the client identifier, both -attestation timestamps, the identity fields, and the authenticated authority, -method, and path of both notarized sessions. The bearer is never a public proof -input. The `code_verifier` is recomputed in circuit per REQ-COMMON-15. The -Consuming Contract compares the endpoint inputs with the `x/v1` profile. +attestation timestamps, the revealed identity-response ranges, and the +authenticated authority, method, and path of both notarized sessions. No +detached identity fields or bearer are public proof inputs. The `code_verifier` +is recomputed in circuit per REQ-COMMON-15. The Consuming Contract compares the +endpoint inputs with the `x/v1` profile. - REQ-PLAT-33 (upholds SP-FRESH-01): The Canonical Runtime MUST complete the token request within X's @@ -561,6 +576,11 @@ the commitments while keeping the client secret from the browser. which either delimiter matches at more than one position, per common REQ-COMMON-19A. The Consuming Contract MUST reject a noncanonical `id` encoding. +- REQ-PLAT-51A (upholds SP-BIND-01): + The Canonical Runtime MUST derive the GitHub `userId` and normalized handle + from those same revealed `id` and `login` bytes. The Final Identity Circuit + MUST NOT expose a second independently supplied representation of either + identity field. - REQ-PLAT-52 (upholds SP-EXCHANGE-01): The Final Identity Circuit MUST verify the `github/v1` token proof and assert the same bearer commitment across that proof and the identity transcript. The @@ -663,6 +683,11 @@ contract. - TEST-PLAT-17 (exercises REQ-PLAT-01, REQ-PLAT-02, REQ-PLAT-03): A resume that substitutes a newer profile is rejected, an unlisted profile is ineligible, and no claim field originates outside verified public inputs. +- 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 runtime + rejects the extra representation; without it, the runtime and Consuming + Contract both derive account B byte for byte. - TEST-PLAT-18 (exercises REQ-PLAT-25, REQ-PLAT-26, REQ-PLAT-27): Launch uses Proxy mode, rejects application or request selection of Browser MPC, uses no application-controlled platform egress, and carries no partial From ac63797a1a656991e13909e311d599fff6019c6a Mon Sep 17 00:00:00 2001 From: Wondertan Date: Mon, 17 Aug 2026 17:52:58 +0100 Subject: [PATCH 5/5] docs(ceremony): define typed Google claims Assisted-by: GPT-5 Signed-off-by: Wondertan --- specs/ceremony-common.md | 38 ++++++++++++++++++++++-------------- specs/platform-ceremonies.md | 27 +++++++++++++++---------- 2 files changed, 40 insertions(+), 25 deletions(-) diff --git a/specs/ceremony-common.md b/specs/ceremony-common.md index c1100260..fcdf6da2 100644 --- a/specs/ceremony-common.md +++ b/specs/ceremony-common.md @@ -327,15 +327,15 @@ browser-local and produce no on-chain effect. A proof authenticates bytes, not fields. The Proving Circuit is responsible for checking the fields the profile needs — and only those fields, never the -whole template. A JSON field check is a pattern match at a witnessed offset: -the full `"field":"` delimiter, the value, and the closing structural byte. -A form-field check likewise asserts a field boundary, the exact ASCII name and -`=`, the value, and the next `&` or body end. Because the authenticated parser -outputs satisfy ASM-PROV-06 and ASM-PROV-07, these local checks provide the -required field meaning without the impractical proving cost of a complete JSON -or form parser. Hidden ranges stay behind blinded hash commitments the circuit -opens; the circuit links transcripts through those commitments and binds the -Claim Digest. +whole template. A JSON string check matches the full `"field":"` delimiter, +the value, and its closing quote. JSON unsigned integers and booleans use the +typed local matches of REQ-COMMON-19D. A form-field check asserts a field +boundary, the exact ASCII name and `=`, the value, and the next `&` or body end. +Because the authenticated parser outputs satisfy ASM-PROV-06 and ASM-PROV-07, +these local checks provide the required field meaning without the impractical +proving cost of a complete JSON or form parser. Hidden ranges stay behind +blinded hash commitments the circuit opens; the circuit links transcripts +through those commitments and binds the Claim Digest. Disclosure and verification are two separate layers. The Platform Profile fixes a minimal set of revealed ranges; every other byte stays behind a @@ -356,12 +356,11 @@ public inputs, which never includes a credential. tile the transcript in the exact layout the profile fixes, with each hidden range bounded by revealed anchor bytes. - REQ-COMMON-19 (upholds SP-EXCHANGE-01): - The Proving Circuit extracting a field MUST receive the field's offset as a - private input supplied by the prover; the circuit performs no search. The - Proving Circuit MUST assert the full `"field":"` delimiter at that offset, - the value bytes, and the closing delimiter. The Proving Circuit MUST - assert that the byte after the match is a structural byte the profile - fixes. + The Proving Circuit extracting a JSON string field MUST receive the field's + offset as a private input supplied by the prover; the circuit performs no + search. The Proving Circuit MUST assert the full `"field":"` delimiter at + that offset, the value bytes, the closing quote, and the following structural + byte fixed by the profile. - REQ-COMMON-19B (upholds SP-EXCHANGE-01): The Proving Circuit MUST constrain the extracted value's charset to exclude the closing delimiter. The Proving Circuit MUST assert the whole match lies @@ -369,6 +368,15 @@ public inputs, which never includes a credential. Necessity: without the charset bound a longer witnessed value extends the match into the neighboring field; without the bounds check a pattern can be planted in zero-padding. +- REQ-COMMON-19D (upholds SP-BIND-01, SP-FRESH-01): + The Proving Circuit extracting an unsigned JSON integer MUST assert the exact + `"field":` delimiter, a canonical `0|[1-9][0-9]*` decimal value bounded by + the profile's integer type, and the following structural byte fixed by the + profile. The Proving Circuit checking a JSON boolean MUST assert the exact + `"field":true` or `"field":false` literal required by the profile and its + following structural byte. The Proving Circuit MUST keep both matches inside + the authenticated payload length and reject quoted, signed, fractional, + exponent, leading-zero, padded, or wrong-type alternatives. - REQ-COMMON-19C (upholds SP-BIND-01, SP-EXCHANGE-01): The Proving Circuit extracting a field from an `application/x-www-form-urlencoded` request MUST assert that the match begins diff --git a/specs/platform-ceremonies.md b/specs/platform-ceremonies.md index 0fdec294..061eacc9 100644 --- a/specs/platform-ceremonies.md +++ b/specs/platform-ceremonies.md @@ -192,13 +192,16 @@ The Proving Circuit and consuming contract enforce all of the following: - REQ-PLAT-16 (upholds SP-CLIENT-01): The Proving Circuit MUST prove Google's signature over the ID Token under a - key whose modulus the registry lists as trusted. + signing modulus and bind that exact modulus to the public input of + REQ-PLAT-16A. - REQ-PLAT-16A (upholds SP-CLIENT-01): The Proving Circuit MUST expose the signing modulus, or a collision-resistant digest of it, as a public proof input. Necessity: the signature is proven in circuit because verifying it on chain would publish - the signed token and disclose its claims; the Consuming Contract checks - only the exposed modulus against the trusted set of REQ-PLAT-23. + the signed token and disclose its claims. The Proving Circuit MUST NOT decide + Registry membership or take the active trusted set as an input. The Consuming + Contract alone checks the exposed modulus against the current trusted set of + REQ-PLAT-23. - REQ-PLAT-17 (upholds SP-BIND-01): The Proving Circuit MUST prove the signed `iss` equals `https://accounts.google.com`. @@ -214,10 +217,11 @@ The Proving Circuit and consuming contract enforce all of the following: address as its handle. - REQ-PLAT-21 (upholds SP-BIND-01): The prover supplies the offset of each checked claim as a private input. - The Proving Circuit MUST assert the JSON pattern of `iss`, `sub`, `aud`, - `exp`, `nonce`, `email`, and `email_verified` at the supplied offsets per - common REQ-COMMON-19 and REQ-COMMON-19B. The Proving Circuit MUST - constrain each extracted value to its required type and charset. + The Proving Circuit MUST check the string claims `iss`, `sub`, `aud`, + `nonce`, and `email` under common REQ-COMMON-19 and REQ-COMMON-19B. The + Proving Circuit MUST check `exp` as a canonical unsigned JSON integer bounded + by `uint64`, and `email_verified` as the exact unquoted JSON boolean `true`, + under common REQ-COMMON-19D. Duplicate-free top-level structure is the issuer's behavior under ASM-PROV-06; the circuit performs no search and no duplicate scan. - REQ-PLAT-22 (upholds SP-FRESH-01): @@ -635,10 +639,13 @@ contract. 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-PLAT-16, REQ-PLAT-16A, REQ-PLAT-17, REQ-PLAT-19, REQ-PLAT-20, REQ-PLAT-21, REQ-PLAT-23): +- TEST-PLAT-06 (exercises REQ-COMMON-19D, REQ-PLAT-16, REQ-PLAT-16A, REQ-PLAT-17, REQ-PLAT-19, REQ-PLAT-20, REQ-PLAT-21, REQ-PLAT-23): A token with a foreign issuer, foreign audience, `email_verified: false`, a - duplicated top-level claim, or an untrusted signing modulus is rejected in - each case. + quoted or non-boolean `email_verified`, a quoted, negative, fractional, + exponent, leading-zero, or overflowing `exp`, a duplicated top-level claim, + or an untrusted signing modulus is rejected in each case. A cryptographically + valid proof under an inactive modulus passes circuit verification but is + rejected by the Consuming Contract. - TEST-PLAT-07 (exercises REQ-PLAT-22, REQ-PLAT-09, REQ-PLAT-09A): A proof at or after `proofValidUntil`, and an attestation timestamp more than `maxFutureAttestationSkew` ahead of `block.timestamp`, are rejected. A later