Hardware custody
A seed in a file is a seed that can be copied. The point of hardware custody is that the private key is generated inside a secure element and never leaves it — you can ask the hardware to sign, but you cannot ask it for the key.
Frost supports this without any special protocol accommodation, because its verifier precompiles implement standard FIPS 204 ML-DSA. Any conformant signer interoperates. There is no Frost-specific signature format to implement.
Why ML-DSA-65 specifically
Section titled “Why ML-DSA-65 specifically”Apple’s CryptoKit, on iOS and macOS, supports ML-DSA-65 and ML-DSA-87 — and not ML-DSA-44.
That single fact determined a design decision: the rotatable v2 account uses
ML-DSA-65 and the 0x14 precompile,
even though ML-DSA-44 would be cheaper. Choosing the cheaper parameter set would
have made hardware custody impossible on the most widely deployed secure element
in the world.
If you use the ML-DSA-44 account type, you are choosing lower gas and giving up Secure Enclave custody.
Apple Secure Enclave
Section titled “Apple Secure Enclave”The flow is the same as software signing, with one substitution: instead of deriving a public key from a seed you hold, you ask the hardware for its public key, and instead of signing locally you ask the hardware to sign.
1. Generate an ML-DSA-65 key inside the Secure Enclave (CryptoKit).2. Export the PUBLIC key — 1,952 bytes. This is all you ever get out.3. Derive the counterfactual address from that public key.4. Build the transaction, compute the sig-hash.5. Ask the enclave to sign the 32-byte sig-hash.6. Attach the signature and submit.Both SDKs accept a raw public key rather than a seed, precisely so that hardware signers work:
// viem-8141: supply the public key, not a seedconst account = counterfactualAddress({ activePublicKey: enclavePublicKey, // 1,952 bytes from the hardware backupPublicKeyHash: backup.publicKeyKeccak,});and expose a pluggable signer interface (WitnessSigner in viem-8141) so an
out-of-process or on-device signer can produce the witness. The CLI takes
-activepk for the same reason — a Secure Enclave never reveals more than the
public key.
CryptoKit’s signatures, including hedged signatures and signatures produced
inside a real Secure Enclave, verify bit-for-bit against the 0x14 precompile.
This is not an assumption: golden vectors captured from a physical device are
part of the execution layer’s test suite.
PQ-capable hardware security modules implementing FIPS 204 work the same way, and typically support all parameter sets. The requirements are:
- Pure ML-DSA, not the pre-hash (HashML-DSA) variant.
- Empty context string.
- Standard FIPS 204 encodings for the public key and signature.
If your HSM offers a context parameter, leave it empty. If it offers only pre-hash mode, its signatures will not verify.
Backup keys and hardware
Section titled “Backup keys and hardware”Hardware custody changes what “lost key” means. A Secure Enclave key dies with the device — you cannot restore it from a backup, because it was never exportable. That is the security property working as intended, and it makes the backup key essential rather than optional.
A sensible arrangement:
- Active key: in the Secure Enclave on your phone. Signs daily. Not exportable, not backed up, and expected to be lost eventually.
- Backup key: generated and stored offline, in a different medium and a different place. Used only to rotate when the device is gone.
When you replace the device, rotate: generate a new enclave key on the new phone, and use the backup key to install it as the new active key. The account address never changes.
Practise this before you rely on it.
Verifying interoperability yourself
Section titled “Verifying interoperability yourself”Sign a known 32-byte message with your hardware, then check the signature against the precompile directly:
# input = msg(32) || signature(3309) || publicKey(1952) = 5293 bytescurl -s https://rpc.frostfi.net -H 'content-type: application/json' \ --data '{"jsonrpc":"2.0","id":1,"method":"eth_call","params":[{ "to":"0x0000000000000000000000000000000000000014", "data":"0x<msg||sig||pk>"},"latest"]}'# → 0x00…01 valid# → 0x00…00 invalidA 0x…01 result means your hardware is compatible. If you get 0x…00, check
that you used pure ML-DSA with an empty context and standard encodings, and that
the total input is exactly 5,293 bytes.
- VERIFY_MLDSA65 — the exact input layout and semantics.
- Rotate a key — the flow that makes device loss survivable.