Skip to content

feat(tlsn): generate the ceremony session fixtures the contracts verify - #19

Merged
xgreenx merged 8 commits into
mainfrom
feat/ceremony-fixtures
Sep 11, 2026
Merged

xgreenx merged 8 commits into
mainfrom
feat/ceremony-fixtures

Conversation

@xgreenx

@xgreenx xgreenx commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

An example binary, cargo run -p libid-tlsn --example ceremony_fixtures -- <dir>, that writes x-ceremony-session.json and github-ceremony-session.json: two signed records per platform, built the way a ceremony builds them minus the MPC.

  • The requests are composed as the browser (buildTokenRequest, buildIdentityRequest, identityRequest on libid feat/ceremony-rebuild-plan) and the Token-Exchange Service (libid-server-rs) compose them, and driven through hyper::client::conn::http1, the encoder under tlsn's prover, against a scripted platform answering as X and GitHub answer.
  • The reveal layouts are libid_transcript::ceremony's; the commitments are tlsn's SHA-256 plaintext hashes with fixed blinders; the record is AttestedData::from_observed, the notary's own path; the signature is EIP-191 over the record's keccak by anvil #0, the key the contract suites trust.
  • The PKCE verifier in each token body is derived from the Authorization Digest as CeremonyAuthorization.digestFor computes it on the test chain (domain libid.claim-identity, version 1, chain 31337, nonce 0x55…, abi.encode(address(0xBEEF))), ported field for field, so the records verify with their signatures unedited.

Deterministic: fixed clock, blinders and bodies. libid-contracts#35 carries the generated files under solidity/contracts/ceremony/test/fixtures/ and each platform suite verifies its two records and reads the claim out; the digest assertion there is what catches a drift in the port.

What this does not cover: the MPC itself and the platforms' own bytes. Those need a real session, which Heorhii's bridge run produces and which would slot into the same files.

🤖 Generated with Claude Code

An example that builds the four launch records the way a ceremony
builds them minus the MPC: each request composed as the browser or the
Token-Exchange Service composes it and driven through hyper's http1
client, the encoder under tlsn's prover, against a scripted platform;
the layouts this workspace computes; tlsn's own SHA-256 plaintext
hashes as commitments; `AttestedData::from_observed`, the notary's path;
and an EIP-191 signature over the record's keccak by anvil #0, the key
the contract suites trust.

The PKCE verifier inside is derived from the digest the contracts
derive on their test chain, ported here field for field, so the records
verify with their signatures unedited and a drift in the port fails the
contract test by name rather than passing on a patched body.

Deterministic -- fixed clock, blinders and bodies -- so the files can be
regenerated and compared. libid-contracts carries them under
test/fixtures and reads the claim out of each.

Assisted-by: Claude Opus 5
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
GitHub pretty-prints its identity response for the media type the
profile pins: a space after every colon, a newline and an indent before
every key. The finders matched `\"login\":\"` and `\"id\":` byte for byte, so
no layout for a real GitHub identity read could be built, and the same
template the verifier refused was refused here first, with the reason
invisible.

The key, then a colon, then the value, with JSON whitespace allowed on
either side of the colon and before an integer's terminator. The
whitespace is inside the member: the verifier reads the revealed range
as the wire carried it and removes the whitespace beside structure
itself before comparing, so the range has to carry it. A key that is
only another member's value is passed over, as the old template passed
it over. `123 4` is still not a number.

The verifier side is libid-org/libID-contracts#37.

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

The canned /user body was compact, composed from the documented shape.
GitHub pretty-prints it for the media type the profile pins -- a
newline and two spaces before every member, a space after every colon
-- and a fixture without that whitespace passed a verifier that refused
every real read. The formatting is the platform's, and the fixture now
carries it, which the finders of the merged whitespace change lay out
with the whitespace inside each revealed member.

Assisted-by: Claude Opus 5
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
The generator reproduces what the platforms send; this records it. A
real MPC-TLS session against the real platform, the verifier in this
process on the other end of a pipe, signing as anvil #0, so the records
verify under the contract suites with their signatures unedited. The
PKCE challenge is derived from the same Authorization Digest the suites
derive, which is what binds the platform's token to that submission.

The operator supplies the app and the consent: a redirect URI on the
app pointing at this tool's listener, then the URL it prints, opened
and consented to. It receives the code, runs the token session and then
the identity session, and writes `<platform>-ceremony-real.json` in the
generated fixtures' format. The bearer, the secret and the code are not
written: the record commits the first two and the third is spent.

The derivations both examples share -- the digest, the verifier, the
challenge, the submission header -- move into a module both include.
Smoke-tested with a bogus client: the listener, the URL and the session
up to GitHub's answer all work; the run stops on the platform's error
with the verifier stopped first.

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

The previous commit described this and did not carry it: a scripted
edit missed the formatted text and the build went ahead without it. On
a failed session the prover's error now aborts the verifier task and is
awaited before the tool exits, so the runtime is not torn down under a
live session, which is what produced a second panic in the smoke test.

Assisted-by: Claude Opus 5
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
`unwrap_or_else(fail)` does not coerce a function returning `!` to the
value type; a closure does. The previous commit was pushed on a chain
that did not gate on the build, which this one does.

Assisted-by: Claude Opus 5
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
The lockfile moved when the examples gained sha2, hex and libid-crypto
as dev-dependencies and was never staged; CI regenerated it silently.

Assisted-by: Claude Opus 5
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
@xgreenx
xgreenx merged commit e491932 into main Sep 11, 2026
7 checks passed
@xgreenx xgreenx mentioned this pull request Sep 11, 2026
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