diff --git a/specs/ccdp-distribution.md b/specs/ccdp-distribution.md new file mode 100644 index 00000000..90d9ca43 --- /dev/null +++ b/specs/ccdp-distribution.md @@ -0,0 +1,417 @@ +# 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 and the registered +Callback document. Any profile-defined Bridge service is separate from this +static Distribution. The Bridge 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. Protocol +resources never redirect. Unknown paths and versions return an inert failure +without fallback or redirect. For a directory path that serves no resource, +the Distribution MAY instead redirect to the same-origin path with a trailing +slash appended. That destination returns an inert failure; neither response +executes CCDP code. 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 and +secure notary WebSockets. 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 asset 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 notary origins satisfying the [origin policy](ccdp.md#origin-policy). +For browser-exchange profiles, token and identity exchanges use the browser +notarization adapter, with code-owned platform destinations rather than +caller-selected endpoints. CSP does not constrain the runtime-selected notary +to an exact origin: 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 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. + +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. Protocol and asset resources never redirect; unknown paths are inert. Any directory slash redirect stays on the same origin and ends in an inert failure. 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..ee1ada82 --- /dev/null +++ b/specs/ccdp.md @@ -0,0 +1,894 @@ +# 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. 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. + 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, and owns OAuth registrations | +| 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 `CeremonyFailed`. 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 +[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. + +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 | +| [`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 +defined below. Unknown fields, coercion, normalization, defaults, and +unrecognized discriminators are invalid. Messages outside the listed direction, +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 + +```ts +interface ProveIdentity { + type: 'prove-identity' + platformId: string + platformCeremonyVersion: number + clientId: string + redirectUri: string + codeVerifier: string | null + notaryAddress: string | null + clientCredential?: string +} +``` + +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, resolved notary address, and +optional public token-exchange credential. +`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` 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. + +When present, `clientCredential` is the nonempty printable ASCII value +without whitespace frozen from the selected public platform configuration. +Application forwards it unchanged; null, empty, wrongly typed, or +whitespace/control-bearing values are invalid. The selected platform requires +it before an exchange that needs it, and otherwise does not use it. GitHub +uses it as `client_secret`. It is public application configuration, not a user +access token, a signing key, or proof of the caller's authority. Prover performs +no configuration fetch. + +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. + +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. +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 + +```ts +interface UserDenied { + type: 'user-denied' +} +``` + +`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 `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). + +### CeremonyFailed + +```ts +interface CeremonyFailed { + type: 'ceremony-failed' + event: string + message: string +} +``` + +`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 +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 + +```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. + +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. + +`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 +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 `CeremonyFailed`. + +#### 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 +`instrumentation.operationId`. X and GitHub use all six proving operations; +their token can become available before its attestation finishes. 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. +Instrumentation 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 `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. + +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. +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 +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 `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 +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. 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. + An optional public token-exchange credential is delivered unchanged; null, + empty, wrong-type, or whitespace/control-bearing values reject. A platform + requiring it rejects omission before exchange. No configuration fetch is made + by Prover. +- TEST-CCDP-06 (exercises REQ-CCDP-05, REQ-CCDP-06): + X and GitHub each emit token-fetch and token-attestation independently; + token availability does not imply attestation completion, and proof delivery + waits for both attestations and the ZK proof. + 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): + 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 + +- 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. 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. 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 + intermediate store, notification, or diagnostic. Opaque error text has the + 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 + 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. 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. + +### 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 `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 failure. 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 +[`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 + +This phase begins only after [Prover](#prover-get-prover) has validated and +accepted the OAuth return in Phase 3. It runs the selected profile's token +exchange, notarization, and proof-generation steps as applicable. The profile +determines which work runs in Prover and which, if any, uses a Bridge service. +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 failure or context loss ends its work. +Observable failure sends [`CeremonyFailed`](#ceremonyfailed). Prover accepts no +second proof request; Application-local cancellation makes any later delivery inert. + +### 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 + 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 +mutually terminal even when they race in transit. Denial resolves denied; +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 +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. + +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: CeremonyFailed + 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: CeremonyFailed + 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) + A-->>P: ProveIdentity + + P->>P: Validate retained OAuth return + break Valid OAuth denial + P-->>A: UserDenied + end + break Invalid OAuth return + 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: CeremonyFailed + 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/ceremony-common.md b/specs/ceremony-common.md index 1f38058f..d57004b5 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. @@ -942,16 +955,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 @@ -1037,6 +1073,36 @@ 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 each maximal run of JSON whitespace bytes (`0x20`, + `0x09`, `0x0a`, `0x0d`) whose immediately preceding or immediately + following byte is a structural byte (`:`, `,`, `{`, `}`, `[`, `]`), and no + other byte. The Platform Verifier MUST match the field's delimiter, read + its value, and judge its terminator inside one revealed range, over the + bytes that removal leaves of that range. The Platform Verifier MUST count + the delimiter's positions under REQ-COMMON-19A over the concatenation of + every revealed range of that direction, in transcript order, after the same + removal, so that a delimiter a range boundary splits is still counted. The + Platform Verifier MUST NOT read a value from that concatenation. The + Implementation MUST reveal a member as the transcript carries it, its JSON + whitespace inside the revealed range at its offsets. The Implementation + MUST NOT commit that whitespace with a bearer. Every compact delimiter this + 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 a run only where a structural byte + bounds it leaves every reader one exact template and makes a member in any + spelling the same member, so a second copy spelled with spaces is still the + duplicate REQ-COMMON-19A rejects, while `123 456` still does not read as + `123456`. Reading and counting want opposite things: a read that crossed a + range boundary would let a prover assemble, from fragments the notary + signed at unrelated offsets, a document that never crossed the wire, and a + count that stopped at one range would miss a second delimiter the prover + cut a boundary through. The concatenation can only over-count, which fails + closed. - REQ-COMMON-20 (upholds SP-EXCHANGE-01): The Proving Circuit MUST constrain every variable value it opens or extracts to the charset the profile states, including values that are never @@ -1267,6 +1333,21 @@ 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; a member whose + whitespace an HTTP chunk boundary splits is not built as a layout; a field + assembled from two revealed ranges, the member's opening in one and its + value's tail in another, is rejected as absent, however the ranges are + ordered; a second copy of the delimiter cut in two by a range boundary is + rejected as a duplicate; and a bearer prefix whose whitespace is pushed + into the committed range, so that the revealed prefix ends before the + value's opening quote, is rejected. - TEST-COMMON-11 (exercises REQ-COMMON-21, REQ-COMMON-21A, REQ-COMMON-21B, REQ-COMMON-21C): The Platform Verifier rejects an authenticated foreign authority, method, or path. The request constructor refuses a media type or `redirect_uri` @@ -1285,7 +1366,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 @@ -1311,13 +1394,14 @@ 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 verification whose native value differs from the current fee is rejected. -- TEST-COMMON-18 (exercises REQ-COMMON-35, REQ-COMMON-36, REQ-COMMON-39, REQ-COMMON-40, REQ-COMMON-43): +- TEST-COMMON-18 (exercises REQ-COMMON-35, REQ-COMMON-36, REQ-COMMON-39, REQ-COMMON-39A, REQ-COMMON-39B, REQ-COMMON-40, REQ-COMMON-43): An identity attestation whose ranges do not sum to the signed request transcript length, or whose ranges leave a gap or an overlap, is rejected; an attestation carrying no signed total transcript length for @@ -1328,7 +1412,16 @@ the constructions that role implements. occurrence inside another header's value is not counted, because the needle is line-anchored; and an attestation whose committed range is not immediately preceded by `\r\nauthorization: Bearer ` and immediately - followed by `\r\n` in the raw bytes is rejected. Every case above runs on + followed by `\r\n` in the raw bytes is rejected. A second `authorization` + header under another scheme, such as `Basic`, is rejected for a duplicate + needle occurrence; a request carrying a header the profile does not list, + such as `accept-encoding`, passes; a header whose name normalizes to + `cookie`, `content-encoding`, `transfer-encoding`, + `x-http-method-override`, `x-http-method` or `x-method-override` -- in + another letter case, with `_` for `-`, or padded before the colon -- is + rejected; and revealed request bytes carrying a bare line feed, a bare + carriage return, or a line beginning with a space or a horizontal tab are + rejected. Every case above runs on an identity-session attestation. A GitHub token-exchange attestation, whose only committed credential is the `client_secret` in its form body, passes verification with no coverage, needle, or framing check applied to that diff --git a/specs/libid.md b/specs/libid.md index 38c9f68a..91227887 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 platform configuration and + callback ingress. +- [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 @@ -43,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 @@ -52,8 +66,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, public OAuth configuration, +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 +80,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 registrations and public application credentials; configures and serves Callback | correct public configuration, Callback delivery, 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 +109,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 and callback ingress | [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..48fadf67 --- /dev/null +++ b/specs/oauth-bridge.md @@ -0,0 +1,307 @@ +# OAuth Bridge Server + +This document defines the HTTP and deployment contract for the OAuth bridge +used by CCDP. The bridge publishes platform +configuration and serves the OAuth callback document. + +[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 and their public client configuration; +- the public ceremony configuration; and +- callback ingress, response policy, and deployment inputs. + +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. The browser-exchange profiles covered here require no Bridge +ceremony state, token exchange, or notarization. + +Bridge-assisted OAuth is a platform-profile choice, not prohibited by CCDP. +Another platform or later ceremony version can define a Bridge service together +with its HTTP contract, credential custody, and trust/evidence requirements. +The route surface below requires no such service for the browser-exchange +profiles; it does not preclude one where a profile requires it. + +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 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 | +|---|---| +| `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, supported ceremony versions, and an optional public `clientCredential` 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 | + +Every enabled platform's OAuth registration uses `/auth/callback` on the +externally reachable Bridge origin as its `redirect_uri`. The path is fixed, +not a deployment option. The operator registers that external URL with the +platform; the Bridge process requires neither its own public-origin setting nor +a redirect URI. Application-side redirect construction is defined below. + +`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 explicit `Origin` admission for configuration GET +and all callback connection authentication. Configuration GET also admits the +same-origin browser case defined below. The set 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. + +The public profile entries match the OAuth registrations used by Callback. +The bridge advertises only platform/version pairs supported by its selected +CCDP Distribution. Selecting a shared Distribution requires no reciprocal +configuration. + +## 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. + +For the profiles covered here, 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 | + +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. + +For these profiles, no separate Callback script, prover, proving-asset, +preparation, continuation, polling, status, result, cancellation, token-exchange, +notarization, 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, 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 + clientCredential?: string +} + +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 Bridge origin it uses. +- Each platform entry has one public client ID and a nonempty, duplicate-free + list of supported ceremony versions. List order has no meaning. +- `clientCredential`, when present, is a nonempty printable ASCII string + without whitespace. It is an intentionally public OAuth application credential, + not a user access token. GitHub requires it and uses it as `client_secret`; + other profiles omit it unless their token exchange needs one. Missing required + credentials, null, empty, or wrongly typed values are invalid. The selected + platform owns any additional constraints. +- 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, confidential credential, user token, + 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 exactly `same-origin`. This browser-supplied relationship +requires neither knowledge of the Bridge's public origin nor its membership in +`allowedOrigins`; the response needs no CORS header. Missing or other +`Sec-Fetch-Site` values reject this absent-Origin case. Do not infer admission from +`Referer`, `Host`, or forwarding headers; an explicit invalid, `null`, or +unlisted `Origin` always fails, even with `Sec-Fetch-Site: same-origin`. +This exception admits only the public, read-only configuration response; it +does not change Callback's connection allowlist. + +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, +public token-exchange credential when present, derived redirect URI, CCDP origin, +and mutually supported platform ceremony version in each live ceremony. It +forwards the credential unchanged through CCDP's `ProveIdentity`; a configuration +refresh does not replace it in a 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: Withdrawn. + +This GitHub profile requires no Bridge token endpoint. Prover performs the +exchange and notarization; the [platform profile](platform-ceremonies.md#6-github-ceremony) +owns their semantics. + +## Compatibility + +- REQ-BRIDGE-06: The Bridge MUST preserve the public API version's semantics + when refreshing compatible Callback artifacts. Necessity: a browser artifact + refresh must not silently change the public configuration 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 OAuth registrations, public application credentials, and +Callback deployment policy. Its code and deployment are trusted for correct +browser code delivery and configuration, not for ledger acceptance. + +CORS and Origin checks protect browser admission, not non-browser +authentication or confidentiality of the configuration. The public +`clientCredential` carries no user authority and is not an application +authentication boundary. Platform ceremony rules own the consequences of +publishing it and the downstream checks; renaming the field does not hide it. +For these profiles, the Bridge receives no notary address and opens no token or +notary connection. +Callback query redaction remains necessary because OAuth codes, unlike public +application credentials, are transient user-ceremony inputs. + +## Conformance + +A Bridge implements configuration and Callback for every enabled platform. +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. + Configuration and Callback work without a server public-origin or redirect-URI + setting; the Application derives the fixed callback URL from its Bridge origin. +- TEST-BRIDGE-02 (exercises REQ-BRIDGE-02): + A configuration GET without Origin succeeds with exactly + `Sec-Fetch-Site: same-origin`, even when the Bridge origin is not allowlisted; + missing, `same-site`, `cross-site`, `none`, or malformed Fetch Metadata reject + that case. An allowed explicit Origin succeeds even with cross-site metadata; + an invalid, `null`, or unlisted Origin rejects even with same-origin metadata. + Rejections occur before dependency work; accepted same-origin GET does not + admit an otherwise unlisted Application to Callback. + For the browser-exchange profiles, the former token route performs no exchange + or notary work, including on POST. +- TEST-BRIDGE-03 (exercises REQ-BRIDGE-03): + Exact public config includes GitHub's public token-exchange credential and has + no redirect field, user token, or notary selection. Missing required, empty, + null, wrongly typed, or whitespace/control-bearing credentials reject; + malformed known profiles reject and unknown platforms are ignored. Application + freezes the credential and forwards the same value to Prover despite later + configuration changes. +- 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: Withdrawn. +- 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. diff --git a/specs/platform-ceremonies.md b/specs/platform-ceremonies.md index 6d8f41ac..08796c78 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 @@ -67,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 @@ -234,16 +247,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 +266,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 +295,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 +330,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 +398,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 @@ -446,13 +470,18 @@ 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: | 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, 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 | @@ -462,22 +491,22 @@ 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 the TLS server identity the Notary Service authenticated under common REQ-COMMON-21, carried in the attested data: the transcript -holds the authority only in a `Host` header this table hides, and a revealed -`Host` header is prover-composed text that says nothing about which server -answered. The timestamp is the signed creation time of the attested data -itself, which is why common REQ-COMMON-25 can forbid inferring it from a +holds the authority only in a `host` header, which the first row reveals and +REQ-PLAT-56A holds to the pinned authority, and that header is +prover-composed text that says nothing about which server answered. The +timestamp is the signed creation time of the attested data itself, which is why common REQ-COMMON-25 can forbid inferring it from a response header. The two delimiter reveals are what anchor the committed range in the received direction, which would otherwise reveal no byte at all and leave that range indistinguishable from a `refresh_token` value. -Those reveals and the in-circuit `code_verifier` opening of REQ-COMMON-15 -reduce the hidden request surface, but revealing a range does not reject a form +Those reveals and the Platform Verifier's `code_verifier` comparison under +REQ-COMMON-15A bind the request, but revealing a range does not reject a form delimiter inside it. The X profile therefore retains ASM-PROV-07 as a soundness dependency. @@ -505,6 +534,52 @@ 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 `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 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 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, `content-length` + excepted, which REQ-PLAT-56B holds to the body it frames. Necessity: common REQ-COMMON-21B fixes the media type + because it "selects the platform's request parser", and a media type nothing + compares is a pin in name only. The forbidden headers change what the + platform does with the request in a way no revealed byte shows: which client + 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 + for nothing. +- REQ-PLAT-56B (upholds SP-EXCHANGE-01): + 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 + 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`. @@ -535,9 +610,12 @@ dependency. ### 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 `, -`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, refuses the names common REQ-COMMON-39B forbids, and +compares no other header. Per common §9, the identity session reveals exactly these request ranges; the bearer value is the only committed request range, and every other @@ -678,6 +756,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 @@ -726,7 +832,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 @@ -744,6 +850,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 @@ -772,20 +881,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. @@ -799,6 +913,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 @@ -816,10 +933,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 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 | 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 +945,32 @@ 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 one +of the two headers the verifier holds to a pinned value, while the +authority continues to reach the verifier as the authenticated TLS server +identity. The timestamp is the signed creation time of the attested data itself, which is why common REQ-COMMON-25 can forbid inferring it from a -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 `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 profile-layout check of common REQ-COMMON-18A makes derivable. - 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, 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 @@ -855,42 +987,50 @@ response header. Revealing more would widen exposure without adding a check. 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 + 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. 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` @@ -905,11 +1045,15 @@ 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: +`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`, 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. The +Platform Verifier compares the request line and the `authorization` line, +refuses the names common REQ-COMMON-39B forbids, and compares no other +header. Per common §9, the identity session reveals exactly these request ranges; the bearer value is the only committed request range, and every other @@ -944,8 +1088,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): @@ -955,7 +1101,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. @@ -1024,8 +1171,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. @@ -1035,22 +1183,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. @@ -1075,7 +1231,7 @@ Platform Verifier, Notary Service, Consumer. - TEST-PLAT-09B (exercises REQ-PLAT-30A, REQ-PLAT-32A): An X transcript that reveals plaintext `access_token` bytes in either session, or omits the bearer hash commitment, is rejected. -- TEST-PLAT-09C (exercises REQ-PLAT-29C, REQ-PLAT-56): +- TEST-PLAT-09C (exercises REQ-PLAT-29C, REQ-PLAT-56, REQ-PLAT-56A, REQ-PLAT-56B, REQ-PLAT-56C): The Platform Verifier rejects an X attestation that hides the `grant_type` or `redirect_uri` range, and the Canonical Runtime rejects a revealed value differing from the canonical form serialization of its deployment profile. @@ -1083,7 +1239,18 @@ Platform Verifier, Notary Service, Consumer. as literal unencoded bytes. The Platform Verifier rejects an attestation whose revealed `grant_type` is `refresh_token`, and one whose `grant_type` differs from `authorization_code` in any byte, even when every - other revealed range and the proof itself check out. + other revealed range and the proof itself check out. The Platform Verifier + accepts a token request whose headers arrive in another order, or carry a + header the profile does not name, and one whose required names are spelled + in another letter case, with `_` for `-`, or padded before the colon; it + rejects a head missing `host` or `content-type`, carrying either twice, or + carrying `authorization`, `cookie`, `content-encoding`, `transfer-encoding` + or a method-override name under any spelling; it rejects a head with no + empty line or a second one, with no `content-length` or two, or with a + count that is not the body's length, is not decimal digits, or carries a + leading zero, and accepts the count wherever it sits in the head; and it + rejects a head carrying a bare line feed, a bare carriage return, an + obsolete line fold, or a line with no colon. - TEST-PLAT-10 (exercises REQ-PLAT-30, REQ-PLAT-31, REQ-PLAT-32, REQ-PLAT-36, REQ-PLAT-51, REQ-PLAT-52): An opened bearer range that is empty, over 4096 bytes, or outside printable ASCII fails to prove; a revealed identity response missing `id` or the @@ -1102,24 +1269,50 @@ 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 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 - 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-14 (exercises REQ-PLAT-41, REQ-PLAT-42, REQ-PLAT-43, REQ-PLAT-43B, REQ-PLAT-43D, REQ-PLAT-43E, REQ-PLAT-56A, REQ-PLAT-56B, REQ-PLAT-56C): + A request selecting 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`, revealing the bearer + range instead of committing it, or hiding the request line, is rejected; + and no proof exposes the bearer or a value from which it can be recovered. + The head vectors of TEST-PLAT-09C run on the exchange request too: an + unlisted header passes, a forbidden or a missing required name is rejected, + and the `content-length` count is held to the revealed body prefix and the + committed `client_secret` together, so a count that stops at the revealed + bytes is rejected. +- TEST-PLAT-15 (exercises REQ-PLAT-44, REQ-PLAT-45, REQ-PLAT-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 @@ -1146,9 +1339,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): @@ -1183,6 +1378,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). @@ -1230,20 +1432,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