Security & Crypto
Signing & WebAuthn
The web application uses passkeys for authentication and a remote signing service for some wallet actions. Passkey authentication is not the same as user-held wallet keys or threshold signing. This page describes the current web path and its limits.
Correction: this page previously described 2-of-2 MPC
docs/CUSTODY-DISCLOSURE.md in the repository records the correction in full.Signing model in the web application
Mobile client source exists in the repository, but FurlPay does not currently distribute a production mobile app. Do not treat unreleased client code as an available self-custody product.
Web app — custodial. Signing runs through /api/mpc/sign to a secure enclave operated by Turnkey, which signs the digest and returns a complete signature. The client verifies that the signature recovers to the expected address, which is a correct check — but it is verification, not participation in a threshold scheme. Anything holding the Turnkey organisation credential can, subject to enclave policy, produce signatures for wallets in that organisation. That makes this path custodial.
Furlpay deliberately calls signRawPayload rather than letting Turnkey build and sign the whole transaction: the client serialises the transaction and computes the digest itself, so the address-recovery check is not Turnkey checking Turnkey.
Policy checks are not threshold signing
The property with genuine independence is authorisation, not key material. A signature request must survive three gates, each able to refuse without opening the others:
- The client refuses to request an out-of-policy signature.
/api/mpc/signrefuses to forward one (lib/services/policyEngine.ts).- Turnkey's enclave policy refuses to produce one.
That is real defence in depth. It is just not key-share splitting, and the old copy conflated the two.
What would make true threshold signing real
A device-held share that is required to produce a signature — each side computing a partial signature, neither sufficient alone. That is on the roadmap and is not built. Until it ships and is verified end to end, this page describes the web path as custodial.
WebAuthn passkeys
Login uses WebAuthn / FIDO2 passkeys — fingerprint or Face ID, no passwords. The Relying Party ID is resolved from explicit server configuration (FURLPAY_RP_ID / Vercel deploy URL), never from the client-controlled Host header — the single most common WebAuthn deployment mistake, and the one that lets a reverse-proxy phishing origin bind credentials. Assertions verify against an allow-list of origins and RP IDs.
- Registration and assertion run through
@simplewebauthn/server(origin, challenge, and signature-counter checks). - Credentials are bound to the registrable eTLD+1, so they can never be replayed on another domain.
Signed sessions
A successful passkey ceremony mints a short-lived, HttpOnly session cookie — a signed JWT (jose, HS256) verified in edge middleware, so tampered or expired tokens are rejected before any handler runs. The signing secret is required in production; the app refuses to start with the weak development fallback.
Agentic payment security (x402)
Production x402 is currently unavailable: on 11 October 2026 the endpoint returned 503 controls_unavailable with reason plan_limit. Settlement is also in development, so an issued challenge is not proof that a payment can complete or a paid response will be delivered. Check the rail status and the x402 guide before integrating.
- Validate the challenge fields and pay-to address before presenting a wallet signature request.
- Release a paid resource only after the configured facilitator confirms settlement.
- Do not treat a prepared authorization, challenge or pending status as a completed payment.
Production boundary
