Appearance
VRF Sessions
VRF sessions turn the VRF‑WebAuthn unlock into a short‑lived session capability: the user approves once (TouchID/WebAuthn), then the wallet can sign multiple actions for a limited window without re‑prompting.
Instead of keeping decrypted keys around, the wallet caches only the minimum capability needed to unwrap the vault inside workers:
- VRF worker (WASM) keeps
{WrapKeySeed, wrapKeySalt}+ policy (ttl_ms,remaining_uses) in memory. - Signer workers (WASM) remain one‑shot; each request gets key material over a fresh
MessageChannel, signs, then terminates.
Why sessions exist
Running a full VRF challenge + WebAuthn assertion for every local signing request adds latency with no extra security when the PRF output never leaves the device. Sessions keep the VRF‑WebAuthn handshake as a mint-once capability:
- Unlock once with TouchID/WebAuthn, then sign multiple actions for a short time or usage budget.
- Reserve per-transaction VRF checks for remote attestation or high-risk actions.
Security properties
- Freshness + replay resistance: VRF challenges are unique; proofs bind the session to a fresh challenge.
- Block binding: Challenges can include recent block height/hash to prevent stale-session reuse.
- User presence: WebAuthn assertion proves touch/biometric presence over the exact VRF challenge.
- Policy enforcement: The worker enforces TTL and usage caps encoded in the challenge or local defaults.
- Auditability: Session minting can be logged with challenge digest + block height without exposing secrets.
Two layers of “session”
1) VRF‑owned session (capability)
- Stored in the VRF worker’s memory and keyed by
sessionId. - Contains
WrapKeySeedbytes andwrapKeySaltplus TTL/usage budget. - Enforced in the VRF worker (expire/exhaust → refuse to dispense).
2) Per‑request signing handshake
- For each signing request, the wallet creates a fresh
MessageChannel. - One port is transferred to the VRF worker and one port to a new signer worker.
- VRF sends
{WrapKeySeed, wrapKeySalt}only over that port; it is never put into a main‑thread JS payload.
Flow (cold vs warm)
Cold path: mint/refresh a VRF session (requires WebAuthn)
- Create a fresh
MessageChanneland attach ports to VRF + signer workers forsessionId. - Run the normal VRF‑WebAuthn confirmation flow (confirm UI + TouchID/WebAuthn → credential with PRF outputs in
clientExtensionResults). - VRF worker derives
WrapKeySeed(fromPRF.first_auth+ in‑memoryvrf_sk) and stores{WrapKeySeed, wrapKeySalt}with TTL/uses in a VRF‑owned session. - VRF worker sends
{WrapKeySeed, wrapKeySalt}to the signer worker over the attached port; signer stores it. - Main thread sends the signing request; if the seed hasn’t arrived yet the signer waits internally, then decrypts, signs, and terminates.
Warm path: reuse a VRF session (no WebAuthn prompt)
- Create a fresh
MessageChanneland a new signer worker for the samesessionId. - Call
DISPENSE_SESSION_KEY(sessionId)in the VRF worker:- VRF enforces TTL/remaining‑uses
- If valid, VRF sends
{WrapKeySeed, wrapKeySalt}over the attached port and closes it
- Main thread sends the signing request; signer waits internally for the seed if needed, then signs and terminates.
- If the session is missing/expired/exhausted, fall back to the cold path to re‑mint.
Enabling warm signing sessions
Warm signing sessions are opt-in and controlled by signingSessionDefaults (global) or signingSession (per login call).
- When
ttlMs: 0orremainingUses: 0, warm signing is effectively disabled (a TouchID/WebAuthn prompt is required for each signing operation). - Warm sessions are in-memory only (cleared on page refresh/close).
Configure defaults
ts
import { PASSKEY_MANAGER_DEFAULT_CONFIGS } from '@tatchi-xyz/sdk/react';
const config = {
...PASSKEY_MANAGER_DEFAULT_CONFIGS,
signingSessionDefaults: {
ttlMs: 5 * 60 * 1000,
remainingUses: 3,
},
};Override per login
ts
await tatchi.loginAndCreateSession('alice.testnet', {
signingSession: { ttlMs: 10 * 60 * 1000, remainingUses: 10 },
});Inspect session status
loginAndCreateSession() returns a signingSession status object when available:
ts
const login = await tatchi.loginAndCreateSession('alice.testnet');
console.log(login.signingSession); // { status: 'active' | 'expired' | ... }Session handshake diagram
mermaid
sequenceDiagram
participant App as dApp (app origin)
box rgb(243, 244, 246) Wallet origin (iframe)
participant UI as Wallet (main thread)
participant VRF as VRF Worker (WASM)
participant Signer as Signer Worker (WASM, one-shot)
end
participant Chain as NEAR RPC / Web3Authn Contract
App->>UI: signTransaction / signTransactionsWithActions
Note over UI,Signer: Fresh MessageChannel per signing request
UI->>VRF: ATTACH_WRAP_KEY_SEED_PORT(sessionId, port1)
UI->>Signer: ATTACH_WRAP_KEY_SEED_PORT(sessionId, port2)
Signer-->>UI: ATTACH_WRAP_KEY_SEED_PORT_OK
alt Warm session available (no prompt)
UI->>VRF: DISPENSE_SESSION_KEY(sessionId, uses)
VRF-->>Signer: MessagePort: WrapKeySeed + wrapKeySalt
else Cold session (mint/refresh)
UI->>UI: Show wallet confirm UI
UI->>UI: WebAuthn (TouchID) → credential (PRF outputs in extensions)
UI->>VRF: MINT_SESSION_KEYS_AND_SEND_TO_SIGNER(sessionId, credential, ttl/uses)
VRF->>Chain: (optional) verify_authentication_response
Chain-->>VRF: verified
VRF->>VRF: Derive WrapKeySeed, cache TTL/uses
VRF-->>Signer: MessagePort: WrapKeySeed + wrapKeySalt
end
UI->>Signer: SIGN_* request (sessionId + vault ciphertext)
Signer-->>UI: signed payload(s)
Signer-->>Signer: zeroize + self.close()
UI-->>App: return signed result(s)Security properties
WrapKeySeednever enters main‑thread JS; it is transferred worker‑to‑worker over aMessagePort.- The signer worker is one‑shot and holds no cross‑request state; session enforcement lives in the VRF worker.
- Warm signing is only possible if the VRF worker has a valid (unexpired, unexhausted) session capability.
Operational invariants
- VRF worker never handles
near_skor vault material; only{WrapKeySeed, wrapKeySalt}crosses workers. - PRF outputs and session secrets never touch the main thread or dApp payloads.
- All prompts originate from the VRF worker flows; signer worker never calls SecureConfirm.