Check a presentation in the browser
A static page verifies a synthetic presentation on your own device with the proof core a verifier runs, compiled to WebAssembly, then refuses it twice on purpose; nothing is sent anywhere.
The page
The browser check is a static page, in its own directory of the release's tree, that verifies a presentation on the reader's own device. The proof is checked by the proof core a verifier runs, compiled to WebAssembly; the other conditions follow the verifier's acceptance rule, in its order and with its reason codes. Nothing is sent anywhere: the page loads its own files — the page, its script, the WebAssembly module, the fixture and the expected results — and makes no other request.
Status
Status (14 October 2026). The page and its module are built and tested. The module is standard 32-bit WebAssembly, which every current browser runs — Chrome, Edge, Firefox and Safari, on desktop and on phones. It is an entry of the signed release manifest (
web) from the first release that carries it.
What the page shows
The fixture is one challenge and the presentation that answers it: a verifier asks for assurance grade A3 or above, age over 18, credential active and credential unexpired, valid for 10 seconds, and a holder answers with one zero-knowledge proof of all four, bound to that challenge. With it come the issuer's public definition and the witnesses that anchor it under the issuer's status root, that root and the chain height the state is proven at, and the verifier's clock.
The fixture is synthetic: a test issuer and a test holder, no person behind it, of the kind the public testnet's KYC sandbox issues. It carries no weight, and it was produced without any network.
The page runs it three times and shows every condition as PASS, FAIL, NOT CHECKED HERE or NOT REACHED:
| Case | Clock | Decision |
|---|---|---|
| As presented | three seconds after the challenge | accepted |
| After its 10-second window | twelve seconds after the challenge | deadline_exceeded (condition 4) |
| One byte flipped | on time, one byte of the proof changed | proof_invalid (condition 5) |
Each case also says whether its result equals the published expected report, row for row.
What it checks, and what it does not
| Condition | In the browser |
|---|---|
| Precondition | the challenge is well formed, with a lifetime of 10 seconds at most |
| 1, 2 | not checked — the verifier, its exact origin and its request key are chain reads |
| 3 | not checked — the nonce store is the verifier's own |
| 4 | the presentation arrived inside its window, against the fixture's clock |
| 5 | the proof verifies, bound to exactly this challenge, and the issuer definition it verified under is anchored under the presented status root |
| 6 | every requested predicate is proven (the proof covers the policy whole) |
| 7 | the issuer is one the challenge accepts; the presented root is the proven one, inside its validity window. The issuer's own state on the chain is not checked |
| 8 | the challenge's height is within the verifier's lag of the proven height |
| 9 | not checked — the nonce is consumed in the verifier's own store |
The page replaces the chain read with the fixture: the root and height it states as proven stand in for what a live verifier reads from the chain, proven against the chain's headers, immediately before it decides. An accepted presentation proves the conditions above and nothing else; each verifier decides on its own service.
The same answer three ways
The fixture and its expected reports live in the proof core's test vectors (identity-proof-core/test-vectors/browser-check/), pinned by digest. Three tests hold the same inputs to the same answer:
- the native build of the check (
cargo test -p proof-core-wasm), report for report; - the WebAssembly module the page loads, under Node, with the page's own case code (
node identity-proof-core/wasm/tests/node/known_answer.mjs), and the same under JavaScriptCore, Safari's engine (identity-proof-core/wasm/tests/jsc/known_answer.mjs, with thejscshell macOS ships); - the Python SDK, whose
validate_presentationwith the compiled engine gives the same decision, reason code and deciding condition for each case (verifier-core/tests/test_browser_check_vector.py).
Build
The script beside this paragraph builds the check for wasm32-unknown-unknown with the repository's pinned stable Rust toolchain and writes the page's module and its JavaScript glue to the pkg/ folder of the browser check's directory, printing each file's digest. It needs the toolchain's wasm32-unknown-unknown target (rustup target add wasm32-unknown-unknown), the wasm-bindgen command-line tool at the version the crate pins (the script checks it), and a C compiler for WebAssembly — the curve arithmetic underneath is C. The committed module is built with zig 0.14.1 (pip install ziglang==0.14.1; the script finds it, or set ZIG to a zig command) on x86-64 Linux, the reference build host, so that anyone with the same tools on the same kind of machine rebuilds it byte for byte: the project's pipeline does so on every push to the main branch and fails if a byte differs. A build on another host (macOS, Linux on ARM) verifies the same way and passes the same tests, but its module carries other internal symbol names, because the Rust toolchain derives them partly from the host it runs on. A clang with the WebAssembly target also builds it (CC_wasm32_unknown_unknown, AR_wasm32_unknown_unknown), with bytes that differ from the committed ones; Apple's clang has no WebAssembly target.
The proof core keeps its numbers 64-bit on every target. The credential library it pins stores numeric claims and range bounds in the platform's pointer-sized integer, which is 32 bits in WebAssembly; the project builds that library from its published source with those values typed as 64-bit integers, a change recorded line by line beside the source (identity-proof-core/vendor/credx/PATCHES.md). On 64-bit machines the two types are the same, so every proof, encoding and test vector is unchanged; in the browser the check verifies the same proofs the native engine makes. The change is part of the scope of the scheduled audit of the proof core.
After a rebuild, run the three tests above; fixture.json and expected.json beside the page are copies of the test vectors, and the native suite fails if they differ.
Regenerate the fixture
Every input is derived from fixed labels — the issuer's seed and identifier, the holder's secret, the challenge — except the proof's own blinding, which the credential library draws from the operating system: a regeneration writes a new proof, a new flipped-byte position and new digests. It is a reviewed change; copy the two files beside the page after it.