Skip to content

docs(specs): on-chain verification path, two-attestation circuits, notary fee - #11

Merged
xgreenx merged 5 commits into
docs/libid-ceremony-specsfrom
docs/ceremony-round3
Aug 20, 2026
Merged

xgreenx merged 5 commits into
docs/libid-ceremony-specsfrom
docs/ceremony-round3

Conversation

@xgreenx

@xgreenx xgreenx commented Aug 19, 2026 •

Copy link
Copy Markdown
Contributor

Draft follow-up into #3. Two commits: the round-3 design changes, then the corrections that two later review rounds found in them.

The concrete type definitions that came out of the same discussion — the attestation byte layout and the handle-normalization algorithm — are not here. They are stacked in #12 so this PR stays reviewable as decisions rather than formats.


Commit 1 — the round-3 design changes

1. The circuit connects two attestations — it does not verify a proof

§6 described GitHub as a server-produced "Token Proof" that a "Final Identity Circuit" verifies in-circuit. That is recursive SNARK verification, and it is not the design. GitHub now matches X exactly: two notarized attestations, both authenticated on chain, one hidden bearer linking them.

Both platforms' circuits expose exactly two public inputs — the two bearer commitments. Client identifier, evidence timestamp, endpoint triples and identity fields are revealed attestation bytes the Platform Verifier reads directly.

This also fixes a defect that made the design unimplementable: REQ-PLAT-43E forbade the bearer commitment as a public input, but that commitment is exactly what ties the circuit to the two attestations the chain verified (it is what the shipped dyaka-noir-token circuit exposes, for the same reason). The bearer stays private; its commitments must be public, and the Platform Verifier must match them against the submitted attestations.

What the ZK proves is now stated plainly: the two TLSNotary sessions use independent blinders, so the same bearer hashes to two different commitments and the chain cannot compare them. Only the circuit can assert one hidden bearer opens both without publishing a live credential.

2. Verification is a four-role on-chain path (new common §5.1)

Consumer -> Proof Verifier -> Platform Verifier -> Notary Service

The Proof Verifier dispatches on (identity platform, Platform Verifier Version), supports several versions concurrently, and returns the authenticated operation domain, Authorized Transaction Data, and client identifier. The operation domain rides in the submission and is authenticated by digest recomputation — name a domain the ceremony did not commit and the digest will not match — so the Consumer can check the domain is its own before selecting a handler.

Naming note: the dispatcher is called Proof Verifier, not "Prover", because the spec already uses prover and Proving Circuit for the party that produces proofs.

3. The Notary Service verifies on chain and charges a fee

It takes attested data and a notary signature and returns one accept-or-reject decision. It charges a fixed fee per verification, paid in the Consumer Chain's native asset over the path the Chain Profile fixes. Added guards: the fee may not vary by content or principal (a per-principal fee is selective censorship of a permissionless service), the current fee is readable before a submission is built, and the Proof Verifier quotes the whole path so a Consumer need not know its topology. A submission whose delivered value does not equal the quotation is rejected — there is no refund path, because there is nothing to refund.

Also

  • Endpoint authority, method and path are authenticated by the attestation rather than exposed by the circuit.
  • The client identifier reaches the Platform Verifier as a proof public input for Google (signed aud) and as a revealed attestation range for X and GitHub — one path per platform, no duplicate representation.
  • The client secret's delimiter-free charset becomes a deployment obligation: with no service-side circuit in the two-attestation model, no party can prove it. GitHub secrets are hex, so this is satisfied by inspection.
  • libid.md records the Proof Verifier as the most concentrated trust root.

Commit 2 — the round-4 and round-5 review findings

Adds no new format or algorithm; corrects what commit 1 decided.

Digest binding is per platform. Google carries the Authorization Digest as a public proof input; X and GitHub bind it through the revealed code_verifier and carry no digest input at all. One requirement asserting both was two requirements (REQ-COMMON-02A/02B/02C).

Nobody was required to verify the proof. No MUST bound any role to check the proof against the artifact its selected version pins. The Platform Verifier now is.

Attestation Count is an exact number of zero or more, and the fee quotation counts attestations rather than assuming one. Google's profile verifies no attestation and pays nothing; a path with nothing to verify carries no value.

The Proof Verifier forwards the digest it recomputed to the Platform Verifier, which no longer recomputes it from a caller-supplied Chain ID.

Endpoint authority is stated as what the notary authenticated and the verifier compares against its pinned constant — not as a revealed range, which the transcript never carries (it appears only in a prover-composed Host header).

Evidence coverage gaps: both token responses gain revealed anchors around the committed access_token, and both identity sessions gain their response ranges.

Also: the Authorization-header rules are scoped off GitHub's body client_secret; grant_type is checked on chain; the bearer commitment wording requires independent blinders across the two sessions; REQ-COMMON-18 no longer makes google/v1 ineligible; REQ-COMMON-06C's comparisons are exercisable; the suite states that no full request grammar is enforced; libid.md no longer credits the circuit with Platform Verifier work; and Security Considerations no longer lists pkceNonce as withheld.

Dropped from commit 1

The Authorization Digest is no longer recorded at the Proof Verifier, and the requirement that operation domains be globally unique across Consumers is gone. A verifier cannot enforce uniqueness over a value each Consumer chooses for itself. Replay protection belongs in the Consumer's own domain.


Commit 3 — six defects the verification pass found in commit 2

Both token reveal tables listed the attestation timestamp as a revealed range. It is not one — it is the attestation's own signed creation time, which is why REQ-COMMON-25 can forbid inferring it from a response header. This is the same correction commit 2 applied one row below it for the endpoint authority, missed on the row above. The GitHub table therefore has seven revealable rows, not eight, and REQ-PLAT-43D and TEST-PLAT-14 said eight.

The Chain ID now belongs to the Proof Verifier. REQ-COMMON-06C had it observe one "under ASM-CHAIN-02," an assumption written about the Consumer; meanwhile REQ-COMMON-01C still obliged the Consumer to source a Chain ID that nothing consumes, since digest recomputation moved to the Proof Verifier. Both rules and TEST-COMMON-02A now name the Proof Verifier.

REQ-COMMON-21 had the Notary Service authenticate the server identity of "the session it verifies." It authenticates what it observes; the verifying side holds no transcript.

REQ-COMMON-18A required every hidden range to be bounded by revealed anchor bytes, which no leading or trailing hidden range can satisfy. The signed transcript length of REQ-COMMON-36 is the anchor at a transcript end — the identity profiles said so locally, the rule now says it once.

libid.md's trust-root list omitted the Platform Verifier, the role that verifies the proof and binds the digest.


Commit 4 — round-6 review

Field-extraction ownership. Common prose credited the Proving Circuit with checking the fields a profile needs, while X and GitHub assign revealed-field reads to the Platform Verifier, and REQ-COMMON-05D claimed the Platform Verifier checks every required field though redirect_uri and redirect-code equality are runtime-only. New REQ-COMMON-19E assigns each field's extraction to exactly one of the three roles, by where the bytes are: hidden to the circuit, revealed to the verifier, browser-local to the runtime. A comparison on an already-extracted value is explicitly not an extraction and may sit elsewhere — otherwise google/v1, which extracts aud in circuit and compares it on chain, would be ineligible under its own rule.

Version retention — withdrawn in commit 5. See below.

Attestation Count is derived. It was a number stated beside the attestation list each profile already names, with nothing forcing agreement. The profile fixes the list; the count is its size. Keeping the term (rather than deleting it) leaves the ~15 downstream references and #12's REQ-COMMON-52 working unchanged.

The Canonical Runtime's Chain ID. Commit 3 made only the Proof Verifier's source normative, but the runtime constructs the digest. Both now read the same Chain Profile value, neither from caller input.

GitHub's integer terminator. The /user layout revealed "the structural byte after" the id integer without saying which, though REQ-COMMON-19D delegates exactly that to the profile. Pinned as , or } — both rather than the comma alone, since JSON member order does not guarantee which one closes the field.

Not changed: Chain IDs stay a hash of whatever identifier a chain gives itself, with no namespace:reference requirement. Uniqueness is the Chain Profile author's to ensure per chain.


Commit 5 — round-7 review

One authoritative extraction, and a permitted mirror. Commit 4's single-extraction rule contradicted both TLSNotary profiles: REQ-PLAT-31 and REQ-PLAT-51 have the Platform Verifier extract the identity fields, while REQ-PLAT-31A and REQ-PLAT-51A have the Canonical Runtime derive them from the same bytes. That derivation is not redundancy — it is what lets the browser show the user the identity the proof will bind, so forbidding it would reopen display-A/proof-B.

REQ-COMMON-19E now fixes one source and one algorithm per field and names one role's extraction authoritative: the Proving Circuit where the bytes stay hidden, the Platform Verifier where they are revealed, the Canonical Runtime only where nothing on chain reads the value. The runtime may repeat an extraction another role owns, over the same bytes by the same algorithm, and nothing on chain may depend on that repeat. Both profiles now state which of their two is authoritative.

Version retention withdrawn. Requiring an outgoing version to survive until every ceremony that "could have begun" under it expires is unobservable and never terminates — nothing stops new ceremonies beginning under it, so the set is never exhausted. Governance decides when a version leaves the Supported Version Set, this specification fixes no minimum overlap, and a stranded ceremony is recoverable because its holder can simply run the ceremony again under a supported version. The Necessity no longer claims a no-stranding property the rule never delivered.


Verification

  • All ten conformance vectors recomputed independently with two toolchains (foundry cast keccak and @noble/hashes, each sanity-checked against published keccak vectors): all correct, including the operationDomain-first preimage.
  • Spec linter: 0 errors, 0 warnings. Four pre-existing defects on the base branch are fixed on the way — a wrapped exercises list that hid four requirements from the conformance graph, and three requirements whose keyword had no actor.
  • New IDs: REQ-COMMON-41..46, REQ-PLAT-54..60, TEST-COMMON-20..23 — contiguous, with docs(specs): define the attestation format and handle normalization #12 continuing from there.

Not changed, deliberately

ASM-PROV-07 stays a soundness dependency. I previously argued that revealing every range plus charset constraints closes the duplicate-field hole structurally; adversarial review refuted that with byte-level counterexamples — an honestly separated duplicate (client_id=GOOD&client_id=EVIL) satisfies every charset and tiling premise, as do percent-encoded field names, query/body parameter merging, and a charset=utf-7 body. Only a full-transcript template pin would replace the assumption, and TLS terminating at a CDN edge limits even that. The prose that overstated this is softened.

🤖 Generated with Claude Code

@xgreenx
xgreenx force-pushed the docs/ceremony-round3 branch 14 times, most recently from 9ed0ff8 to 1ea2aca Compare August 19, 2026 22:38
Rewrites the ceremony specs around how the system actually works, from
the protocol owner's description.

1. The circuit connects two attestations; it does not verify a proof.

   Section 6 described GitHub as a server-produced "Token Proof" that a
   "Final Identity Circuit" verifies in-circuit — recursive proof
   verification, which the design does not use. GitHub now matches X:
   two notarized attestations, both authenticated by the Notary
   Service, one hidden bearer linking them.

   Each circuit exposes exactly three public inputs: the Authorization
   Digest and the two bearer commitments. Everything else — client
   identifier, evidence time, endpoints, identity fields — is revealed
   attestation bytes the Platform Verifier reads.

   This fixes a defect that made the design unimplementable:
   REQ-PLAT-43E forbade the bearer commitment as a public input, but
   that commitment is what ties the circuit to the two attestations.

   Circuits no longer verify response schema. `token_type` and the
   granted scope are gone from the proof statement: nothing on chain
   acts on them, so the runtime may check them locally.

2. Verification is four roles.

   Consumer -> Proof Verifier -> Platform Verifier -> Notary Service.
   The Proof Verifier owns the Supported Version Set, recomputes and
   records the Authorization Digest, and returns the authenticated
   operation domain, Authorized Transaction Data, client identifier,
   `userId`, handle bytes, and `metadataObservedAt`. The Platform
   Verifier owns every platform constant and every platform check. The
   Consumer owns only domain ownership, decoding, authorization,
   storage, and handle normalization.

   The Notary Service answers for its signature, the transcript
   commitment, and whether revealed bytes match that transcript —
   nothing profile-specific. Range layout against a profile is the
   Platform Verifier's, since a notary holds no profile.

   It charges a fixed Notary Fee in the Consumer Chain's native asset;
   the fee may not vary by content or principal, the current fee is
   readable, value above it returns to the Consumer, and the Proof
   Verifier quotes the whole path.

3. Client identifier is uniform and authenticated.

   Every platform returns its exact bytes. Google reaches that with no
   circuit change: the submission carries `aud` and the Platform
   Verifier requires SHA256 of those bytes to equal the circuit's
   audience public input. A Platform Verifier must reject bytes its
   evidence does not authenticate.

Also: PKCE_V1 loses its version — a change to that construction is a
change to the proof statement, which bumps `platformVerifierVersion`
anyway; PKCE_DOMAIN keeps domain separation at zero cost, since 64- and
96-byte preimages both occupy two SHA-256 blocks. X and GitHub gain the
`state`-to-ceremony match the Google profile always had. Vectors
recomputed and independently verified.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
@xgreenx
xgreenx force-pushed the docs/ceremony-round3 branch from 1ea2aca to 9a4ef65 Compare August 19, 2026 23:13
Two rounds of review against the verification path landed in PR #11 turned
up defects in what that PR already decided. This corrects them; it adds no
new format or algorithm.

Digest binding is now per platform. Google carries the Authorization Digest
as a public proof input; X and GitHub bind it through the revealed
code_verifier and carry no digest input at all. One requirement asserting
both was two requirements.

A profile's Attestation Count is an exact number of zero or more, and the
fee quotation counts attestations rather than assuming one. A path with
nothing to verify carries no value.

The Platform Verifier is now required to verify the proof against the
artifact its selected version pins. Nothing in the suite said so.

The Proof Verifier forwards the digest it recomputed to the Platform
Verifier, which no longer recomputes it from a caller-supplied Chain ID.

Endpoint authority is stated as what the notary authenticated and the
verifier compares, not as a revealed range the transcript never carries.
Both token responses gain revealed anchors around the committed
access_token, and both identity sessions gain their response ranges.

The Authorization-header rules are scoped off GitHub's body client_secret,
grant_type is checked on chain, and the commitment wording of the bearer
requires independent blinders across the two sessions.

Also: REQ-COMMON-18 no longer makes google/v1 ineligible; the unexercisable
comparisons of REQ-COMMON-06C are exercised; the stale scope-enforcement
and replay claims are gone; the suite states that no full grammar is
enforced; libid.md no longer credits the circuit with Platform Verifier
work; and Security Considerations no longer lists pkceNonce as withheld.

REQ-COMMON-41..46, REQ-PLAT-54..60 and TEST-COMMON-20..23 are new here.

Signed-off-by: xgreenx <xgreenx9999@gmail.com>
…rifier's

Verification of the previous commit turned up six more defects of the same
kind it was fixing.

Both token reveal tables listed the attestation timestamp as a revealed
range. It is not one: it is the attestation's own signed creation time, which
is why REQ-COMMON-25 can forbid inferring it from a response header. This is
the correction already applied one row below it for the endpoint authority,
missed on the row above. The GitHub table therefore had seven revealable
rows, not eight, and REQ-PLAT-43D and TEST-PLAT-14 said eight.

REQ-COMMON-06C had the Proof Verifier observe a Chain ID under ASM-CHAIN-02,
an assumption written about the Consumer. At the same time REQ-COMMON-01C
still obliged the Consumer to source a Chain ID that nothing consumes, since
digest recomputation moved to the Proof Verifier. Both now name the Proof
Verifier, and so does TEST-COMMON-02A.

REQ-COMMON-21 had the Notary Service authenticate the server identity of
"the session it verifies". It authenticates what it observes; the verifying
side holds no transcript.

REQ-COMMON-18A required every hidden range to be bounded by revealed anchor
bytes, which no leading or trailing hidden range can satisfy. The signed
transcript length of REQ-COMMON-36 is the anchor at a transcript end. The
identity profiles said so locally; the rule now says it once.

libid.md's trust-root list omitted the Platform Verifier, which is the role
that verifies the proof and binds the digest.

Signed-off-by: xgreenx <xgreenx9999@gmail.com>
…ount

Round-6 review. Four defects plus the runtime half of a fifth.

The Canonical Runtime constructs the Authorization Digest, but the previous
commit made only the Proof Verifier's Chain ID source normative. Both now
take it from the same Chain Profile value, and neither from caller input. A
value the two source differently produces a recomputation that never matches.

Attestation Count was a number stated beside the list of attestations each
profile already names, with nothing requiring the two to agree. The profile
now fixes the list; the count is its size and is never stated separately.

Field-checking ownership was contradictory. Common prose credited the Proving
Circuit with checking the fields a profile needs, while X and GitHub assign
revealed-field reads to the Platform Verifier, and REQ-COMMON-05D claimed the
Platform Verifier checks every required field though redirect_uri and
redirect-code equality are runtime-only. REQ-COMMON-19E now assigns each
field's extraction to exactly one of the three roles, by where the bytes are:
hidden to the circuit, revealed to the verifier, browser-local to the runtime.
A comparison on an already-extracted value is not an extraction and may sit
elsewhere, which is what the Google audience does.

Concurrent version support did not deliver the no-stranding property its own
Necessity claimed, because nothing stopped governance retiring the outgoing
version the moment its replacement activated. It is retained until every
ceremony that could have begun under it passes proofValidUntil, with an
explicit emergency removal that is allowed to strand unsafe work.

GitHub's `/user` layout revealed "the structural byte after" the id integer
without saying which byte, though REQ-COMMON-19D delegates exactly that to the
profile. It is `,` or `}`. Both are pinned rather than the comma alone,
because JSON member order does not guarantee which one closes the field.

Chain IDs stay a hash of whatever identifier a chain gives itself, with no
namespacing requirement: uniqueness is the Chain Profile author's to ensure.

Signed-off-by: xgreenx <xgreenx9999@gmail.com>
…rlap

The single-extraction rule of the previous commit contradicted both TLSNotary
profiles. REQ-PLAT-31 and REQ-PLAT-51 have the Platform Verifier extract the
identity fields; REQ-PLAT-31A and REQ-PLAT-51A have the Canonical Runtime
derive them from the same bytes. That runtime derivation is not redundancy —
it is what lets the browser show the user the identity the proof will bind,
so forbidding it would reopen the gap where the display names one account and
the proof binds another.

REQ-COMMON-19E now fixes one source and one algorithm per field and names one
role's extraction authoritative: the Proving Circuit where the bytes stay
hidden, the Platform Verifier where they are revealed, the Canonical Runtime
only where nothing on chain reads the value. The runtime may repeat an
extraction another role owns, over the same bytes by the same algorithm, and
no proof statement or chain component may depend on that repeat. Both
profiles now say which of their two is the authoritative one.

The version-retention rule is withdrawn. Requiring an outgoing version to
survive until every ceremony that "could have begun" under it expires is
unobservable and never terminates, since nothing stops new ceremonies
beginning under it. Governance decides when a version leaves the Supported
Version Set, this specification fixes no minimum overlap, and a stranded
ceremony is recoverable because its holder can run the ceremony again under a
supported version. The Necessity no longer claims a no-stranding property the
rule never delivered.

Signed-off-by: xgreenx <xgreenx9999@gmail.com>
@xgreenx
xgreenx marked this pull request as ready for review August 20, 2026 09:01
@xgreenx
xgreenx merged commit 1e966d7 into docs/libid-ceremony-specs Aug 20, 2026
4 checks passed
Wondertan added a commit that referenced this pull request Aug 20, 2026
…tary fee (#11)

Define the four-role verification path, two-attestation X and GitHub
ceremonies, Platform Verifier dispatch, and fixed Notary Service fees.

Close the subsequent verification findings around digest binding, proof and
attestation ownership, Chain ID sourcing, field extraction, transcript ranges,
and version overlap without changing the resulting specification tree.

Co-authored-by: Green Baneling <XgreenX9999@gmail.com>
Assisted-by: GPT-5
Signed-off-by: Wondertan <hlibwondertan@gmail.com>
Wondertan added a commit that referenced this pull request Aug 20, 2026
…tary fee (#11)

Define the four-role verification path, two-attestation X and GitHub
ceremonies, Platform Verifier dispatch, and fixed Notary Service fees.

Close the subsequent verification findings around digest binding, proof and
attestation ownership, Chain ID sourcing, field extraction, transcript ranges,
and version overlap without changing the resulting specification tree.

Co-authored-by: Green Baneling <XgreenX9999@gmail.com>
Assisted-by: GPT-5
Signed-off-by: Green Baneling <XgreenX9999@gmail.com>
Signed-off-by: Wondertan <hlibwondertan@gmail.com>
@Wondertan
Wondertan deleted the docs/ceremony-round3 branch October 2, 2026 19:49
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.

1 participant