Skip to content

docs(specs): define CCDP and ceremony service contracts - #13

Open
Wondertan wants to merge 21 commits into
docs/popup-connection-architecturefrom
docs/ceremony-browser-architecture
Open

docs(specs): define CCDP and ceremony service contracts#13
Wondertan wants to merge 21 commits into
docs/popup-connection-architecturefrom
docs/ceremony-browser-architecture

Conversation

@Wondertan

@Wondertan Wondertan commented Aug 20, 2026

Copy link
Copy Markdown
Member

Normative browser and service contracts

CCDP relies on the popup transport specification in #22; this PR is based on that branch. It also includes #31's framing prerequisite without reverting its canonical changes.

The existing readable document structure is retained, with stable requirement/test anchors and explicit security boundaries. Browser validation ownership is reconciled separately: Prover parses and correlates evidence, Application validates delivered structure, and ledger verification remains authoritative. Neither endpoint adds local notary-signature verification or a separate Google nonce-versus-expected-digest comparison.

Implementation APIs, module layout, proving/notary integration, asset build tooling, UI, metrics, and qualification now live in implementation PR #28, with rendered package docs. No implementation code is changed here.

The fixed-callback migration and Callback-to-Prover authenticated-origin handoff remain explicitly tracked implementation work in #28. The previous architecture snapshot is preserved; this PR's package placeholders and implementation documents are removed from its scope.

@Wondertan
Wondertan force-pushed the docs/ceremony-browser-architecture branch 2 times, most recently from 1795a93 to 42298b3 Compare August 20, 2026 02:10
@Wondertan Wondertan changed the title docs(ceremony): add package architecture docs(ceremony): add package architecture and test plan Aug 20, 2026
@Wondertan
Wondertan force-pushed the docs/ceremony-browser-architecture branch 10 times, most recently from cebe2ed to f055711 Compare August 20, 2026 09:40
@Wondertan
Wondertan force-pushed the docs/libid-ceremony-specs branch 3 times, most recently from b98ba12 to da4f501 Compare August 20, 2026 10:05
@Wondertan
Wondertan force-pushed the docs/ceremony-browser-architecture branch from 52db663 to 4deb2a5 Compare August 20, 2026 16:49
@Wondertan Wondertan changed the title docs(ceremony): add package architecture and test plan docs(ceremony): define browser architecture Aug 20, 2026
@Wondertan
Wondertan changed the base branch from docs/libid-ceremony-specs to main August 20, 2026 16:49
@Wondertan
Wondertan force-pushed the docs/ceremony-browser-architecture branch 5 times, most recently from fe6cf6c to 1add82b Compare August 20, 2026 17:49
@Wondertan
Wondertan force-pushed the docs/ceremony-browser-architecture branch 2 times, most recently from 9074ebb to 04bb956 Compare August 24, 2026 12:51
@Wondertan
Wondertan changed the base branch from main to docs/popup-connection-architecture September 12, 2026 23:39
@Wondertan Wondertan self-assigned this Sep 13, 2026
Wondertan added a commit that referenced this pull request Sep 13, 2026
Align Callback artifact, embedded deployment data, URL clearing, version selection and browser qualification with architecture PR #13 at d42c0c4.

Assisted-by: GPT-6
Signed-off-by: Wondertan <hlibwondertan@gmail.com>
Wondertan added a commit that referenced this pull request Sep 13, 2026
Match architecture PR #13 at 8d5f46b: remove default/override fallback and fail locally when the selected version has no valid deployment tuple.

Assisted-by: GPT-6
Signed-off-by: Wondertan <hlibwondertan@gmail.com>
xgreenx and others added 18 commits September 13, 2026 15:56
…checks them

The token request's headers were hidden. §5.2 left the request direction's
headers out of its table, and §6.4 said so outright -- REQ-PLAT-43D bounded
the disclosure to "the seven rows marked `yes`", and the prose named the
`Host` header as one "this table hides".

That leaves a region of the request the Platform Verifier cannot read, in the
one direction it has to reason about. It reads `grant_type`, `client_id` and
`code_verifier` out of the body with a form-encoding reading -- and common
REQ-COMMON-21B fixes the media type precisely because it "selects the
platform's request parser". A media type the verifier cannot see is a value
the profile pins and nothing checks, and the platform may have parsed those
bytes into fields other than the ones read.

So the request line and every request header are revealed, and REQ-PLAT-56A
has the verifier compare them against a fixed list, byte for byte and in
order. Both halves are needed: revealing without comparing would leave the
bytes public AND unconstrained, which is neither private nor checked. Both
profiles' requests are now enumerated the way §5.3 and §6.5 already enumerate
the identity request's headers, which is the precedent this follows.

Nothing here is user data. The request is composed byte for byte by the
implementation and driven over a raw MPC-TLS session, not the browser's HTTP
stack, so there is no cookie jar and no ambient credential: the four headers
are profile constants. Revealing them discloses nothing and leaves the sent
direction with no region a verifier cannot read -- and concealment would not
have protected a user in any case, since the implementation is what would have
put anything private there.

The response direction is unchanged and still hides its status line and
headers: those are the platform's own bytes and nothing reads them. That
asymmetry is now stated rather than left to be inferred.

The `Host` argument survives and had to. A revealed `Host` header is still not
the authority -- it is prover-composed text -- so the authority still reaches
the verifier as the authenticated TLS server identity, and the header is
compared against the profile like every other.

`client_secret` is untouched: still committed, still ordered last by
REQ-COMMON-22 so the revealed run stays contiguous.

The linter reports what it reported before this change: 3 errors, 9 warnings,
all pre-existing.

Assisted-by: Claude Opus 5
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
… around it

The header list in the previous commit was wrong, and wrong in the way that
would have been found by a failing launch rather than by review: it enumerated
four headers and required the verifier to reject any other set, but a request
carrying a body carries a `Content-Length`. hyper emits one for every
known-size body -- `set_length` calls `set_content_length` -- and libid sets it
nowhere, so the GitHub Token Service's own exchange would have produced a head
of five headers and been refused by its own profile. As written the rule
rejected every genuine attestation.

`Content-Length` cannot be a profile constant: its value is the body's byte
count. So REQ-PLAT-56B pins the value against something the verifying side can
derive rather than against a literal -- the signed transcript length of the
sent direction less the head is the body, whether the body is revealed whole
as in X's request or revealed up to a committed suffix as in GitHub's, because
common REQ-COMMON-35 makes the direction tile exactly.

That is not bookkeeping. The verifier takes the body to be everything after
the sole CRLFCRLF; the platform takes it to be `Content-Length` bytes. Where
those disagree the fields the verifier reads are not the fields the platform
parsed, which is the same divergence REQ-PLAT-56's `grant_type` check exists
to stop, reached by a different route. `Transfer-Encoding` overrides
`Content-Length` outright and so is refused rather than described.

REQ-PLAT-56C carries the line-ending discipline over from the identity
request. Common REQ-COMMON-39 already refuses a bare line feed and an obsolete
line fold there, because a parser that accepts either ends the head somewhere
the verifier does not -- and the token head, which no requirement had ever
covered, is the same head with the same parsers reading it.

REQ-PLAT-56A now also says the match is exhaustive rather than a presence
test. "These five appear" is satisfied by a request carrying a sixth.

Found by an audit of the previous commit, which was written from a reading of
the request that no HTTP client produces.

Assisted-by: Claude Opus 5
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
Order carries no meaning here. Field order is insignificant in HTTP except for
repeated names, which REQ-PLAT-56A forbids outright, and the
`transfer-encoding`/`content-length` precedence REQ-PLAT-56B settles is by
presence rather than position. Nothing a reordering does is not already refused
by requiring the exact set.

What fixing it would cost is real. It binds every prover to the header order
its HTTP library happens to emit: the browser reaches the wire through tlsn's
wasm prover, whose `HttpRequest` carries headers in a `HashMap`, so the order
is whatever that iteration gives on the day. Pinning it also pins
`content-length` last, which is not a promise anyone made -- it is where
hyper's `set_length` inserts it, and a patch release could move it.

So the set is fixed and the order is not, and the prose says so once rather
than three times.

Assisted-by: Claude Opus 5
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
Sections 5.3 and 6.5 said "in this order" of headers no verifier orders:
the identity request is held to coverage, one line-anchored
authorization and the bearer framing, and common section 6 already says
header order carries no proof semantics unless a profile commits it.
The token request's headers were freed of order in this branch; these
two now say the same.

Assisted-by: Claude Opus 5
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
…rows

The tables listed each body field as its own revealed range. The
verifiers, the prover and the attested record carry the request as one
range -- the record cannot hold adjacent ranges apart, it merges them
before signing -- so a browser planning one range per field would see
its count change under it. Say so, once per platform.

Assisted-by: Claude Opus 5
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
api.github.com refuses any request without one: 403, with a body that
says so, where the same request with one gets the 401 an absent bearer
earns. Section 6.5 listed five headers and left it out, so a prover
built from the text alone could never reach the identity read. The
browser draft already sends one. X's endpoint and both token endpoints
do not care, checked the same way, so the token request's pinned set is
unchanged.

The value is the runtime's to choose: nothing verifies it, and GitHub
requires only that it exist.

Assisted-by: Claude Opus 5
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
Both identity requests said "exactly N headers". No verifier holds them
to that: the contract compares the request line and the authorization
line, and the prover's layout finds the same line and nothing else. A
count nothing checks is a rule nobody can rely on and a prover can
only fail to meet, as GitHub's user-agent just showed. Say which lines
are compared and leave the rest to the runtime.

The token request is unchanged: there the verifier does hold the set.

Assisted-by: Claude Opus 5
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
REQ-PLAT-56A had the verifier reject any header outside the profile's
list. A header outside it changes only what the platform answers, and a
wrong answer is a response the verifier cannot read, not one it can be
fooled by; the rule bound every prover to one HTTP library's habits for
nothing. It now requires `host` and the media type, forbids the five
names that change what the platform does with the request in a way no
revealed byte shows, and ignores the rest. `transfer-encoding` moves
from 56B to that list, beside `content-encoding`.

Assisted-by: Claude Opus 5
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
The linter says so, and it is right: two MUSTs in one sentence read as
one rule with a clause.

Assisted-by: Claude Opus 5
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
REQ-PLAT-56A said values are compared exactly; the verifier removes the
optional whitespace around them first, and lowercases names with their
whitespace removed, which is the normalization REQ-COMMON-39 already
gives the identity request. A verifier built from the text alone would
have refused `content-type:application/...`, which the contract
accepts. 56C now also names a line with no colon, which the contract
refuses.

Assisted-by: Claude Opus 5
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
…uests

REQ-COMMON-39 counted `authorization:bearer` lines, so a second header
under Basic or a platform's token scheme was never counted and the
Identity Platform answered for whichever credential it honoured; the
committed bearer is the one thing the cross-bind fixes. The needle is
now `authorization:` under any scheme.

REQ-COMMON-39A states the line-ending rule the verifier already applies
to the identity request -- no bare line feed, no fold -- which nothing
in this file said, and adds the bare carriage return. REQ-COMMON-39B
forbids on the identity request the names that change what the platform
does with it, `cookie` above all, with `_` read as `-`; the token
request's REQ-PLAT-56A now refers to that one list plus `authorization`,
and 56C names the bare carriage return and points at 39A.

Assisted-by: Claude Opus 5
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
The verifier requires exactly one empty line in the revealed token
request and a declared length with no leading zero; REQ-PLAT-56B said
neither. A second empty line is a second place a parser could end the
head, and a second spelling of the count is a second thing to compare
one spelling of.

Assisted-by: Claude Opus 5
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
GitHub pretty-prints `/user` for the media type the profile pins, so the
compact delimiters this specification spells matched nothing it serves.
REQ-COMMON-19F fixes what libid-org/libid-contracts#37 does: the Platform
Verifier removes the JSON whitespace touching a structural byte and matches,
counts and reads over what is left; the Implementation reveals a member as
the wire carried it, whitespace inside, and never commits that whitespace
with a bearer; the compact spellings name the member after removal.
TEST-COMMON-10A lists the vectors. REQ-PLAT-51 judges GitHub's terminator
after that removal and REQ-PLAT-60 keeps the whitespace in the reveal.

Carries the vectors of #32 in the form the merged verifier
implements.

Co-authored-by: Wondertan <hlibwondertan@gmail.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
Assisted-by: GPT-5
Signed-off-by: Wondertan <hlibwondertan@gmail.com>
Preserve downstream verification while assigning local parsing and correlation to Prover, keeping original requirement IDs and the framing prerequisite.

Assisted-by: GPT-5
Signed-off-by: Wondertan <hlibwondertan@gmail.com>
Keep the readable routes, messages, events and phases. Delegate transport to the popup specification and move implementation APIs, build tooling and qualification to the implementation PR.

Assisted-by: GPT-5
Signed-off-by: Wondertan <hlibwondertan@gmail.com>
Report valid OAuth denial with one-way Denied. Application cancellation retires the local run; popup navigation and closure stay composition-owned.

Assisted-by: GPT-5
Signed-off-by: Wondertan <hlibwondertan@gmail.com>
Define Event as an interface like the other messages. Nest operationId and attributes under optional instrumentation and preserve their validation and protocol boundaries.

Assisted-by: GPT-5
Signed-off-by: Wondertan <hlibwondertan@gmail.com>
Wondertan added a commit that referenced this pull request Sep 13, 2026
Align Callback artifact, embedded deployment data, URL clearing, version selection and browser qualification with architecture PR #13 at d42c0c4.

Assisted-by: GPT-6
Signed-off-by: Wondertan <hlibwondertan@gmail.com>
@Wondertan
Wondertan force-pushed the docs/ceremony-browser-architecture branch from 2ff7040 to 01d5fb0 Compare September 13, 2026 14:00
Wondertan added a commit that referenced this pull request Sep 13, 2026
Match architecture PR #13 at 8d5f46b: remove default/override fallback and fail locally when the selected version has no valid deployment tuple.

Assisted-by: GPT-6
Signed-off-by: Wondertan <hlibwondertan@gmail.com>
Rename Denied to UserDenied and Abort to CeremonyFailed, including wire discriminators, message links, conformance cases, and sequence labels. Preserve outcome semantics and local cancellation.

Assisted-by: GPT-5
Signed-off-by: Wondertan <hlibwondertan@gmail.com>
Accept a resolved notary address for any platform without requiring notarization. Keep nullable wire input and let the selected profile require an address only when needed.

Assisted-by: GPT-5
Signed-off-by: Wondertan <hlibwondertan@gmail.com>
Align browser identity ownership, GitHub routing and admission, and transport failure and closure semantics. Distinguish ledger-local verifier versions from ceremony versions without changing the browser flow or ledger verification.

Assisted-by: GPT-5
Signed-off-by: Wondertan <hlibwondertan@gmail.com>
Wondertan added a commit that referenced this pull request Sep 13, 2026
Align Callback artifact, embedded deployment data, URL clearing, version selection and browser qualification with architecture PR #13 at d42c0c4.

Assisted-by: GPT-6
Signed-off-by: Wondertan <hlibwondertan@gmail.com>
Wondertan added a commit that referenced this pull request Sep 13, 2026
Match architecture PR #13 at 8d5f46b: remove default/override fallback and fail locally when the selected version has no valid deployment tuple.

Assisted-by: GPT-6
Signed-off-by: Wondertan <hlibwondertan@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants