Skip to content
Tenzro
Documentation menu
Keys and identity

Hardware signer SDK

Sign with passkeys, TPM 2.0 and Secure Enclave keys from your own application through tenzro-sdk, with hybrid P-256 and ML-DSA-65 signatures.

tenzro-sdk gives your application the same hardware-rooted signing the console uses. People sign with passkeys; machines and agents sign with a TPM 2.0 or Secure Enclave key. Your application never holds a key: it asks the hardware to sign, sends the result to the network and forgets it.

bash
npm install tenzro-sdk      # TypeScript
cargo add tenzro-sdk        # Rust

The model

The SDK separates three things:

PieceJob
SignerProduces a signature over a 32-byte hash. Implemented by the hardware: a passkey authenticator, a TPM or a Secure Enclave.
ValidatorPacks that signature into the form the account's on-chain ERC-7579 validator module expects.
AccountA smart account whose validator modules decide what counts as a valid signature. See Smart-account policies.

Every signer produces a hybrid signature: the classical leg from the hardware (P-256) and an ML-DSA-65 leg from a post-quantum key derived from the same root on demand. The account checks both legs as one composite signature. See Hardware-rooted keys.

Passkey signers

Create a wallet

ts
import {
  TenzroClient,
  createPasskeyWallet,
  productionConfig,
} from "tenzro-sdk";

const client = new TenzroClient({ endpoint: "https://rpc.tenzro.xyz" });

// The ML-DSA-65 verifying key derived from this passkey for the post-quantum leg.
// It is derived during the ceremony, used in memory and never stored.
const mlDsaPublicKey: Uint8Array = pqVerifyingKey;

const wallet = await createPasskeyWallet(productionConfig("example.com"), {
  mlDsaPublicKey,
});

const account = await client.passkeyRpc.enroll(
  wallet.enrollParams({ displayName: "Alice" }),
);

console.log(account.did);                    // did:tenzro:human:...
console.log(account.smart_account_address);  // 0x...

productionConfig(rpId) requires user verification and a platform authenticator. If the device has no platform authenticator, createPasskeyWallet fails with backend-unavailable instead of falling back to a software key; call wallet.startCrossDeviceLink() to render a QR code and complete the ceremony on a phone.

Sign an operation

ts
import { signWithPasskey } from "tenzro-sdk";

wallet.bindValidatorModule(webauthnValidatorAddress); // from client.passkeyRpc.getSmartAccount(...)

const signature = await signWithPasskey(wallet, {
  opHash,  // 32-byte user operation hash
  rawOp,   // the packed user operation
});
// signature goes into userOp.signature

To have the node verify a hybrid assertion over an operation hash without submitting it, call client.passkeyRpc.sign(...) (tenzro_signWithPasskey). It returns verified, validator and op_hash_hex. If the account's policy is two_credentials, pass the second device's assertion and ML-DSA-65 signature in the second_* fields.

Custody changes

Adding or removing a device, changing the policy, granting a session key, setting a spending limit, adding a hardware signer and adding a guardian all take two steps: get a challenge, then sign it with a device already on the account.

ts
const challenge = await client.passkeyRpc.createCustodyChallenge({
  account_address: account.smart_account_address,
  operation: "remove_passkey",
  target_hex: lostCredentialIdHex,
});

// Sign challenge.challenge_hex with an enrolled passkey (P-256 + ML-DSA-65),
// then send the change with the authorization attached.
await client.passkeyRpc.removePasskey({
  account_address: account.smart_account_address,
  credential_id_hex: lostCredentialIdHex,
  authorization: {
    challenge_id: challenge.challenge_id,
    credential_id_hex: approvingCredentialIdHex,
    assertion,
    ml_dsa_signature_hex: mlDsaSignatureHex,
  },
});

The challenge is bound to the account, the operation and its target, can be used once and expires after a few minutes. The client methods are addPasskey, removePasskey, listPasskeys, setPolicy, getPolicy, addGuardian, initiateRecovery, submitRecoverySignature, finalizeRecovery, listPendingRecoveries, grantSessionKey, revokeSessionKey, setSpendingLimit, addHardwareSigner, getSmartAccount and listSmartAccounts. See Device linking and recovery.

TPM 2.0 and Secure Enclave signers

Machines and agents sign with the device's own hardware through the same Signer interface.

ts
interface Signer {
  describe(): SignerKind;
  sign(hash: Uint8Array, context: SignContext): Promise<SignerSignature>;
}
  • On Linux and Windows the key lives in the TPM 2.0. On Apple hardware it lives in the Secure Enclave, which signs with P-256.
  • The ML-DSA-65 companion key is derived from the hardware root when a signature is needed, used in memory and wiped.
  • SignContext carries a promptReason for a user-presence prompt where the key requires one, a domainTag the signer must match before it signs, and a deadlineMs.
  • Storage backends report their capabilities(). A hardware-backed key reports hardwareBacked: true and exportable: false; there is no export path for it.

The same hardware key gives the machine its DID (did:tenzro:machine:...). A machine that acts for a person is registered under that person's human DID with a delegation scope; see Identity and Agents.

In Rust the trait has the same shape:

rust
use tenzro_sdk::TenzroClient;

let client = TenzroClient::new("https://rpc.tenzro.xyz").await?;
let passkeys = client.passkey_rpc();
let account = passkeys.get_smart_account("0xYourAccount").await?;

Errors

A signer fails with a typed SignerError:

KindMeaning
user-cancelledThe user dismissed the prompt
authentication-failedUser verification failed or the key was rejected
timeoutThe deadline passed
domain-tag-mismatchThe request's domain tag did not match the signer's
backend-unavailableNo suitable hardware or authenticator is present
transportThe request could not reach the authenticator or node