@waaskey/client-wasm
v0.2.0
Published
Browser/WASM MPC device-party core for Waaskey embedded wallets: client-side threshold ECDSA keygen & signing (cggmp24) over the relay, so the device holds its own key share.
Readme
client-wasm — browser MPC device party (+ client prime pool)
The browser (WASM) counterpart of the server-side party-runner: it drives the
same cggmp24 ceremonies (aux-info-then-keygen, threshold sign) over the same
WebSocket relay, so the device runs its own party share in the browser. The
@waaskey/sdk package loads this module and calls its exports.
Curve-generic like the runner — one set of entry points serves secp256k1,
secp256r1 and stark, dispatched on a curve string at the WASM boundary.
Exports (each takes one JSON string, resolves a JSON object)
| Export | Input | Resolves |
|---|---|---|
| keygen | { curve, relay_url, session_id, role, peer_role, party_index, peer_party_index, parties, threshold, pregenerated_primes_json? } | { aux_info_json, keyshare_json, shared_public_key_json } |
| sign | { curve, relay…, share, participants, signer_position, digest } | { signature_json } |
| keygenMember | { curve, relay_url, session_id, roles, party_index, threshold, relay_token?, pregenerated_primes_json? } | { aux_info_json, keyshare_json, shared_public_key_json } |
| signMember | { curve, relay_url, session_id, roles, party_index, share, participants, digest, relay_token?, sign_nonce?, tweak? } | { signature_json } |
| pregeneratePrimes | { curve } | { primes_json } |
keygen/sign are the fixed 2-party (device + server) ceremonies. keygenMember/signMember
are their n-party (member-bound wallet, #353) counterparts: every member device + the platform
join one relay session addressed by the full ordered roles roster (this device is roles[party_index]),
so a t-of-n wallet can keygen/sign — routing each recipient-specific p2p message the 2-party path
cannot. share on the sign wire is the KeyShare object (the exports deserialize it with
serde_json::from_value), not a JSON string.
The device key share returned by keygen is sealed and stored on the device;
it is never sent to the server. Secret material (shares, scalars, primes) is never
logged — the console diagnostics are protocol phase markers only.
Client-side prime pool (waas-sdk #1, #2)
Paillier safe-prime generation is the slow part of a device keygen (minutes in single-threaded browser WASM). Primes are generated client-side and never served by the API — a server-held prime pool would let the server reconstruct the device's key share, collapsing the 2-of-3 design to custodial.
So generation stays on the client but moves off the keygen hot path:
- Ahead of time (a Web Worker during onboarding / idle) the SDK calls
pregenerate_primes({ curve }), which generates aPregeneratedPrimesand returns it serialized underprimes_json. The SDK caches it in itsPrimePool. - At keygen time the SDK passes that cached string back as
keygen({ …, pregenerated_primes_json }). When present, keygen deserializes it and feeds it straight toaux_info_geninstead of generating; when absent, keygen generates inline exactly as before (the unchanged default / slow fallback, so keygen never fails on a cold pool).
primes_json and pregenerated_primes_json carry the same string — the exact
PregeneratedPrimes serialization, round-tripped losslessly (a unit test pins
this, and a gated real-MPC test proves a pool-primed keygen yields the same wallet
public key as the inline path).
Primes are curve-agnostic. Safe-prime size is fixed by the security level
(SecurityLevel128), not the curve, so one pregenerated set works for any of our
curves. pregenerate_primes still takes (and validates) curve for a stable SDK
call shape and forward-compatibility.
Building the wasm module
The SDK imports the wasm-pack output. Build it with wasm-pack (install once:
cargo install wasm-pack):
# from the workspace root
just build-wasm # → client-wasm/pkg/ (target: web)
just build-wasm bundler # ESM for tsup/webpack (wasm-pack default)
just build-wasm nodejs # CommonJS, for host smoke tests
# or directly (`-- --features reshare` exposes the device-side reshare exports the SDK needs):
wasm-pack build client-wasm --release --target web --scope waaskey --out-dir pkg -- --features reshare- Package name:
--scope waaskeymakespkg/package.jsonname@waaskey/client-wasm— the optional peer dependency the SDK'sloadClientWasmdynamically imports (waas-sdk #1). Release it via thePublish @waaskey/client-wasmworkflow (.github/workflows/client-wasm-publish.yml): push aclient-wasm-v*tag (or run it manually) and it builds the--target webpackage, prints the SHA-384 SRI to the job summary, and — with theNPM_TOKENsecret set — runsnpm publish --access public(skipping safely if the version is already on the registry; bumpclient-wasm/Cargo.tomlversionto release a new one). - Output dir:
client-wasm/pkg/(git-ignored — a generated artifact, never committed; the SDK / CI builds it on demand and dynamicallyimport()s it). - Target:
webmatches the existingfront/vite-projectconsumer (import init, { … } from "../pkg/client_wasm.js"; await init();). Usebundlerfor tsup/webpack pipelines that handle theinitthemselves. - Export naming: the module exposes
keygen,sign,keygenMember,signMember, andpregeneratePrimes(plus thereshare-gatedreshareAssemble/completeReshare) — camelCase names are set via#[wasm_bindgen(js_name = …)]so the JS surface matches the SDK'sClientWasmModuledirectly, no loader-side rename needed.
Tests
cargo test -p client-wasm # fast serde/contract tests
cargo test -p client-wasm -- --ignored # + the real-MPC prime-pool test (minutes)The fast tests pin the wire contract (params shapes, prime round-trip). The gated
prime_pool_keygen_matches_inline_shared_public_key runs a real 3-party
aux_info_gen + keygen through the in-memory simulation and proves the pool path
produces the same, usable wallet key as the inline path.
