Security Model
The threat model behind Hyperauth — why passkeys replace passwords, how the WASM boundary contains key material, what encrypted shares protect against, and where the trust boundaries actually lie.
Security Model
Security models are most useful when they are explicit about what they protect against and honest about what they do not. Hyperauth is designed around a specific threat model: a user whose credentials could be phished, a server that could be compromised, and an attacker who might obtain one half of a split secret. The system is not designed to be impervious to a compromised device or a malicious user agent — no client-side system can offer that guarantee. What it can offer is a clear account of where trust is placed, what each layer protects, and what the consequences of a breach at each layer are.
Why Passkeys Rather Than Passwords
Passwords fail in two ways that are intrinsic to their design, not incidental to their implementation. First, they are phishable: any system that requires a user to type a secret into an input field can be replicated by an attacker who controls a lookalike page. The user has no reliable way to distinguish the real site from the fake one before the password is transmitted. Second, they are shared secrets: the server must store a representation of the password, which means any breach of the server's credential store potentially exposes all users simultaneously.
Passkeys solve both problems at the protocol level. A WebAuthn credential is a public-private key pair generated by the user's authenticator. The private key never leaves the authenticator. The server receives only the public key at registration time, and thereafter receives signed assertions. An attacker who controls a phishing page cannot harvest a credential — the credential is domain-scoped, meaning the authenticator refuses to sign an assertion for a domain other than the one registered. Even a user who is perfectly deceived by a phishing page gains the attacker nothing, because the signature produced by the authenticator is valid only for the legitimate origin.
The parse_webauthn export in the enclave takes this further: it verifies the WebAuthn assertion inside the WASM sandbox, extracting the credential ID and public key coordinates from the attestation data. The public key coordinates (X and Y on the P-256 curve) become the ownership parameters of the smart account. The passkey does not merely authenticate the user to a session — it is the root of authority for all on-chain operations.
The WASM Boundary
The WASM enclave is not a hardware security module and it does not claim to be. What it provides is a software isolation boundary: the Go code compiled to WASM runs in a separate linear memory space from the JavaScript host. The host can call exports and receive results, but it cannot directly read the enclave's memory. A JavaScript runtime compromise — an injected script, a prototype pollution attack, a supply-chain compromise in a dependency — can intercept the inputs and outputs of enclave calls, but it cannot reach into the enclave's memory and extract the private key shares while they are being used.
The sign export illustrates what this means in practice. The caller passes in the encrypted shares, the data to be signed, and receives back a signature. The private key scalar is reconstructed inside the enclave by calling combineShares(share1, share2, ecCurve.Params().N) — an addition of two big integers modulo the curve order — and then immediately used for signing. The reconstructed scalar exists only for the duration of that call, inside the WASM linear memory, and is never returned to the host. The host sees a JSON object containing a signature. It does not see the key.
This is a meaningful but bounded protection. It protects against passive memory inspection from the JavaScript context. It does not protect against an attacker who can execute arbitrary JavaScript before the enclave call and intercept the shares as they are passed in. The realistic threat model for the WASM boundary is: an attacker who can inspect memory but not intercept function arguments. Against a fully compromised runtime, the boundary provides limited protection — but then, no client-side system can.
Encrypted Shares and What They Protect
The private key is never stored whole. The splitSecret function in mpc/simple.go generates a random 32-byte value as share1, computes share2 = (secret - share1) mod order, and stores the two values separately. Reconstruction requires adding them modulo the curve order: combineShares does exactly this. An attacker who obtains one share learns nothing about the private key — a random value modulo a 256-bit prime is computationally indistinguishable from random noise.
The practical significance of this split is that the two shares can be stored in different locations with different security properties. The val_share (validator share) and user_share (user share) are named to suggest a threshold arrangement where one share is held by the user and one by a validator service, though the current implementation stores both in the encrypted client-side database. The naming anticipates a multi-party computation model where signing requires cooperation from two parties, neither of whom can sign alone. Even in the current single-device implementation, the split means that a database dump of the client-side storage yields two opaque byte arrays rather than a usable private key.
The mpc_refresh function implements share rotation: it reconstructs the scalar from existing shares, splits it into two fresh random shares, and updates the database. The public key is unchanged. The derived addresses are unchanged. Any signed data from before the refresh remains valid. But the specific share values that an attacker might have copied are now worthless. Share rotation is the mechanism for responding to a suspected partial compromise — if a user has reason to believe one share has been exposed, refreshing invalidates it without requiring any on-chain action or user re-enrollment.
Vault Encryption and Auto-Lock
The lock export calls state.Clear() and sets the locked flag. Any subsequent operation against the enclave returns an error until unlock is called. The auto-lock mechanism is intended to enforce a timeout: after a period of inactivity, the vault locks itself, requiring the user to authenticate again before any cryptographic operation can proceed. The status export returns last_activity precisely so that the application layer can implement time-based lock policies.
The encrypted vault export (export_vault) returns the encrypted database blob — not decrypted key material, but the opaque encrypted container itself. This is intentional: a vault backup is useful only to someone who can unlock it, which requires the original passkey. A stolen backup without the authenticator is useless. The security of the backup is the security of the authenticator, which is hardware-bound on modern devices (Touch ID, Face ID, Windows Hello) and cannot be extracted or cloned.
Session Tokens and Soulbound Semantics
The SessionSBT contract implements ERC-721 with a crucial override: the _update function reverts any transfer where both from and to are non-zero addresses. Minting (from the zero address) and burning (to the zero address) are permitted; transferring between users is not. This is the soulbound property — the token is bound to the account that received it and cannot be sold, delegated, or stolen through a token transfer.
The practical effect is that session tokens cannot be harvested and reused. An attacker who gains access to a user's session token cannot transfer it to a wallet they control. The token's validity is tied to the address it was minted to, and that address's validity is tied to the passkey that controls it. Revoking a session requires calling revokeSession on the contract — only the contract owner (the authorized bundler or factory) can do this, which means revocation is an explicit administrative action rather than something an attacker can trigger unilaterally.
Trust Boundaries
It is worth being explicit about what each part of the system trusts and what it verifies independently.
The client trusts the WASM binary it loads. If the binary has been tampered with, the enclave's guarantees are void. The binary is served from R2 with a content-addressed path (enclave/latest/enclave.wasm), and applications that need stronger guarantees should verify the binary's hash before loading it.
The client does not need to trust the Vault Worker with key material. The Vault Worker receives credential IDs, DID strings, transaction hashes, and session metadata — none of which are secrets. A compromised Vault Worker can deny service, corrupt session records, or return false alias availability results, but it cannot extract private keys or forge signatures.
The chain trusts only what can be cryptographically verified. The HyperAuthAccount._validateSignature function checks a P-256 signature against the stored public key coordinates using the RIP-7212 precompile at address 0x100 on Base. No off-chain attestation, no server-signed JWT, no session cookie is accepted by the smart account. The only valid authorization is a signature from the passkey that registered the account. This is the deepest trust boundary in the system: even if every layer above it is compromised, the chain will not execute operations that lack a valid passkey signature.
The Indexer is trusted for availability and performance, not for correctness. Its data is a derived view of on-chain events. Any response it gives can be independently verified against the chain. The Vault Worker applies an allowlist to Indexer proxy requests, which limits the attack surface if the Indexer is compromised, but the correctness guarantee ultimately rests on the chain.
This layered model — where each layer's security depends on the layer below it, and the chain is the final arbiter — reflects a deliberate opinion about where trust should be anchored. Cryptographic proofs are preferred over administrative assertions at every level where the choice exists.
The Registration Pipeline
A deep examination of the twelve-phase registration process — why it is structured as a pipeline rather than a single call, where the failure boundaries are, and what each phase is actually doing and why.
Smart Account Abstraction
Why ERC-4337 account abstraction is central to Hyperauth, how HyperAuthFactory and CREATE2 make account addresses deterministic, and how passkey signatures work on-chain via the RIP-7212 P-256 precompile.