--- name: webauthn-prf-wallet description: Build an iframe-isolated, passkey-derived Ethereum wallet using the WebAuthn PRF extension. Use when the user wants to implement a passkey-based wallet, derive an EVM private key from a passkey without server custody, add a non-custodial wallet pattern to a web app, harden wallet key handling against XSS via an isolated iframe, or check whether a client platform supports the WebAuthn PRF extension. Covers PRF + HKDF → secp256k1 key derivation, LongBlob fallback for compatibility, cross-frame RPC via Postmate, and browser/OS platform support gating. --- # WebAuthn PRF Wallet A reusable pattern for deriving an Ethereum private key from a user's passkey entirely on the client, with the key never leaving an isolated iframe. This skill captures the implementation from the 1Shot Payments app, distilled so that it can be dropped into any web application. **What you get:** - A deterministic `passkey ⇒ EVM private key` derivation that produces the same wallet every time the user authenticates with the same passkey. - An isolated wallet iframe that holds the derived key in memory and signs transactions via a narrow RPC surface — the key is never reachable from the parent page's JavaScript, substantially reducing XSS/supply-chain risk. - Platform support gating (PRF is not universally available) with a LongBlob + recovery-phrase fallback so users on incompatible authenticators can still have an account. **What you do NOT get from this skill alone:** - A product. You still need to wire up registration UI, session management, a relying party (RP) configuration, and whatever use case you're building (signing, payments, delegations, etc.). - A server. The skill shows what the server must do (challenge storage, signature verification) but does not prescribe the stack — Next.js is shown as an example in `references/nextjs-example.md`. ## Quick Reference **Detailed references — read the one(s) relevant to your task:** - [references/prf-derivation.md](./references/prf-derivation.md) — How PRF output becomes a valid secp256k1 private key. Read this when implementing the derivation function or debugging "wrong address" issues. - [references/platform-support.md](./references/platform-support.md) — Browser/OS compatibility matrix, webview detection, `isPlatformSupported()` helper. Read this when gating registration or showing a "not supported" message. - [references/iframe-isolation.md](./references/iframe-isolation.md) — Postmate cross-frame RPC, iframe `allow` attribute, the user-activation / visibility gotcha that breaks WebAuthn in hidden iframes. Read this when setting up the wallet iframe or debugging "no passkey prompt appears" in the iframe. - [references/longblob-fallback.md](./references/longblob-fallback.md) — `credBlob` / `largeBlob` storage, recovery-phrase encryption, multi-passkey support. Read this when you want PRF-incompatible devices to still work, or when supporting multiple passkeys per account. - [references/server-integration.md](./references/server-integration.md) — Generating registration/authentication options with PRF salts, verifying responses, and what to persist. Framework-agnostic. - [references/nextjs-example.md](./references/nextjs-example.md) — Concrete walkthrough using Next.js App Router + `@simplewebauthn` + Redis for challenges. **Copy-ready code (in `assets/`):** - `assets/prfToValidEthPrivKey.ts` — HKDF → secp256k1 derivation (browser-safe, Web Crypto API). - `assets/platformSupport.ts` — Browser/OS/webview detection using Bowser. - `assets/WalletIframeSketch.ts` — Minimal Postmate `Model` that derives the key and exposes a `signMessage` RPC. Strip out what you don't need. ## Package Installation ```bash npm install @simplewebauthn/browser @simplewebauthn/server ethers viem postmate bowser ``` At runtime the pattern uses the platform `crypto.subtle` API (no polyfill needed in modern browsers). Server-side you need a KV store for challenges with a short TTL (Redis is used in the reference, any equivalent works). ## Core Concepts ### 1. The PRF extension is a deterministic, per-credential secret `PRF` (Pseudo-Random Function) is a WebAuthn extension that lets the authenticator evaluate an HMAC-SHA-256 over an input you provide (the "salt") using a secret key it keeps for that credential. Properties you care about: - **Deterministic.** The same credential + same `eval.first` salt always produces the same 32-byte output. - **Scoped to a credential.** A different passkey produces a different PRF output, even with the same input. This is why you generally bind **one wallet = one credential** unless you use the LongBlob fallback (see `references/longblob-fallback.md`). - **Never leaves the authenticator without user presence + verification.** The user consents to each evaluation via biometrics / PIN. - **Not available everywhere.** See `references/platform-support.md`. You use the PRF output as keying material, then run **HKDF → secp256k1** to produce an Ethereum private key. The PRF output is not itself a valid key — it could be zero or above the curve order. ### 2. The derivation must converge on the same key forever The derivation inputs are: - `prfOutput` — 32 bytes from the authenticator, **depends on the `info` label you pass as `eval.first`**. - `infoLabel` — a UTF-8 string you choose, e.g. `"com.example.eth-key-v1"`. - HKDF parameters — salt (32 zero bytes is fine; PRF output is already scoped to the credential), info (infoLabel ‖ counter byte), hash = SHA-256, output length = 32 bytes. **The `infoLabel` is a forever decision.** If you change it after users have registered, their derived key changes and they lose access to their wallet. Version it in the string itself (e.g. `-v1`) and never change that version. The HKDF output may occasionally be ≥ the secp256k1 curve order or zero. Retry with `counter = 0, 1, 2, …, 15`; in practice counter 0 succeeds with overwhelming probability. See `assets/prfToValidEthPrivKey.ts` for the full implementation. ### 3. Isolate the key in an iframe The derived private key must never be reachable from the main application's JavaScript context. If the parent page is compromised (XSS, malicious dependency), the attacker gets the user's funds. The pattern: - Host a dedicated wallet page at a same-origin route (e.g. `/wallet/local`). This page has **minimal dependencies** — only what's needed for passkey auth and signing. - Embed that page in a hidden `