Skip to content
Tenzro
← All tutorials
Tutorial · Keys and identity

Link a device and set up recovery

Add a second passkey on another device, require two devices for high-value accounts, name guardians, and recover an account after a lost device with a timelock and veto.

Intermediate30 min

A device-bound passkey cannot be copied off its hardware. That is what makes it a sound root for a wallet, and it is also why a wallet on one device cannot survive losing that device. Recovery on Network 1 therefore comes from more than one independent root, never from weakening any single one:

  • More devices. A passkey on your phone, another on your laptop, a third on a security key. Each is device-bound; together they survive the loss of any one.
  • Guardians. Other Tenzro identities that can co-sign the enrolment of a new device when you have none left.
  • A timelock with a veto. A recovery does not take effect immediately. While it waits, any passkey still enrolled on the account can cancel it.

In this tutorial you link a second device to the wallet from Build a passkey wallet, check that the wallet is ready to hold value, add guardians and walk through a recovery.

Prerequisites

  • A passkey wallet with its smart account address and human DID (from the previous tutorial or the console wallet).
  • A second device with a passkey-capable authenticator, such as a phone.
  • The tenzro CLI (see CLI reference). The examples pass --rpc https://rpc.tenzro.xyz; point them at your own node if you run one.
bash
export ACCOUNT=0x...                 # your smart account
export DID=did:tenzro:human:...      # your human DID
export RPC=https://rpc.tenzro.xyz

1. Check whether the wallet is ready

A wallet should not sit behind a single device. Ask the node:

bash
tenzro device wallet-readiness $DID --rpc $RPC

With one device enrolled you see something like:

json
{
  "identity_did": "did:tenzro:human:...",
  "ready": false,
  "blocker": "a wallet needs a second device before it can be created ...",
  "remedy": "bind_second_device",
  "bound_devices": 1,
  "hardware_bound_devices": 1
}

Use ready to decide what your app offers. If it is false, prompt the user to link a device before offering to receive funds.

Adding a device is a custody change, so it takes two ceremonies: proof of control from a passkey already on the account, then the new passkey itself. Knowing the account address proves nothing; it is a public identifier.

From the CLI, the flow runs in a browser. It opens the node's passkey page, you approve with the passkey you already have, and the page shows a QR code. Scan it with your phone and create the new passkey there (the FIDO cross-device flow):

bash
tenzro passkey add --account-address $ACCOUNT --label "Alice's phone" --rpc $RPC

From your own app, do the same in three calls. hybridAssert, createPasskey, hex and fromHex are the helpers from Build a passkey wallet:

ts
// 1. The existing device proves control.
const ch = await client.passkeyRpc.createCustodyChallenge({
  account_address: ACCOUNT,
  operation: "add_passkey",
});
const proof = await hybridAssert(existingCredentialId, fromHex(ch.challenge_hex));

// 2. The new device creates its passkey (run this on the new device).
const next = await createPasskey("alice-phone");

// 3. Add it, carrying the proof.
const added = await client.passkeyRpc.addPasskey({
  account_address: ACCOUNT,
  new_passkey_public_key_hex: hex(next.publicKey),
  new_credential_id_hex: hex(next.credentialId),
  label: "Alice's phone",
  authorization: {
    challenge_id: ch.challenge_id,
    credential_id_hex: hex(existingCredentialId),
    assertion: proof.assertion,
    ml_dsa_signature_hex: proof.mlDsaSignatureHex,
  },
});
console.log(added);

Expected output:

json
{
  "account_address": "0x...",
  "credential_id_hex": "...",
  "credentials_total": 2,
  "label": "Alice's phone"
}

Custody challenges are single-use, bound to one account, one operation and (where there is one) one target, and they expire after a few minutes. A challenge left in a terminal's scrollback is not a standing authorisation.

3. Review what each device proved

List the credentials on the account and the devices bound to your identity:

bash
tenzro passkey list --account-address $ACCOUNT --rpc $RPC
tenzro device list $DID --rpc $RPC

The device list shows what each registration proved about its hardware:

json
{
  "identity_did": "did:tenzro:human:...",
  "count": 2,
  "hardware_bound_count": 2,
  "wallet_ready": true,
  "devices": [
    {
      "credential_id": "...",
      "label": "Alice's phone",
      "hardware_bound": true,
      "attestation_format": "...",
      "key_protection": "...",
      "backup_eligible": false,
      "backed_up": false,
      "bound_at_ms": 1790000000000
    }
  ]
}

hardware_bound comes from the attestation the node verified, not from anything the device claimed. A synced passkey (backup_eligible: true) is accepted at a lower tier and never counts as the only root. Run tenzro device wallet-readiness again: with two independent devices it reports ready: true.

4. Require two devices for every operation (optional)

For a treasury or an operator account you can require two distinct passkeys to co-sign every operation:

bash
tenzro passkey set-policy --account-address $ACCOUNT \
  --second-factor two_credentials --rpc $RPC
tenzro passkey get-policy --account-address $ACCOUNT --rpc $RPC
json
{
  "account_address": "0x...",
  "second_factor": "two_credentials",
  "required_signatures": 2,
  "credentials_enrolled": 2
}

With this policy each signature carries two hybrid legs from two different devices. Switch back with --second-factor single_credential.

5. Add guardians

Guardians are other identities, such as a partner, a colleague or a second organisation, that can co-sign the enrolment of a new device if you lose every device you have. Each guardian gives you their hybrid public keys (an Ed25519 key and a 1952-byte ML-DSA-65 verifying key):

bash
tenzro passkey add-guardian --account-address $ACCOUNT \
  --guardian-ed25519-hex <guardian-1-ed25519> \
  --guardian-ml-dsa-hex <guardian-1-ml-dsa-65> \
  --label "Bob" --rpc $RPC

tenzro passkey add-guardian --account-address $ACCOUNT \
  --guardian-ed25519-hex <guardian-2-ed25519> \
  --guardian-ml-dsa-hex <guardian-2-ml-dsa-65> \
  --label "Carol" --threshold 2 --rpc $RPC
json
{ "account_address": "0x...", "guardian_count": 2, "threshold": 2 }

Adding a guardian is a custody change like any other, so it is authorised by a passkey already on the account.

6. Recover after losing a device

If you still have another enrolled device, you do not need recovery: sign in on that device, add a replacement, and remove the lost one (step 7).

If you have lost every device, start a recovery from a new device. Create a passkey there and derive its ML-DSA-65 key as in Build a passkey wallet, then:

bash
tenzro passkey initiate-recovery --account-address $ACCOUNT \
  --new-passkey-pubkey-hex <new-p256-sec1> \
  --new-credential-id-hex <new-credential-id> \
  --new-ml-dsa-pubkey-hex <new-ml-dsa-65> \
  --rpc $RPC
json
{
  "recovery_id": "...",
  "account_address": "0x...",
  "recovery_op_hash_hex": "...",
  "expires_at_ms": 1790086400000,
  "guardians_required": 2,
  "guardians_total": 2
}

Send recovery_id and recovery_op_hash_hex to your guardians. Each signs the hash with their hybrid key and submits a composite signature (the Ed25519 signature followed by the ML-DSA-65 signature):

bash
tenzro passkey submit-recovery-signature --recovery-id <id> \
  --guardian-index 0 --composite-signature-hex <ed25519||ml-dsa-65> --rpc $RPC
json
{
  "recovery_id": "...",
  "guardian_signatures_collected": 2,
  "guardians_required": 2,
  "quorum_reached": true
}

The recovery then waits out its timelock. During that window any passkey still enrolled on the account can cancel it, so a recovery started by someone else fails as soon as you notice it. Watch in-flight recoveries with:

bash
tenzro passkey list-pending-recoveries --account-address $ACCOUNT --rpc $RPC

When the timelock has passed unopposed, finalise:

bash
tenzro passkey finalize-recovery --recovery-id <id> --rpc $RPC

The new passkey is enrolled on the same account. Your DID, address, balance and reputation are unchanged.

7. Remove a lost device

Unbind the lost device and end every session it authorised in one action, then remove its credential from the account:

bash
tenzro device revoke $DID <lost-credential-id> --rpc $RPC
tenzro passkey remove --account-address $ACCOUNT \
  --credential-id-hex <lost-credential-id> --rpc $RPC

Removing a credential is authorised by a passkey that remains on the account, which is exactly the protection you want: nobody who merely knows your address can lock you out.

Next steps