Ask a security engineer what actually keeps them up at night and it's rarely the cryptography. It's the secrets sitting still: password hashes waiting patiently in a table, session tokens in a log file, an API key in a screenshot. A secret at rest is a liability with no expiry date — every backup, every replica, every laptop that ever ran a database dump is another copy of the thing that ends the company if it leaks.
The ZuriFi terminal is the surface where our custody of stablecoins and digital assets meets a web browser. Deposits, treasury, withdrawals to four chain families — real value, moved by people, through a UI. When we designed authentication for it, we started from one constraint and refused to negotiate:
If a secret can be stolen from us, someone will eventually steal it. So logging in — and moving money — must not depend on any secret we store.
The result is a system with no password column. A login begins with a one-time code that proves possession of a phone number — a bootstrap, not a stored credential — and everything after that is a passkey: a hardware-guarded key pair that signs challenges instead of revealing secrets. This post is about how those passkeys are wired into Turnkey, the key infrastructure that also holds our custody wallets, and about the property that falls out of the design: our own servers verify every login, yet hold nothing that would let them forge one.
Passkeys in ninety seconds
A passkey is a WebAuthn credential: an asymmetric key pair minted by the browser via navigator.credentials and stored in the device's secure hardware — the same chip that guards Face ID and fingerprint data. Three properties make it categorically different from anything typed into a form:
- The private key never travels. Not to us, not to the browser's JavaScript, not even to the operating system in extractable form. The device signs challenges; the key stays in silicon. What a server ever sees is a public key and signatures.
- Every use is gated by the OS. A signature requires a live biometric or device-credential check at the moment of signing. Possession of an unlocked browser tab is not enough.
- The credential is bound to our domain. A phishing site can clone our UI pixel-for-pixel and still walk away with nothing: the browser will not exercise a credential on a domain it wasn't created for, and there is no password to type into the fake form anyway. The entire category of “user got tricked into entering their credentials” stops existing.
That's the standard pitch, and it's all true. The interesting engineering starts with the next question: a passkey produces signatures — who verifies them, and what do they authorize?
The Turnkey sub-organization that owns nothing
Our custody runs on Turnkey: wallet private keys are generated inside its secure enclaves and never leave them. Application code builds transactions; Turnkey signs them; every signature is an authenticated, immutably logged activity. Those custody wallets — per-user deposit addresses and treasury wallets across EVM chains, Solana, TRON, and Bitcoin — all live in our parent organization, which our backend addresses with a narrowly scoped API key.
Passkeys live somewhere else entirely. At enrollment, the browser mints a WebAuthn credential and we forward its attestation to Turnkey's createSubOrganization — creating a dedicated sub-organization for that person whose one and only root user is their passkey. Every user gets one; every admin gets one; the machinery is identical.
Here's the deliberate inversion. Most teams use Turnkey sub-organizations as embedded wallets — the sub-org holds the user's funds. Ours hold nothing. The sub-organization has zero custody power: it cannot move a cent, touch a treasury wallet, or see another tenant. Its only capability is producing signatures that Turnkey will vouch for. We use it as a verification oracle — a tamper-proof witness that says “yes, the human enrolled behind this sub-org just proved presence on their device.” Custody stays behind the treasury boundary; identity gets its own tenant with an empty vault.
Logging in is signing
We never wrote WebAuthn verification code, and that was the point. Parsing attestation objects, validating CBOR, checking signature counters — that's precisely the kind of security-critical parser where authentication systems grow bugs. Instead we reuse the most battle-tested path available: the one that guards Turnkey's own API.
Turnkey authenticates every request with a stamp — a signature over the request body, carried in a header. Passkeys are first-class stampers. So a login on the terminal is literally a signing ceremony:
- Our server mints a random 32-byte challenge — five-minute lifetime, single-use, bound to the specific account and purpose that requested it.
- The browser asks the passkey to stamp a Turnkey
sign_raw_payloadrequest whose payload is that challenge. The OS shows the biometric prompt; the secure hardware signs; the browser hands our server the stamped request — body, signature header, nothing more. - Our server forwards the stamped request to Turnkey. If the activity comes back
ACTIVITY_STATUS_COMPLETED, Turnkey — which registered the credential and holds the public key — has verified the WebAuthn signature for us. Only then is the challenge consumed and a session issued.
The verification on our side is deliberately paranoid, and fail-closed at every step. A WebAuthn stamp signs the request body, not the URL it's sent to — which means our server can, and must, choose the destination itself. So the forwarding logic pins the exact Turnkey origin and the exact sign_raw_payload path; nothing client-supplied is ever fetched. Before trusting anything, it parses the stamped body and requires that the organization is the caller's own sub-org, the activity type is exactly the one expected, and the signed payload equals the challenge we minted. Any mismatch — any at all — is a 401. And the challenge is only marked consumed by a conditional database update after Turnkey accepts, so a replayed or raced stamp finds the challenge already spent.
Step back and notice what this buys. Our database holds only expired one-time challenges and public identifiers. Our servers hold no passkey key material whatsoever — the backend that verifies every login is cryptographically incapable of forging one. A fully compromised application server still cannot manufacture the one thing a login requires: a signature from hardware it doesn't have, vouched for by an enclave it doesn't control.
A passkey approves one operation, not a session
Sessions are fine for reading dashboards. They are not fine for moving money — a stolen session token shouldn't be worth a single withdrawal. So the terminal treats value movement as its own authentication event, with a mechanism we call action tokens.
When someone initiates a withdrawal, the client first states the exact operation: destination, asset, network, amount — everything. The server hashes that request into an opHash and mints a challenge whose purpose string welds all of it together: withdraw:<idempotency-key>:<opHash>. The passkey ceremony runs exactly as at login — biometric prompt, stamp, pinned forward to Turnkey, COMPLETED or nothing. What comes back is a five-minute, single-purpose token with the operation hash sealed inside.
The withdrawal endpoint then re-hashes the body it actually received and compares it to the hash the passkey approved. Change one character of the destination address after approval — malware in the browser, a tampering proxy, a bug — and the hashes diverge and the request dies with a 401. The token can't be reused for a second withdrawal, can't be repointed at a different destination, and can't outlive its five minutes.
A passkey signature approves one exact operation — not a session, not a category of actions, not “whatever the server meant.”
The keys that move money never leave the enclave
The other half of the story is what happens after an approval, when the treasury actually pays out. Withdrawals span four chain families, and each one signs differently: EVM chains get a pre-hashed transaction digest, Solana signs raw serialized message bytes, TRON signs the transaction ID, Bitcoin flows through a PSBT. Our code builds all of those transactions — and never touches a private key while doing it. Every signature comes out of a Turnkey enclave; nothing in our repositories has ever held raw key material, which means nothing in our repositories can leak it.
The backend's own credentials follow the same least-privilege instinct. Two API key pairs exist: a worker key the service runs with, scoped to signing only — it cannot export wallets, rewrite policies, or delete sub-organizations — and an ops key used once to provision the treasury and then kept offline. And the whole custody path fails closed: if signing infrastructure isn't configured, money endpoints return an error rather than degrading to something weaker. Every signature the worker key requests lands in Turnkey's append-only activity log, so the audit trail is a property of the platform, not a discipline we have to remember to maintain.
The threat model, honestly
Security posts love adjectives. Here is the same claim as a table — what specific attacks are actually worth against this design:
| The attacker has… | What it's worth |
|---|---|
| A copy of our database | Public identifiers and consumed one-time challenges. No hash to crack, no credential to replay. |
| A pixel-perfect phishing site | Nothing. The browser won't exercise a passkey off-domain, and there's no password to harvest. |
| A stolen session token | Read access until expiry. Every money operation demands a fresh passkey ceremony the attacker can't perform. |
| A stolen device | A biometric prompt. The passkey requires an OS-level presence check at each use, not just an unlocked screen. |
| Our own application servers | No passkey material exists there, so no login can be forged. The custody key is scoped to signing, can't export keys, and every request it makes is immutably logged. |
None of these are “we'd detect it quickly” answers. They're structural: the attack fails because the thing it needs to steal was never created, never stored, or never in our hands.
Why a payments company builds like this
Nothing in this post is visible in the UI. Users see a fingerprint prompt and a withdrawal that clears; the sub-organizations, the pinned forwarding, the operation hashes — all of it is invisible when it works, which is the only state it's allowed to be in. But it's exactly the layer where a financial platform earns the right to exist. Moving stablecoins across borders is easy to demo and hard to defend, and defense is architecture, not vigilance.
Nothing we store can impersonate you, and nothing that moves your money moves without fresh, hardware-backed proof that it's you.
That sentence is the whole design. Everything above is just the engineering required to make it true.