Skip to content
This repository has been archived by the owner on Nov 19, 2024. It is now read-only.

[FINAL] Management canister API for tSchnorr signatures #288

Merged
merged 10 commits into from
Jul 23, 2024
30 changes: 30 additions & 0 deletions spec/_attachments/ic.did
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@ type ecdsa_curve = variant {
secp256k1;
};

type schnorr_algorithm = variant {
bip340secp256k1;
ed25519;
};

fspreiss marked this conversation as resolved.
Show resolved Hide resolved
type satoshi = nat64;

type bitcoin_network = variant {
Expand Down Expand Up @@ -303,6 +308,27 @@ type sign_with_ecdsa_result = record {
signature : blob;
};

type schnorr_public_key_args = record {
canister_id : opt canister_id;
derivation_path : vec blob;
key_id : record { algorithm : schnorr_algorithm; name : text };
};

type schnorr_public_key_result = record {
public_key : blob;
chain_code : blob;
};

type sign_with_schnorr_args = record {
message : blob;
derivation_path : vec blob;
key_id : record { algorithm : schnorr_algorithm; name : text };
};

type sign_with_schnorr_result = record {
signature : blob;
};

type node_metrics_history_args = record {
subnet_id : principal;
start_at_timestamp_nanos : nat64;
Expand Down Expand Up @@ -377,6 +403,10 @@ service ic : {
ecdsa_public_key : (ecdsa_public_key_args) -> (ecdsa_public_key_result);
sign_with_ecdsa : (sign_with_ecdsa_args) -> (sign_with_ecdsa_result);

// Threshold Schnorr signature
schnorr_public_key : (schnorr_public_key_args) -> (schnorr_public_key_result);
sign_with_schnorr : (sign_with_schnorr_args) -> (sign_with_schnorr_result);

// bitcoin interface
bitcoin_get_balance : (bitcoin_get_balance_args) -> (bitcoin_get_balance_result);
bitcoin_get_balance_query : (bitcoin_get_balance_query_args) -> (bitcoin_get_balance_query_result) query;
Expand Down
1 change: 1 addition & 0 deletions spec/_attachments/interface-spec-changelog.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
## Changelog {#changelog}

### ∞ (unreleased)
* EXPERIMENTAL: Management canister API for threshold Schnorr signatures.

### 0.25.0 (2024-06-14) {#0_25_0}
* Query call statistics.
Expand Down
124 changes: 121 additions & 3 deletions spec/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -2253,11 +2253,13 @@ This method takes no input and returns 32 pseudo-random bytes to the caller. The

This method can only be called by canisters, i.e., it cannot be called by external users via ingress messages.

This method returns a [SEC1](https://www.secg.org/sec1-v2.pdf) encoded ECDSA public key for the given canister using the given derivation path. If the `canister_id` is unspecified, it will default to the canister id of the caller. The `derivation_path` is a vector of variable length byte strings. Each byte string may be of arbitrary length, including empty. The total number of byte strings in the `derivation_path` must be at most 255. The `key_id` is a struct specifying both a curve and a name. The availability of a particular `key_id` depends on implementation.
This method returns a [SEC1](https://www.secg.org/sec1-v2.pdf) encoded ECDSA public key for the given canister using the given derivation path. If the `canister_id` is unspecified, it will default to the canister id of the caller. The `derivation_path` is a vector of variable length byte strings. Each byte string may be of arbitrary length, including empty. The total number of byte strings in the `derivation_path` must be at most 255. The `key_id` is a struct specifying both a curve and a name. The availability of a particular `key_id` depends on the implementation.

For curve `secp256k1`, the public key is derived using a generalization of BIP32 (see [ia.cr/2021/1330, Appendix D](https://ia.cr/2021/1330)). To derive (non-hardened) [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki)-compatible public keys, each byte string (`blob`) in the `derivation_path` must be a 4-byte big-endian encoding of an unsigned integer less than 2<sup>31</sup>. If the `derivation_path` contains a byte string that is not a 4-byte big-endian encoding of an unsigned integer less than 2<sup>31</sup>, then a derived public key will be returned, but that key derivation process will not be compatible with the [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) standard.

The return result is an extended public key consisting of an ECDSA `public_key`, encoded in [SEC1](https://www.secg.org/sec1-v2.pdf) compressed form, and a `chain_code`, which can be used to deterministically derive child keys of the `public_key`.
The return value is an extended public key consisting of an ECDSA `public_key`, encoded in [SEC1](https://www.secg.org/sec1-v2.pdf) compressed form, and a `chain_code`, which can be used to deterministically derive child keys of the `public_key`.

This call requires that an ECDSA key with ID `key_id` was generated by the IC. Otherwise, the call is rejected.

### IC method `sign_with_ecdsa` {#ic-sign_with_ecdsa}

Expand All @@ -2267,7 +2269,123 @@ This method returns a new [ECDSA](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FI

The signatures are encoded as the concatenation of the [SEC1](https://www.secg.org/sec1-v2.pdf) encodings of the two values r and s. For curve `secp256k1`, this corresponds to 32-byte big-endian encoding.

This call requires that the ECDSA feature is enabled, the caller is a canister, and `message_hash` is 32 bytes long. Otherwise it will be rejected.
This call requires that an ECDSA key with ID `key_id` was generated by the IC, the signing functionality for that key was enabled, and `message_hash` is 32 bytes long. Otherwise, the call is is rejected.

Cycles to pay for the call must be explicitly transferred with the call, i.e., they are not automatically deducted from the caller's balance implicitly (e.g., as for inter-canister calls).

### IC method `schnorr_public_key` {#ic-schnorr_public_key}

:::note

Threshold Schnorr API is EXPERIMENTAL and there might be breaking changes of the behavior in the future. Use at your own risk!

:::

This method can only be called by canisters, i.e., it cannot be called by external users via ingress messages.

This method returns a (derived) Schnorr public key for the given canister using the given derivation path. If the `canister_id` is unspecified, it will default to the canister id of the caller. The `derivation_path` is a vector of variable length byte strings. Each byte string may be of arbitrary length, including empty. The total number of byte strings in the `derivation_path` must be at most 255. The `key_id` is a struct specifying both an algorithm and a name. The availability of a particular `key_id` depends on the implementation.

The return value is an extended Schnorr public key consisting of a Schnorr `public_key` and a `chain_code`. The chain code can be used to deterministically derive child keys of the `public_key`. Both the derivation and the encoding of the public key depends on the key ID's `algorithm`:

- For algorithm `bip340secp256k1`, the public key is derived using the generalization of BIP32 defined in [ia.cr/2021/1330, Appendix D](https://ia.cr/2021/1330). To derive (non-hardened) [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki)-compatible public keys, each byte string (`blob`) in the `derivation_path` must be a 4-byte big-endian encoding of an unsigned integer less than 2<sup>31</sup>. If the `derivation_path` contains a byte string that is not a 4-byte big-endian encoding of an unsigned integer less than 2<sup>31</sup>, then a derived public key will be returned, but that key derivation process will not be compatible with the [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) standard.

The public key is encoded in [SEC1](https://www.secg.org/sec1-v2.pdf) compressed form. To use BIP32 public keys to verify BIP340 Schnorr signatures, the first byte of the (33-byte) SEC1-encoded public key must be removed (see [BIP-340, Public Key Conversion](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki#public-key-conversion)).

- For algorithm `ed25519`, the public key is derived using the scheme specified in [Ed25519 hierarchical key derivation](#ed25519-key-derivation).

The public key is encoded in standard 32-byte compressed form (see [RFC8032, 5.1.2 Encoding](https://datatracker.ietf.org/doc/html/rfc8032#section-5.1.2)).

This call requires that a Schnorr key with ID `key_id` was generated by the IC. Otherwise, the call is rejected.

#### Ed25519 hierarchical key derivation {#ed25519-key-derivation}

This section describes a child key derivation (CKD) function for computing child public keys from Ed25519 parent public keys.
The section is inspired by [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) and uses similar wording and structure.

##### Motivation

To support the Ed25519 variant of threshold Schnorr signatures on the Internet Computer, a key derivation scheme compatible with Ed25519 signatures is required.
For a respective signing service on the Internet Computer to be efficient, the signing subnet maintains only a single master key pair and _derives_ signing child keys for each canister.
Although there exist various hierarchical key derivation schemes (e.g., [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki), [SLIP10](https://github.com/satoshilabs/slips/blob/master/slip-0010.md), [BIP32-Ed25519](https://input-output-hk.github.io/adrestia/static/Ed25519_BIP.pdf), [Schnorrkel](https://github.com/w3f/schnorrkel)), all of the analyzed schemes are either incompatible in a threshold setting (e.g., use hardened key derivation only), comply with clamping which adds unnecessary complexity, or otherwise rely on non-standard primitives.
For these reasons, a new derivation scheme is specified here.
This scheme does not make use of _clamping_ (see [RFC8032, Section 5.1.5, Item 2](https://datatracker.ietf.org/doc/html/rfc8032#section-5.1.5)), because it is unnecessary in the given setting, and satisfies the following requirements:

- Off-chain availability: New public keys can be computed off-chain from a master public key without requiring interaction with the IC.
- Hierarchical derivation: Derived keys are organized in a tree such that from any public key it is possible to derive new child keys. The first level is used to derive unique canister-specific keys from the master key.
- Simplicity: The scheme is simple to implement using existing libraries.

##### Conventions

We will assume the elliptic curve (EC) operations using the field and curve parameters as defined by Ed25519 (see [RFC8032, Section 5.1](https://datatracker.ietf.org/doc/html/rfc8032#section-5.1)). Variables below are either:

- Integers modulo the order of the curve's prime order subgroup (referred to as L).
- Points on the curve.
- Byte sequences.

Addition (+) of two points is defined as application of the EC group operation.
Concatenation (||) is the operation of appending one byte sequence onto another.

We assume the following functions:

- point(p): returns the point resulting from EC point multiplication (repeated application of the EC group operation) of the Ed25519 base point with the integer p.
- ser<sub>P</sub>(P): serializes the point to a byte sequence using standard 32-byte compressed form (see [RFC8032, 5.1.2 Encoding](https://datatracker.ietf.org/doc/html/rfc8032#section-5.1.2)).
- utf8(s): returns the UTF-8 encoding of string s.
- parse<sub>512</sub>(p): interprets a 64-byte sequence as a 512-bit number, most significant byte first.
- HKDF(salt,IKM,info,N) -> OKM: HMAC-based key derivation function (see [RFC5869](https://datatracker.ietf.org/doc/html/rfc5869)) using HMAC-SHA512 (see [RFC4231](https://datatracker.ietf.org/doc/html/rfc4231)) calculating N-bytes long output key material (OKM) from (byte sequences) salt, input key material (IKM), and application specific information *info*.

##### Extended keys

Public keys are extended with an extra 32 bytes of entropy, which extension is called chain code.
An extended public key is represented as (K, c), with K = point(k) and c being the chain code, for some private key k.
Each extended key can have an arbitrary number of child keys.
The scheme does not support hardened derivation of child keys.

##### Child key derivation (CKD) function

Given a parent extended public key and an index i, it is possible to compute the corresponding child extended public key.
The function CKDpub computes a child extended public key from a parent extended public key and an index i, where i is a byte sequence of arbitrary length (including empty).

CKDpub((K<sub>par</sub>, c<sub>par</sub>), i) → (K<sub>i</sub>, c<sub>i</sub>):
- let IKM = ser<sub>P</sub>(K<sub>par</sub>) || i.
- let OKM = HKDF(c<sub>par</sub>, IKM, utf8("Ed25519"), 96).
- Split OKM into a 64-byte and a 32-byte sequence, tweak and c<sub>i</sub>.
- let K<sub>i</sub> = K<sub>par</sub> + point(parse<sub>512</sub>(tweak) mod L).
- return (K<sub>i</sub>, c<sub>i</sub>).

##### Key tree

A key tree can be built by repeatedly applying CKDpub, starting with one root, called the master extended public key M.
Computing CKDpub(M, i) for different values of i results in a number of level-0 derived keys.
As each of these is again an extended key, CKDpub can be applied to those as well.
The sequence of indices used when repeatedly applying CKDpub is called the _derivation path_.

The function KTpub computes a child extended public key from a parent extended public key and a derivation path d.

KTpub((K<sub>par</sub>, c<sub>par</sub>), d) → (K<sub>d</sub>, c<sub>d</sub>):
- let (K<sub>d</sub>, c<sub>d</sub>) = (K<sub>par</sub>, c<sub>par</sub>)
- for all indices i in d:
(K<sub>d</sub>, c<sub>d</sub>) = CKDpub((K<sub>d</sub>, c<sub>d</sub>), i)
- return (K<sub>d</sub>, c<sub>d</sub>).

### IC method `sign_with_schnorr` {#ic-sign_with_schnorr}

:::note

Threshold Schnorr API is EXPERIMENTAL and there might be breaking changes of the behavior in the future. Use at your own risk!

:::

This method can only be called by canisters, i.e., it cannot be called by external users via ingress messages.

This method returns a Schnorr signature of the given `message` that can be verified against a (derived) public key obtained by calling `schnorr_public_key` using the caller's `canister_id` and the given `derivation_path` and `key_id`.

The encoding of the signature depends on the key ID's `algorithm`:

- For algorithm `bip340secp256k1`, the signature is encoded in 64 bytes according to [BIP340](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki).

- For algorithm `ed25519`, the signature is encoded in 64 bytes according to [RFC8032, 5.1.6 Sign](https://datatracker.ietf.org/doc/html/rfc8032#section-5.1.6).

This call requires that a Schnorr key with ID `key_id` was generated by the IC and the signing functionality for that key was enabled. Otherwise, the call is is rejected.

Cycles to pay for the call must be explicitly transferred with the call, i.e., they are not automatically deducted from the caller's balance implicitly (e.g., as for inter-canister calls).

Expand Down
Loading