🥷 Try it live — no local setup. ICP Ninja is a web-based IDE that builds and deploys this project to the mainnet for free, right in your browser. Click the badge above, or hit Deploy if you're already in Ninja. To build and run it locally instead, follow the steps below.
This example demonstrates threshold ECDSA signing, part of ICP's chain-key cryptography. The canister acts as a signing oracle: callers can request a threshold ECDSA public key derived from their principal, and sign arbitrary messages using the corresponding private key — without the canister ever holding the key material itself.
- Node.js v18+
- icp-cli:
npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm - Rust with
wasm32-unknown-unknowntarget:rustup target add wasm32-unknown-unknown
git clone https://github.com/dfinity/examples
cd examples/rust/threshold-ecdsaicp network start -d
icp deploy
bash test.sh
icp network stopThe canister is configured with KEY_NAME = "test_key_1" by default (the master test key, works on both the local network and mainnet). To use the production key, update KEY_NAME in backend/lib.rs:
"test_key_1"— default, mainnet test key"key_1"— mainnet production key
Call public_key() to retrieve the ECDSA public key derived for the calling principal. The canister uses the caller's principal bytes as the derivation path, so different callers receive different keys.
To obtain a key below the root in the BIP-32 hierarchy, a derivation path must be specified. Each element in the derivation path array is either a 32-bit integer encoded as 4 bytes in big-endian, or a byte array of arbitrary length.
Computing threshold ECDSA signatures is the core functionality of this feature. Canisters do not hold ECDSA keys themselves, but keys are derived from a master key held by dedicated subnets. A canister can request the computation of a signature through the management canister API. The request is then routed to a subnet holding the specified key and the subnet computes the requested signature using threshold cryptography. Thereby, it derives the canister root key or a key obtained through further derivation, as part of the signature protocol, from a shared secret and the requesting canister's principal identifier. Thus, a canister can only request signatures to be created for its canister root key or a key derived from it. This means that canisters "control" their private ECDSA keys in that they decide when signatures are to be created with them, but don't hold a private key themselves.
The created signatures can be verified with the public key corresponding to the same canister and derivation path. Example verification in JavaScript using the secp256k1 npm package:
const { ecdsaVerify } = require("secp256k1");
const crypto = require("crypto");
const public_key = /* Uint8Array from public_key() */;
const message = "hello world";
const message_hash = new Uint8Array(crypto.createHash("sha256").update(message, "utf-8").digest());
const signature = /* Uint8Array from sign(message) */;
const verified = ecdsaVerify(signature, message_hash, public_key);
console.log("verified =", verified); // trueSimilar verifications can be done in many other languages with the help of cryptographic libraries that support the secp256k1 curve.
Refer to the security best practices for information on security and best practices for your ICP app.