Advanced
Simple Signup (PIN/KDF)
IBEXFI API — Simple Signup (PIN-Derived Safe Signer)
Design note for a signup flow that issues a JWT without opening a wallet, then lets the user set a secret (PIN/password) to deterministically derive a Safe signer key that is never stored and can be recovered via Safe guardians/passkeys.
Goals
- No passkey required at signup v1.2; only
ExternalUserId+ JWT are created. - User later defines a secret (PIN/password) that we never see; key is re-derived on demand.
- Safe signer derived from KDF + HKDF; challenge only for proof-of-possession (not in KDF).
- Support recovery via existing Safe recovery (guardian/multi-signer, passkey).
- Limit brute force, replay, phishing; allow KDF/parameter versioning.
Cryptographic choices
- KDF: Argon2id (example: memory 64 MiB, iterations 3, parallelism 1; versioned).
- Key derivation:
kdfKey = Argon2id(secret, salt, params)thenprivateKey = HKDF(kdfKey, info="ibex-safe-signer|appId|env|ExternalUserId");publicKey/EOA derived client-side. IncludingappId(or env) andExternalUserIdin HKDF info domains the key to this app/user and prevents cross-use. - Challenge: random, expiring, single-use; used only in the signed proof message, not in KDF.
- Optional PAKE: OPAQUE (recommended if secrets can be weak PINs); prevents offline guessing and authenticates the server.
- Encoding: base64 for binary blobs, hex for addresses/public keys; canonical JSON (stable key order) for messages.
Enrollment (one-time, when the user sets the secret)
If PAKE is enabled:
- Client runs OPAQUE register with
secret. - Server stores:
envelope,serverPub,salt,saltVersion,kdfParamsVersion. If PAKE is not used, onlysalt(perExternalUserId) and KDF params are stored.
start-derive endpoint (wallet=kdf or wallet=email)
- POST
/auth/start-derive(conceptual; in v1.2 use GET /auth/sign-up withwallet=kdf|email) - Auth: Bearer JWT containing
ExternalUserId. - Request: optional
deviceInfoobject. - Response:
Client-side derivation (per session)
kdfKey = Argon2id(secret, salt, params)privateKey = HKDF(kdfKey, info="ibex-safe-signer|appId|env|ExternalUserId")publicKey(or EOA) derived fromprivateKey- Build canonical message to sign:
signature = sig(privateKey, canonicalMessage)- If PAKE is enabled, produce
opaqueLoginRequestfrom OPAQUE usingsecret+envelope.
finish-derive endpoint
- POST
/auth/finish-derive - Request:
- Server processing:
- Verify
serverSignature,challengevalidity (exp, single-use, bound toExternalUserIdandappId). Reject replay (challenge already used) and checktimestampis within an acceptable skew; optionally reject reused(challenge, nonce). - If PAKE: process
opaqueLoginRequest, returnopaqueLoginResponse, derive session key (auth of secret; server proves itself). - Verify
signaturewithpublicKeyover canonical message. - Enforce lifecycle: first-time bind
ExternalUserId↔publicKeyif none exists; otherwise ensurepublicKeyis authorized/expected (supportssignerVersion/keyIdfor rotation). - If OK, authorize Safe ownership by
publicKey; optionally return a short-lived token to proceed with Safe deploy.
- Verify
- Response (example):
Recovery & rotation
- Recovery is handled by existing Safe recovery (guardian/passkey, multi-signer). If the user forgets the secret, recovery adds/rotates a signer.
saltVersionandkdfParamsVersionare stored; allow multiple active versions during migration. Prompt re-derivation on nextfinish-derivewhen upgrading parameters.- Forced KDF upgrade scenario: if client sends
kdfParamsVersionlower thancurrentMinVersion, server can require re-derivation with a fresh challenge and the new parameters, keeping both versions valid during a migration window.
Security notes
- Brute force: Argon2id cost, minimum secret length policy, server rate limiting on
finish-derive. - Replay/phishing: short-lived, single-use challenge; include
appId/origin in signed message; with PAKE, the server must prove identity. - Data-at-rest: store only
salt, versions, and (if PAKE) OPAQUE envelope + server public key; never store the derived key. - UX: tune Argon2id params to balance latency on target devices; adjust
parallelismif clients can leverage multiple cores. - Rate limiting: per
ExternalUserId, per IP, optionally per device; consider exponential backoff after N failures and step-up auth (OTP/guardian) after a threshold. BindingappIdto origin/bundleId reduces phishing. - Canonical JSON: strict lexicographic key order, UTF-8, no extraneous spaces/newlines; same serializer across platforms to avoid signature mismatches.
- UX/perf guidance: benchmark per platform; consider profiles (mobile vs desktop). Show user feedback during KDF (“Securing your signer…”). Explain clearly: the Safe key is derived client-side from the secret; backend never sees the secret nor the key.
Server signature (challenge integrity)
- Example:
serverSignature = sig_server(challenge || challengeExpiresAt || externalUserId || appId)using Ed25519 or secp256k1. - Include
serverKeyIdin the response to support key rotation. Clients verify with the advertised server public key matchingserverKeyId. - Binding
appIdprevents a challenge from app A being reused on app B.
Signer lifecycle (first bind, re-derivation, rotation)
- First bind: on the first successful
finish-derive, bindExternalUserIdtopublicKey(Safe signer). Reject a second “first bind” unless explicitly allowed. - Re-derivation/login: on subsequent
finish-derive, ensurepublicKeymatches an authorized signer for thatExternalUserId, or belongs to an allowed rotation slot (signerVersion/keyId). - Rotation: allow adding a new signer with a controlled flow (challenge + policy) and marking the old signer as deprecated; keep both during migration if needed.
Endpoint strategy (reuse vs dedicated)
- You can keep existing
/auth/signupand/auth/signinand add a mode flag, e.g.passkeys=false,flow=pin-kdf, plus payload fields for the derive flow (envelope/serverPub,challenge,opaqueLoginRequest/Response, etc.). - Pro: backward compatibility and fewer new routes; fits “Simple ExternalUserId creation (passkeys=false, no email)” and “Email validation (passkeys=false, email provided)”.
- Con: risk of overloading semantics; ensure the responses clearly include the derive materials when
flow=pin-kdf. Dedicated endpoints (/auth/start-derive,/auth/finish-derive) stay clearer and reduce accidental misuse. - Whichever you pick, keep the contract stable: challenge single-use, serverKeyId for signatures, optional OPAQUE fields, and explicit versions (
saltVersion,kdfParamsVersion). - If
passkeys=true, ignore/reject the PIN-KDF-specific fields to avoid mixing flows. Ifpasskeys=false, accept the PIN-KDF fields (salt/challenge/envelope/opaqueLoginRequest, etc.) in the same endpoints.
Minimal non-PAKE variant
If PAKE is not implemented, keep the same start-derive/finish-derive contracts, omit envelope/serverPub/opaqueLoginRequest, and rely on challenge + KDF + signature. This is simpler but weaker for short secrets (more exposed to offline brute force if salts leak).