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.
npm install tenzro-sdk # TypeScript
cargo add tenzro-sdk # RustThe model
The SDK separates three things:
| Piece | Job |
|---|---|
Signer | Produces a signature over a 32-byte hash. Implemented by the hardware: a passkey authenticator, a TPM or a Secure Enclave. |
Validator | Packs that signature into the form the account's on-chain ERC-7579 validator module expects. |
| Account | A 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
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
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.signatureTo 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.
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.
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.
SignContextcarries apromptReasonfor a user-presence prompt where the key requires one, adomainTagthe signer must match before it signs, and adeadlineMs.- Storage backends report their
capabilities(). A hardware-backed key reportshardwareBacked: trueandexportable: 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:
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:
| Kind | Meaning |
|---|---|
user-cancelled | The user dismissed the prompt |
authentication-failed | User verification failed or the key was rejected |
timeout | The deadline passed |
domain-tag-mismatch | The request's domain tag did not match the signer's |
backend-unavailable | No suitable hardware or authenticator is present |
transport | The request could not reach the authenticator or node |