Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .github/workflows/threshold-ecdsa.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: threshold-ecdsa

on:
push:
branches: [master]
pull_request:
paths:
- motoko/threshold-ecdsa/**
- .github/workflows/threshold-ecdsa.yml

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
motoko-threshold-ecdsa:
runs-on: ubuntu-24.04
container: ghcr.io/dfinity/icp-dev-env-motoko:0.3.2
env:
ICP_CLI_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Deploy and test
working-directory: motoko/threshold-ecdsa
run: |
icp network start -d
icp deploy
make test
20 changes: 0 additions & 20 deletions motoko/threshold-ecdsa/.devcontainer/devcontainer.json

This file was deleted.

113 changes: 0 additions & 113 deletions motoko/threshold-ecdsa/BUILD.md

This file was deleted.

18 changes: 18 additions & 0 deletions motoko/threshold-ecdsa/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
.PHONY: test

test:
@echo "=== Test 1/3: public_key() returns a hex-encoded public key ==="
@result=$$(icp canister call backend public_key '()') && \
echo "$$result" && \
echo "$$result" | grep -q 'public_key_hex' && \
echo "PASS" || (echo "FAIL" && exit 1)

@echo "=== Test 2/3: sign() returns a hex-encoded signature ==="
@result=$$(icp canister call backend sign '("hello world")') && \
echo "$$result" && \
echo "$$result" | grep -q 'signature_hex' && \
echo "PASS" || (echo "FAIL" && exit 1)

@echo "=== Test 3/3: signature verifies cryptographically (secp256k1) ==="
@npm install --silent
@chmod +x test.sh && ./test.sh "hello world" && echo "PASS" || (echo "FAIL" && exit 1)
115 changes: 36 additions & 79 deletions motoko/threshold-ecdsa/README.md
Original file line number Diff line number Diff line change
@@ -1,108 +1,65 @@
# Threshold ECDSA sample
# Threshold ECDSA

We present a minimal example canister smart contract for showcasing the [threshold ECDSA](https://internetcomputer.org/docs/building-apps/network-features/signatures/t-ecdsa) API.
This example demonstrates the [threshold ECDSA](https://internetcomputer.org/docs/building-apps/network-features/signatures/t-ecdsa) API on the Internet Computer. 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.

The example canister is a signing oracle that creates ECDSA signatures with keys derived from an input string.
## Build and deploy from the command line

More specifically:
### Prerequisites
- Node.js
- icp-cli: `npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm`

- The sample canister receives a request that provides a message.
- The sample canister hashes the message and uses the key derivation string for the derivation path.
- The sample canister uses the above to request a signature from the threshold ECDSA [subnet](https://wiki.internetcomputer.org/wiki/Subnet_blockchain) (the threshold ECDSA is a subnet specializing in generating threshold ECDSA signatures).

This tutorial gives a complete overview of the development, starting with downloading the [IC SDK](https://internetcomputer.org/docs/current/developer-docs/getting-started/install), up to the deployment and trying out the code on the IC mainnet.

## Deploying from ICP Ninja

[![](https://icp.ninja/assets/open.svg)](https://icp.ninja/editor?g=https://github.com/dfinity/examples/tree/master/motoko/threshold-ecdsa)

### 1. Update source code with the right key ID

To deploy the sample code from ICP Ninja, the canister needs the right key ID for the right environment. Specifically, one needs to replace the value of the `key_id` in the `src/ecdsa_example_motoko/main.mo` file of the sample code. Before deploying to the mainnet from ICP Ninja, one should modify the code to use the right name of the `key_id`.

There are three options:

* `dfx_test_key`: a default key ID that is used in deploying to a local version of IC (via IC SDK).
* `test_key_1`: a master **test** key ID that is used in mainnet.
* `key_1`: a master **production** key ID that is used in mainnet.

For example, the default code in `src/ecdsa_example_motoko/main.mo` includes the following lines and can be deployed locally:

:::caution
The following example is two **code snippets** that are part of a larger code file. These snippets may return an error if run on their own.
:::

```motoko
let { public_key } = await ic.ecdsa_public_key({
canister_id = null;
derivation_path = [ caller ];
key_id = { curve = #secp256k1; name = "dfx_test_key" };
});
### Install
```bash
git clone https://github.com/dfinity/examples
cd examples/motoko/threshold-ecdsa
```

```motoko
let { signature } = await ic.sign_with_ecdsa({
message_hash;
derivation_path = [ caller ];
key_id = { curve = #secp256k1; name = "dfx_test_key" };
});
### Deploy and test
```bash
icp network start -d
icp deploy
make test
icp network stop
```

> [!WARNING]
> To deploy to IC mainnet, one needs to replace the value in `key_id` fields with the values `"dfx_test_key"` to instead have either `"test_key_1"` or `"key_1"` depending on the desired intent.


## Build and deploy from the command-line
## Key IDs

### 1. [Download and install the IC SDK.](https://internetcomputer.org/docs/building-apps/getting-started/install)
The canister is configured with `key_id = "test_key_1"` by default (the master test key on mainnet). To use a different environment, update `key_id` in `backend/app.mo`:

### 2. Download your project from ICP Ninja using the 'Download files' button on the upper left corner, or [clone the GitHub examples repository.](https://github.com/dfinity/examples/)

### 3. Navigate into the project's directory.

### 4. Deploy the project to your local environment:

```
dfx start --background --clean && dfx deploy
```
- `"test_key_1"` — mainnet test key
- `"key_1"` — mainnet production key

## Obtaining public keys

If you deployed your canister locally or to the mainnet, you should have a URL to the Candid web UI where you can access the public methods. We can call the `public-key` method.

### Canister root public key

For obtaining the canister's root public key, the derivation path in the API can be simply left empty.

### Key derivation

- For obtaining a canister's public key below its root key in the BIP-32 key derivation hierarchy, a derivation path needs to be specified. As explained in the general documentation, each element in the array of the derivation path is either a 32-bit integer encoded as 4 bytes in big endian or a byte array of arbitrary length. The element is used to derive the key in the corresponding level at the derivation hierarchy.
- In the example code above, we use the bytes extracted from the `msg.caller` principal in the `derivation_path`, so that different callers of `public_key()` method of our canister will be able to get their own public keys.
Call `public_key()` to retrieve the ECDSA public key derived for the calling principal. Different callers receive different keys based on their principal used as the derivation path.

## Signing

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.
Call `sign(message)` with any UTF-8 text message. The canister hashes the message with SHA-256 and requests a threshold ECDSA signature from the management canister. The signature can be verified with the public key returned by `public_key()`.

## Signature verification

For completeness of the example, we show that the created signatures can be verified with the public key corresponding to the same canister and derivation path.

The following shows how this verification can be done in Javascript, with the [secp256k1](https://www.npmjs.com/package/secp256k1) npm package:
Example verification in JavaScript using the [secp256k1](https://www.npmjs.com/package/secp256k1) npm package:

```javascript
let { ecdsaVerify } = require("secp256k1")
const { ecdsaVerify } = require("secp256k1");
const crypto = require("crypto");

let public_key = ... // Uint8Array type, the result of calling the above canister "public_key" function.
let hash = ... // 32-byte Uint8Array representing a binary hash (e.g. sha256).
let signature = ... // Uint8Array type, the result of calling the above canister "sign" function on `hash`.
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) */;

let verified = ecdsaVerify(signature, hash, public_key)
const verified = ecdsaVerify(signature, message_hash, public_key);
console.log("verified =", verified); // true
```

The call to `ecdsaVerify` function should always return `true`.
## Updating the Candid interface

Similar verifications can be done in many other languages with the help of cryptographic libraries that support the `secp256k1` curve.
```bash
$(mops toolchain bin moc) --idl -o backend/backend.did backend/app.mo
```

## Security considerations and best practices

If you base your application on this example, it is recommended that you familiarize yourself with and adhere to the [security best practices](https://internetcomputer.org/docs/building-apps/security/overview) for developing on ICP. This example may not implement all the best practices.
Refer to the [security best practices](https://docs.internetcomputer.org/guides/security/overview) for information on security and best practices for your ICP app.
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,12 @@ import Error "mo:core/Error";
import Principal "mo:core/Principal";
import Text "mo:core/Text";
import Blob "mo:core/Blob";
import Hex "./utils/Hex";
import SHA256 "./utils/SHA256";
import Hex "./Hex";
import SHA256 "./SHA256";
import { ic } "mo:ic";

persistent actor {
type IC = actor {
ecdsa_public_key : ({
canister_id : ?Principal;
derivation_path : [Blob];
key_id : { curve : { #secp256k1 }; name : Text };
}) -> async ({ public_key : Blob; chain_code : Blob });
sign_with_ecdsa : ({
message_hash : Blob;
derivation_path : [Blob];
key_id : { curve : { #secp256k1 }; name : Text };
}) -> async ({ signature : Blob });
};

transient let key_id : Text = "test_key_1"; // Use "key_1" for production and "dfx_test_key" locally
transient let ic : IC = actor ("aaaaa-aa");
persistent actor ThresholdEcdsa {
transient let key_id : Text = "test_key_1"; // Use "key_1" for mainnet production

public shared (msg) func public_key() : async {
#Ok : { public_key_hex : Text };
Expand Down
13 changes: 0 additions & 13 deletions motoko/threshold-ecdsa/dfx.json

This file was deleted.

4 changes: 4 additions & 0 deletions motoko/threshold-ecdsa/icp.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
canisters:
- name: backend
recipe:
type: "@dfinity/motoko@v5.0.0"
10 changes: 7 additions & 3 deletions motoko/threshold-ecdsa/mops.toml
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
[toolchain]
moc = "1.5.1"
moc = "1.9.0"

[dependencies]
core = "2.4.0"
core = "2.5.0"
ic = "4.0.0"

[moc]
# M0236: use context dot notation
# M0237: redundant explicit implicit arguments
# M0223: redundant type instantiation
args = ["-W=M0236,M0237,M0223"]
args = ["--default-persistent-actors", "-W=M0236,M0237,M0223"]

[canisters.backend]
main = "backend/app.mo"
Loading
Loading