suideveloper
All questions
Accounts and wallets

How do passkeys work on Sui?

November 3, 2026

Sui accepts passkey (WebAuthn) signatures at the protocol level, as specified in SIP-9. A passkey is a secp256r1 key that stays in an authenticator such as Face ID, Touch ID, Windows Hello, a phone or a hardware security key. To sign, the authenticator receives the transaction's intent hash as its WebAuthn challenge. The account address is BLAKE2b-256(0x06 || compressed_public_key). Passkeys are enabled on mainnet, both on their own and as multisig members.

How a passkey signature is built

  1. Your app builds the transaction and computes intent || blake2b_hash(tx_data).
  2. That value, base64url-encoded, becomes the WebAuthn challenge in navigator.credentials.get().
  3. The authenticator checks the user (PIN or biometric) and signs over its authenticatorData and the clientDataJSON, which contains the challenge.
  4. The Sui signature is flag 0x06 plus a BCS PasskeyAuthenticator that holds authenticatorData, clientDataJson and a userSignature. The userSignature is the secp256r1 flag 0x02, a 64-byte r || s signature with low s, and the 33-byte compressed public key.
  5. Validators parse clientDataJson, check that the challenge matches the transaction, and verify the signature.

Only secp256r1 is accepted, even though WebAuthn supports other algorithms.

Using it from TypeScript

@mysten/sui/keypairs/passkey provides PasskeyKeypair, which works as a normal Signer. The SDK marks it @experimental.

TypeScript
import {
  BrowserPasskeyProvider,
  PasskeyKeypair,
  type BrowserPasswordProviderOptions,
} from '@mysten/sui/keypairs/passkey'

const provider = new BrowserPasskeyProvider('My Sui App', {
  rpName: 'My Sui App',
  rpId: window.location.hostname,
  authenticatorSelection: { authenticatorAttachment: 'platform' }, // or 'cross-platform'
} as BrowserPasswordProviderOptions)

// First time only: creates a new passkey credential and wallet
const keypair = await PasskeyKeypair.getPasskeyInstance(provider)
const address = keypair.getPublicKey().toSuiAddress()

// Later: sign like any other keypair
const { signature } = await keypair.signTransaction(txBytes)

Recovering the public key

WebAuthn returns the public key only when the credential is created, not when it signs. The SDK docs therefore recommend caching the PasskeyKeypair, or at least its public key. If you lose it, for example because the user switched browsers, you can recover it:

TypeScript
import { findCommonPublicKey, PasskeyKeypair } from '@mysten/sui/keypairs/passkey'

const pks1 = await PasskeyKeypair.signAndRecover(provider, new TextEncoder().encode('hello'))
const pks2 = await PasskeyKeypair.signAndRecover(provider, new TextEncoder().encode('world'))
const publicKey = findCommonPublicKey(pks1, pks2)
const keypair = new PasskeyKeypair(publicKey.toRawBytes(), provider)

Each ECDSA signature yields two candidate public keys. Two signatures over different messages identify the right one.

Gotchas

  • One wallet per origin. Passkeys are bound to the relying-party ID (your domain). A passkey made on one site can't sign on another, which is what makes passkeys phishing-resistant. It also means a new domain gets a new address. The SDK docs advise letting users create a passkey wallet only once per origin.
  • Calling getPasskeyInstance again creates a new wallet. Each call creates a fresh credential with a new address. For returning users, use signAndRecover.
  • No key export. You can't extract the private key or move a passkey to another authenticator. Recovery depends on the platform's passkey sync (iCloud Keychain, Google Password Manager) or on a backup.
  • High-value accounts. The Sui docs warn against relying on a single synced passkey for admin or high-value accounts. Put the passkey in a multisig with a hardware key or a zkLogin signer.

For other ways to sign without a seed phrase, see What is zkLogin? and Which signature schemes does Sui support?.

Sources

More on accounts and wallets