Tenzro
Custody and wallet

Passkey onboarding.

The default consumer wallet on Tenzro is a passkey-bound ERC-4337 smart account. The signing key lives in the user’s hardware secure element (Apple Secure Enclave, Windows Hello / TPM, Android StrongBox, YubiKey). No seed phrase. Rotation, recovery, and agent delegation all happen on-chain through ERC-7579 modular validators.

A passkey counts as hardware-bound only when both hold: the credential cannot be replicated off the device, and an attestation verified against a pinned vendor root says its key lives in a TEE or secure element. The first without the second is a claim made by the same software making every other claim, and a synced passkey proves control of a cloud account rather than possession of a device. No platform account is an identity authority— trust rests on a signature from vendor-placed hardware over a challenge Tenzro chose, checked against the FIDO Metadata Service by AAGUID or the platform’s own root.

A wallet cannot be created behind a single device: the machine you are on is the first, and a genuinely separate hardware-bound device — a phone, another machine — must be bound before there is anything to lose. Sessions name the device that authorised them, so losing a phone removes that phone’s access without signing you out of the laptop in your hands.
STATUS
Testnet
CRATE
tenzro-node
STABILITY
Stable
REFERENCE
passkey-onboarding
01

The model

Three pillars: the smart account address is the identity, not the signing key. Keys rotate without the address changing. Custody is enforced on-chain at signing time through composable ERC-7579 validator modules.

Reference implementations Tenzro is aligned with: Coinbase Smart Wallet, Daimo, Privy, Safe with modular validators, Argent on StarkNet. All share the same shape — passkey primary + guardian quorum recovery + session keys for app grants.

02

Substrate

tenzro-crypto::p256              # P-256 keypair, signer, verifier
tenzro-crypto::webauthn         # full WebAuthn assertion verifier
tenzro-vm precompile 0x100      # EIP-7951 / RIP-7212 P256VERIFY
tenzro-vm::WebAuthnValidator    # ERC-7579 primary validator
tenzro-vm::SocialRecoveryValidator   # 0x101d N-of-M guardians
tenzro-vm::SessionKeyValidator       # 0x101e scoped session keys
tenzro-vm::SpendingLimitValidator    # 0x101f per-tx + daily caps
tenzro-vm::AccountFactory       # ERC-4337 v0.8 EntryPoint compatible
03

Enrollment

The client acquires a WebAuthn registration credential from the platform authenticator and passes the P-256 public key + opaque credential ID + ML-DSA-65 verifying key (for the post-quantum leg) to tenzro_enrollPasskey. The node creates a TDIP human identity, deploys a smart account via the shared AccountFactory, installs WebAuthnValidator as the primary signer, and persists everything to CF_AGENTS + CF_VALIDATOR_MODULES.

POST https://rpc.tenzro.xyz
{
  "jsonrpc": "2.0",
  "method": "tenzro_enrollPasskey",
  "params": {
    "display_name": "Hilal's iPhone",
    "passkey_public_key_hex": "0x04...",
    "credential_id_hex": "0x...",
    "ml_dsa_public_key_hex": "0x..."
  },
  "id": 1
}
04

Signing

The client builds a UserOperation, hashes it per ERC-4337 v0.8, calls navigator.credentials.get() with the hash as the challenge, and submits the resulting WebAuthn assertion + ML-DSA-65 signature to tenzro_signWithPasskey. The node verifies the challenge matches the op hash, the P-256 leg validates against the registered public key, and the PQ leg validates against the registered ML-DSA verifying key.

For production transactions, the same hybrid signature packed as HybridWebAuthnSignature is supplied as the userOp.signature field on the standard eth_sendRawTransaction / ERC-4337 EntryPoint path. Each assertion carries the credential_id it was produced with, so the validator selects the right registered public key when an account has several devices enrolled. The on-chain WebAuthnValidator runs the same verification through the 0x100 P256VERIFY precompile.

05

Browser-launch login

A WebAuthn ceremony has to run in a browser context, next to the platform authenticator. For CLI and headless callers, Tenzro brokers the ceremony through a short-lived session the same way a cloud CLI brokers a device login. The caller opens tenzro_createPasskeySession, which returns a session id and a URL; the node serves the page at /auth/passkey?session=<id>. The page runs navigator.credentials.create() or get() and posts the outcome back against the session. The caller polls tenzro_getPasskeySession until the session reaches a terminal state, then reads the enrolled account or verified assertion off the result.

The session kind is one of enroll (new account via tenzro_enrollPasskey), add (additional device credential via tenzro_addPasskey), or sign (op-hash assertion via tenzro_signWithPasskey). Per-kind parameters are validated at createPasskeySession time so an error surfaces at the CLI rather than on a dead browser page. Sessions persist to CF_VALIDATOR_MODULES with a MAC-tagged row, so an in-flight login survives a node restart and a tampered row is refused at read time; expired sessions return a terminal expired status.

# gcloud-style browser login  creates an account
tenzro passkey login

# add another device to an existing account
tenzro passkey add --account 0x...

# approve one operation hash from the browser
tenzro passkey sign --account 0x... --op-hash 0x...
06

QR device handoff

The account owner does not have to complete the ceremony on the machine that started it. The node-served page renders an inline SVG QR code of its own session URL. Scanning it with a phone opens the same page on the phone, so the WebAuthn credential is created in the phone’s secure element (Apple Secure Enclave, Android StrongBox) and bound to the account. This is the “add my phone as a device” path: start tenzro passkey add on a laptop, scan the QR with the phone, complete Face ID / fingerprint there.

The private key never leaves the device that created it. A single Tenzro identity can carry several device credentials — a laptop platform authenticator, a phone, a hardware key — any one of which authorises operations unless a stricter second-factor policy is set.

An added device only supplies its WebAuthn P-256 credential. The node mints that credential’s ML-DSA-65 post-quantum leg in its TEE, so a phone or hardware key never has to carry a post-quantum key of its own.

07

Devices and second-factor policy

Credentials are managed per account: tenzro_addPasskey registers another device, tenzro_listPasskeys lists the enrolled credentials, and tenzro_removePasskey revokes one by credential_id. Removing a device does not change the smart account address.

The per-account authorisation requirement is a policy: tenzro_setPasskeyPolicy takes single_credential (any one enrolled credential authorises — the default) or two_credentials (two distinct enrolled credentials must both sign the same op hash). tenzro_getPasskeyPolicy reports the current policy and the enrolled-credential count.

A hardware second factor is not a policy variant — it is expressed by installing the hardware-signer validator module alongside the passkey validator (see below). The ValidatorRegistry AND-combines every installed module, so a two-credential policy and a hardware signer compose rather than conflict.

tenzro passkey add          --account 0x...   # register a device
tenzro passkey list         --account 0x...   # enrolled credentials
tenzro passkey remove       --account 0x... --credential-id 0x...
tenzro passkey set-policy   --account 0x... --policy two_credentials
tenzro passkey get-policy   --account 0x...
08

Social recovery

When the user enrolls, they nominate guardians — typically family members, a recovery service, or a backup hardware key. Each guardian holds an Ed25519 + ML-DSA-65 composite key. To rotate the account to a new passkey (after device loss), the user enrolls a new passkey on a new device, calls tenzro_initiateRecovery, and shares the returned recovery_op_hash with the guardians. Each guardian signs and submits via tenzro_submitRecoverySignature. Once quorum is reached, the user calls tenzro_finalizeRecoveryand the node installs the new passkey as the smart account’s primary validator. The smart account address never changes.

tenzro_addGuardian                  # register one guardian
tenzro_initiateRecovery             # open the rotation ceremony
tenzro_submitRecoverySignature      # one per guardian
tenzro_finalizeRecovery             # install new passkey on quorum
tenzro_listPendingRecoveries        # observe ceremony state
09

Session keys for agents

Agents never hold the human’s passkey. The human grants the agent a scoped session key: tenzro_grantSessionKey installs a SessionKeyValidator config with allowed function selectors, target contracts, per-call value cap, cumulative lifetime cap, and validity window. The agent uses its own Ed25519 key to sign user ops within the scope; anything outside the scope is refused at the on-chain validator.

Revocation is one call: tenzro_revokeSessionKey. The session validator disengages immediately and the agent loses signing authority.

10

Hardware signers

Power users layer a Ledger / Trezor / GridPlus / YubiKey as an additional ANDed validator: tenzro_addHardwareSigner installs a hardware validator module that the EntryPoint AND-combines with the primary passkey validator. Configure required_always: true for a hardware second factor on every operation, or set required_above_wei for a value threshold above which the hardware key is mandatory.

11

Surfaces

The passkey RPCs map 1:1 to client surfaces:

  • Rust SDK: TenzroClient::passkey_rpc() returns a PasskeyClient.
  • TypeScript SDK: TenzroClient.passkeyRpc.
  • CLI: tenzro passkey login | add | list | remove | sign | set-policy | get-policy | enroll | add-guardian | initiate-recovery | grant-session-key | add-hardware-signer | list-smart-accounts
  • MCP tools: enroll_passkey, sign_with_passkey, add_passkey_guardian, initiate_passkey_recovery, submit_recovery_signature, finalize_passkey_recovery, grant_session_key, revoke_session_key, set_spending_limit, add_hardware_signer, get_smart_account, list_smart_accounts, list_pending_recoveries, add_passkey, list_passkeys, remove_passkey, set_passkey_policy, get_passkey_policy, create_passkey_session, get_passkey_session.
  • A2A: passkey-wallet skill.
12

Binding devices to an identity

A Tenzro identity links devices the way a platform account links them — except the link is not a platform account. Binding a device records what its attestation actually proved, verified against a vendor root the operator pinned, so “hardware-bound” is a fact rather than a claim the same software made about itself.

A device counts as hardware-bound only when it is not backup-eligible and its attestation says the key is in a TEE or secure element. Backup-eligibility is disqualifying rather than informational: a credential that may sync proves control of a cloud account, not possession of a device. Where the chain cannot be verified the grade is degraded (chain_verified: false) instead of the parse failing — an operator who has pinned no roots gets an honest answer, not a silent pass.

Revoking a device unbinds it and ends every session it authorised, in one action. Doing only the first would leave a lost phone’s access live, which is the exact situation the user is trying to fix.

Machines move between identities under one of two authorities that are not interchangeable: controller, for a machine some party delegated, and hardware_root, for a machine nobody did. Holding the hardware cannot take a machine that has an accountable party. The authorisation is TTL-bounded so it cannot be replayed against a machine that has since changed hands.

# What can authenticate as this identity, and what each proved
tenzro device list did:tenzro:alice

# May a wallet be created yet? If not, what is the remedy?
tenzro device wallet-readiness did:tenzro:alice

# Lost phone: unbind it and end its sessions in one action
tenzro device revoke did:tenzro:alice <credential-id>

# Hand a machine to another identity
tenzro device transfer did:tenzro:machine did:tenzro:bob \
  --authority controller --controller-did did:tenzro:alice

Surfaces. RPC tenzro_bindDevice, tenzro_listBoundDevices, tenzro_walletReadiness, tenzro_revokeBoundDevice, tenzro_transferMachineOwnership (the first, fourth and fifth are admin-token gated). CLI tenzro device. SDKs client.device() in both Rust and TypeScript. MCP list_bound_devices and wallet_readiness. A2A device-binding skill. The roots used to verify attestations are webauthn_trusted_rootsin the node config — base64 DER; an entry that does not decode is dropped rather than trusted, so a typo narrows what the node accepts instead of widening it.

13

Hybrid post-quantum

Every passkey-bound smart account on Tenzro carries a hybrid PQ leg in addition to the classical P-256. The WebAuthnValidatorAND-combines the two — both must verify for the user op to validate. The PQ key (ML-DSA-65, 1952-byte vk, 3309-byte sig) lives alongside the passkey in the user’s key store and is enrolled at the same time as the passkey. This aligns Tenzro with the genesis v3 PQ-hybrid posture: classical + ML-DSA on every validator and identity.

Related
← All docs