From f96215070d890afe86724002597e7e186cf7b530 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Tue, 8 Oct 2024 17:39:57 +0200 Subject: [PATCH 01/45] Added parsing for musig(); generalized key placeholders in wallet policies to more general key expressions --- src/common/wallet.c | 174 +++++++++++++++++++----- src/common/wallet.h | 47 +++++-- src/handler/lib/policy.c | 103 +++++++------- src/handler/lib/policy.h | 2 +- src/handler/sign_psbt.c | 2 +- src/handler/sign_psbt/sign_psbt_cache.c | 2 +- src/handler/sign_psbt/sign_psbt_cache.h | 2 +- unit-tests/test_wallet.c | 148 ++++++++++++++------ 8 files changed, 334 insertions(+), 146 deletions(-) diff --git a/src/common/wallet.c b/src/common/wallet.c index 6b956ab13..dccb20251 100644 --- a/src/common/wallet.c +++ b/src/common/wallet.c @@ -424,23 +424,120 @@ int parse_policy_map_key_info(buffer_t *buffer, policy_map_key_info_t *out, int return 0; } -// parses a placeholder from in_buf, storing it in out. On success, the pointed placeholder_index is -// stored in out->placeholder_index, and then it's incremented. -static int parse_placeholder(buffer_t *in_buf, - int version, - policy_node_key_placeholder_t *out, - uint16_t *placeholder_index) { +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wcomment" +// The compiler doesn't like /** inside a block comment, so we disable this warning temporarily. + +/** + * Parses a key expression, in one of the following forms: + * - Single key index: + * - @IDX/** + * - @IDX//* + * - MuSig2 aggregate key (only if is_taproot is true): + * - musig(@IDX,@IDX,...,@IDX)/** + * - musig(@IDX,@IDX,...,@IDX)//* + * where IDX is a key index. + */ +#pragma GCC diagnostic pop +static int parse_keyexpr(buffer_t *in_buf, + int version, + policy_node_keyexpr_t *out, + bool is_taproot, + buffer_t *out_buf, + uint16_t *placeholder_index) { char c; - if (!buffer_read_u8(in_buf, (uint8_t *) &c) || c != '@') { - return WITH_ERROR(-1, "Expected key placeholder starting with '@'"); + if (!buffer_read_u8(in_buf, (uint8_t *) &c)) { + return WITH_ERROR(-1, "Expected key placeholder"); } - uint32_t k; - if (parse_unsigned_decimal(in_buf, &k) == -1 || k > INT16_MAX) { - return WITH_ERROR(-1, "The key index in a placeholder must be at most 32767"); - } + if (c == '@') { + out->type = KEY_EXPRESSION_NORMAL; + + uint32_t k; + if (parse_unsigned_decimal(in_buf, &k) == -1 || k > INT16_MAX) { + return WITH_ERROR(-1, "The key index in a placeholder must be at most 32767"); + } + + out->key_index = (int16_t) k; + } else if (c == 'm') { + // parse a musig(key1,...,keyn) expression, where each key is a key expression + if (!consume_characters(in_buf, "usig(", 5)) { + return WITH_ERROR(-1, "Expected musig key expression"); + } + + if (!is_taproot) { + return WITH_ERROR(-1, "musig is only allows in taproot"); + } + + out->type = KEY_EXPRESSION_MUSIG; + + if (version != WALLET_POLICY_VERSION_V2) { + return WITH_ERROR(-1, "musig key expressions are only supported with version number 2"); + } + + uint16_t key_placeholders[MAX_PUBKEYS_PER_MUSIG]; + int n_musig_keys = 0; + + // parse comma-separated list of @NUM + while (true) { + if (!buffer_read_u8(in_buf, (uint8_t *) &c) || c != '@') { + return WITH_ERROR(-1, "Expected key placeholder starting with '@'"); + } + + uint32_t k; + if (parse_unsigned_decimal(in_buf, &k) == -1 || k > INT16_MAX) { + return WITH_ERROR(-1, "The key index in a placeholder must be at most 32767"); + } - out->key_index = (int16_t) k; + if (n_musig_keys >= MAX_PUBKEYS_PER_MUSIG) { + return WITH_ERROR(-1, "Too many keys in musig"); + } + + key_placeholders[n_musig_keys] = (uint16_t) k; + ++n_musig_keys; + + // the next character must be "," if there are more keys, or ')' otherwise + if (!buffer_read_u8(in_buf, (uint8_t *) &c)) { + return WITH_ERROR(-1, "Expression terminated prematurely"); + } + + if (c == ')') { + break; + } else if (c != ',') { + return WITH_ERROR(-1, "Invalid character in musig; expected ',' or ')'"); + } + } + + if (n_musig_keys < 2) { + return WITH_ERROR(-1, "musig must have at least 2 key indexes"); + } + if (n_musig_keys > MAX_PUBKEYS_PER_MUSIG) { + return WITH_ERROR(-1, "Too many keys in musig"); + } + + // allocate musig structures + + musig_aggr_key_info_t *musig_info = + (musig_aggr_key_info_t *) buffer_alloc(out_buf, sizeof(musig_info), true); + + if (musig_info == NULL) { + return WITH_ERROR(-1, "Out of memory"); + } + + uint16_t *key_indexes = + (uint16_t *) buffer_alloc(out_buf, sizeof(uint16_t) * n_musig_keys, true); + if (key_indexes == NULL) { + return WITH_ERROR(-1, "Out of memory"); + } + memcpy(key_indexes, key_placeholders, sizeof(uint16_t) * n_musig_keys); + + musig_info->n = n_musig_keys; + i_uint16(&musig_info->key_indexes, key_indexes); + + i_musig_aggr_key_info(&out->musig_info, musig_info); + } else { + return WITH_ERROR(-1, "Expected key placeholder starting with '@', or musig"); + } if (version == WALLET_POLICY_VERSION_V1) { // default values for compatibility with the new code @@ -1394,13 +1491,13 @@ static int parse_script(buffer_t *in_buf, return WITH_ERROR(-1, "Out of memory"); } - policy_node_key_placeholder_t *key_placeholder = - buffer_alloc(out_buf, sizeof(policy_node_key_placeholder_t), true); + policy_node_keyexpr_t *key_placeholder = + buffer_alloc(out_buf, sizeof(policy_node_keyexpr_t), true); if (key_placeholder == NULL) { return WITH_ERROR(-1, "Out of memory"); } - i_policy_node_key_placeholder(&node->key_placeholder, key_placeholder); + i_policy_node_keyexpr(&node->key_placeholder, key_placeholder); if (token == TOKEN_WPKH) { if (depth > 0 && ((context_flags & CONTEXT_WITHIN_SH) == 0)) { @@ -1412,7 +1509,13 @@ static int parse_script(buffer_t *in_buf, node->base.type = token; - if (0 > parse_placeholder(in_buf, version, key_placeholder, &key_placeholder_count)) { + bool is_taproot = (context_flags & CONTEXT_WITHIN_TR) != 0; + if (0 > parse_keyexpr(in_buf, + version, + key_placeholder, + is_taproot, + out_buf, + &key_placeholder_count)) { return WITH_ERROR(-1, "Couldn't parse key placeholder"); } @@ -1475,14 +1578,19 @@ static int parse_script(buffer_t *in_buf, return WITH_ERROR(-1, "Out of memory"); } - policy_node_key_placeholder_t *key_placeholder = - buffer_alloc(out_buf, sizeof(policy_node_key_placeholder_t), true); + policy_node_keyexpr_t *key_placeholder = + buffer_alloc(out_buf, sizeof(policy_node_keyexpr_t), true); if (key_placeholder == NULL) { return WITH_ERROR(-1, "Out of memory"); } - i_policy_node_key_placeholder(&node->key_placeholder, key_placeholder); + i_policy_node_keyexpr(&node->key_placeholder, key_placeholder); - if (0 > parse_placeholder(in_buf, version, key_placeholder, &key_placeholder_count)) { + if (0 > parse_keyexpr(in_buf, + version, + key_placeholder, + true, + out_buf, + &key_placeholder_count)) { return WITH_ERROR(-1, "Couldn't parse key placeholder"); } @@ -1559,7 +1667,8 @@ static int parse_script(buffer_t *in_buf, return WITH_ERROR(-1, "Out of memory"); } - if ((context_flags & CONTEXT_WITHIN_TR) != 0) { + bool is_taproot = (context_flags & CONTEXT_WITHIN_TR) != 0; + if (is_taproot) { if (token != TOKEN_MULTI_A && token != TOKEN_SORTEDMULTI_A) { return WITH_ERROR( -1, @@ -1597,7 +1706,7 @@ static int parse_script(buffer_t *in_buf, // We allocate the array of key indices at the current position in the output buffer // (on success) buffer_alloc(out_buf, 0, true); // ensure alignment of current pointer - i_policy_node_key_placeholder(&node->key_placeholders, buffer_get_cur(out_buf)); + i_policy_node_keyexpr(&node->key_placeholders, buffer_get_cur(out_buf)); node->n = 0; while (true) { @@ -1612,18 +1721,21 @@ static int parse_script(buffer_t *in_buf, return WITH_ERROR(-1, "Expected ','"); } - policy_node_key_placeholder_t *key_placeholder = - (policy_node_key_placeholder_t *) buffer_alloc( - out_buf, - sizeof(policy_node_key_placeholder_t), - true); // we align this pointer, as there's padding in an array of - // structures + policy_node_keyexpr_t *key_placeholder = (policy_node_keyexpr_t *) buffer_alloc( + out_buf, + sizeof(policy_node_keyexpr_t), + true); // we align this pointer, as there's padding in an array of + // structures if (key_placeholder == NULL) { return WITH_ERROR(-1, "Out of memory"); } - if (0 > - parse_placeholder(in_buf, version, key_placeholder, &key_placeholder_count)) { + if (0 > parse_keyexpr(in_buf, + version, + key_placeholder, + is_taproot, + out_buf, + &key_placeholder_count)) { return WITH_ERROR(-1, "Error parsing key placeholder"); } diff --git a/src/common/wallet.h b/src/common/wallet.h index 219111ad1..eff17d2a9 100644 --- a/src/common/wallet.h +++ b/src/common/wallet.h @@ -19,6 +19,10 @@ // bitcoin-core supports up to 20, but we limit to 16 as bigger pushes require special handling. #define MAX_PUBKEYS_PER_MULTISIG 16 +// The maximum number of keys supported in a musig() key expression +// It is basically unlimited in theory, but we need to set a practical limit. +#define MAX_PUBKEYS_PER_MUSIG MAX_PUBKEYS_PER_MULTISIG + #define WALLET_POLICY_VERSION_V1 1 // the legacy version of the first release #define WALLET_POLICY_VERSION_V2 2 // the current full version @@ -292,19 +296,39 @@ typedef struct policy_node_ext_info_s { * array don't have extra derivation steps. */ #pragma GCC diagnostic pop -// 12 bytes + +DEFINE_REL_PTR(uint16, uint16_t) + +typedef struct { + int16_t n; // number of key indexes + rptr_uint16_t key_indexes; // pointer to an array of exactly n key indexes +} musig_aggr_key_info_t; + +DEFINE_REL_PTR(musig_aggr_key_info, musig_aggr_key_info_t) + +typedef enum { + KEY_EXPRESSION_NORMAL = 0, // a key expression with a single key placeholder + KEY_EXPRESSION_MUSIG = 1 // a key expression containing a musig() +} KeyExpressionType; + +// 16 bytes typedef struct { // the following fields are only used in V2 uint32_t num_first; // NUM_a of //* uint32_t num_second; // NUM_b of //* - // common between V1 and V2 - int16_t key_index; // index of the key + KeyExpressionType type; + union { + // type == 0 + int16_t key_index; // index of the key (common between V1 and V2) + // type == 1 + rptr_musig_aggr_key_info_t musig_info; // only used in V2 + }; int16_t placeholder_index; // index of the placeholder in the descriptor template, in parsing order -} policy_node_key_placeholder_t; +} policy_node_keyexpr_t; -DEFINE_REL_PTR(policy_node_key_placeholder, policy_node_key_placeholder_t) +DEFINE_REL_PTR(policy_node_keyexpr, policy_node_keyexpr_t) // 4 bytes typedef struct { @@ -335,7 +359,7 @@ typedef policy_node_with_script3_t policy_node_with_scripts_t; // 4 bytes typedef struct { struct policy_node_s base; - rptr_policy_node_key_placeholder_t key_placeholder; + rptr_policy_node_keyexpr_t key_placeholder; } policy_node_with_key_t; // 8 bytes @@ -346,11 +370,10 @@ typedef struct { // 12 bytes typedef struct { - struct policy_node_s base; // type is TOKEN_MULTI or TOKEN_SORTEDMULTI - int16_t k; // threshold - int16_t n; // number of keys - rptr_policy_node_key_placeholder_t - key_placeholders; // pointer to array of exactly n key placeholders + struct policy_node_s base; // type is TOKEN_MULTI or TOKEN_SORTEDMULTI + int16_t k; // threshold + int16_t n; // number of keys + rptr_policy_node_keyexpr_t key_placeholders; // pointer to array of exactly n key placeholders } policy_node_multisig_t; // 8 bytes @@ -400,7 +423,7 @@ typedef struct policy_node_tree_s { typedef struct { struct policy_node_s base; - rptr_policy_node_key_placeholder_t key_placeholder; + rptr_policy_node_keyexpr_t key_placeholder; rptr_policy_node_tree_t tree; // NULL if tr(KP) } policy_node_tr_t; diff --git a/src/handler/lib/policy.c b/src/handler/lib/policy.c index 63c8e61a7..ae289fadc 100644 --- a/src/handler/lib/policy.c +++ b/src/handler/lib/policy.c @@ -456,7 +456,7 @@ __attribute__((noinline, warn_unused_result)) static int get_extended_pubkey( __attribute__((warn_unused_result)) static int get_derived_pubkey( dispatcher_context_t *dispatcher_context, const wallet_derivation_info_t *wdi, - const policy_node_key_placeholder_t *key_placeholder, + const policy_node_keyexpr_t *key_placeholder, uint8_t out[static 33]) { PRINT_STACK_POINTER(); @@ -575,11 +575,10 @@ __attribute__((warn_unused_result)) static int process_generic_node(policy_parse const policy_node_with_key_t *policy = (const policy_node_with_key_t *) node->policy_node; uint8_t compressed_pubkey[33]; - if (-1 == - get_derived_pubkey(state->dispatcher_context, - state->wdi, - r_policy_node_key_placeholder(&policy->key_placeholder), - compressed_pubkey)) { + if (-1 == get_derived_pubkey(state->dispatcher_context, + state->wdi, + r_policy_node_keyexpr(&policy->key_placeholder), + compressed_pubkey)) { return -1; } @@ -597,11 +596,10 @@ __attribute__((warn_unused_result)) static int process_generic_node(policy_parse const policy_node_with_key_t *policy = (const policy_node_with_key_t *) node->policy_node; uint8_t compressed_pubkey[33]; - if (-1 == - get_derived_pubkey(state->dispatcher_context, - state->wdi, - r_policy_node_key_placeholder(&policy->key_placeholder), - compressed_pubkey)) { + if (-1 == get_derived_pubkey(state->dispatcher_context, + state->wdi, + r_policy_node_keyexpr(&policy->key_placeholder), + compressed_pubkey)) { return -1; } if (!state->is_taproot) { @@ -690,7 +688,7 @@ __attribute__((warn_unused_result)) static int process_pkh_wpkh_node(policy_pars if (-1 == get_derived_pubkey(state->dispatcher_context, state->wdi, - r_policy_node_key_placeholder(&policy->key_placeholder), + r_policy_node_keyexpr(&policy->key_placeholder), compressed_pubkey)) { return -1; } else if (policy->base.type == TOKEN_PKH) { @@ -817,11 +815,10 @@ __attribute__((warn_unused_result)) static int process_multi_sortedmulti_node( uint8_t compressed_pubkey[33]; if (policy->base.type == TOKEN_MULTI) { - if (-1 == - get_derived_pubkey(state->dispatcher_context, - state->wdi, - &r_policy_node_key_placeholder(&policy->key_placeholders)[i], - compressed_pubkey)) { + if (-1 == get_derived_pubkey(state->dispatcher_context, + state->wdi, + &r_policy_node_keyexpr(&policy->key_placeholders)[i], + compressed_pubkey)) { return -1; } } else { @@ -843,11 +840,11 @@ __attribute__((warn_unused_result)) static int process_multi_sortedmulti_node( for (int j = 0; j < policy->n; j++) { if (!bitvector_get(used, j)) { uint8_t cur_pubkey[33]; - if (-1 == get_derived_pubkey( - state->dispatcher_context, - state->wdi, - &r_policy_node_key_placeholder(&policy->key_placeholders)[j], - cur_pubkey)) { + if (-1 == + get_derived_pubkey(state->dispatcher_context, + state->wdi, + &r_policy_node_keyexpr(&policy->key_placeholders)[j], + cur_pubkey)) { return -1; } @@ -895,11 +892,10 @@ __attribute__((warn_unused_result)) static int process_multi_a_sortedmulti_a_nod uint8_t compressed_pubkey[33]; if (policy->base.type == TOKEN_MULTI_A) { - if (-1 == - get_derived_pubkey(state->dispatcher_context, - state->wdi, - &r_policy_node_key_placeholder(&policy->key_placeholders)[i], - compressed_pubkey)) { + if (-1 == get_derived_pubkey(state->dispatcher_context, + state->wdi, + &r_policy_node_keyexpr(&policy->key_placeholders)[i], + compressed_pubkey)) { return -1; } } else { @@ -911,11 +907,11 @@ __attribute__((warn_unused_result)) static int process_multi_a_sortedmulti_a_nod for (int j = 0; j < policy->n; j++) { if (!bitvector_get(used, j)) { uint8_t cur_pubkey[33]; - if (-1 == get_derived_pubkey( - state->dispatcher_context, - state->wdi, - &r_policy_node_key_placeholder(&policy->key_placeholders)[j], - cur_pubkey)) { + if (-1 == + get_derived_pubkey(state->dispatcher_context, + state->wdi, + &r_policy_node_keyexpr(&policy->key_placeholders)[j], + cur_pubkey)) { return -1; } @@ -1024,7 +1020,7 @@ int get_wallet_script(dispatcher_context_t *dispatcher_context, policy_node_with_key_t *pkh_policy = (policy_node_with_key_t *) policy; if (0 > get_derived_pubkey(dispatcher_context, wdi, - r_policy_node_key_placeholder(&pkh_policy->key_placeholder), + r_policy_node_keyexpr(&pkh_policy->key_placeholder), compressed_pubkey)) { return -1; } @@ -1043,7 +1039,7 @@ int get_wallet_script(dispatcher_context_t *dispatcher_context, policy_node_with_key_t *wpkh_policy = (policy_node_with_key_t *) policy; if (0 > get_derived_pubkey(dispatcher_context, wdi, - r_policy_node_key_placeholder(&wpkh_policy->key_placeholder), + r_policy_node_keyexpr(&wpkh_policy->key_placeholder), compressed_pubkey)) { return -1; } @@ -1122,7 +1118,7 @@ int get_wallet_script(dispatcher_context_t *dispatcher_context, if (0 > get_derived_pubkey(dispatcher_context, wdi, - r_policy_node_key_placeholder(&tr_policy->key_placeholder), + r_policy_node_keyexpr(&tr_policy->key_placeholder), compressed_pubkey)) { return -1; } @@ -1350,16 +1346,16 @@ __attribute__((noinline)) int get_wallet_internal_script_hash( // For a standard descriptor template, return the corresponding BIP44 purpose // Otherwise, returns -1. static int get_bip44_purpose(const policy_node_t *descriptor_template) { - const policy_node_key_placeholder_t *kp = NULL; + const policy_node_keyexpr_t *kp = NULL; int purpose = -1; switch (descriptor_template->type) { case TOKEN_PKH: - kp = r_policy_node_key_placeholder( + kp = r_policy_node_keyexpr( &((const policy_node_with_key_t *) descriptor_template)->key_placeholder); purpose = 44; // legacy break; case TOKEN_WPKH: - kp = r_policy_node_key_placeholder( + kp = r_policy_node_keyexpr( &((const policy_node_with_key_t *) descriptor_template)->key_placeholder); purpose = 84; // native segwit break; @@ -1370,8 +1366,7 @@ static int get_bip44_purpose(const policy_node_t *descriptor_template) { return -1; } - kp = r_policy_node_key_placeholder( - &((const policy_node_with_key_t *) inner)->key_placeholder); + kp = r_policy_node_keyexpr(&((const policy_node_with_key_t *) inner)->key_placeholder); purpose = 49; // nested segwit break; } @@ -1381,7 +1376,7 @@ static int get_bip44_purpose(const policy_node_t *descriptor_template) { return -1; } - kp = r_policy_node_key_placeholder( + kp = r_policy_node_keyexpr( &((const policy_node_tr_t *) descriptor_template)->key_placeholder); purpose = 86; // standard single-key P2TR break; @@ -1522,7 +1517,7 @@ bool check_wallet_hmac(const uint8_t wallet_id[static 32], const uint8_t wallet_ static int get_key_placeholder_by_index_in_tree(const policy_node_tree_t *tree, unsigned int i, const policy_node_t **out_tapleaf_ptr, - policy_node_key_placeholder_t *out_placeholder) { + policy_node_keyexpr_t *out_placeholder) { if (tree->is_leaf) { int ret = get_key_placeholder_by_index(r_policy_node(&tree->script), i, NULL, out_placeholder); @@ -1552,9 +1547,9 @@ static int get_key_placeholder_by_index_in_tree(const policy_node_tree_t *tree, int get_key_placeholder_by_index(const policy_node_t *policy, unsigned int i, const policy_node_t **out_tapleaf_ptr, - policy_node_key_placeholder_t *out_placeholder) { + policy_node_keyexpr_t *out_placeholder) { // make sure that out_placeholder is a valid pointer, if the output is not needed - policy_node_key_placeholder_t tmp; + policy_node_keyexpr_t tmp; if (out_placeholder == NULL) { out_placeholder = &tmp; } @@ -1580,8 +1575,8 @@ int get_key_placeholder_by_index(const policy_node_t *policy, if (i == 0) { policy_node_with_key_t *wpkh = (policy_node_with_key_t *) policy; memcpy(out_placeholder, - r_policy_node_key_placeholder(&wpkh->key_placeholder), - sizeof(policy_node_key_placeholder_t)); + r_policy_node_keyexpr(&wpkh->key_placeholder), + sizeof(policy_node_keyexpr_t)); } return 1; } @@ -1589,8 +1584,8 @@ int get_key_placeholder_by_index(const policy_node_t *policy, policy_node_tr_t *tr = (policy_node_tr_t *) policy; if (i == 0) { memcpy(out_placeholder, - r_policy_node_key_placeholder(&tr->key_placeholder), - sizeof(policy_node_key_placeholder_t)); + r_policy_node_keyexpr(&tr->key_placeholder), + sizeof(policy_node_keyexpr_t)); } if (!isnull_policy_node_tree(&tr->tree)) { int ret_tree = get_key_placeholder_by_index_in_tree( @@ -1616,9 +1611,9 @@ int get_key_placeholder_by_index(const policy_node_t *policy, const policy_node_multisig_t *node = (const policy_node_multisig_t *) policy; if (i < (unsigned int) node->n) { - policy_node_key_placeholder_t *placeholders = - r_policy_node_key_placeholder(&node->key_placeholders); - memcpy(out_placeholder, &placeholders[i], sizeof(policy_node_key_placeholder_t)); + policy_node_keyexpr_t *placeholders = + r_policy_node_keyexpr(&node->key_placeholders); + memcpy(out_placeholder, &placeholders[i], sizeof(policy_node_keyexpr_t)); } return node->n; @@ -1737,7 +1732,7 @@ int count_distinct_keys_info(const policy_node_t *policy) { } for (int cur = 0; cur < n_placeholders; ++cur) { - policy_node_key_placeholder_t placeholder; + policy_node_keyexpr_t placeholder; if (0 > get_key_placeholder_by_index(policy, cur, NULL, &placeholder)) { return -1; } @@ -1928,12 +1923,12 @@ int is_policy_sane(dispatcher_context_t *dispatcher_context, // proportional to the depth of the wallet policy's abstract syntax tree. for (int i = 0; i < n_placeholders - 1; i++) { // no point in running this for the last placeholder - policy_node_key_placeholder_t kp_i; + policy_node_keyexpr_t kp_i; if (0 > get_key_placeholder_by_index(policy, i, NULL, &kp_i)) { return WITH_ERROR(-1, "Unexpected error retrieving placeholders from the policy"); } for (int j = i + 1; j < n_placeholders; j++) { - policy_node_key_placeholder_t kp_j; + policy_node_keyexpr_t kp_j; if (0 > get_key_placeholder_by_index(policy, j, NULL, &kp_j)) { return WITH_ERROR(-1, "Unexpected error retrieving placeholders from the policy"); } diff --git a/src/handler/lib/policy.h b/src/handler/lib/policy.h index ad9f897b3..68ac71d5a 100644 --- a/src/handler/lib/policy.h +++ b/src/handler/lib/policy.h @@ -197,7 +197,7 @@ __attribute__((warn_unused_result)) int get_key_placeholder_by_index( const policy_node_t *policy, unsigned int i, const policy_node_t **out_tapleaf_ptr, - policy_node_key_placeholder_t *out_placeholder); + policy_node_keyexpr_t *out_placeholder); /** * Determines the expected number of unique keys in the provided policy's key information. diff --git a/src/handler/sign_psbt.c b/src/handler/sign_psbt.c index 419d6dbee..1b8a4ae62 100644 --- a/src/handler/sign_psbt.c +++ b/src/handler/sign_psbt.c @@ -106,7 +106,7 @@ typedef struct { } output_info_t; typedef struct { - policy_node_key_placeholder_t placeholder; + policy_node_keyexpr_t placeholder; int cur_index; uint32_t fingerprint; uint8_t key_derivation_length; diff --git a/src/handler/sign_psbt/sign_psbt_cache.c b/src/handler/sign_psbt/sign_psbt_cache.c index c89f17af2..30803afaa 100644 --- a/src/handler/sign_psbt/sign_psbt_cache.c +++ b/src/handler/sign_psbt/sign_psbt_cache.c @@ -1,7 +1,7 @@ #include "sign_psbt_cache.h" int derive_first_step_for_pubkey(const serialized_extended_pubkey_t *base_key, - const policy_node_key_placeholder_t *placeholder, + const policy_node_keyexpr_t *placeholder, sign_psbt_cache_t *cache, bool is_change, serialized_extended_pubkey_t *out_pubkey) { diff --git a/src/handler/sign_psbt/sign_psbt_cache.h b/src/handler/sign_psbt/sign_psbt_cache.h index e0c7b38d5..5baf44ecd 100644 --- a/src/handler/sign_psbt/sign_psbt_cache.h +++ b/src/handler/sign_psbt/sign_psbt_cache.h @@ -53,7 +53,7 @@ number of BIP-32 derivations is cut almost by half when using the cache. * @return 0 on success, -1 on failure. */ int derive_first_step_for_pubkey(const serialized_extended_pubkey_t *base_key, - const policy_node_key_placeholder_t *placeholder, + const policy_node_keyexpr_t *placeholder, sign_psbt_cache_t *cache, bool is_change, serialized_extended_pubkey_t *out_pubkey); \ No newline at end of file diff --git a/unit-tests/test_wallet.c b/unit-tests/test_wallet.c index 975545487..c330f430e 100644 --- a/unit-tests/test_wallet.c +++ b/unit-tests/test_wallet.c @@ -32,16 +32,35 @@ static int parse_policy(const char *descriptor_template, uint8_t *out, size_t ou // about half of the memory would be needed #define MAX_WALLET_POLICY_MEMORY_SIZE 512 -// convenience function to compactly check common assertions on a key placeholder pointer -static void check_key_placeholder(const policy_node_key_placeholder_t *ptr, - int key_index, - uint32_t num_first, - uint32_t num_second) { +// convenience function to compactly check common assertions on a pointer to a key expression with a +// single placeholder +static void check_key_expr_plain(const policy_node_keyexpr_t *ptr, + int key_index, + uint32_t num_first, + uint32_t num_second) { + assert_int_equal(ptr->type, KEY_EXPRESSION_NORMAL); assert_int_equal(ptr->key_index, key_index); assert_int_equal(ptr->num_first, num_first); assert_int_equal(ptr->num_second, num_second); } +// convenience function to compactly check assertions on a pointer to a key expression with a musig +static void check_key_expr_musig(const policy_node_keyexpr_t *ptr, + int n_musig_keys, + const uint16_t *key_indices, + uint32_t num_first, + uint32_t num_second) { + assert_int_equal(ptr->type, KEY_EXPRESSION_MUSIG); + musig_aggr_key_info_t *musig_info = r_musig_aggr_key_info(&ptr->musig_info); + assert_int_equal(musig_info->n, n_musig_keys); + uint16_t *musig_key_indexes = r_uint16(&musig_info->key_indexes); + for (int i = 0; i < n_musig_keys; i++) { + assert_int_equal(musig_key_indexes[i], key_indices[i]); + } + assert_int_equal(ptr->num_first, num_first); + assert_int_equal(ptr->num_second, num_second); +} + static void test_parse_policy_map_singlesig_1(void **state) { (void) state; @@ -53,7 +72,7 @@ static void test_parse_policy_map_singlesig_1(void **state) { policy_node_with_key_t *node_1 = (policy_node_with_key_t *) out; assert_int_equal(node_1->base.type, TOKEN_PKH); - check_key_placeholder(r_policy_node_key_placeholder(&node_1->key_placeholder), 0, 0, 1); + check_key_expr_plain(r_policy_node_keyexpr(&node_1->key_placeholder), 0, 0, 1); } static void test_parse_policy_map_singlesig_2(void **state) { @@ -71,7 +90,7 @@ static void test_parse_policy_map_singlesig_2(void **state) { policy_node_with_key_t *inner = (policy_node_with_key_t *) r_policy_node(&root->script); assert_int_equal(inner->base.type, TOKEN_WPKH); - check_key_placeholder(r_policy_node_key_placeholder(&inner->key_placeholder), 0, 0, 1); + check_key_expr_plain(r_policy_node_keyexpr(&inner->key_placeholder), 0, 0, 1); } static void test_parse_policy_map_singlesig_3(void **state) { @@ -93,7 +112,7 @@ static void test_parse_policy_map_singlesig_3(void **state) { policy_node_with_key_t *inner = (policy_node_with_key_t *) r_policy_node(&mid->script); assert_int_equal(inner->base.type, TOKEN_PKH); - check_key_placeholder(r_policy_node_key_placeholder(&inner->key_placeholder), 0, 0, 1); + check_key_expr_plain(r_policy_node_keyexpr(&inner->key_placeholder), 0, 0, 1); } static void test_parse_policy_map_multisig_1(void **state) { @@ -109,9 +128,9 @@ static void test_parse_policy_map_multisig_1(void **state) { assert_int_equal(node_1->base.type, TOKEN_SORTEDMULTI); assert_int_equal(node_1->k, 2); assert_int_equal(node_1->n, 3); - check_key_placeholder(&r_policy_node_key_placeholder(&node_1->key_placeholders)[0], 0, 0, 1); - check_key_placeholder(&r_policy_node_key_placeholder(&node_1->key_placeholders)[1], 1, 0, 1); - check_key_placeholder(&r_policy_node_key_placeholder(&node_1->key_placeholders)[2], 2, 0, 1); + check_key_expr_plain(&r_policy_node_keyexpr(&node_1->key_placeholders)[0], 0, 0, 1); + check_key_expr_plain(&r_policy_node_keyexpr(&node_1->key_placeholders)[1], 1, 0, 1); + check_key_expr_plain(&r_policy_node_keyexpr(&node_1->key_placeholders)[2], 2, 0, 1); } static void test_parse_policy_map_multisig_2(void **state) { @@ -132,7 +151,7 @@ static void test_parse_policy_map_multisig_2(void **state) { assert_int_equal(inner->k, 3); assert_int_equal(inner->n, 5); for (int i = 0; i < 5; i++) { - check_key_placeholder(&r_policy_node_key_placeholder(&inner->key_placeholders)[i], i, 0, 1); + check_key_expr_plain(&r_policy_node_keyexpr(&inner->key_placeholders)[i], i, 0, 1); } } @@ -158,7 +177,7 @@ static void test_parse_policy_map_multisig_3(void **state) { assert_int_equal(inner->k, 3); assert_int_equal(inner->n, 5); for (int i = 0; i < 5; i++) { - check_key_placeholder(&r_policy_node_key_placeholder(&inner->key_placeholders)[i], i, 0, 1); + check_key_expr_plain(&r_policy_node_keyexpr(&inner->key_placeholders)[i], i, 0, 1); } } @@ -175,7 +194,7 @@ static void test_parse_policy_tr(void **state) { policy_node_tr_t *root = (policy_node_tr_t *) out; assert_true(isnull_policy_node_tree(&root->tree)); - check_key_placeholder(r_policy_node_key_placeholder(&root->key_placeholder), 0, 0, 1); + check_key_expr_plain(r_policy_node_keyexpr(&root->key_placeholder), 0, 0, 1); // Simple tr with a TREE that is a simple script res = parse_policy("tr(@0/**,pk(@1/**))", out, sizeof(out)); @@ -183,7 +202,7 @@ static void test_parse_policy_tr(void **state) { assert_true(res >= 0); root = (policy_node_tr_t *) out; - check_key_placeholder(r_policy_node_key_placeholder(&root->key_placeholder), 0, 0, 1); + check_key_expr_plain(r_policy_node_keyexpr(&root->key_placeholder), 0, 0, 1); assert_int_equal(r_policy_node_tree(&root->tree)->is_leaf, true); @@ -191,7 +210,7 @@ static void test_parse_policy_tr(void **state) { (policy_node_with_key_t *) r_policy_node(&r_policy_node_tree(&root->tree)->script); assert_int_equal(tapscript->base.type, TOKEN_PK); - check_key_placeholder(r_policy_node_key_placeholder(&tapscript->key_placeholder), 1, 0, 1); + check_key_expr_plain(r_policy_node_keyexpr(&tapscript->key_placeholder), 1, 0, 1); // Simple tr with a TREE with two tapleaves res = parse_policy("tr(@0/**,{pk(@1/**),pk(@2/<5;7>/*)})", out, sizeof(out)); @@ -199,7 +218,7 @@ static void test_parse_policy_tr(void **state) { assert_true(res >= 0); root = (policy_node_tr_t *) out; - check_key_placeholder(r_policy_node_key_placeholder(&root->key_placeholder), 0, 0, 1); + check_key_expr_plain(r_policy_node_keyexpr(&root->key_placeholder), 0, 0, 1); policy_node_tree_t *taptree = r_policy_node_tree(&root->tree); @@ -212,7 +231,7 @@ static void test_parse_policy_tr(void **state) { (policy_node_with_key_t *) r_policy_node(&taptree_left->script); assert_int_equal(tapscript_left->base.type, TOKEN_PK); - check_key_placeholder(r_policy_node_key_placeholder(&tapscript_left->key_placeholder), 1, 0, 1); + check_key_expr_plain(r_policy_node_keyexpr(&tapscript_left->key_placeholder), 1, 0, 1); policy_node_tree_t *taptree_right = (policy_node_tree_t *) r_policy_node_tree(&taptree->right_tree); @@ -221,10 +240,7 @@ static void test_parse_policy_tr(void **state) { (policy_node_with_key_t *) r_policy_node(&taptree_right->script); assert_int_equal(tapscript_right->base.type, TOKEN_PK); - check_key_placeholder(r_policy_node_key_placeholder(&tapscript_right->key_placeholder), - 2, - 5, - 7); + check_key_expr_plain(r_policy_node_keyexpr(&tapscript_right->key_placeholder), 2, 5, 7); } static void test_parse_policy_tr_multisig(void **state) { @@ -242,9 +258,9 @@ static void test_parse_policy_tr_multisig(void **state) { policy_node_tr_t *root = (policy_node_tr_t *) out; - assert_int_equal(r_policy_node_key_placeholder(&root->key_placeholder)->key_index, 0); - assert_int_equal(r_policy_node_key_placeholder(&root->key_placeholder)->num_first, 0); - assert_int_equal(r_policy_node_key_placeholder(&root->key_placeholder)->num_second, 1); + assert_int_equal(r_policy_node_keyexpr(&root->key_placeholder)->key_index, 0); + assert_int_equal(r_policy_node_keyexpr(&root->key_placeholder)->num_first, 0); + assert_int_equal(r_policy_node_keyexpr(&root->key_placeholder)->num_second, 1); policy_node_tree_t *taptree = r_policy_node_tree(&root->tree); @@ -259,14 +275,8 @@ static void test_parse_policy_tr_multisig(void **state) { assert_int_equal(tapscript_left->base.type, TOKEN_MULTI_A); assert_int_equal(tapscript_left->k, 1); assert_int_equal(tapscript_left->n, 2); - check_key_placeholder(&r_policy_node_key_placeholder(&tapscript_left->key_placeholders)[0], - 1, - 0, - 1); - check_key_placeholder(&r_policy_node_key_placeholder(&tapscript_left->key_placeholders)[1], - 2, - 0, - 1); + check_key_expr_plain(&r_policy_node_keyexpr(&tapscript_left->key_placeholders)[0], 1, 0, 1); + check_key_expr_plain(&r_policy_node_keyexpr(&tapscript_left->key_placeholders)[1], 2, 0, 1); policy_node_tree_t *taptree_right = (policy_node_tree_t *) r_policy_node_tree(&taptree->right_tree); @@ -277,18 +287,58 @@ static void test_parse_policy_tr_multisig(void **state) { assert_int_equal(tapscript_right->base.type, TOKEN_SORTEDMULTI_A); assert_int_equal(tapscript_right->k, 2); assert_int_equal(tapscript_right->n, 3); - check_key_placeholder(&r_policy_node_key_placeholder(&tapscript_right->key_placeholders)[0], - 3, - 0, - 1); - check_key_placeholder(&r_policy_node_key_placeholder(&tapscript_right->key_placeholders)[1], - 4, - 0, - 1); - check_key_placeholder(&r_policy_node_key_placeholder(&tapscript_right->key_placeholders)[2], - 5, - 0, - 1); + check_key_expr_plain(&r_policy_node_keyexpr(&tapscript_right->key_placeholders)[0], 3, 0, 1); + check_key_expr_plain(&r_policy_node_keyexpr(&tapscript_right->key_placeholders)[1], 4, 0, 1); + check_key_expr_plain(&r_policy_node_keyexpr(&tapscript_right->key_placeholders)[2], 5, 0, 1); +} + +static void test_parse_policy_tr_musig_keypath(void **state) { + (void) state; + + uint8_t out[MAX_WALLET_POLICY_MEMORY_SIZE]; + int res; + + res = parse_policy("tr(musig(@2,@0,@1)/<3;13>/*)", out, sizeof(out)); + + assert_true(res >= 0); + + policy_node_tr_t *root = (policy_node_tr_t *) out; + assert_int_equal(root->base.type, TOKEN_TR); + assert_true(isnull_policy_node_tree(&root->tree)); + + check_key_expr_musig(r_policy_node_keyexpr(&root->key_placeholder), + 3, + (uint16_t[]){2, 0, 1}, + 3, + 13); +} + +static void test_parse_policy_tr_musig_scriptpath(void **state) { + (void) state; + + uint8_t out[MAX_WALLET_POLICY_MEMORY_SIZE]; + int res; + + // tr with a musig in the script path + res = parse_policy("tr(@1/**,pk(musig(@2,@0,@3)/**))", out, sizeof(out)); + + assert_true(res >= 0); + + policy_node_tr_t *root = (policy_node_tr_t *) out; + assert_int_equal(root->base.type, TOKEN_TR); + + assert_false(isnull_policy_node_tree(&root->tree)); + policy_node_tree_t *tree = r_policy_node_tree(&root->tree); + assert_true(tree->is_leaf); + + policy_node_with_key_t *script_pk = (policy_node_with_key_t *) r_policy_node(&tree->script); + assert_int_equal(script_pk->base.type, TOKEN_PK); + + check_key_expr_musig(r_policy_node_keyexpr(&script_pk->key_placeholder), + 3, + (uint16_t[]){2, 0, 3}, + 0, + 1); } static void test_get_policy_segwit_version(void **state) { @@ -377,6 +427,14 @@ static void test_failures(void **state) { assert_true(0 > parse_policy("tr(@0/**,sortedmulti(2,@1,@2))", out, sizeof(out))); assert_true(0 > parse_policy("tr(@0/**,sh(pk(@0/**)))", out, sizeof(out))); assert_true(0 > parse_policy("tr(@0/**,wsh(pk(@0/**)))", out, sizeof(out))); + + // invalid usages of musig expressions + assert_true(0 > parse_policy("tr(musig(@0,@1))", out, sizeof(out))); // missing derivations + assert_true(0 > parse_policy("tr(musig()/**)", out, sizeof(out))); // empty musig + assert_true(0 > parse_policy("tr(musig(@0)/**)", out, sizeof(out))); // needs at least two keys + assert_true(0 > parse_policy("wpkh(musig(@0,@1)/**)", out, sizeof(out))); // not taproot + assert_true( + 0 > parse_policy("tr(musig(@0,musig(@1,@2))/**)", out, sizeof(out))); // can't nest musig } enum TestMode { From 5efbc36aadf1ec3e19db7f17aaded2cb333d2542 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Tue, 8 Oct 2024 18:38:45 +0200 Subject: [PATCH 02/45] Rename "key placeholder" with "key expression" where appropriate; added some comments. Generalizing to key expressions containing musig() makes it necessary to distinguish the key expressions in the wallet policy from the actual key placeholders that are just indexes to the list of key informations (@num in the descriptor template), whereas the two concepts were often not clearly separated in the code base. Renaming to "key expressions" makes the distinction more clear. --- src/common/wallet.c | 74 +++--- src/common/wallet.h | 41 ++-- src/handler/lib/policy.c | 212 +++++++++-------- src/handler/lib/policy.h | 31 ++- src/handler/sign_psbt.c | 294 ++++++++++++------------ src/handler/sign_psbt/sign_psbt_cache.c | 22 +- src/handler/sign_psbt/sign_psbt_cache.h | 10 +- unit-tests/test_wallet.c | 58 ++--- 8 files changed, 357 insertions(+), 385 deletions(-) diff --git a/src/common/wallet.c b/src/common/wallet.c index dccb20251..628d1277a 100644 --- a/src/common/wallet.c +++ b/src/common/wallet.c @@ -444,10 +444,10 @@ static int parse_keyexpr(buffer_t *in_buf, policy_node_keyexpr_t *out, bool is_taproot, buffer_t *out_buf, - uint16_t *placeholder_index) { + uint16_t *keyexpr_index) { char c; if (!buffer_read_u8(in_buf, (uint8_t *) &c)) { - return WITH_ERROR(-1, "Expected key placeholder"); + return WITH_ERROR(-1, "Expected key expression"); } if (c == '@') { @@ -475,7 +475,7 @@ static int parse_keyexpr(buffer_t *in_buf, return WITH_ERROR(-1, "musig key expressions are only supported with version number 2"); } - uint16_t key_placeholders[MAX_PUBKEYS_PER_MUSIG]; + uint16_t keys[MAX_PUBKEYS_PER_MUSIG]; int n_musig_keys = 0; // parse comma-separated list of @NUM @@ -493,7 +493,7 @@ static int parse_keyexpr(buffer_t *in_buf, return WITH_ERROR(-1, "Too many keys in musig"); } - key_placeholders[n_musig_keys] = (uint16_t) k; + keys[n_musig_keys] = (uint16_t) k; ++n_musig_keys; // the next character must be "," if there are more keys, or ')' otherwise @@ -529,14 +529,14 @@ static int parse_keyexpr(buffer_t *in_buf, if (key_indexes == NULL) { return WITH_ERROR(-1, "Out of memory"); } - memcpy(key_indexes, key_placeholders, sizeof(uint16_t) * n_musig_keys); + memcpy(key_indexes, keys, sizeof(uint16_t) * n_musig_keys); musig_info->n = n_musig_keys; i_uint16(&musig_info->key_indexes, key_indexes); i_musig_aggr_key_info(&out->musig_info, musig_info); } else { - return WITH_ERROR(-1, "Expected key placeholder starting with '@', or musig"); + return WITH_ERROR(-1, "Expected key expression starting with '@', or musig"); } if (version == WALLET_POLICY_VERSION_V1) { @@ -550,12 +550,12 @@ static int parse_keyexpr(buffer_t *in_buf, || !buffer_peek(in_buf, &next_character) // we must be able to read the next character || !(next_character == '*' || next_character == '<') // and it must be '*' or '<' ) { - return WITH_ERROR(-1, "Expected /** or //* in key placeholder"); + return WITH_ERROR(-1, "Expected /** or //* in key expression"); } if (next_character == '*') { if (!consume_characters(in_buf, "**", 2)) { - return WITH_ERROR(-1, "Expected /** or //* in key placeholder"); + return WITH_ERROR(-1, "Expected /** or //* in key expression"); } out->num_first = 0; out->num_second = 1; @@ -565,18 +565,18 @@ static int parse_keyexpr(buffer_t *in_buf, out->num_first > 0x80000000u) { return WITH_ERROR( -1, - "Expected /** or //* in key placeholder, with unhardened M and N"); + "Expected /** or //* in key expression, with unhardened M and N"); } if (!consume_character(in_buf, ';')) { - return WITH_ERROR(-1, "Expected /** or //* in key placeholder"); + return WITH_ERROR(-1, "Expected /** or //* in key expression"); } if (parse_unsigned_decimal(in_buf, &out->num_second) == -1 || out->num_second > 0x80000000u) { return WITH_ERROR( -1, - "Expected /** or //* in key placeholder, with unhardened M and N"); + "Expected /** or //* in key expression, with unhardened M and N"); } if (out->num_first == out->num_second) { @@ -584,15 +584,15 @@ static int parse_keyexpr(buffer_t *in_buf, } if (!consume_characters(in_buf, ">/*", 3)) { - return WITH_ERROR(-1, "Expected /** or //* in key placeholder"); + return WITH_ERROR(-1, "Expected /** or //* in key expression"); } } } else { return WITH_ERROR(-1, "Invalid version number"); } - out->placeholder_index = *placeholder_index; - ++(*placeholder_index); + out->keyexpr_index = *keyexpr_index; + ++(*keyexpr_index); return 0; } @@ -648,13 +648,13 @@ static int parse_script(buffer_t *in_buf, unsigned int context_flags) { int n_wrappers = 0; - // Keep track of how many key placeholders have been created while parsing + // Keep track of how many key expressions have been created while parsing // This allows to know the counter even in recursive calls - static uint16_t key_placeholder_count = 0; + static uint16_t key_expression_count = 0; if (depth == 0) { // reset the counter on function entry, but not in recursive calls - key_placeholder_count = 0; + key_expression_count = 0; } policy_node_t *outermost_node = (policy_node_t *) buffer_get_cur(out_buf); @@ -1491,13 +1491,13 @@ static int parse_script(buffer_t *in_buf, return WITH_ERROR(-1, "Out of memory"); } - policy_node_keyexpr_t *key_placeholder = + policy_node_keyexpr_t *key_expr = buffer_alloc(out_buf, sizeof(policy_node_keyexpr_t), true); - if (key_placeholder == NULL) { + if (key_expr == NULL) { return WITH_ERROR(-1, "Out of memory"); } - i_policy_node_keyexpr(&node->key_placeholder, key_placeholder); + i_policy_node_keyexpr(&node->key, key_expr); if (token == TOKEN_WPKH) { if (depth > 0 && ((context_flags & CONTEXT_WITHIN_SH) == 0)) { @@ -1512,11 +1512,11 @@ static int parse_script(buffer_t *in_buf, bool is_taproot = (context_flags & CONTEXT_WITHIN_TR) != 0; if (0 > parse_keyexpr(in_buf, version, - key_placeholder, + key_expr, is_taproot, out_buf, - &key_placeholder_count)) { - return WITH_ERROR(-1, "Couldn't parse key placeholder"); + &key_expression_count)) { + return WITH_ERROR(-1, "Couldn't parse key expression"); } if (token == TOKEN_WPKH) { @@ -1578,20 +1578,16 @@ static int parse_script(buffer_t *in_buf, return WITH_ERROR(-1, "Out of memory"); } - policy_node_keyexpr_t *key_placeholder = + policy_node_keyexpr_t *key_expr = buffer_alloc(out_buf, sizeof(policy_node_keyexpr_t), true); - if (key_placeholder == NULL) { + if (key_expr == NULL) { return WITH_ERROR(-1, "Out of memory"); } - i_policy_node_keyexpr(&node->key_placeholder, key_placeholder); + i_policy_node_keyexpr(&node->key, key_expr); - if (0 > parse_keyexpr(in_buf, - version, - key_placeholder, - true, - out_buf, - &key_placeholder_count)) { - return WITH_ERROR(-1, "Couldn't parse key placeholder"); + if (0 > + parse_keyexpr(in_buf, version, key_expr, true, out_buf, &key_expression_count)) { + return WITH_ERROR(-1, "Couldn't parse key expression"); } uint8_t c; @@ -1706,7 +1702,7 @@ static int parse_script(buffer_t *in_buf, // We allocate the array of key indices at the current position in the output buffer // (on success) buffer_alloc(out_buf, 0, true); // ensure alignment of current pointer - i_policy_node_keyexpr(&node->key_placeholders, buffer_get_cur(out_buf)); + i_policy_node_keyexpr(&node->keys, buffer_get_cur(out_buf)); node->n = 0; while (true) { @@ -1721,22 +1717,22 @@ static int parse_script(buffer_t *in_buf, return WITH_ERROR(-1, "Expected ','"); } - policy_node_keyexpr_t *key_placeholder = (policy_node_keyexpr_t *) buffer_alloc( + policy_node_keyexpr_t *key_expr = (policy_node_keyexpr_t *) buffer_alloc( out_buf, sizeof(policy_node_keyexpr_t), true); // we align this pointer, as there's padding in an array of // structures - if (key_placeholder == NULL) { + if (key_expr == NULL) { return WITH_ERROR(-1, "Out of memory"); } if (0 > parse_keyexpr(in_buf, version, - key_placeholder, + key_expr, is_taproot, out_buf, - &key_placeholder_count)) { - return WITH_ERROR(-1, "Error parsing key placeholder"); + &key_expression_count)) { + return WITH_ERROR(-1, "Error parsing key expression"); } ++node->n; diff --git a/src/common/wallet.h b/src/common/wallet.h index eff17d2a9..9c0dc1aef 100644 --- a/src/common/wallet.h +++ b/src/common/wallet.h @@ -284,19 +284,6 @@ typedef struct policy_node_ext_info_s { unsigned int x : 1; // the last opcode is not EQUAL, CHECKSIG, or CHECKMULTISIG } policy_node_ext_info_t; -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wcomment" -// The compiler doesn't like /** inside a block comment, so we disable this warning temporarily. - -/** Structure representing a key placeholder. - * In V1, it's the index of a key expression in the key informations array, which includes the final - * / ** step. - * In V2, it's the index of a key expression in the key informations array, plus the two - * numbers a, b in the //* derivation steps; here, the xpubs in the key informations - * array don't have extra derivation steps. - */ -#pragma GCC diagnostic pop - DEFINE_REL_PTR(uint16, uint16_t) typedef struct { @@ -307,10 +294,22 @@ typedef struct { DEFINE_REL_PTR(musig_aggr_key_info, musig_aggr_key_info_t) typedef enum { - KEY_EXPRESSION_NORMAL = 0, // a key expression with a single key placeholder + KEY_EXPRESSION_NORMAL = 0, // a key expression with a single key expression KEY_EXPRESSION_MUSIG = 1 // a key expression containing a musig() } KeyExpressionType; +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wcomment" +// The compiler doesn't like /** inside a block comment, so we disable this warning temporarily. + +/** Structure representing a key expression. + * In V1, it's the index of a key in the key informations array, which includes the final /** step. + * In V2, it's the index of a key in the key informations array, plus the two numbers a, b in the + * //* derivation steps; here, the xpubs in the key informations array don't have extra + * derivation steps. + */ +#pragma GCC diagnostic pop + // 16 bytes typedef struct { // the following fields are only used in V2 @@ -325,7 +324,7 @@ typedef struct { rptr_musig_aggr_key_info_t musig_info; // only used in V2 }; int16_t - placeholder_index; // index of the placeholder in the descriptor template, in parsing order + keyexpr_index; // index of the key expression in the descriptor template, in parsing order } policy_node_keyexpr_t; DEFINE_REL_PTR(policy_node_keyexpr, policy_node_keyexpr_t) @@ -359,7 +358,7 @@ typedef policy_node_with_script3_t policy_node_with_scripts_t; // 4 bytes typedef struct { struct policy_node_s base; - rptr_policy_node_keyexpr_t key_placeholder; + rptr_policy_node_keyexpr_t key; } policy_node_with_key_t; // 8 bytes @@ -370,10 +369,10 @@ typedef struct { // 12 bytes typedef struct { - struct policy_node_s base; // type is TOKEN_MULTI or TOKEN_SORTEDMULTI - int16_t k; // threshold - int16_t n; // number of keys - rptr_policy_node_keyexpr_t key_placeholders; // pointer to array of exactly n key placeholders + struct policy_node_s base; // type is TOKEN_MULTI or TOKEN_SORTEDMULTI + int16_t k; // threshold + int16_t n; // number of keys + rptr_policy_node_keyexpr_t keys; // pointer to array of exactly n key expressions } policy_node_multisig_t; // 8 bytes @@ -423,7 +422,7 @@ typedef struct policy_node_tree_s { typedef struct { struct policy_node_s base; - rptr_policy_node_keyexpr_t key_placeholder; + rptr_policy_node_keyexpr_t key; rptr_policy_node_tree_t tree; // NULL if tr(KP) } policy_node_tr_t; diff --git a/src/handler/lib/policy.c b/src/handler/lib/policy.c index ae289fadc..e4875dae3 100644 --- a/src/handler/lib/policy.c +++ b/src/handler/lib/policy.c @@ -456,13 +456,13 @@ __attribute__((noinline, warn_unused_result)) static int get_extended_pubkey( __attribute__((warn_unused_result)) static int get_derived_pubkey( dispatcher_context_t *dispatcher_context, const wallet_derivation_info_t *wdi, - const policy_node_keyexpr_t *key_placeholder, + const policy_node_keyexpr_t *key_expr, uint8_t out[static 33]) { PRINT_STACK_POINTER(); serialized_extended_pubkey_t ext_pubkey; - int ret = get_extended_pubkey(dispatcher_context, wdi, key_placeholder->key_index, &ext_pubkey); + int ret = get_extended_pubkey(dispatcher_context, wdi, key_expr->key_index, &ext_pubkey); if (ret < 0) { return -1; } @@ -470,7 +470,7 @@ __attribute__((warn_unused_result)) static int get_derived_pubkey( // we derive the // child of this pubkey // we reuse the same memory of ext_pubkey if (0 > derive_first_step_for_pubkey(&ext_pubkey, - key_placeholder, + key_expr, wdi->sign_psbt_cache, wdi->change, &ext_pubkey)) { @@ -577,7 +577,7 @@ __attribute__((warn_unused_result)) static int process_generic_node(policy_parse uint8_t compressed_pubkey[33]; if (-1 == get_derived_pubkey(state->dispatcher_context, state->wdi, - r_policy_node_keyexpr(&policy->key_placeholder), + r_policy_node_keyexpr(&policy->key), compressed_pubkey)) { return -1; } @@ -598,7 +598,7 @@ __attribute__((warn_unused_result)) static int process_generic_node(policy_parse uint8_t compressed_pubkey[33]; if (-1 == get_derived_pubkey(state->dispatcher_context, state->wdi, - r_policy_node_keyexpr(&policy->key_placeholder), + r_policy_node_keyexpr(&policy->key), compressed_pubkey)) { return -1; } @@ -688,7 +688,7 @@ __attribute__((warn_unused_result)) static int process_pkh_wpkh_node(policy_pars if (-1 == get_derived_pubkey(state->dispatcher_context, state->wdi, - r_policy_node_keyexpr(&policy->key_placeholder), + r_policy_node_keyexpr(&policy->key), compressed_pubkey)) { return -1; } else if (policy->base.type == TOKEN_PKH) { @@ -817,7 +817,7 @@ __attribute__((warn_unused_result)) static int process_multi_sortedmulti_node( if (policy->base.type == TOKEN_MULTI) { if (-1 == get_derived_pubkey(state->dispatcher_context, state->wdi, - &r_policy_node_keyexpr(&policy->key_placeholders)[i], + &r_policy_node_keyexpr(&policy->keys)[i], compressed_pubkey)) { return -1; } @@ -840,11 +840,10 @@ __attribute__((warn_unused_result)) static int process_multi_sortedmulti_node( for (int j = 0; j < policy->n; j++) { if (!bitvector_get(used, j)) { uint8_t cur_pubkey[33]; - if (-1 == - get_derived_pubkey(state->dispatcher_context, - state->wdi, - &r_policy_node_keyexpr(&policy->key_placeholders)[j], - cur_pubkey)) { + if (-1 == get_derived_pubkey(state->dispatcher_context, + state->wdi, + &r_policy_node_keyexpr(&policy->keys)[j], + cur_pubkey)) { return -1; } @@ -894,7 +893,7 @@ __attribute__((warn_unused_result)) static int process_multi_a_sortedmulti_a_nod if (policy->base.type == TOKEN_MULTI_A) { if (-1 == get_derived_pubkey(state->dispatcher_context, state->wdi, - &r_policy_node_keyexpr(&policy->key_placeholders)[i], + &r_policy_node_keyexpr(&policy->keys)[i], compressed_pubkey)) { return -1; } @@ -907,11 +906,10 @@ __attribute__((warn_unused_result)) static int process_multi_a_sortedmulti_a_nod for (int j = 0; j < policy->n; j++) { if (!bitvector_get(used, j)) { uint8_t cur_pubkey[33]; - if (-1 == - get_derived_pubkey(state->dispatcher_context, - state->wdi, - &r_policy_node_keyexpr(&policy->key_placeholders)[j], - cur_pubkey)) { + if (-1 == get_derived_pubkey(state->dispatcher_context, + state->wdi, + &r_policy_node_keyexpr(&policy->keys)[j], + cur_pubkey)) { return -1; } @@ -1020,7 +1018,7 @@ int get_wallet_script(dispatcher_context_t *dispatcher_context, policy_node_with_key_t *pkh_policy = (policy_node_with_key_t *) policy; if (0 > get_derived_pubkey(dispatcher_context, wdi, - r_policy_node_keyexpr(&pkh_policy->key_placeholder), + r_policy_node_keyexpr(&pkh_policy->key), compressed_pubkey)) { return -1; } @@ -1039,7 +1037,7 @@ int get_wallet_script(dispatcher_context_t *dispatcher_context, policy_node_with_key_t *wpkh_policy = (policy_node_with_key_t *) policy; if (0 > get_derived_pubkey(dispatcher_context, wdi, - r_policy_node_keyexpr(&wpkh_policy->key_placeholder), + r_policy_node_keyexpr(&wpkh_policy->key), compressed_pubkey)) { return -1; } @@ -1118,7 +1116,7 @@ int get_wallet_script(dispatcher_context_t *dispatcher_context, if (0 > get_derived_pubkey(dispatcher_context, wdi, - r_policy_node_keyexpr(&tr_policy->key_placeholder), + r_policy_node_keyexpr(&tr_policy->key), compressed_pubkey)) { return -1; } @@ -1350,13 +1348,13 @@ static int get_bip44_purpose(const policy_node_t *descriptor_template) { int purpose = -1; switch (descriptor_template->type) { case TOKEN_PKH: - kp = r_policy_node_keyexpr( - &((const policy_node_with_key_t *) descriptor_template)->key_placeholder); + kp = + r_policy_node_keyexpr(&((const policy_node_with_key_t *) descriptor_template)->key); purpose = 44; // legacy break; case TOKEN_WPKH: - kp = r_policy_node_keyexpr( - &((const policy_node_with_key_t *) descriptor_template)->key_placeholder); + kp = + r_policy_node_keyexpr(&((const policy_node_with_key_t *) descriptor_template)->key); purpose = 84; // native segwit break; case TOKEN_SH: { @@ -1366,7 +1364,7 @@ static int get_bip44_purpose(const policy_node_t *descriptor_template) { return -1; } - kp = r_policy_node_keyexpr(&((const policy_node_with_key_t *) inner)->key_placeholder); + kp = r_policy_node_keyexpr(&((const policy_node_with_key_t *) inner)->key); purpose = 49; // nested segwit break; } @@ -1376,8 +1374,7 @@ static int get_bip44_purpose(const policy_node_t *descriptor_template) { return -1; } - kp = r_policy_node_keyexpr( - &((const policy_node_tr_t *) descriptor_template)->key_placeholder); + kp = r_policy_node_keyexpr(&((const policy_node_tr_t *) descriptor_template)->key); purpose = 86; // standard single-key P2TR break; } @@ -1514,44 +1511,47 @@ bool check_wallet_hmac(const uint8_t wallet_id[static 32], const uint8_t wallet_ // make sure that the compiler gives an error if any PolicyNodeType is missed #pragma GCC diagnostic error "-Wswitch-enum" -static int get_key_placeholder_by_index_in_tree(const policy_node_tree_t *tree, - unsigned int i, - const policy_node_t **out_tapleaf_ptr, - policy_node_keyexpr_t *out_placeholder) { +static int get_keyexpr_by_index_in_tree(const policy_node_tree_t *tree, + unsigned int i, + const policy_node_t **out_tapleaf_ptr, + policy_node_keyexpr_t *out_keyexpr) { if (tree->is_leaf) { - int ret = - get_key_placeholder_by_index(r_policy_node(&tree->script), i, NULL, out_placeholder); + int ret = get_keyexpr_by_index(r_policy_node(&tree->script), i, NULL, out_keyexpr); if (ret >= 0 && out_tapleaf_ptr != NULL && i < (unsigned) ret) { *out_tapleaf_ptr = r_policy_node(&tree->script); } return ret; } else { - int ret1 = get_key_placeholder_by_index_in_tree(r_policy_node_tree(&tree->left_tree), - i, - out_tapleaf_ptr, - out_placeholder); + int ret1 = get_keyexpr_by_index_in_tree(r_policy_node_tree(&tree->left_tree), + i, + out_tapleaf_ptr, + out_keyexpr); if (ret1 < 0) return -1; bool found = i < (unsigned int) ret1; - int ret2 = get_key_placeholder_by_index_in_tree(r_policy_node_tree(&tree->right_tree), - found ? 0 : i - ret1, - found ? NULL : out_tapleaf_ptr, - found ? NULL : out_placeholder); + int ret2 = get_keyexpr_by_index_in_tree(r_policy_node_tree(&tree->right_tree), + found ? 0 : i - ret1, + found ? NULL : out_tapleaf_ptr, + found ? NULL : out_keyexpr); if (ret2 < 0) return -1; return ret1 + ret2; } } -int get_key_placeholder_by_index(const policy_node_t *policy, - unsigned int i, - const policy_node_t **out_tapleaf_ptr, - policy_node_keyexpr_t *out_placeholder) { - // make sure that out_placeholder is a valid pointer, if the output is not needed +// TODO: generalize for musig. Note that this is broken for musig, as out_keyexpr +// can't be filled in for musig key expressions (as it's dynamic and contains +// relative pointers). We should probably refactor to return the pointer to the +// key expression and removing the out_keyexpr argument. +int get_keyexpr_by_index(const policy_node_t *policy, + unsigned int i, + const policy_node_t **out_tapleaf_ptr, + policy_node_keyexpr_t *out_keyexpr) { + // make sure that out_keyexpr is a valid pointer, if the output is not needed policy_node_keyexpr_t tmp; - if (out_placeholder == NULL) { - out_placeholder = &tmp; + if (out_keyexpr == NULL) { + out_keyexpr = &tmp; } switch (policy->type) { @@ -1574,8 +1574,8 @@ int get_key_placeholder_by_index(const policy_node_t *policy, case TOKEN_WPKH: { if (i == 0) { policy_node_with_key_t *wpkh = (policy_node_with_key_t *) policy; - memcpy(out_placeholder, - r_policy_node_keyexpr(&wpkh->key_placeholder), + memcpy(out_keyexpr, + r_policy_node_keyexpr(&wpkh->key), sizeof(policy_node_keyexpr_t)); } return 1; @@ -1583,17 +1583,15 @@ int get_key_placeholder_by_index(const policy_node_t *policy, case TOKEN_TR: { policy_node_tr_t *tr = (policy_node_tr_t *) policy; if (i == 0) { - memcpy(out_placeholder, - r_policy_node_keyexpr(&tr->key_placeholder), - sizeof(policy_node_keyexpr_t)); + memcpy(out_keyexpr, r_policy_node_keyexpr(&tr->key), sizeof(policy_node_keyexpr_t)); } if (!isnull_policy_node_tree(&tr->tree)) { - int ret_tree = get_key_placeholder_by_index_in_tree( + int ret_tree = get_keyexpr_by_index_in_tree( r_policy_node_tree(&tr->tree), i == 0 ? 0 : i - 1, i == 0 ? NULL : out_tapleaf_ptr, - i == 0 ? NULL : out_placeholder); // if i == 0, we already found it; so we - // recur with out_placeholder set to NULL + i == 0 ? NULL : out_keyexpr); // if i == 0, we already found it; so we + // recur with out_keyexpr set to NULL if (ret_tree < 0) { return -1; } @@ -1611,9 +1609,8 @@ int get_key_placeholder_by_index(const policy_node_t *policy, const policy_node_multisig_t *node = (const policy_node_multisig_t *) policy; if (i < (unsigned int) node->n) { - policy_node_keyexpr_t *placeholders = - r_policy_node_keyexpr(&node->key_placeholders); - memcpy(out_placeholder, &placeholders[i], sizeof(policy_node_keyexpr_t)); + policy_node_keyexpr_t *key_expressions = r_policy_node_keyexpr(&node->keys); + memcpy(out_keyexpr, &key_expressions[i], sizeof(policy_node_keyexpr_t)); } return node->n; @@ -1632,11 +1629,11 @@ int get_key_placeholder_by_index(const policy_node_t *policy, case TOKEN_N: case TOKEN_L: case TOKEN_U: { - return get_key_placeholder_by_index( + return get_keyexpr_by_index( r_policy_node(&((const policy_node_with_script_t *) policy)->script), i, out_tapleaf_ptr, - out_placeholder); + out_keyexpr); } // nodes with exactly two child scripts @@ -1648,17 +1645,17 @@ int get_key_placeholder_by_index(const policy_node_t *policy, case TOKEN_OR_D: case TOKEN_OR_I: { const policy_node_with_script2_t *node = (const policy_node_with_script2_t *) policy; - int ret1 = get_key_placeholder_by_index(r_policy_node(&node->scripts[0]), - i, - out_tapleaf_ptr, - out_placeholder); + int ret1 = get_keyexpr_by_index(r_policy_node(&node->scripts[0]), + i, + out_tapleaf_ptr, + out_keyexpr); if (ret1 < 0) return -1; bool found = i < (unsigned int) ret1; - int ret2 = get_key_placeholder_by_index(r_policy_node(&node->scripts[1]), - found ? 0 : i - ret1, - found ? NULL : out_tapleaf_ptr, - found ? NULL : out_placeholder); + int ret2 = get_keyexpr_by_index(r_policy_node(&node->scripts[1]), + found ? 0 : i - ret1, + found ? NULL : out_tapleaf_ptr, + found ? NULL : out_keyexpr); if (ret2 < 0) return -1; return ret1 + ret2; @@ -1667,24 +1664,24 @@ int get_key_placeholder_by_index(const policy_node_t *policy, // nodes with exactly three child scripts case TOKEN_ANDOR: { const policy_node_with_script3_t *node = (const policy_node_with_script3_t *) policy; - int ret1 = get_key_placeholder_by_index(r_policy_node(&node->scripts[0]), - i, - out_tapleaf_ptr, - out_placeholder); + int ret1 = get_keyexpr_by_index(r_policy_node(&node->scripts[0]), + i, + out_tapleaf_ptr, + out_keyexpr); if (ret1 < 0) return -1; bool found = i < (unsigned int) ret1; - int ret2 = get_key_placeholder_by_index(r_policy_node(&node->scripts[1]), - found ? 0 : i - ret1, - found ? NULL : out_tapleaf_ptr, - found ? NULL : out_placeholder); + int ret2 = get_keyexpr_by_index(r_policy_node(&node->scripts[1]), + found ? 0 : i - ret1, + found ? NULL : out_tapleaf_ptr, + found ? NULL : out_keyexpr); if (ret2 < 0) return -1; found = i < (unsigned int) (ret1 + ret2); - int ret3 = get_key_placeholder_by_index(r_policy_node(&node->scripts[2]), - found ? 0 : i - ret1 - ret2, - found ? NULL : out_tapleaf_ptr, - found ? NULL : out_placeholder); + int ret3 = get_keyexpr_by_index(r_policy_node(&node->scripts[2]), + found ? 0 : i - ret1 - ret2, + found ? NULL : out_tapleaf_ptr, + found ? NULL : out_keyexpr); if (ret3 < 0) return -1; return ret1 + ret2 + ret3; } @@ -1700,10 +1697,10 @@ int get_key_placeholder_by_index(const policy_node_t *policy, "The script should always have exactly n child scripts"); found = i < (unsigned int) ret; - int ret_partial = get_key_placeholder_by_index(r_policy_node(&cur_child->script), - found ? 0 : i - ret, - found ? NULL : out_tapleaf_ptr, - found ? NULL : out_placeholder); + int ret_partial = get_keyexpr_by_index(r_policy_node(&cur_child->script), + found ? 0 : i - ret, + found ? NULL : out_tapleaf_ptr, + found ? NULL : out_keyexpr); if (ret_partial < 0) return -1; ret += ret_partial; @@ -1726,17 +1723,17 @@ int get_key_placeholder_by_index(const policy_node_t *policy, int count_distinct_keys_info(const policy_node_t *policy) { int ret = -1; - int n_placeholders = get_key_placeholder_by_index(policy, 0, NULL, NULL); - if (n_placeholders < 0) { + int n_key_expressions = get_keyexpr_by_index(policy, 0, NULL, NULL); + if (n_key_expressions < 0) { return -1; } - for (int cur = 0; cur < n_placeholders; ++cur) { - policy_node_keyexpr_t placeholder; - if (0 > get_key_placeholder_by_index(policy, cur, NULL, &placeholder)) { + for (int cur = 0; cur < n_key_expressions; ++cur) { + policy_node_keyexpr_t key_expression; + if (0 > get_keyexpr_by_index(policy, cur, NULL, &key_expression)) { return -1; } - ret = MAX(ret, placeholder.key_index + 1); + ret = MAX(ret, key_expression.key_index + 1); } return ret; } @@ -1910,35 +1907,36 @@ int is_policy_sane(dispatcher_context_t *dispatcher_context, } } - // check that all the key placeholders for the same xpub do indeed have different + // check that all the key expressions for the same xpub do indeed have different // derivations - int n_placeholders = get_key_placeholder_by_index(policy, 0, NULL, NULL); - if (n_placeholders < 0) { - return WITH_ERROR(-1, "Unexpected error while counting placeholders"); + int n_key_expressions = get_keyexpr_by_index(policy, 0, NULL, NULL); + if (n_key_expressions < 0) { + return WITH_ERROR(-1, "Unexpected error while counting key expressions"); } // The following loop computationally very inefficient (quadratic in the number of - // placeholders), but more efficient solutions likely require a substantial amount of RAM - // (proportional to the number of key placeholders). Instead, this only requires stack depth + // key expressions), but more efficient solutions likely require a substantial amount of RAM + // (proportional to the number of key expressions). Instead, this only requires stack depth // proportional to the depth of the wallet policy's abstract syntax tree. - for (int i = 0; i < n_placeholders - 1; - i++) { // no point in running this for the last placeholder + for (int i = 0; i < n_key_expressions - 1; + i++) { // no point in running this for the last key expression policy_node_keyexpr_t kp_i; - if (0 > get_key_placeholder_by_index(policy, i, NULL, &kp_i)) { - return WITH_ERROR(-1, "Unexpected error retrieving placeholders from the policy"); + if (0 > get_keyexpr_by_index(policy, i, NULL, &kp_i)) { + return WITH_ERROR(-1, "Unexpected error retrieving key expressions from the policy"); } - for (int j = i + 1; j < n_placeholders; j++) { + for (int j = i + 1; j < n_key_expressions; j++) { policy_node_keyexpr_t kp_j; - if (0 > get_key_placeholder_by_index(policy, j, NULL, &kp_j)) { - return WITH_ERROR(-1, "Unexpected error retrieving placeholders from the policy"); + if (0 > get_keyexpr_by_index(policy, j, NULL, &kp_j)) { + return WITH_ERROR(-1, + "Unexpected error retrieving key expressions from the policy"); } - // placeholders for the same key must have disjoint derivation options + // key expressions for the same key must have disjoint derivation options if (kp_i.key_index == kp_j.key_index) { if (kp_i.num_first == kp_j.num_first || kp_i.num_first == kp_j.num_second || kp_i.num_second == kp_j.num_first || kp_i.num_second == kp_j.num_second) { return WITH_ERROR(-1, - "Key placeholders with repeated derivations in miniscript"); + "Key expressions with repeated derivations in miniscript"); } } } diff --git a/src/handler/lib/policy.h b/src/handler/lib/policy.h index 68ac71d5a..e5bce4bdc 100644 --- a/src/handler/lib/policy.h +++ b/src/handler/lib/policy.h @@ -46,7 +46,7 @@ typedef struct { // WALLET_POLICY_VERSION_V2 const uint8_t *keys_merkle_root; // The Merkle root of the tree of key informations in the policy - uint32_t n_keys; // The number of key information placeholders in the policy + uint32_t n_keys; // The number of key information elements in the policy size_t address_index; // The address index to use in the derivation bool change; // whether a change address or a receive address is derived sign_psbt_cache_t @@ -179,31 +179,30 @@ bool compute_wallet_hmac(const uint8_t wallet_id[static 32], uint8_t wallet_hmac bool check_wallet_hmac(const uint8_t wallet_id[static 32], const uint8_t wallet_hmac[static 32]); /** - * Copies the i-th placeholder (indexing from 0) of the given policy into `out_placeholder` (if not + * Copies the i-th key expression (indexing from 0) of the given policy into `out_keyexpr` (if not * null). * * @param[in] policy * Pointer to the root node of the policy * @param[in] i - * Index of the wanted placeholder. Ignored if out_placeholder is NULL. + * Index of the wanted key expression. Ignored if out_keyexpr is NULL. * @param[out] out_tapleaf_ptr - * If not NULL, and if the i-th placeholder is in a tapleaf of the policy, receives the pointer to - * the tapleaf's script. - * @param[out] out_placeholder - * If not NULL, it is a pointer that will receive the i-th placeholder of the policy. - * @return the number of placeholders in the policy on success; -1 in case of error. + * If not NULL, and if the i-th key expression is in a tapleaf of the policy, receives the pointer + * to the tapleaf's script. + * @param[out] out_keyexpr + * If not NULL, it is a pointer that will receive the i-th key expression of the policy. + * @return the number of key expressions in the policy on success; -1 in case of error. */ -__attribute__((warn_unused_result)) int get_key_placeholder_by_index( - const policy_node_t *policy, - unsigned int i, - const policy_node_t **out_tapleaf_ptr, - policy_node_keyexpr_t *out_placeholder); +__attribute__((warn_unused_result)) int get_keyexpr_by_index(const policy_node_t *policy, + unsigned int i, + const policy_node_t **out_tapleaf_ptr, + policy_node_keyexpr_t *out_keyexpr); /** * Determines the expected number of unique keys in the provided policy's key information. - * The function calculates this by finding the maximum key index from placeholders and increments it - * by 1. For instance, if the maximum key index found in the placeholders is `n`, then the result - * would be `n + 1`. + * The function calculates this by finding the maximum key index from key expressions and increments + * it by 1. For instance, if the maximum key index found in the key expressions is `n`, then the + * result would be `n + 1`. * * @param[in] policy * Pointer to the root node of the policy diff --git a/src/handler/sign_psbt.c b/src/handler/sign_psbt.c index 1b8a4ae62..da8b860f3 100644 --- a/src/handler/sign_psbt.c +++ b/src/handler/sign_psbt.c @@ -65,7 +65,8 @@ typedef struct { // PSBT_{IN,OUT}_BIP32_DERIVATION or // PSBT_{IN,OUT}_TAP_BIP32_DERIVATION is not the correct length. - bool placeholder_found; // Set to true if a matching placeholder is found in the input info + bool key_expression_found; // Set to true if the input/output info in the psbt was correctly + // matched with the current key expression in the signing flow bool is_change; int address_index; @@ -106,7 +107,7 @@ typedef struct { } output_info_t; typedef struct { - policy_node_keyexpr_t placeholder; + policy_node_keyexpr_t key_expression; int cur_index; uint32_t fingerprint; uint8_t key_derivation_length; @@ -114,7 +115,7 @@ typedef struct { serialized_extended_pubkey_t pubkey; bool is_tapscript; // true if signing with a BIP342 tapleaf script path spend uint8_t tapleaf_hash[32]; // only used for tapscripts -} placeholder_info_t; +} keyexpr_info_t; // Cache for partial hashes during segwit signing (avoid quadratic hashing for segwit transactions) typedef struct { @@ -371,9 +372,10 @@ static int get_amount_scriptpubkey_from_psbt( // Convenience function to share common logic when processing all the // PSBT_{IN|OUT}_{TAP}?_BIP32_DERIVATION fields. +// TODO: not generalized for musig2 static int read_change_and_index_from_psbt_bip32_derivation( dispatcher_context_t *dc, - placeholder_info_t *placeholder_info, + keyexpr_info_t *keyexpr_info, in_out_info_t *in_out, sign_psbt_cache_t *sign_psbt_cache, int psbt_key_type, @@ -416,13 +418,13 @@ static int read_change_and_index_from_psbt_bip32_derivation( return -1; } - // if this derivation path matches the internal placeholder, + // if this derivation path matches the internal key expression, // we use it to detect whether the current input is change or not, // and store its address index - if (fpt_der[0] == placeholder_info->fingerprint && - der_len == placeholder_info->key_derivation_length + 2) { - for (int i = 0; i < placeholder_info->key_derivation_length; i++) { - if (placeholder_info->key_derivation[i] != fpt_der[1 + i]) { + if (fpt_der[0] == keyexpr_info->fingerprint && + der_len == keyexpr_info->key_derivation_length + 2) { + for (int i = 0; i < keyexpr_info->key_derivation_length; i++) { + if (keyexpr_info->key_derivation[i] != fpt_der[1 + i]) { return 0; } } @@ -430,21 +432,21 @@ static int read_change_and_index_from_psbt_bip32_derivation( uint32_t change_step = fpt_der[1 + der_len - 2]; uint32_t addr_index = fpt_der[1 + der_len - 1]; - // check if the 'change' derivation step is indeed coherent with placeholder - if (change_step == placeholder_info->placeholder.num_first) { + // check if the 'change' derivation step is indeed coherent with key expression + if (change_step == keyexpr_info->key_expression.num_first) { in_out->is_change = false; in_out->address_index = addr_index; - } else if (change_step == placeholder_info->placeholder.num_second) { + } else if (change_step == keyexpr_info->key_expression.num_second) { in_out->is_change = true; in_out->address_index = addr_index; } else { return 0; } - // check that we can indeed derive the same key from the current placeholder + // check that we can indeed derive the same key from the current key expression serialized_extended_pubkey_t pubkey; - if (0 > derive_first_step_for_pubkey(&placeholder_info->pubkey, - &placeholder_info->placeholder, + if (0 > derive_first_step_for_pubkey(&keyexpr_info->pubkey, + &keyexpr_info->key_expression, sign_psbt_cache, in_out->is_change, &pubkey)) @@ -456,7 +458,7 @@ static int read_change_and_index_from_psbt_bip32_derivation( return 0; } - in_out->placeholder_found = true; + in_out->key_expression_found = true; return 1; } return 0; @@ -474,9 +476,9 @@ static int is_in_out_internal(dispatcher_context_t *dispatcher_context, sign_psbt_cache_t *sign_psbt_cache, const in_out_info_t *in_out_info, bool is_input) { - // If we did not find any info about the pubkey associated to the placeholder we're considering, - // then it's external - if (!in_out_info->placeholder_found) { + // If we did not find any info about the pubkey associated to the key expression we're + // considering, then it's external + if (!in_out_info->key_expression_found) { return 0; } @@ -661,17 +663,16 @@ init_global_state(dispatcher_context_t *dc, sign_psbt_state_t *st) { return true; } -static bool __attribute__((noinline)) -fill_placeholder_info_if_internal(dispatcher_context_t *dc, - sign_psbt_state_t *st, - placeholder_info_t *placeholder_info) { +static bool __attribute__((noinline)) fill_keyexpr_info_if_internal(dispatcher_context_t *dc, + sign_psbt_state_t *st, + keyexpr_info_t *keyexpr_info) { policy_map_key_info_t key_info; { uint8_t key_info_str[MAX_POLICY_KEY_INFO_LEN]; int key_info_len = call_get_merkle_leaf_element(dc, st->wallet_header.keys_info_merkle_root, st->wallet_header.n_keys, - placeholder_info->placeholder.key_index, + keyexpr_info->key_expression.key_index, key_info_str, sizeof(key_info_str)); @@ -701,56 +702,55 @@ fill_placeholder_info_if_internal(dispatcher_context_t *dc, if (0 > get_extended_pubkey_at_path(key_info.master_key_derivation, key_info.master_key_derivation_len, BIP32_PUBKEY_VERSION, - &placeholder_info->pubkey)) { + &keyexpr_info->pubkey)) { SEND_SW(dc, SW_BAD_STATE); return false; } - if (memcmp(&key_info.ext_pubkey, - &placeholder_info->pubkey, - sizeof(placeholder_info->pubkey)) != 0) { + if (memcmp(&key_info.ext_pubkey, &keyexpr_info->pubkey, sizeof(keyexpr_info->pubkey)) != + 0) { return false; } - placeholder_info->key_derivation_length = key_info.master_key_derivation_len; + keyexpr_info->key_derivation_length = key_info.master_key_derivation_len; for (int i = 0; i < key_info.master_key_derivation_len; i++) { - placeholder_info->key_derivation[i] = key_info.master_key_derivation[i]; + keyexpr_info->key_derivation[i] = key_info.master_key_derivation[i]; } - placeholder_info->fingerprint = read_u32_be(key_info.master_key_fingerprint, 0); + keyexpr_info->fingerprint = read_u32_be(key_info.master_key_fingerprint, 0); } return true; } -// finds the first placeholder that corresponds to an internal key -static bool find_first_internal_key_placeholder(dispatcher_context_t *dc, - sign_psbt_state_t *st, - placeholder_info_t *placeholder_info) { - placeholder_info->cur_index = 0; +// finds the first key expression that corresponds to an internal key +static bool find_first_internal_keyexpr(dispatcher_context_t *dc, + sign_psbt_state_t *st, + keyexpr_info_t *keyexpr_info) { + keyexpr_info->cur_index = 0; // find and parse our registered key info in the wallet while (true) { - int n_key_placeholders = get_key_placeholder_by_index(st->wallet_policy_map, - placeholder_info->cur_index, - NULL, - &placeholder_info->placeholder); - if (n_key_placeholders < 0) { + int n_key_expressions = get_keyexpr_by_index(st->wallet_policy_map, + keyexpr_info->cur_index, + NULL, + &keyexpr_info->key_expression); + if (n_key_expressions < 0) { SEND_SW(dc, SW_BAD_STATE); // should never happen return false; } - if (placeholder_info->cur_index >= n_key_placeholders) { + if (keyexpr_info->cur_index >= n_key_expressions) { // all keys have been processed break; } - if (fill_placeholder_info_if_internal(dc, st, placeholder_info)) { + if (fill_keyexpr_info_if_internal(dc, st, keyexpr_info)) { return true; } // Not an internal key, move on - ++placeholder_info->cur_index; + ++keyexpr_info->cur_index; } PRINTF("No internal key found in wallet policy"); @@ -759,7 +759,7 @@ static bool find_first_internal_key_placeholder(dispatcher_context_t *dc, } typedef struct { - placeholder_info_t *placeholder_info; + keyexpr_info_t *keyexpr_info; input_info_t *input; sign_psbt_cache_t *sign_psbt_cache; } input_keys_callback_data_t; @@ -787,16 +787,15 @@ static void input_keys_callback(dispatcher_context_t *dc, callback_data->input->has_sighash_type = true; } else if ((key_type == PSBT_IN_BIP32_DERIVATION || key_type == PSBT_IN_TAP_BIP32_DERIVATION) && - !callback_data->input->in_out.placeholder_found) { - if (0 > - read_change_and_index_from_psbt_bip32_derivation(dc, - callback_data->placeholder_info, - &callback_data->input->in_out, - callback_data->sign_psbt_cache, - key_type, - data, - map_commitment, - i)) { + !callback_data->input->in_out.key_expression_found) { + if (0 > read_change_and_index_from_psbt_bip32_derivation(dc, + callback_data->keyexpr_info, + &callback_data->input->in_out, + callback_data->sign_psbt_cache, + key_type, + data, + map_commitment, + i)) { callback_data->input->in_out.unexpected_pubkey_error = true; } } @@ -812,10 +811,10 @@ preprocess_inputs(dispatcher_context_t *dc, memset(internal_inputs, 0, BITVECTOR_REAL_SIZE(MAX_N_INPUTS_CAN_SIGN)); - placeholder_info_t placeholder_info; - memset(&placeholder_info, 0, sizeof(placeholder_info)); + keyexpr_info_t keyexpr_info; + memset(&keyexpr_info, 0, sizeof(keyexpr_info)); - if (!find_first_internal_key_placeholder(dc, st, &placeholder_info)) return false; + if (!find_first_internal_keyexpr(dc, st, &keyexpr_info)) return false; // process each input for (unsigned int cur_input_index = 0; cur_input_index < st->n_inputs; cur_input_index++) { @@ -823,7 +822,7 @@ preprocess_inputs(dispatcher_context_t *dc, memset(&input, 0, sizeof(input)); input_keys_callback_data_t callback_data = {.input = &input, - .placeholder_info = &placeholder_info, + .keyexpr_info = &keyexpr_info, .sign_psbt_cache = sign_psbt_cache}; int res = call_get_merkleized_map_with_callback( dc, @@ -1025,7 +1024,7 @@ preprocess_inputs(dispatcher_context_t *dc, } typedef struct { - placeholder_info_t *placeholder_info; + keyexpr_info_t *keyexpr_info; output_info_t *output; sign_psbt_cache_t *sign_psbt_cache; } output_keys_callback_data_t; @@ -1045,16 +1044,15 @@ static void output_keys_callback(dispatcher_context_t *dc, buffer_read_u8(data, &key_type); if ((key_type == PSBT_OUT_BIP32_DERIVATION || key_type == PSBT_OUT_TAP_BIP32_DERIVATION) && - !callback_data->output->in_out.placeholder_found) { - if (0 > - read_change_and_index_from_psbt_bip32_derivation(dc, - callback_data->placeholder_info, - &callback_data->output->in_out, - callback_data->sign_psbt_cache, - key_type, - data, - map_commitment, - i)) { + !callback_data->output->in_out.key_expression_found) { + if (0 > read_change_and_index_from_psbt_bip32_derivation(dc, + callback_data->keyexpr_info, + &callback_data->output->in_out, + callback_data->sign_psbt_cache, + key_type, + data, + map_commitment, + i)) { callback_data->output->in_out.unexpected_pubkey_error = true; } } @@ -1074,10 +1072,10 @@ preprocess_outputs(dispatcher_context_t *dc, LOG_PROCESSOR(__FILE__, __LINE__, __func__); - placeholder_info_t placeholder_info; - memset(&placeholder_info, 0, sizeof(placeholder_info)); + keyexpr_info_t keyexpr_info; + memset(&keyexpr_info, 0, sizeof(keyexpr_info)); - if (!find_first_internal_key_placeholder(dc, st, &placeholder_info)) return false; + if (!find_first_internal_keyexpr(dc, st, &keyexpr_info)) return false; memset(&st->outputs, 0, sizeof(st->outputs)); @@ -1090,7 +1088,7 @@ preprocess_outputs(dispatcher_context_t *dc, memset(&output, 0, sizeof(output)); output_keys_callback_data_t callback_data = {.output = &output, - .placeholder_info = &placeholder_info, + .keyexpr_info = &keyexpr_info, .sign_psbt_cache = sign_psbt_cache}; int res = call_get_merkleized_map_with_callback( dc, @@ -1978,7 +1976,7 @@ static bool __attribute__((noinline)) compute_sighash_segwitv1(dispatcher_contex segwit_hashes_t *hashes, input_info_t *input, unsigned int cur_input_index, - placeholder_info_t *placeholder_info, + keyexpr_info_t *keyexpr_info, uint8_t sighash[static 32]) { LOG_PROCESSOR(__FILE__, __LINE__, __func__); @@ -2013,7 +2011,7 @@ static bool __attribute__((noinline)) compute_sighash_segwitv1(dispatcher_contex } // ext_flag - uint8_t ext_flag = placeholder_info->is_tapscript ? 1 : 0; + uint8_t ext_flag = keyexpr_info->is_tapscript ? 1 : 0; // annex is not supported const uint8_t annex_present = 0; uint8_t spend_type = ext_flag * 2 + annex_present; @@ -2097,9 +2095,9 @@ static bool __attribute__((noinline)) compute_sighash_segwitv1(dispatcher_contex crypto_hash_update(&sighash_context.header, tmp, 32); } - if (placeholder_info->is_tapscript) { + if (keyexpr_info->is_tapscript) { // If spending a tapscript, append the Common Signature Message Extension per BIP-0342 - crypto_hash_update(&sighash_context.header, placeholder_info->tapleaf_hash, 32); + crypto_hash_update(&sighash_context.header, keyexpr_info->tapleaf_hash, 32); crypto_hash_update_u8(&sighash_context.header, 0x00); // key_version crypto_hash_update_u32(&sighash_context.header, 0xffffffff); // no OP_CODESEPARATOR } @@ -2151,25 +2149,24 @@ static bool __attribute__((noinline)) yield_signature(dispatcher_context_t *dc, return true; } -static bool __attribute__((noinline)) -sign_sighash_ecdsa_and_yield(dispatcher_context_t *dc, - sign_psbt_state_t *st, - placeholder_info_t *placeholder_info, - input_info_t *input, - unsigned int cur_input_index, - uint8_t sighash[static 32]) { +static bool __attribute__((noinline)) sign_sighash_ecdsa_and_yield(dispatcher_context_t *dc, + sign_psbt_state_t *st, + keyexpr_info_t *keyexpr_info, + input_info_t *input, + unsigned int cur_input_index, + uint8_t sighash[static 32]) { LOG_PROCESSOR(__FILE__, __LINE__, __func__); uint32_t sign_path[MAX_BIP32_PATH_STEPS]; - for (int i = 0; i < placeholder_info->key_derivation_length; i++) { - sign_path[i] = placeholder_info->key_derivation[i]; + for (int i = 0; i < keyexpr_info->key_derivation_length; i++) { + sign_path[i] = keyexpr_info->key_derivation[i]; } - sign_path[placeholder_info->key_derivation_length] = - input->in_out.is_change ? placeholder_info->placeholder.num_second - : placeholder_info->placeholder.num_first; - sign_path[placeholder_info->key_derivation_length + 1] = input->in_out.address_index; + sign_path[keyexpr_info->key_derivation_length] = input->in_out.is_change + ? keyexpr_info->key_expression.num_second + : keyexpr_info->key_expression.num_first; + sign_path[keyexpr_info->key_derivation_length + 1] = input->in_out.address_index; - int sign_path_len = placeholder_info->key_derivation_length + 2; + int sign_path_len = keyexpr_info->key_derivation_length + 2; uint8_t sig[MAX_DER_SIG_LEN + 1]; // extra byte for the appended sighash-type @@ -2196,13 +2193,12 @@ sign_sighash_ecdsa_and_yield(dispatcher_context_t *dc, return true; } -static bool __attribute__((noinline)) -sign_sighash_schnorr_and_yield(dispatcher_context_t *dc, - sign_psbt_state_t *st, - placeholder_info_t *placeholder_info, - input_info_t *input, - unsigned int cur_input_index, - uint8_t sighash[static 32]) { +static bool __attribute__((noinline)) sign_sighash_schnorr_and_yield(dispatcher_context_t *dc, + sign_psbt_state_t *st, + keyexpr_info_t *keyexpr_info, + input_info_t *input, + unsigned int cur_input_index, + uint8_t sighash[static 32]) { LOG_PROCESSOR(__FILE__, __LINE__, __func__); if (st->wallet_policy_map->type != TOKEN_TR) { @@ -2230,15 +2226,15 @@ sign_sighash_schnorr_and_yield(dispatcher_context_t *dc, uint32_t sign_path[MAX_BIP32_PATH_STEPS]; - for (int i = 0; i < placeholder_info->key_derivation_length; i++) { - sign_path[i] = placeholder_info->key_derivation[i]; + for (int i = 0; i < keyexpr_info->key_derivation_length; i++) { + sign_path[i] = keyexpr_info->key_derivation[i]; } - sign_path[placeholder_info->key_derivation_length] = - input->in_out.is_change ? placeholder_info->placeholder.num_second - : placeholder_info->placeholder.num_first; - sign_path[placeholder_info->key_derivation_length + 1] = input->in_out.address_index; + sign_path[keyexpr_info->key_derivation_length] = + input->in_out.is_change ? keyexpr_info->key_expression.num_second + : keyexpr_info->key_expression.num_first; + sign_path[keyexpr_info->key_derivation_length + 1] = input->in_out.address_index; - int sign_path_len = placeholder_info->key_derivation_length + 2; + int sign_path_len = keyexpr_info->key_derivation_length + 2; if (bip32_derive_init_privkey_256(CX_CURVE_256K1, sign_path, @@ -2251,7 +2247,7 @@ sign_sighash_schnorr_and_yield(dispatcher_context_t *dc, policy_node_tr_t *policy = (policy_node_tr_t *) st->wallet_policy_map; - if (!placeholder_info->is_tapscript) { + if (!keyexpr_info->is_tapscript) { if (isnull_policy_node_tree(&policy->tree)) { // tweak as specified in BIP-86 and BIP-386 crypto_tr_tweak_seckey(seckey, (uint8_t[]){}, 0, seckey); @@ -2263,7 +2259,7 @@ sign_sighash_schnorr_and_yield(dispatcher_context_t *dc, } } else { // tapscript, we need to yield the tapleaf hash together with the pubkey - tapleaf_hash = placeholder_info->tapleaf_hash; + tapleaf_hash = keyexpr_info->tapleaf_hash; } // generate corresponding public key @@ -2451,7 +2447,7 @@ static bool __attribute__((noinline)) sign_transaction_input(dispatcher_context_ sign_psbt_state_t *st, sign_psbt_cache_t *sign_psbt_cache, segwit_hashes_t *hashes, - placeholder_info_t *placeholder_info, + keyexpr_info_t *keyexpr_info, input_info_t *input, unsigned int cur_input_index) { LOG_PROCESSOR(__FILE__, __LINE__, __func__); @@ -2497,12 +2493,7 @@ static bool __attribute__((noinline)) sign_transaction_input(dispatcher_context_ uint8_t sighash[32]; if (!compute_sighash_legacy(dc, st, input, cur_input_index, sighash)) return false; - if (!sign_sighash_ecdsa_and_yield(dc, - st, - placeholder_info, - input, - cur_input_index, - sighash)) + if (!sign_sighash_ecdsa_and_yield(dc, st, keyexpr_info, input, cur_input_index, sighash)) return false; } else { { @@ -2569,7 +2560,7 @@ static bool __attribute__((noinline)) sign_transaction_input(dispatcher_context_ if (!sign_sighash_ecdsa_and_yield(dc, st, - placeholder_info, + keyexpr_info, input, cur_input_index, sighash)) @@ -2585,12 +2576,12 @@ static bool __attribute__((noinline)) sign_transaction_input(dispatcher_context_ hashes, input, cur_input_index, - placeholder_info, + keyexpr_info, sighash)) return false; policy_node_tr_t *policy = (policy_node_tr_t *) st->wallet_policy_map; - if (!placeholder_info->is_tapscript && !isnull_policy_node_tree(&policy->tree)) { + if (!keyexpr_info->is_tapscript && !isnull_policy_node_tree(&policy->tree)) { // keypath spend, we compute the taptree hash so that we find it ready // later in sign_sighash_schnorr_and_yield (which has less available stack). if (0 > compute_taptree_hash( @@ -2612,7 +2603,7 @@ static bool __attribute__((noinline)) sign_transaction_input(dispatcher_context_ if (!sign_sighash_schnorr_and_yield(dc, st, - placeholder_info, + keyexpr_info, input, cur_input_index, sighash)) @@ -2627,12 +2618,12 @@ static bool __attribute__((noinline)) sign_transaction_input(dispatcher_context_ } static bool __attribute__((noinline)) -fill_taproot_placeholder_info(dispatcher_context_t *dc, - sign_psbt_state_t *st, - const input_info_t *input, - const policy_node_t *tapleaf_ptr, - placeholder_info_t *placeholder_info, - sign_psbt_cache_t *sign_psbt_cache) { +fill_taproot_keyexpr_info(dispatcher_context_t *dc, + sign_psbt_state_t *st, + const input_info_t *input, + const policy_node_t *tapleaf_ptr, + keyexpr_info_t *keyexpr_info, + sign_psbt_cache_t *sign_psbt_cache) { cx_sha256_t hash_context; crypto_tr_tapleaf_hash_init(&hash_context); @@ -2663,7 +2654,7 @@ fill_taproot_placeholder_info(dispatcher_context_t *dc, &hash_context.header)) { return false; // should never happen! } - crypto_hash_digest(&hash_context.header, placeholder_info->tapleaf_hash, 32); + crypto_hash_digest(&hash_context.header, keyexpr_info->tapleaf_hash, 32); return true; } @@ -2675,7 +2666,7 @@ sign_transaction(dispatcher_context_t *dc, const uint8_t internal_inputs[static BITVECTOR_REAL_SIZE(MAX_N_INPUTS_CAN_SIGN)]) { LOG_PROCESSOR(__FILE__, __LINE__, __func__); - int placeholder_index = 0; + int key_expression_index = 0; segwit_hashes_t hashes; @@ -2687,42 +2678,41 @@ sign_transaction(dispatcher_context_t *dc, return false; } - // Iterate over all the placeholders that correspond to keys owned by us + // Iterate over all the key expressions that correspond to keys owned by us while (true) { - placeholder_info_t placeholder_info; - memset(&placeholder_info, 0, sizeof(placeholder_info)); + keyexpr_info_t keyexpr_info; + memset(&keyexpr_info, 0, sizeof(keyexpr_info)); const policy_node_t *tapleaf_ptr = NULL; - int n_key_placeholders = get_key_placeholder_by_index(st->wallet_policy_map, - placeholder_index, - &tapleaf_ptr, - &placeholder_info.placeholder); + int n_key_expressions = get_keyexpr_by_index(st->wallet_policy_map, + key_expression_index, + &tapleaf_ptr, + &keyexpr_info.key_expression); - if (n_key_placeholders < 0) { + if (n_key_expressions < 0) { SEND_SW(dc, SW_BAD_STATE); // should never happen return false; } - if (placeholder_index >= n_key_placeholders) { - // all placeholders were processed + if (key_expression_index >= n_key_expressions) { + // all key expressions were processed break; } if (tapleaf_ptr != NULL) { - // get_key_placeholder_by_index returns the pointer to the tapleaf only if the key being + // get_keyexpr_by_index returns the pointer to the tapleaf only if the key being // spent is indeed in a tapleaf - placeholder_info.is_tapscript = true; + keyexpr_info.is_tapscript = true; } - if (fill_placeholder_info_if_internal(dc, st, &placeholder_info) == true) { + if (fill_keyexpr_info_if_internal(dc, st, &keyexpr_info) == true) { for (unsigned int i = 0; i < st->n_inputs; i++) if (bitvector_get(internal_inputs, i)) { input_info_t input; memset(&input, 0, sizeof(input)); - input_keys_callback_data_t callback_data = { - .input = &input, - .placeholder_info = &placeholder_info}; + input_keys_callback_data_t callback_data = {.input = &input, + .keyexpr_info = &keyexpr_info}; int res = call_get_merkleized_map_with_callback( dc, (void *) &callback_data, @@ -2736,19 +2726,19 @@ sign_transaction(dispatcher_context_t *dc, return false; } - if (tapleaf_ptr != NULL && !fill_taproot_placeholder_info(dc, - st, - &input, - tapleaf_ptr, - &placeholder_info, - sign_psbt_cache)) + if (tapleaf_ptr != NULL && !fill_taproot_keyexpr_info(dc, + st, + &input, + tapleaf_ptr, + &keyexpr_info, + sign_psbt_cache)) return false; if (!sign_transaction_input(dc, st, sign_psbt_cache, &hashes, - &placeholder_info, + &keyexpr_info, &input, i)) { // we do not send a status word, since sign_transaction_input @@ -2758,7 +2748,7 @@ sign_transaction(dispatcher_context_t *dc, } } - ++placeholder_index; + ++key_expression_index; } return true; @@ -2824,7 +2814,7 @@ void handler_sign_psbt(dispatcher_context_t *dc, uint8_t protocol_version) { /** SIGNING FLOW * - * For each internal placeholder, and for each internal input, sign using the + * For each internal key expression, and for each internal input, sign using the * appropriate algorithm. */ int sign_result = sign_transaction(dc, &st, &cache, internal_inputs); diff --git a/src/handler/sign_psbt/sign_psbt_cache.c b/src/handler/sign_psbt/sign_psbt_cache.c index 30803afaa..00bf8b027 100644 --- a/src/handler/sign_psbt/sign_psbt_cache.c +++ b/src/handler/sign_psbt/sign_psbt_cache.c @@ -1,34 +1,32 @@ #include "sign_psbt_cache.h" int derive_first_step_for_pubkey(const serialized_extended_pubkey_t *base_key, - const policy_node_keyexpr_t *placeholder, + const policy_node_keyexpr_t *keyexpr, sign_psbt_cache_t *cache, bool is_change, serialized_extended_pubkey_t *out_pubkey) { - uint32_t change_step = is_change ? placeholder->num_second : placeholder->num_first; + uint32_t change_step = is_change ? keyexpr->num_second : keyexpr->num_first; // make sure a cache was provided, and the index is less than the size of the cache - if (placeholder->placeholder_index >= MAX_CACHED_KEY_EXPRESSIONS || !cache) { + if (keyexpr->keyexpr_index >= MAX_CACHED_KEY_EXPRESSIONS || !cache) { // do not use the cache, derive the key directly return bip32_CKDpub(base_key, change_step, out_pubkey); } - if (!cache->derived_child[placeholder->placeholder_index] - .is_child_pubkey_initialized[is_change]) { + if (!cache->derived_child[keyexpr->keyexpr_index].is_child_pubkey_initialized[is_change]) { // key not in cache; compute it and store it in the cache - if (0 > bip32_CKDpub( - base_key, - change_step, - &cache->derived_child[placeholder->placeholder_index].child_pubkeys[is_change])) + if (0 > + bip32_CKDpub(base_key, + change_step, + &cache->derived_child[keyexpr->keyexpr_index].child_pubkeys[is_change])) return -1; - cache->derived_child[placeholder->placeholder_index] - .is_child_pubkey_initialized[is_change] = true; + cache->derived_child[keyexpr->keyexpr_index].is_child_pubkey_initialized[is_change] = true; } // now that we are guaranteed that the key is in cache, we just copy it memcpy(out_pubkey, - &cache->derived_child[placeholder->placeholder_index].child_pubkeys[is_change], + &cache->derived_child[keyexpr->keyexpr_index].child_pubkeys[is_change], sizeof(serialized_extended_pubkey_t)); return 0; diff --git a/src/handler/sign_psbt/sign_psbt_cache.h b/src/handler/sign_psbt/sign_psbt_cache.h index 5baf44ecd..fb87c73da 100644 --- a/src/handler/sign_psbt/sign_psbt_cache.h +++ b/src/handler/sign_psbt/sign_psbt_cache.h @@ -39,12 +39,12 @@ number of BIP-32 derivations is cut almost by half when using the cache. */ /** - * Derives the first step for a public key in a placeholder, using a precomputed value from the + * Derives the first step for a public key in a key expression, using a precomputed value from the * cache if available. If the key is not in the cache, it is computed and stored in the cache, - * unless the index is placeholder index is too large. + * unless the key expression index is too large. * * @param[in] base_key Pointer to the base serialized extended public key. - * @param[in] placeholder Pointer to the policy node key placeholder, which contains derivation + * @param[in] keyexpr Pointer to the policy node key expression, which contains derivation * information. * @param[in] cache Pointer to the cache structure used to store derived child keys. * @param[in] is_change true if deriving the change address, false otherwise. @@ -53,7 +53,7 @@ number of BIP-32 derivations is cut almost by half when using the cache. * @return 0 on success, -1 on failure. */ int derive_first_step_for_pubkey(const serialized_extended_pubkey_t *base_key, - const policy_node_keyexpr_t *placeholder, + const policy_node_keyexpr_t *keyexpr, sign_psbt_cache_t *cache, bool is_change, - serialized_extended_pubkey_t *out_pubkey); \ No newline at end of file + serialized_extended_pubkey_t *out_pubkey); diff --git a/unit-tests/test_wallet.c b/unit-tests/test_wallet.c index c330f430e..406c4a609 100644 --- a/unit-tests/test_wallet.c +++ b/unit-tests/test_wallet.c @@ -33,7 +33,7 @@ static int parse_policy(const char *descriptor_template, uint8_t *out, size_t ou #define MAX_WALLET_POLICY_MEMORY_SIZE 512 // convenience function to compactly check common assertions on a pointer to a key expression with a -// single placeholder +// single key static void check_key_expr_plain(const policy_node_keyexpr_t *ptr, int key_index, uint32_t num_first, @@ -72,7 +72,7 @@ static void test_parse_policy_map_singlesig_1(void **state) { policy_node_with_key_t *node_1 = (policy_node_with_key_t *) out; assert_int_equal(node_1->base.type, TOKEN_PKH); - check_key_expr_plain(r_policy_node_keyexpr(&node_1->key_placeholder), 0, 0, 1); + check_key_expr_plain(r_policy_node_keyexpr(&node_1->key), 0, 0, 1); } static void test_parse_policy_map_singlesig_2(void **state) { @@ -90,7 +90,7 @@ static void test_parse_policy_map_singlesig_2(void **state) { policy_node_with_key_t *inner = (policy_node_with_key_t *) r_policy_node(&root->script); assert_int_equal(inner->base.type, TOKEN_WPKH); - check_key_expr_plain(r_policy_node_keyexpr(&inner->key_placeholder), 0, 0, 1); + check_key_expr_plain(r_policy_node_keyexpr(&inner->key), 0, 0, 1); } static void test_parse_policy_map_singlesig_3(void **state) { @@ -112,7 +112,7 @@ static void test_parse_policy_map_singlesig_3(void **state) { policy_node_with_key_t *inner = (policy_node_with_key_t *) r_policy_node(&mid->script); assert_int_equal(inner->base.type, TOKEN_PKH); - check_key_expr_plain(r_policy_node_keyexpr(&inner->key_placeholder), 0, 0, 1); + check_key_expr_plain(r_policy_node_keyexpr(&inner->key), 0, 0, 1); } static void test_parse_policy_map_multisig_1(void **state) { @@ -128,9 +128,9 @@ static void test_parse_policy_map_multisig_1(void **state) { assert_int_equal(node_1->base.type, TOKEN_SORTEDMULTI); assert_int_equal(node_1->k, 2); assert_int_equal(node_1->n, 3); - check_key_expr_plain(&r_policy_node_keyexpr(&node_1->key_placeholders)[0], 0, 0, 1); - check_key_expr_plain(&r_policy_node_keyexpr(&node_1->key_placeholders)[1], 1, 0, 1); - check_key_expr_plain(&r_policy_node_keyexpr(&node_1->key_placeholders)[2], 2, 0, 1); + check_key_expr_plain(&r_policy_node_keyexpr(&node_1->keys)[0], 0, 0, 1); + check_key_expr_plain(&r_policy_node_keyexpr(&node_1->keys)[1], 1, 0, 1); + check_key_expr_plain(&r_policy_node_keyexpr(&node_1->keys)[2], 2, 0, 1); } static void test_parse_policy_map_multisig_2(void **state) { @@ -151,7 +151,7 @@ static void test_parse_policy_map_multisig_2(void **state) { assert_int_equal(inner->k, 3); assert_int_equal(inner->n, 5); for (int i = 0; i < 5; i++) { - check_key_expr_plain(&r_policy_node_keyexpr(&inner->key_placeholders)[i], i, 0, 1); + check_key_expr_plain(&r_policy_node_keyexpr(&inner->keys)[i], i, 0, 1); } } @@ -177,7 +177,7 @@ static void test_parse_policy_map_multisig_3(void **state) { assert_int_equal(inner->k, 3); assert_int_equal(inner->n, 5); for (int i = 0; i < 5; i++) { - check_key_expr_plain(&r_policy_node_keyexpr(&inner->key_placeholders)[i], i, 0, 1); + check_key_expr_plain(&r_policy_node_keyexpr(&inner->keys)[i], i, 0, 1); } } @@ -194,7 +194,7 @@ static void test_parse_policy_tr(void **state) { policy_node_tr_t *root = (policy_node_tr_t *) out; assert_true(isnull_policy_node_tree(&root->tree)); - check_key_expr_plain(r_policy_node_keyexpr(&root->key_placeholder), 0, 0, 1); + check_key_expr_plain(r_policy_node_keyexpr(&root->key), 0, 0, 1); // Simple tr with a TREE that is a simple script res = parse_policy("tr(@0/**,pk(@1/**))", out, sizeof(out)); @@ -202,7 +202,7 @@ static void test_parse_policy_tr(void **state) { assert_true(res >= 0); root = (policy_node_tr_t *) out; - check_key_expr_plain(r_policy_node_keyexpr(&root->key_placeholder), 0, 0, 1); + check_key_expr_plain(r_policy_node_keyexpr(&root->key), 0, 0, 1); assert_int_equal(r_policy_node_tree(&root->tree)->is_leaf, true); @@ -210,7 +210,7 @@ static void test_parse_policy_tr(void **state) { (policy_node_with_key_t *) r_policy_node(&r_policy_node_tree(&root->tree)->script); assert_int_equal(tapscript->base.type, TOKEN_PK); - check_key_expr_plain(r_policy_node_keyexpr(&tapscript->key_placeholder), 1, 0, 1); + check_key_expr_plain(r_policy_node_keyexpr(&tapscript->key), 1, 0, 1); // Simple tr with a TREE with two tapleaves res = parse_policy("tr(@0/**,{pk(@1/**),pk(@2/<5;7>/*)})", out, sizeof(out)); @@ -218,7 +218,7 @@ static void test_parse_policy_tr(void **state) { assert_true(res >= 0); root = (policy_node_tr_t *) out; - check_key_expr_plain(r_policy_node_keyexpr(&root->key_placeholder), 0, 0, 1); + check_key_expr_plain(r_policy_node_keyexpr(&root->key), 0, 0, 1); policy_node_tree_t *taptree = r_policy_node_tree(&root->tree); @@ -231,7 +231,7 @@ static void test_parse_policy_tr(void **state) { (policy_node_with_key_t *) r_policy_node(&taptree_left->script); assert_int_equal(tapscript_left->base.type, TOKEN_PK); - check_key_expr_plain(r_policy_node_keyexpr(&tapscript_left->key_placeholder), 1, 0, 1); + check_key_expr_plain(r_policy_node_keyexpr(&tapscript_left->key), 1, 0, 1); policy_node_tree_t *taptree_right = (policy_node_tree_t *) r_policy_node_tree(&taptree->right_tree); @@ -240,7 +240,7 @@ static void test_parse_policy_tr(void **state) { (policy_node_with_key_t *) r_policy_node(&taptree_right->script); assert_int_equal(tapscript_right->base.type, TOKEN_PK); - check_key_expr_plain(r_policy_node_keyexpr(&tapscript_right->key_placeholder), 2, 5, 7); + check_key_expr_plain(r_policy_node_keyexpr(&tapscript_right->key), 2, 5, 7); } static void test_parse_policy_tr_multisig(void **state) { @@ -258,9 +258,9 @@ static void test_parse_policy_tr_multisig(void **state) { policy_node_tr_t *root = (policy_node_tr_t *) out; - assert_int_equal(r_policy_node_keyexpr(&root->key_placeholder)->key_index, 0); - assert_int_equal(r_policy_node_keyexpr(&root->key_placeholder)->num_first, 0); - assert_int_equal(r_policy_node_keyexpr(&root->key_placeholder)->num_second, 1); + assert_int_equal(r_policy_node_keyexpr(&root->key)->key_index, 0); + assert_int_equal(r_policy_node_keyexpr(&root->key)->num_first, 0); + assert_int_equal(r_policy_node_keyexpr(&root->key)->num_second, 1); policy_node_tree_t *taptree = r_policy_node_tree(&root->tree); @@ -275,8 +275,8 @@ static void test_parse_policy_tr_multisig(void **state) { assert_int_equal(tapscript_left->base.type, TOKEN_MULTI_A); assert_int_equal(tapscript_left->k, 1); assert_int_equal(tapscript_left->n, 2); - check_key_expr_plain(&r_policy_node_keyexpr(&tapscript_left->key_placeholders)[0], 1, 0, 1); - check_key_expr_plain(&r_policy_node_keyexpr(&tapscript_left->key_placeholders)[1], 2, 0, 1); + check_key_expr_plain(&r_policy_node_keyexpr(&tapscript_left->keys)[0], 1, 0, 1); + check_key_expr_plain(&r_policy_node_keyexpr(&tapscript_left->keys)[1], 2, 0, 1); policy_node_tree_t *taptree_right = (policy_node_tree_t *) r_policy_node_tree(&taptree->right_tree); @@ -287,9 +287,9 @@ static void test_parse_policy_tr_multisig(void **state) { assert_int_equal(tapscript_right->base.type, TOKEN_SORTEDMULTI_A); assert_int_equal(tapscript_right->k, 2); assert_int_equal(tapscript_right->n, 3); - check_key_expr_plain(&r_policy_node_keyexpr(&tapscript_right->key_placeholders)[0], 3, 0, 1); - check_key_expr_plain(&r_policy_node_keyexpr(&tapscript_right->key_placeholders)[1], 4, 0, 1); - check_key_expr_plain(&r_policy_node_keyexpr(&tapscript_right->key_placeholders)[2], 5, 0, 1); + check_key_expr_plain(&r_policy_node_keyexpr(&tapscript_right->keys)[0], 3, 0, 1); + check_key_expr_plain(&r_policy_node_keyexpr(&tapscript_right->keys)[1], 4, 0, 1); + check_key_expr_plain(&r_policy_node_keyexpr(&tapscript_right->keys)[2], 5, 0, 1); } static void test_parse_policy_tr_musig_keypath(void **state) { @@ -306,11 +306,7 @@ static void test_parse_policy_tr_musig_keypath(void **state) { assert_int_equal(root->base.type, TOKEN_TR); assert_true(isnull_policy_node_tree(&root->tree)); - check_key_expr_musig(r_policy_node_keyexpr(&root->key_placeholder), - 3, - (uint16_t[]){2, 0, 1}, - 3, - 13); + check_key_expr_musig(r_policy_node_keyexpr(&root->key), 3, (uint16_t[]){2, 0, 1}, 3, 13); } static void test_parse_policy_tr_musig_scriptpath(void **state) { @@ -334,11 +330,7 @@ static void test_parse_policy_tr_musig_scriptpath(void **state) { policy_node_with_key_t *script_pk = (policy_node_with_key_t *) r_policy_node(&tree->script); assert_int_equal(script_pk->base.type, TOKEN_PK); - check_key_expr_musig(r_policy_node_keyexpr(&script_pk->key_placeholder), - 3, - (uint16_t[]){2, 0, 3}, - 0, - 1); + check_key_expr_musig(r_policy_node_keyexpr(&script_pk->key), 3, (uint16_t[]){2, 0, 3}, 0, 1); } static void test_get_policy_segwit_version(void **state) { From 8e249947da19fdcaea5edb2c9c3e11bd7cad19f8 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Wed, 9 Oct 2024 09:46:34 +0200 Subject: [PATCH 03/45] Refactored policy_node_keyexpr_t to explicitly label which of the union type is used; generalized some parts of the code that are not generalized to musig key expressions, and annotated some others. --- src/common/wallet.c | 4 +-- src/common/wallet.h | 8 ++++-- src/handler/lib/policy.c | 60 +++++++++++++++++++++++++--------------- src/handler/lib/policy.h | 5 ++-- src/handler/sign_psbt.c | 39 ++++++++++++++------------ unit-tests/test_wallet.c | 6 ++-- 6 files changed, 73 insertions(+), 49 deletions(-) diff --git a/src/common/wallet.c b/src/common/wallet.c index 628d1277a..17f41d3a4 100644 --- a/src/common/wallet.c +++ b/src/common/wallet.c @@ -458,7 +458,7 @@ static int parse_keyexpr(buffer_t *in_buf, return WITH_ERROR(-1, "The key index in a placeholder must be at most 32767"); } - out->key_index = (int16_t) k; + out->k.key_index = (int16_t) k; } else if (c == 'm') { // parse a musig(key1,...,keyn) expression, where each key is a key expression if (!consume_characters(in_buf, "usig(", 5)) { @@ -534,7 +534,7 @@ static int parse_keyexpr(buffer_t *in_buf, musig_info->n = n_musig_keys; i_uint16(&musig_info->key_indexes, key_indexes); - i_musig_aggr_key_info(&out->musig_info, musig_info); + i_musig_aggr_key_info(&out->m.musig_info, musig_info); } else { return WITH_ERROR(-1, "Expected key expression starting with '@', or musig"); } diff --git a/src/common/wallet.h b/src/common/wallet.h index 9c0dc1aef..84db3ecf4 100644 --- a/src/common/wallet.h +++ b/src/common/wallet.h @@ -319,9 +319,13 @@ typedef struct { KeyExpressionType type; union { // type == 0 - int16_t key_index; // index of the key (common between V1 and V2) + struct { + int16_t key_index; // index of the key (common between V1 and V2) + } k; // type == 1 - rptr_musig_aggr_key_info_t musig_info; // only used in V2 + struct { + rptr_musig_aggr_key_info_t musig_info; // only used in V2 + } m; }; int16_t keyexpr_index; // index of the key expression in the descriptor template, in parsing order diff --git a/src/handler/lib/policy.c b/src/handler/lib/policy.c index e4875dae3..eb536180b 100644 --- a/src/handler/lib/policy.c +++ b/src/handler/lib/policy.c @@ -462,7 +462,12 @@ __attribute__((warn_unused_result)) static int get_derived_pubkey( serialized_extended_pubkey_t ext_pubkey; - int ret = get_extended_pubkey(dispatcher_context, wdi, key_expr->key_index, &ext_pubkey); + if (key_expr->type != KEY_EXPRESSION_NORMAL) { + PRINTF("Not implemented\n"); // TODO + return -1; + } + + int ret = get_extended_pubkey(dispatcher_context, wdi, key_expr->k.key_index, &ext_pubkey); if (ret < 0) { return -1; } @@ -1382,7 +1387,12 @@ static int get_bip44_purpose(const policy_node_t *descriptor_template) { return -1; } - if (kp->key_index != 0 || kp->num_first != 0 || kp->num_second != 1) { + if (kp->type != KEY_EXPRESSION_NORMAL) { + // any key expression that is not a plain xpub is not BIP-44 compliant + return -1; + } + + if (kp->k.key_index != 0 || kp->num_first != 0 || kp->num_second != 1) { return -1; } @@ -1514,7 +1524,7 @@ bool check_wallet_hmac(const uint8_t wallet_id[static 32], const uint8_t wallet_ static int get_keyexpr_by_index_in_tree(const policy_node_tree_t *tree, unsigned int i, const policy_node_t **out_tapleaf_ptr, - policy_node_keyexpr_t *out_keyexpr) { + policy_node_keyexpr_t **out_keyexpr) { if (tree->is_leaf) { int ret = get_keyexpr_by_index(r_policy_node(&tree->script), i, NULL, out_keyexpr); if (ret >= 0 && out_tapleaf_ptr != NULL && i < (unsigned) ret) { @@ -1540,16 +1550,12 @@ static int get_keyexpr_by_index_in_tree(const policy_node_tree_t *tree, } } -// TODO: generalize for musig. Note that this is broken for musig, as out_keyexpr -// can't be filled in for musig key expressions (as it's dynamic and contains -// relative pointers). We should probably refactor to return the pointer to the -// key expression and removing the out_keyexpr argument. int get_keyexpr_by_index(const policy_node_t *policy, unsigned int i, const policy_node_t **out_tapleaf_ptr, - policy_node_keyexpr_t *out_keyexpr) { + policy_node_keyexpr_t **out_keyexpr) { // make sure that out_keyexpr is a valid pointer, if the output is not needed - policy_node_keyexpr_t tmp; + policy_node_keyexpr_t *tmp; if (out_keyexpr == NULL) { out_keyexpr = &tmp; } @@ -1574,16 +1580,14 @@ int get_keyexpr_by_index(const policy_node_t *policy, case TOKEN_WPKH: { if (i == 0) { policy_node_with_key_t *wpkh = (policy_node_with_key_t *) policy; - memcpy(out_keyexpr, - r_policy_node_keyexpr(&wpkh->key), - sizeof(policy_node_keyexpr_t)); + *out_keyexpr = r_policy_node_keyexpr(&wpkh->key); } return 1; } case TOKEN_TR: { policy_node_tr_t *tr = (policy_node_tr_t *) policy; if (i == 0) { - memcpy(out_keyexpr, r_policy_node_keyexpr(&tr->key), sizeof(policy_node_keyexpr_t)); + *out_keyexpr = r_policy_node_keyexpr(&tr->key); } if (!isnull_policy_node_tree(&tr->tree)) { int ret_tree = get_keyexpr_by_index_in_tree( @@ -1610,7 +1614,7 @@ int get_keyexpr_by_index(const policy_node_t *policy, if (i < (unsigned int) node->n) { policy_node_keyexpr_t *key_expressions = r_policy_node_keyexpr(&node->keys); - memcpy(out_keyexpr, &key_expressions[i], sizeof(policy_node_keyexpr_t)); + *out_keyexpr = &key_expressions[i]; } return node->n; @@ -1722,18 +1726,28 @@ int get_keyexpr_by_index(const policy_node_t *policy, int count_distinct_keys_info(const policy_node_t *policy) { int ret = -1; - + policy_node_keyexpr_t *key_expression_ptr; int n_key_expressions = get_keyexpr_by_index(policy, 0, NULL, NULL); if (n_key_expressions < 0) { return -1; } for (int cur = 0; cur < n_key_expressions; ++cur) { - policy_node_keyexpr_t key_expression; - if (0 > get_keyexpr_by_index(policy, cur, NULL, &key_expression)) { + if (0 > get_keyexpr_by_index(policy, cur, NULL, &key_expression_ptr)) { return -1; } - ret = MAX(ret, key_expression.key_index + 1); + if (key_expression_ptr->type == KEY_EXPRESSION_NORMAL) { + ret = MAX(ret, key_expression_ptr->k.key_index + 1); + } else if (key_expression_ptr->type == KEY_EXPRESSION_MUSIG) { + musig_aggr_key_info_t *musig_info = + r_musig_aggr_key_info(&key_expression_ptr->m.musig_info); + uint16_t *key_indexes = r_uint16(&musig_info->key_indexes); + for (int i = 0; i < musig_info->n; i++) { + ret = MAX(ret, key_indexes[i] + 1); + } + } else { + LEDGER_ASSERT(false, "Unknown key expression type"); + } } return ret; } @@ -1920,21 +1934,21 @@ int is_policy_sane(dispatcher_context_t *dispatcher_context, // proportional to the depth of the wallet policy's abstract syntax tree. for (int i = 0; i < n_key_expressions - 1; i++) { // no point in running this for the last key expression - policy_node_keyexpr_t kp_i; + policy_node_keyexpr_t *kp_i; if (0 > get_keyexpr_by_index(policy, i, NULL, &kp_i)) { return WITH_ERROR(-1, "Unexpected error retrieving key expressions from the policy"); } for (int j = i + 1; j < n_key_expressions; j++) { - policy_node_keyexpr_t kp_j; + policy_node_keyexpr_t *kp_j; if (0 > get_keyexpr_by_index(policy, j, NULL, &kp_j)) { return WITH_ERROR(-1, "Unexpected error retrieving key expressions from the policy"); } // key expressions for the same key must have disjoint derivation options - if (kp_i.key_index == kp_j.key_index) { - if (kp_i.num_first == kp_j.num_first || kp_i.num_first == kp_j.num_second || - kp_i.num_second == kp_j.num_first || kp_i.num_second == kp_j.num_second) { + if (kp_i->k.key_index == kp_j->k.key_index) { + if (kp_i->num_first == kp_j->num_first || kp_i->num_first == kp_j->num_second || + kp_i->num_second == kp_j->num_first || kp_i->num_second == kp_j->num_second) { return WITH_ERROR(-1, "Key expressions with repeated derivations in miniscript"); } diff --git a/src/handler/lib/policy.h b/src/handler/lib/policy.h index e5bce4bdc..6bcbc105a 100644 --- a/src/handler/lib/policy.h +++ b/src/handler/lib/policy.h @@ -190,13 +190,14 @@ bool check_wallet_hmac(const uint8_t wallet_id[static 32], const uint8_t wallet_ * If not NULL, and if the i-th key expression is in a tapleaf of the policy, receives the pointer * to the tapleaf's script. * @param[out] out_keyexpr - * If not NULL, it is a pointer that will receive the i-th key expression of the policy. + * If not NULL, it is a pointer that will receive a pointer to the i-th key expression of the + * policy. * @return the number of key expressions in the policy on success; -1 in case of error. */ __attribute__((warn_unused_result)) int get_keyexpr_by_index(const policy_node_t *policy, unsigned int i, const policy_node_t **out_tapleaf_ptr, - policy_node_keyexpr_t *out_keyexpr); + policy_node_keyexpr_t **out_keyexpr); /** * Determines the expected number of unique keys in the provided policy's key information. diff --git a/src/handler/sign_psbt.c b/src/handler/sign_psbt.c index da8b860f3..ac050234c 100644 --- a/src/handler/sign_psbt.c +++ b/src/handler/sign_psbt.c @@ -107,7 +107,7 @@ typedef struct { } output_info_t; typedef struct { - policy_node_keyexpr_t key_expression; + policy_node_keyexpr_t *key_expression_ptr; int cur_index; uint32_t fingerprint; uint8_t key_derivation_length; @@ -433,10 +433,10 @@ static int read_change_and_index_from_psbt_bip32_derivation( uint32_t addr_index = fpt_der[1 + der_len - 1]; // check if the 'change' derivation step is indeed coherent with key expression - if (change_step == keyexpr_info->key_expression.num_first) { + if (change_step == keyexpr_info->key_expression_ptr->num_first) { in_out->is_change = false; in_out->address_index = addr_index; - } else if (change_step == keyexpr_info->key_expression.num_second) { + } else if (change_step == keyexpr_info->key_expression_ptr->num_second) { in_out->is_change = true; in_out->address_index = addr_index; } else { @@ -446,7 +446,7 @@ static int read_change_and_index_from_psbt_bip32_derivation( // check that we can indeed derive the same key from the current key expression serialized_extended_pubkey_t pubkey; if (0 > derive_first_step_for_pubkey(&keyexpr_info->pubkey, - &keyexpr_info->key_expression, + keyexpr_info->key_expression_ptr, sign_psbt_cache, in_out->is_change, &pubkey)) @@ -669,12 +669,17 @@ static bool __attribute__((noinline)) fill_keyexpr_info_if_internal(dispatcher_c policy_map_key_info_t key_info; { uint8_t key_info_str[MAX_POLICY_KEY_INFO_LEN]; - int key_info_len = call_get_merkle_leaf_element(dc, - st->wallet_header.keys_info_merkle_root, - st->wallet_header.n_keys, - keyexpr_info->key_expression.key_index, - key_info_str, - sizeof(key_info_str)); + + // TODO: generalize for musig: keyexpr_info->key_expression_ptr->k.key_index is wrong + LEDGER_ASSERT(keyexpr_info->key_expression_ptr->type == KEY_EXPRESSION_NORMAL, "TODO"); + + int key_info_len = + call_get_merkle_leaf_element(dc, + st->wallet_header.keys_info_merkle_root, + st->wallet_header.n_keys, + keyexpr_info->key_expression_ptr->k.key_index, + key_info_str, + sizeof(key_info_str)); if (key_info_len < 0) { SEND_SW(dc, SW_BAD_STATE); // should never happen @@ -734,7 +739,7 @@ static bool find_first_internal_keyexpr(dispatcher_context_t *dc, int n_key_expressions = get_keyexpr_by_index(st->wallet_policy_map, keyexpr_info->cur_index, NULL, - &keyexpr_info->key_expression); + &keyexpr_info->key_expression_ptr); if (n_key_expressions < 0) { SEND_SW(dc, SW_BAD_STATE); // should never happen return false; @@ -2161,9 +2166,9 @@ static bool __attribute__((noinline)) sign_sighash_ecdsa_and_yield(dispatcher_co for (int i = 0; i < keyexpr_info->key_derivation_length; i++) { sign_path[i] = keyexpr_info->key_derivation[i]; } - sign_path[keyexpr_info->key_derivation_length] = input->in_out.is_change - ? keyexpr_info->key_expression.num_second - : keyexpr_info->key_expression.num_first; + sign_path[keyexpr_info->key_derivation_length] = + input->in_out.is_change ? keyexpr_info->key_expression_ptr->num_second + : keyexpr_info->key_expression_ptr->num_first; sign_path[keyexpr_info->key_derivation_length + 1] = input->in_out.address_index; int sign_path_len = keyexpr_info->key_derivation_length + 2; @@ -2230,8 +2235,8 @@ static bool __attribute__((noinline)) sign_sighash_schnorr_and_yield(dispatcher_ sign_path[i] = keyexpr_info->key_derivation[i]; } sign_path[keyexpr_info->key_derivation_length] = - input->in_out.is_change ? keyexpr_info->key_expression.num_second - : keyexpr_info->key_expression.num_first; + input->in_out.is_change ? keyexpr_info->key_expression_ptr->num_second + : keyexpr_info->key_expression_ptr->num_first; sign_path[keyexpr_info->key_derivation_length + 1] = input->in_out.address_index; int sign_path_len = keyexpr_info->key_derivation_length + 2; @@ -2687,7 +2692,7 @@ sign_transaction(dispatcher_context_t *dc, int n_key_expressions = get_keyexpr_by_index(st->wallet_policy_map, key_expression_index, &tapleaf_ptr, - &keyexpr_info.key_expression); + &keyexpr_info.key_expression_ptr); if (n_key_expressions < 0) { SEND_SW(dc, SW_BAD_STATE); // should never happen diff --git a/unit-tests/test_wallet.c b/unit-tests/test_wallet.c index 406c4a609..cd66a9253 100644 --- a/unit-tests/test_wallet.c +++ b/unit-tests/test_wallet.c @@ -39,7 +39,7 @@ static void check_key_expr_plain(const policy_node_keyexpr_t *ptr, uint32_t num_first, uint32_t num_second) { assert_int_equal(ptr->type, KEY_EXPRESSION_NORMAL); - assert_int_equal(ptr->key_index, key_index); + assert_int_equal(ptr->k.key_index, key_index); assert_int_equal(ptr->num_first, num_first); assert_int_equal(ptr->num_second, num_second); } @@ -51,7 +51,7 @@ static void check_key_expr_musig(const policy_node_keyexpr_t *ptr, uint32_t num_first, uint32_t num_second) { assert_int_equal(ptr->type, KEY_EXPRESSION_MUSIG); - musig_aggr_key_info_t *musig_info = r_musig_aggr_key_info(&ptr->musig_info); + musig_aggr_key_info_t *musig_info = r_musig_aggr_key_info(&ptr->m.musig_info); assert_int_equal(musig_info->n, n_musig_keys); uint16_t *musig_key_indexes = r_uint16(&musig_info->key_indexes); for (int i = 0; i < n_musig_keys; i++) { @@ -258,7 +258,7 @@ static void test_parse_policy_tr_multisig(void **state) { policy_node_tr_t *root = (policy_node_tr_t *) out; - assert_int_equal(r_policy_node_keyexpr(&root->key)->key_index, 0); + assert_int_equal(r_policy_node_keyexpr(&root->key)->k.key_index, 0); assert_int_equal(r_policy_node_keyexpr(&root->key)->num_first, 0); assert_int_equal(r_policy_node_keyexpr(&root->key)->num_second, 1); From 3dbf46411ab0fd52fd0c05bbaf6c36c476df1de4 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Wed, 9 Oct 2024 10:01:44 +0200 Subject: [PATCH 04/45] Add PSBT constants related to MuSig2; deleted unused constant --- src/common/psbt.h | 95 ++++++++++++++++++++++++----------------------- 1 file changed, 49 insertions(+), 46 deletions(-) diff --git a/src/common/psbt.h b/src/common/psbt.h index a566cc135..d18ce9d9b 100644 --- a/src/common/psbt.h +++ b/src/common/psbt.h @@ -3,55 +3,58 @@ // clang-format off enum PsbtGlobalType { - PSBT_GLOBAL_UNSIGNED_TX = 0x00, - PSBT_GLOBAL_XPUB = 0x01, - PSBT_GLOBAL_TX_VERSION = 0x02, - PSBT_GLOBAL_FALLBACK_LOCKTIME = 0x03, - PSBT_GLOBAL_INPUT_COUNT = 0x04, - PSBT_GLOBAL_OUTPUT_COUNT = 0x05, - PSBT_GLOBAL_TX_MODIFIABLE = 0x06, - PSBT_GLOBAL_SIGHASH_SINGLE_INPUTS = 0x07, - PSBT_GLOBAL_VERSION = 0xFB, - PSBT_GLOBAL_PROPRIETARY = 0xFC + PSBT_GLOBAL_UNSIGNED_TX = 0x00, + PSBT_GLOBAL_XPUB = 0x01, + PSBT_GLOBAL_TX_VERSION = 0x02, + PSBT_GLOBAL_FALLBACK_LOCKTIME = 0x03, + PSBT_GLOBAL_INPUT_COUNT = 0x04, + PSBT_GLOBAL_OUTPUT_COUNT = 0x05, + PSBT_GLOBAL_TX_MODIFIABLE = 0x06, + PSBT_GLOBAL_VERSION = 0xFB, + PSBT_GLOBAL_PROPRIETARY = 0xFC }; enum PsbtInputType { - PSBT_IN_NON_WITNESS_UTXO = 0x00, - PSBT_IN_WITNESS_UTXO = 0x01, - PSBT_IN_PARTIAL_SIG = 0x02, - PSBT_IN_SIGHASH_TYPE = 0x03, - PSBT_IN_REDEEM_SCRIPT = 0x04, - PSBT_IN_WITNESS_SCRIPT = 0x05, - PSBT_IN_BIP32_DERIVATION = 0x06, - PSBT_IN_FINAL_SCRIPTSIG = 0x07, - PSBT_IN_FINAL_SCRIPTWITNESS = 0x08, - PSBT_IN_POR_COMMITMENT = 0x09, - PSBT_IN_RIPEMD160 = 0x0A, - PSBT_IN_SHA256 = 0x0B, - PSBT_IN_HASH160 = 0x0C, - PSBT_IN_HASH256 = 0x0D, - PSBT_IN_PREVIOUS_TXID = 0x0E, - PSBT_IN_OUTPUT_INDEX = 0x0F, - PSBT_IN_SEQUENCE = 0x10, - PSBT_IN_REQUIRED_TIME_LOCKTIME = 0x11, - PSBT_IN_REQUIRED_HEIGHT_LOCKTIME = 0x12, - PSBT_IN_TAP_KEY_SIG = 0x13, - PSBT_IN_TAP_SCRIPT_SIG = 0x14, - PSBT_IN_TAP_LEAF_SCRIPT = 0x15, - PSBT_IN_TAP_BIP32_DERIVATION = 0x16, - PSBT_IN_TAP_INTERNAL_KEY = 0x17, - PSBT_IN_TAP_MERKLE_ROOT = 0x18, - PSBT_IN_PROPRIETARY = 0xFC + PSBT_IN_NON_WITNESS_UTXO = 0x00, + PSBT_IN_WITNESS_UTXO = 0x01, + PSBT_IN_PARTIAL_SIG = 0x02, + PSBT_IN_SIGHASH_TYPE = 0x03, + PSBT_IN_REDEEM_SCRIPT = 0x04, + PSBT_IN_WITNESS_SCRIPT = 0x05, + PSBT_IN_BIP32_DERIVATION = 0x06, + PSBT_IN_FINAL_SCRIPTSIG = 0x07, + PSBT_IN_FINAL_SCRIPTWITNESS = 0x08, + PSBT_IN_POR_COMMITMENT = 0x09, + PSBT_IN_RIPEMD160 = 0x0A, + PSBT_IN_SHA256 = 0x0B, + PSBT_IN_HASH160 = 0x0C, + PSBT_IN_HASH256 = 0x0D, + PSBT_IN_PREVIOUS_TXID = 0x0E, + PSBT_IN_OUTPUT_INDEX = 0x0F, + PSBT_IN_SEQUENCE = 0x10, + PSBT_IN_REQUIRED_TIME_LOCKTIME = 0x11, + PSBT_IN_REQUIRED_HEIGHT_LOCKTIME = 0x12, + PSBT_IN_TAP_KEY_SIG = 0x13, + PSBT_IN_TAP_SCRIPT_SIG = 0x14, + PSBT_IN_TAP_LEAF_SCRIPT = 0x15, + PSBT_IN_TAP_BIP32_DERIVATION = 0x16, + PSBT_IN_TAP_INTERNAL_KEY = 0x17, + PSBT_IN_TAP_MERKLE_ROOT = 0x18, + PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS = 0x1A, + PSBT_IN_MUSIG2_PUB_NONCE = 0x1B, + PSBT_IN_MUSIG2_PARTIAL_SIG = 0x1C, + PSBT_IN_PROPRIETARY = 0xFC }; enum PsbtOutputType { - PSBT_OUT_REDEEM_SCRIPT = 0x00, - PSBT_OUT_WITNESS_SCRIPT = 0x01, - PSBT_OUT_BIP32_DERIVATION = 0x02, - PSBT_OUT_AMOUNT = 0x03, - PSBT_OUT_SCRIPT = 0x04, - PSBT_OUT_TAP_INTERNAL_KEY = 0x05, - PSBT_OUT_TAP_TREE = 0x06, - PSBT_OUT_TAP_BIP32_DERIVATION = 0x07, - PSBT_OUT_PROPRIETARY = 0xFC -}; \ No newline at end of file + PSBT_OUT_REDEEM_SCRIPT = 0x00, + PSBT_OUT_WITNESS_SCRIPT = 0x01, + PSBT_OUT_BIP32_DERIVATION = 0x02, + PSBT_OUT_AMOUNT = 0x03, + PSBT_OUT_SCRIPT = 0x04, + PSBT_OUT_TAP_INTERNAL_KEY = 0x05, + PSBT_OUT_TAP_TREE = 0x06, + PSBT_OUT_TAP_BIP32_DERIVATION = 0x07, + PSBT_OUT_MUSIG2_PARTICIPANT_PUBKEYS = 0x08, + PSBT_OUT_PROPRIETARY = 0xFC +}; From 92f1b54fc4951f3bc3d7a6d3bab44ebc6bec439d Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Wed, 28 Feb 2024 17:12:10 +0100 Subject: [PATCH 05/45] Moved secp256k1 constants to a separate module --- src/crypto.c | 34 +--------------------------------- src/secp256k1.c | 23 +++++++++++++++++++++++ src/secp256k1.h | 24 ++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 33 deletions(-) create mode 100644 src/secp256k1.c create mode 100644 src/secp256k1.h diff --git a/src/crypto.c b/src/crypto.c index ccb95cfe6..feaf4e7f7 100644 --- a/src/crypto.c +++ b/src/crypto.c @@ -39,39 +39,7 @@ #include "crypto.h" -/** - * Generator for secp256k1, value 'g' defined in "Standards for Efficient Cryptography" - * (SEC2) 2.7.1. - */ -// clang-format off -static const uint8_t secp256k1_generator[] = { - 0x04, - 0x79, 0xBE, 0x66, 0x7E, 0xF9, 0xDC, 0xBB, 0xAC, 0x55, 0xA0, 0x62, 0x95, 0xCE, 0x87, 0x0B, 0x07, - 0x02, 0x9B, 0xFC, 0xDB, 0x2D, 0xCE, 0x28, 0xD9, 0x59, 0xF2, 0x81, 0x5B, 0x16, 0xF8, 0x17, 0x98, - 0x48, 0x3A, 0xDA, 0x77, 0x26, 0xA3, 0xC4, 0x65, 0x5D, 0xA4, 0xFB, 0xFC, 0x0E, 0x11, 0x08, 0xA8, - 0xFD, 0x17, 0xB4, 0x48, 0xA6, 0x85, 0x54, 0x19, 0x9C, 0x47, 0xD0, 0x8F, 0xFB, 0x10, 0xD4, 0xB8}; -// clang-format on - -/** - * Modulo for secp256k1 - */ -static const uint8_t secp256k1_p[] = { - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0xff, 0xff, 0xfc, 0x2f}; - -/** - * Curve order for secp256k1 - */ -static const uint8_t secp256k1_n[] = { - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, - 0xba, 0xae, 0xdc, 0xe6, 0xaf, 0x48, 0xa0, 0x3b, 0xbf, 0xd2, 0x5e, 0x8c, 0xd0, 0x36, 0x41, 0x41}; - -/** - * (p + 1)/4, used to calculate square roots in secp256k1 - */ -static const uint8_t secp256k1_sqr_exponent[] = { - 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xbf, 0xff, 0xff, 0x0c}; +#include "secp256k1.h" /* BIP0341 tags for computing the tagged hashes when tweaking public keys */ static const uint8_t BIP0341_taptweak_tag[] = {'T', 'a', 'p', 'T', 'w', 'e', 'a', 'k'}; diff --git a/src/secp256k1.c b/src/secp256k1.c new file mode 100644 index 000000000..2ddb714a6 --- /dev/null +++ b/src/secp256k1.c @@ -0,0 +1,23 @@ +#include "secp256k1.h" + +// clang-format off +const uint8_t secp256k1_generator[65] = { + 0x04, + 0x79, 0xBE, 0x66, 0x7E, 0xF9, 0xDC, 0xBB, 0xAC, 0x55, 0xA0, 0x62, 0x95, 0xCE, 0x87, 0x0B, 0x07, + 0x02, 0x9B, 0xFC, 0xDB, 0x2D, 0xCE, 0x28, 0xD9, 0x59, 0xF2, 0x81, 0x5B, 0x16, 0xF8, 0x17, 0x98, + 0x48, 0x3A, 0xDA, 0x77, 0x26, 0xA3, 0xC4, 0x65, 0x5D, 0xA4, 0xFB, 0xFC, 0x0E, 0x11, 0x08, 0xA8, + 0xFD, 0x17, 0xB4, 0x48, 0xA6, 0x85, 0x54, 0x19, 0x9C, 0x47, 0xD0, 0x8F, 0xFB, 0x10, 0xD4, 0xB8}; + +const uint8_t secp256k1_p[32] = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0xff, 0xff, 0xfc, 0x2f}; + +const uint8_t secp256k1_n[32] = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, + 0xba, 0xae, 0xdc, 0xe6, 0xaf, 0x48, 0xa0, 0x3b, 0xbf, 0xd2, 0x5e, 0x8c, 0xd0, 0x36, 0x41, 0x41}; + +const uint8_t secp256k1_sqr_exponent[32] = { + 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xbf, 0xff, 0xff, 0x0c}; + +// clang-format on diff --git a/src/secp256k1.h b/src/secp256k1.h new file mode 100644 index 000000000..c6ead0f33 --- /dev/null +++ b/src/secp256k1.h @@ -0,0 +1,24 @@ +#pragma once + +#include + +/** + * Generator for secp256k1, value 'g' defined in "Standards for Efficient Cryptography" + * (SEC2) 2.7.1. + */ +extern const uint8_t secp256k1_generator[65]; + +/** + * Modulo for secp256k1 + */ +extern const uint8_t secp256k1_p[32]; + +/** + * Curve order for secp256k1 + */ +extern const uint8_t secp256k1_n[32]; + +/** + * (p + 1)/4, used to calculate square roots in secp256k1 + */ +extern const uint8_t secp256k1_sqr_exponent[32]; From 4e6bae5d7541ee8bf276b663b1cdeea372619713 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Thu, 29 Feb 2024 09:49:27 +0100 Subject: [PATCH 06/45] Added address generation tests for musig --- tests/test_get_wallet_address.py | 35 ++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/test_get_wallet_address.py b/tests/test_get_wallet_address.py index 199b2a82e..5aae1d5c3 100644 --- a/tests/test_get_wallet_address.py +++ b/tests/test_get_wallet_address.py @@ -303,6 +303,41 @@ def test_get_wallet_address_tr_script_sortedmulti(client: RaggerClient): assert res == "tb1pdzk72dnvz3246474p4m5a97u43h6ykt2qcjrrhk6y0fkg8hx2mvswwgvv7" +def test_get_wallet_address_tr_musig_keypath(client: RaggerClient): + wallet = WalletPolicy( + name="Musig in keypath", + descriptor_template="tr(musig(@0,@1)/**)", + keys_info=[ + "[f5acc2fd/44'/1'/0']tpubDCwYjpDhUdPGP5rS3wgNg13mTrrjBuG8V9VpWbyptX6TRPbNoZVXsoVUSkCjmQ8jJycjuDKBb9eataSymXakTTaGifxR6kmVsfFehH1ZgJT", + "tpubDCwYjpDhUdPGQWG6wG6hkBJuWFZEtrn7j3xwG3i8XcQabcGC53xWZm1hSXrUPFS5UvZ3QhdPSjXWNfWmFGTioARHuG5J7XguEjgg7p8PxAm" + ] + ) + + wallet_hmac = bytes.fromhex( + "05b7b4bccd3188effc24de8fd67e83231d8486772800884db0d81bad19f2be3e") + + res = client.get_wallet_address(wallet, wallet_hmac, 0, 3, False) + assert res == "tb1p7j9azx5xwt5fp3t99wnj4885yfcqynhz645ntflxr9dxr2g8ndnq32xa2m" + + +def test_get_wallet_address_tr_musig_scriptpath(client: RaggerClient): + wallet = WalletPolicy( + name="Musig in script path", + descriptor_template="tr(@0/**,pk(musig(@1,@2)/**))", + keys_info=[ + "tpubD6NzVbkrYhZ4WLczPJWReQycCJdd6YVWXubbVUFnJ5KgU5MDQrD998ZJLSmaB7GVcCnJSDWprxmrGkJ6SvgQC6QAffVpqSvonXmeizXcrkN", + "[f5acc2fd/44'/1'/0']tpubDCwYjpDhUdPGP5rS3wgNg13mTrrjBuG8V9VpWbyptX6TRPbNoZVXsoVUSkCjmQ8jJycjuDKBb9eataSymXakTTaGifxR6kmVsfFehH1ZgJT", + "tpubDCwYjpDhUdPGQWG6wG6hkBJuWFZEtrn7j3xwG3i8XcQabcGC53xWZm1hSXrUPFS5UvZ3QhdPSjXWNfWmFGTioARHuG5J7XguEjgg7p8PxAm" + ] + ) + + wallet_hmac = bytes.fromhex( + "b22397b717949ede59c3c9f31c987acda098471211f754b6633c87054c1efb51") + + res = client.get_wallet_address(wallet, wallet_hmac, 0, 3, False) + assert res == "tb1pmx5syrz67lwdy8dsmvlta5h5ahfn6k9pg8qw3y0jn698xj0duxpq3k94zq" + + def test_get_wallet_address_large_addr_index(client: RaggerClient): # 2**31 - 1 is the largest index allowed, per BIP-32 From 654e1e228b2ad86d97fab57a5529d4e4aacc6073 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Thu, 29 Feb 2024 09:50:34 +0100 Subject: [PATCH 07/45] Made crypto_tr_lift_x and crypto_tr_tagged_hash functions public --- src/crypto.c | 16 ++++++++-------- src/crypto.h | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/src/crypto.c b/src/crypto.c index feaf4e7f7..67578f5f4 100644 --- a/src/crypto.c +++ b/src/crypto.c @@ -434,7 +434,7 @@ void crypto_tr_tapleaf_hash_init(cx_sha256_t *hash_context) { crypto_tr_tagged_hash_init(hash_context, BIP0341_tapleaf_tag, sizeof(BIP0341_tapleaf_tag)); } -static int crypto_tr_lift_x(const uint8_t x[static 32], uint8_t out[static 65]) { +int crypto_tr_lift_x(const uint8_t x[static 32], uint8_t out[static 65]) { // save memory by reusing output buffer for intermediate results uint8_t *y = out + 1 + 32; // we use the memory for the x-coordinate of the output as a temporary variable @@ -473,13 +473,13 @@ static int crypto_tr_lift_x(const uint8_t x[static 32], uint8_t out[static 65]) // Computes a tagged hash according to BIP-340. // If data2_len > 0, then data2 must be non-NULL and the `data` and `data2` arrays are concatenated. -static void crypto_tr_tagged_hash(const uint8_t *tag, - uint16_t tag_len, - const uint8_t *data, - uint16_t data_len, - const uint8_t *data2, - uint16_t data2_len, - uint8_t out[static CX_SHA256_SIZE]) { +void crypto_tr_tagged_hash(const uint8_t *tag, + uint16_t tag_len, + const uint8_t *data, + uint16_t data_len, + const uint8_t *data2, + uint16_t data2_len, + uint8_t out[static CX_SHA256_SIZE]) { // First compute hashtag, reuse out buffer for that cx_sha256_hash(tag, tag_len, out); diff --git a/src/crypto.h b/src/crypto.h index cb8394bb5..f56e24350 100644 --- a/src/crypto.h +++ b/src/crypto.h @@ -343,6 +343,43 @@ int crypto_ecdsa_sign_sha256_hash_with_key(const uint32_t bip32_path[], */ void crypto_tr_tagged_hash_init(cx_sha256_t *hash_context, const uint8_t *tag, uint16_t tag_len); +/** + * Implementation of the lift_x procedure as defined by BIP-0340. + * + * @param[in] x + * Pointer to a 32-byte array. + * @param[out] out + * Pointer to an array that will received the output as an uncompressed 65-bytes pubkey. + */ +int crypto_tr_lift_x(const uint8_t x[static 32], uint8_t out[static 65]); + +/** + * A tagged hash as defined in BIP-0340. + * + * @param[in] tag + * Pointer to an array containing the tag of the tagged hash. + * @param[in] tag_len + * Length of the tag. + * @param[in] data + * Pointer to an array of data. + * @param[in] data_len + * Length of the array pointed by `data`. + * @param[in] data2 + * If NULL, ignored. If not null, a pointer to an array of data; the tagged hash for the + * concatenation of `data` and `data2` is computed. + * @param[in] data2_len + * If `data2` is NULL, ignored. Otherwise, the length the array pointed by `data2`. + * @param[out] out + * Pointer to a 32-byte array that will receive the result. + */ +void crypto_tr_tagged_hash(const uint8_t *tag, + uint16_t tag_len, + const uint8_t *data, + uint16_t data_len, + const uint8_t *data2, + uint16_t data2_len, + uint8_t out[static CX_SHA256_SIZE]); + /** * Initializes the "tagged" SHA256 hash with tag "TapLeaf", used for tapscript leaves. * From bbde126d9724f29b353393b11f8df306dd720b60 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Thu, 29 Feb 2024 10:25:09 +0100 Subject: [PATCH 08/45] Musig key aggregation and address generation --- src/handler/lib/policy.c | 35 ++++++++-- src/musig/musig.c | 146 +++++++++++++++++++++++++++++++++++++++ src/musig/musig.h | 45 ++++++++++++ 3 files changed, 219 insertions(+), 7 deletions(-) create mode 100644 src/musig/musig.c create mode 100644 src/musig/musig.h diff --git a/src/handler/lib/policy.c b/src/handler/lib/policy.c index eb536180b..d8ec5276a 100644 --- a/src/handler/lib/policy.c +++ b/src/handler/lib/policy.c @@ -5,6 +5,7 @@ #include "../lib/get_merkle_leaf_element.h" #include "../lib/get_preimage.h" #include "../../crypto.h" +#include "../../musig/musig.h" #include "../../common/base58.h" #include "../../common/bitvector.h" #include "../../common/read.h" @@ -462,14 +463,34 @@ __attribute__((warn_unused_result)) static int get_derived_pubkey( serialized_extended_pubkey_t ext_pubkey; - if (key_expr->type != KEY_EXPRESSION_NORMAL) { - PRINTF("Not implemented\n"); // TODO - return -1; - } + if (key_expr->type == KEY_EXPRESSION_NORMAL) { + if (0 > get_extended_pubkey(dispatcher_context, wdi, key_expr->k.key_index, &ext_pubkey)) { + return -1; + } + } else if (key_expr->type == KEY_EXPRESSION_MUSIG) { + musig_aggr_key_info_t *musig_info = r_musig_aggr_key_info(&key_expr->m.musig_info); + uint16_t *key_indexes = r_uint16(&musig_info->key_indexes); + plain_pk_t keys[MAX_PUBKEYS_PER_MUSIG]; + for (int i = 0; i < musig_info->n; i++) { + // we use ext_pubkey as a temporary variable; will overwrite later + if (0 > get_extended_pubkey(dispatcher_context, wdi, key_indexes[i], &ext_pubkey)) { + return -1; + } + memcpy(keys[i], ext_pubkey.compressed_pubkey, sizeof(ext_pubkey.compressed_pubkey)); + } - int ret = get_extended_pubkey(dispatcher_context, wdi, key_expr->k.key_index, &ext_pubkey); - if (ret < 0) { - return -1; + musig_keyagg_context_t musig_ctx; + musig_key_agg(keys, musig_info->n, &musig_ctx); + + // compute the aggregated extended pubkey + memset(&ext_pubkey, 0, sizeof(ext_pubkey)); + write_u32_be(ext_pubkey.version, 0, BIP32_PUBKEY_VERSION); + + ext_pubkey.compressed_pubkey[0] = (musig_ctx.Q.y[31] % 2 == 0) ? 2 : 3; + memcpy(&ext_pubkey.compressed_pubkey[1], musig_ctx.Q.x, sizeof(musig_ctx.Q.x)); + memcpy(&ext_pubkey.chain_code, BIP_MUSIG_CHAINCODE, sizeof(BIP_MUSIG_CHAINCODE)); + } else { + LEDGER_ASSERT(false, "Unreachable code"); } // we derive the // child of this pubkey diff --git a/src/musig/musig.c b/src/musig/musig.c new file mode 100644 index 000000000..b4ffb4432 --- /dev/null +++ b/src/musig/musig.c @@ -0,0 +1,146 @@ +#include + +#include "musig.h" + +#include "../crypto.h" +#include "../secp256k1.h" + +static const uint8_t BIP0327_keyagg_coeff_tag[] = + {'K', 'e', 'y', 'A', 'g', 'g', ' ', 'c', 'o', 'e', 'f', 'f', 'i', 'c', 'i', 'e', 'n', 't'}; +static const uint8_t BIP0327_keyagg_list_tag[] = + {'K', 'e', 'y', 'A', 'g', 'g', ' ', 'l', 'i', 's', 't'}; + +static inline bool is_point_infinite(const point_t *P) { + return P->prefix == 0; +} + +static inline void set_point_infinite(point_t *P) { + memset(P->raw, 0, sizeof(point_t)); +} + +static int point_add(const point_t *P1, const point_t *P2, point_t *out) { + if (is_point_infinite(P1)) { + memmove(out->raw, P2->raw, sizeof(point_t)); + return CX_OK; + } + if (is_point_infinite(P2)) { + memmove(out->raw, P1->raw, sizeof(point_t)); + return CX_OK; + } + if (memcmp(P1->x, P2->x, 32) == 0 && memcmp(P1->y, P2->y, 32) != 0) { + memset(out->raw, 0, sizeof(point_t)); + return CX_OK; + } + return cx_ecfp_add_point_no_throw(CX_CURVE_SECP256K1, out->raw, P1->raw, P2->raw); +} + +// out can be equal to P +static int point_negate(const point_t *P, point_t *out) { + if (is_point_infinite(P)) { + set_point_infinite(out); + return 0; + } + memmove(out->x, P->x, 32); + + if (CX_OK != cx_math_sub_no_throw(out->y, secp256k1_p, P->y, 32)) return -1; + + out->prefix = 4; + return 0; +} + +static int cpoint(const uint8_t x[33], point_t *out) { + crypto_tr_lift_x(&x[1], out->raw); + if (is_point_infinite(out)) { + PRINTF("Invalid compressed point\n"); + return -1; + } + if (x[0] == 2) { + return 0; + } else if (x[0] == 3) { + if (0 > point_negate(out, out)) { + return -1; + } + return 0; + } else { + PRINTF("Invalid compressed point: invalid prefix\n"); + return -1; + } +} + +static void musig_get_second_key(const plain_pk_t pubkeys[], size_t n_keys, plain_pk_t out) { + for (size_t i = 0; i < n_keys; i++) { + if (memcmp(pubkeys[0], pubkeys[i], sizeof(plain_pk_t)) != 0) { + memcpy(out, pubkeys[i], sizeof(plain_pk_t)); + return; + } + } + memset(out, 0, sizeof(plain_pk_t)); +} + +static void musig_hash_keys(const plain_pk_t pubkeys[], size_t n_keys, uint8_t out[static 32]) { + cx_sha256_t hash_context; + crypto_tr_tagged_hash_init(&hash_context, + BIP0327_keyagg_list_tag, + sizeof(BIP0327_keyagg_list_tag)); + for (size_t i = 0; i < n_keys; i++) { + crypto_hash_update(&hash_context.header, pubkeys[i], sizeof(plain_pk_t)); + } + crypto_hash_digest(&hash_context.header, out, 32); +} + +static void musig_key_agg_coeff_internal(const plain_pk_t pubkeys[], + size_t n_keys, + const plain_pk_t pk_, + const plain_pk_t pk2, + uint8_t out[static CX_SHA256_SIZE]) { + uint8_t L[CX_SHA256_SIZE]; + musig_hash_keys(pubkeys, n_keys, L); + if (memcmp(pk_, pk2, sizeof(plain_pk_t)) == 0) { + memset(out, 0, CX_SHA256_SIZE); + out[31] = 1; + } else { + crypto_tr_tagged_hash(BIP0327_keyagg_coeff_tag, + sizeof(BIP0327_keyagg_coeff_tag), + L, + sizeof(L), + pk_, + sizeof(plain_pk_t), + out); + + // result modulo secp256k1_n + int res = cx_math_modm_no_throw(out, CX_SHA256_SIZE, secp256k1_n, sizeof(secp256k1_n)); + + LEDGER_ASSERT(res == CX_OK, "Modular reduction failed"); + } +} + +int musig_key_agg(const plain_pk_t pubkeys[], size_t n_keys, musig_keyagg_context_t *ctx) { + plain_pk_t pk2; + musig_get_second_key(pubkeys, n_keys, pk2); + + set_point_infinite(&ctx->Q); + for (size_t i = 0; i < n_keys; i++) { + point_t P; + + // set P := P_i + if (0 > cpoint(pubkeys[i], &P)) { + PRINTF("Invalid pubkey in musig_key_agg\n"); + return -1; + } + + uint8_t a_i[32]; + musig_key_agg_coeff_internal(pubkeys, n_keys, pubkeys[i], pk2, a_i); + + // set P := a_i * P_i + if (CX_OK != cx_ecfp_scalar_mult_no_throw(CX_CURVE_SECP256K1, P.raw, a_i, 32)) { + PRINTF("Scalar multiplication failed in musig_key_agg\n"); + return -1; + } + + point_add(&ctx->Q, &P, &ctx->Q); + } + memset(ctx->tacc, 0, sizeof(ctx->tacc)); + memset(ctx->gacc, 0, sizeof(ctx->gacc)); + ctx->gacc[31] = 1; + return 0; +} diff --git a/src/musig/musig.h b/src/musig/musig.h new file mode 100644 index 000000000..d17f89b67 --- /dev/null +++ b/src/musig/musig.h @@ -0,0 +1,45 @@ +#pragma once + +#include +#include + +// TODO: rename once BIP number is assigned +static uint8_t BIP_MUSIG_CHAINCODE[32] = { + 0x86, 0x80, 0x87, 0xCA, 0x02, 0xA6, 0xF9, 0x74, 0xC4, 0x59, 0x89, 0x24, 0xC3, 0x6B, 0x57, 0x76, + 0x2D, 0x32, 0xCB, 0x45, 0x71, 0x71, 0x67, 0xE3, 0x00, 0x62, 0x2C, 0x71, 0x67, 0xE3, 0x89, 0x65}; + +typedef uint8_t plain_pk_t[33]; +typedef uint8_t xonly_pk_t[32]; + +// An uncompressed pubkey, encoded as 04||x||y, where x and y are 32-byte big-endian coordinates. +// If the first byte (prefix) is 0, encodes the point at infinity. +typedef struct { + union { + uint8_t raw[65]; + struct { + uint8_t prefix; // 0 for the point at infinity, otherwise 4. + uint8_t x[32]; + uint8_t y[32]; + }; + }; +} point_t; + +typedef struct musig_keyagg_context_s { + point_t Q; + uint8_t gacc[32]; + uint8_t tacc[32]; +} musig_keyagg_context_t; + +/** + * Computes the KeyAgg Context per BIP-0327. + * + * @param[in] pubkeys + * Pointer to a list of pubkeys. + * @param[in] n_keys + * Number of pubkeys. + * @param[out] musig_keyagg_context_t + * Pointer to receive the musig KeyAgg Context. + * + * @return 0 on success, a negative number in case of error. + */ +int musig_key_agg(const plain_pk_t pubkeys[], size_t n_keys, musig_keyagg_context_t *ctx); From 7b7af767fc5c6f4f96a0f671e76e20eac0b7b969 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Fri, 1 Mar 2024 15:13:28 +0100 Subject: [PATCH 09/45] Compute aggregate xpub for musig() in descriptors in the python client library --- bitcoin_client/ledger_bitcoin/bip0327.py | 177 +++++++++++++++++++++++ bitcoin_client/ledger_bitcoin/client.py | 56 ++++++- 2 files changed, 232 insertions(+), 1 deletion(-) create mode 100644 bitcoin_client/ledger_bitcoin/bip0327.py diff --git a/bitcoin_client/ledger_bitcoin/bip0327.py b/bitcoin_client/ledger_bitcoin/bip0327.py new file mode 100644 index 000000000..8d4680791 --- /dev/null +++ b/bitcoin_client/ledger_bitcoin/bip0327.py @@ -0,0 +1,177 @@ +# extracted from the BIP327 reference implementation: https://github.com/bitcoin/bips/blob/b3701faef2bdb98a0d7ace4eedbeefa2da4c89ed/bip-0327/reference.py + +# Only contains the key aggregation part of the library + +# The code in this source file is distributed under the BSD-3-Clause. + +# autopep8: off + +from typing import List, Optional, Tuple, NewType, NamedTuple +import hashlib + +# +# The following helper functions were copied from the BIP-340 reference implementation: +# https://github.com/bitcoin/bips/blob/master/bip-0340/reference.py +# + +p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F +n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 + +# Points are tuples of X and Y coordinates and the point at infinity is +# represented by the None keyword. +G = (0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798, 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8) + +Point = Tuple[int, int] + +# This implementation can be sped up by storing the midstate after hashing +# tag_hash instead of rehashing it all the time. +def tagged_hash(tag: str, msg: bytes) -> bytes: + tag_hash = hashlib.sha256(tag.encode()).digest() + return hashlib.sha256(tag_hash + tag_hash + msg).digest() + +def is_infinite(P: Optional[Point]) -> bool: + return P is None + +def x(P: Point) -> int: + assert not is_infinite(P) + return P[0] + +def y(P: Point) -> int: + assert not is_infinite(P) + return P[1] + +def point_add(P1: Optional[Point], P2: Optional[Point]) -> Optional[Point]: + if P1 is None: + return P2 + if P2 is None: + return P1 + if (x(P1) == x(P2)) and (y(P1) != y(P2)): + return None + if P1 == P2: + lam = (3 * x(P1) * x(P1) * pow(2 * y(P1), p - 2, p)) % p + else: + lam = ((y(P2) - y(P1)) * pow(x(P2) - x(P1), p - 2, p)) % p + x3 = (lam * lam - x(P1) - x(P2)) % p + return (x3, (lam * (x(P1) - x3) - y(P1)) % p) + +def point_mul(P: Optional[Point], n: int) -> Optional[Point]: + R = None + for i in range(256): + if (n >> i) & 1: + R = point_add(R, P) + P = point_add(P, P) + return R + +def bytes_from_int(x: int) -> bytes: + return x.to_bytes(32, byteorder="big") + +def lift_x(b: bytes) -> Optional[Point]: + x = int_from_bytes(b) + if x >= p: + return None + y_sq = (pow(x, 3, p) + 7) % p + y = pow(y_sq, (p + 1) // 4, p) + if pow(y, 2, p) != y_sq: + return None + return (x, y if y & 1 == 0 else p-y) + +def int_from_bytes(b: bytes) -> int: + return int.from_bytes(b, byteorder="big") + +def has_even_y(P: Point) -> bool: + assert not is_infinite(P) + return y(P) % 2 == 0 + +# +# End of helper functions copied from BIP-340 reference implementation. +# + +PlainPk = NewType('PlainPk', bytes) +XonlyPk = NewType('XonlyPk', bytes) + +# There are two types of exceptions that can be raised by this implementation: +# - ValueError for indicating that an input doesn't conform to some function +# precondition (e.g. an input array is the wrong length, a serialized +# representation doesn't have the correct format). +# - InvalidContributionError for indicating that a signer (or the +# aggregator) is misbehaving in the protocol. +# +# Assertions are used to (1) satisfy the type-checking system, and (2) check for +# inconvenient events that can't happen except with negligible probability (e.g. +# output of a hash function is 0) and can't be manually triggered by any +# signer. + +# This exception is raised if a party (signer or nonce aggregator) sends invalid +# values. Actual implementations should not crash when receiving invalid +# contributions. Instead, they should hold the offending party accountable. +class InvalidContributionError(Exception): + def __init__(self, signer, contrib): + self.signer = signer + # contrib is one of "pubkey", "pubnonce", "aggnonce", or "psig". + self.contrib = contrib + +infinity = None + +def xbytes(P: Point) -> bytes: + return bytes_from_int(x(P)) + +def cbytes(P: Point) -> bytes: + a = b'\x02' if has_even_y(P) else b'\x03' + return a + xbytes(P) + +def point_negate(P: Optional[Point]) -> Optional[Point]: + if P is None: + return P + return (x(P), p - y(P)) + +def cpoint(x: bytes) -> Point: + if len(x) != 33: + raise ValueError('x is not a valid compressed point.') + P = lift_x(x[1:33]) + if P is None: + raise ValueError('x is not a valid compressed point.') + if x[0] == 2: + return P + elif x[0] == 3: + P = point_negate(P) + assert P is not None + return P + else: + raise ValueError('x is not a valid compressed point.') + +KeyAggContext = NamedTuple('KeyAggContext', [('Q', Point), + ('gacc', int), + ('tacc', int)]) + +def key_agg(pubkeys: List[PlainPk]) -> KeyAggContext: + pk2 = get_second_key(pubkeys) + u = len(pubkeys) + Q = infinity + for i in range(u): + try: + P_i = cpoint(pubkeys[i]) + except ValueError: + raise InvalidContributionError(i, "pubkey") + a_i = key_agg_coeff_internal(pubkeys, pubkeys[i], pk2) + Q = point_add(Q, point_mul(P_i, a_i)) + # Q is not the point at infinity except with negligible probability. + assert(Q is not None) + gacc = 1 + tacc = 0 + return KeyAggContext(Q, gacc, tacc) + +def hash_keys(pubkeys: List[PlainPk]) -> bytes: + return tagged_hash('KeyAgg list', b''.join(pubkeys)) + +def get_second_key(pubkeys: List[PlainPk]) -> PlainPk: + u = len(pubkeys) + for j in range(1, u): + if pubkeys[j] != pubkeys[0]: + return pubkeys[j] + return PlainPk(b'\x00'*33) + +def key_agg_coeff_internal(pubkeys: List[PlainPk], pk_: PlainPk, pk2: PlainPk) -> int: + L = hash_keys(pubkeys) + if pk_ == pk2: + return 1 + return int_from_bytes(tagged_hash('KeyAgg coefficient', L + pk_)) % n diff --git a/bitcoin_client/ledger_bitcoin/client.py b/bitcoin_client/ledger_bitcoin/client.py index 351370320..94c22aed8 100644 --- a/bitcoin_client/ledger_bitcoin/client.py +++ b/bitcoin_client/ledger_bitcoin/client.py @@ -3,6 +3,7 @@ import base64 from io import BytesIO, BufferedReader +from .embit import base58 from .embit.base import EmbitError from .embit.descriptor import Descriptor from .embit.networks import NETWORKS @@ -17,9 +18,10 @@ from .merkle import get_merkleized_map_commitment from .wallet import WalletPolicy, WalletType from .psbt import PSBT, normalize_psbt -from . import segwit_addr from ._serialize import deser_string +from .bip0327 import key_agg, cbytes + def parse_stream_to_map(f: BufferedReader) -> Mapping[bytes, bytes]: result = {} @@ -39,6 +41,53 @@ def parse_stream_to_map(f: BufferedReader) -> Mapping[bytes, bytes]: return result +def aggr_xpub(pubkeys: List[bytes], chain: Chain) -> str: + BIP_MUSIG_CHAINCODE = bytes.fromhex( + "868087ca02a6f974c4598924c36b57762d32cb45717167e300622c7167e38965") + ctx = key_agg(pubkeys) + compressed_pubkey = cbytes(ctx.Q) + + # Serialize according to BIP-32 + if chain == Chain.MAIN: + version = 0x0488B21E + else: + version = 0x043587CF + + return base58.encode_check(b''.join([ + version.to_bytes(4, byteorder='big'), + b'\x00', # depth + b'\x00\x00\x00\x00', # parent fingerprint + b'\x00\x00\x00\x00', # child number + BIP_MUSIG_CHAINCODE, + compressed_pubkey + ])) + + +# Given a valid descriptor, replaces each musig() (if any) with the +# corresponding synthetic xpub/tpub. +def replace_musigs(desc: str, chain: Chain) -> str: + while True: + musig_start = desc.find("musig(") + if musig_start == -1: + break + musig_end = desc.find(")", musig_start) + if musig_end == -1: + raise ValueError("Invalid descriptor template") + + key_and_origs = desc[musig_start+6:musig_end].split(",") + pubkeys = [] + for key_orig in key_and_origs: + orig_end = key_orig.find("]") + xpub = key_orig if orig_end == -1 else key_orig[orig_end+1:] + pubkeys.append(base58.decode_check(xpub)[-33:]) + + # replace with the aggregate xpub + desc = desc[:musig_start] + \ + aggr_xpub(pubkeys, chain) + desc[musig_end+1:] + + return desc + + def _make_partial_signature(pubkey_augm: bytes, signature: bytes) -> PartialSignature: if len(pubkey_augm) == 64: # tapscript spend: pubkey_augm is the concatenation of: @@ -273,6 +322,11 @@ def sign_message(self, message: Union[str, bytes], bip32_path: str) -> str: def _derive_address_for_policy(self, wallet: WalletPolicy, change: bool, address_index: int) -> Optional[str]: desc_str = wallet.get_descriptor(change) + + # Since embit does not support musig() in descriptors, we replace each + # occurrence with the corresponding aggregated xpub + desc_str = replace_musigs(desc_str, self.chain) + try: desc = Descriptor.from_string(desc_str) From 4ebfc259bdd93f60fb99e1e834f398c7a3f471cf Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Tue, 9 Apr 2024 11:50:53 +0200 Subject: [PATCH 10/45] Add musig2 fields to PSBT class --- bitcoin_client/ledger_bitcoin/psbt.py | 92 +++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/bitcoin_client/ledger_bitcoin/psbt.py b/bitcoin_client/ledger_bitcoin/psbt.py index 16de47d23..37c4e626b 100644 --- a/bitcoin_client/ledger_bitcoin/psbt.py +++ b/bitcoin_client/ledger_bitcoin/psbt.py @@ -1,6 +1,8 @@ # Original version: https://github.com/bitcoin-core/HWI/blob/3fe369d0379212fae1c72729a179d133b0adc872/hwilib/key.py # Distributed under the MIT License. +# fmt: off + """ PSBT Classes and Utilities ************************** @@ -107,6 +109,9 @@ class PartiallySignedInput: PSBT_IN_TAP_BIP32_DERIVATION = 0x16 PSBT_IN_TAP_INTERNAL_KEY = 0x17 PSBT_IN_TAP_MERKLE_ROOT = 0x18 + PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS = 0x1a + PSBT_IN_MUSIG2_PUB_NONCE = 0x1b + PSBT_IN_MUSIG2_PARTIAL_SIG = 0x1c def __init__(self, version: int) -> None: self.non_witness_utxo: Optional[CTransaction] = None @@ -129,6 +134,9 @@ def __init__(self, version: int) -> None: self.tap_bip32_paths: Dict[bytes, Tuple[Set[bytes], KeyOriginInfo]] = {} self.tap_internal_key = b"" self.tap_merkle_root = b"" + self.musig2_participant_pubkeys: Dict[bytes, List[bytes]] = {} + self.musig2_pub_nonces: Dict[Tuple[bytes, bytes, Optional[bytes]], bytes] = {} + self.musig2_partial_sigs: Dict[Tuple[bytes, bytes, Optional[bytes]], bytes] = {} self.unknown: Dict[bytes, bytes] = {} self.version: int = version @@ -355,6 +363,51 @@ def deserialize(self, f: Readable) -> None: self.tap_merkle_root = deser_string(f) if len(self.tap_merkle_root) != 32: raise PSBTSerializationError("Input Taproot merkle root is not 32 bytes") + elif key_type == PartiallySignedInput.PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS: + if key in key_lookup: + raise PSBTSerializationError("Duplicate key, input Musig2 participant pubkeys already provided") + elif len(key) != 1 + 33: + raise PSBTSerializationError("Input Musig2 aggregate compressed pubkey is not 33 bytes") + + pubkeys_cat = deser_string(f) + if len(pubkeys_cat) == 0: + raise PSBTSerializationError("The list of compressed pubkeys for Musig2 cannot be empty") + if (len(pubkeys_cat) % 33) != 0: + raise PSBTSerializationError("The compressed pubkeys for Musig2 must be exactly 33 bytes long") + pubkeys = [] + for i in range(0, len(pubkeys_cat), 33): + pubkeys.append(pubkeys_cat[i: i + 33]) + + self.musig2_participant_pubkeys[key[1:]] = pubkeys + elif key_type == PartiallySignedInput.PSBT_IN_MUSIG2_PUB_NONCE: + if key in key_lookup: + raise PSBTSerializationError("Duplicate key, Musig2 public nonce already provided") + elif len(key) not in [1 + 33 + 33, 1 + 33 + 33 + 32]: + raise PSBTSerializationError("Invalid key length for Musig2 public nonce") + + providing_pubkey = key[1:1+33] + aggregate_pubkey = key[1+33:1+33+33] + tapleaf_hash = None if len(key) == 1 + 33 + 33 else key[1+33+33:] + + public_nonces = deser_string(f) + if len(public_nonces) != 66: + raise PSBTSerializationError("The length of the public nonces in Musig2 must be exactly 66 bytes") + + self.musig2_pub_nonces[(providing_pubkey, aggregate_pubkey, tapleaf_hash)] = public_nonces + elif key_type == PartiallySignedInput.PSBT_IN_MUSIG2_PARTIAL_SIG: + if key in key_lookup: + raise PSBTSerializationError("Duplicate key, Musig2 partial signature already provided") + elif len(key) not in [1 + 33 + 33, 1 + 33 + 33 + 32]: + raise PSBTSerializationError("Invalid key length for Musig2 partial signature") + + providing_pubkey = key[1:1+33] + aggregate_pubkey = key[1+33:1+33+33] + tapleaf_hash = None if len(key) == 1 + 33 + 33 else key[1+33+33:] + + partial_sig = deser_string(f) + if len(partial_sig) != 32: + raise PSBTSerializationError("The length of the partial signature in Musig2 must be exactly 32 bytes") + self.musig2_partial_sigs[(providing_pubkey, aggregate_pubkey, tapleaf_hash)] = partial_sig else: if key in self.unknown: raise PSBTSerializationError("Duplicate key, key for unknown value already provided") @@ -466,6 +519,20 @@ def serialize(self) -> bytes: r += ser_string(ser_compact_size(PartiallySignedInput.PSBT_IN_REQUIRED_HEIGHT_LOCKTIME)) r += ser_string(struct.pack(" None: self.redeem_script = b"" @@ -497,6 +565,9 @@ def __init__(self, version: int) -> None: self.tap_internal_key = b"" self.tap_tree = b"" self.tap_bip32_paths: Dict[bytes, Tuple[Set[bytes], KeyOriginInfo]] = {} + + self.musig2_participant_pubkeys: Dict[bytes, List[bytes]] = {} + self.unknown: Dict[bytes, bytes] = {} self.version: int = version @@ -593,6 +664,22 @@ def deserialize(self, f: Readable) -> None: for i in range(0, num_hashes): leaf_hashes.add(vs.read(32)) self.tap_bip32_paths[xonly] = (leaf_hashes, KeyOriginInfo.deserialize(vs.read())) + elif key_type == PartiallySignedOutput.PSBT_OUT_MUSIG2_PARTICIPANT_PUBKEYS: + if key in key_lookup: + raise PSBTSerializationError("Duplicate key, output Musig2 participant pubkeys already provided") + elif len(key) != 1 + 33: + raise PSBTSerializationError("Output Musig2 aggregate compressed pubkey is not 33 bytes") + + pubkeys_cat = deser_string(f) + if len(pubkeys_cat) == 0: + raise PSBTSerializationError("The list of compressed pubkeys for Musig2 cannot be empty") + if (len(pubkeys_cat) % 33) != 0: + raise PSBTSerializationError("The compressed pubkeys for Musig2 must be exactly 33 bytes long") + pubkeys = [] + for i in range(0, len(pubkeys_cat), 33): + pubkeys.append(pubkeys_cat[i: i + 33]) + + self.musig2_participant_pubkeys[key[1:]] = pubkeys else: if key in self.unknown: raise PSBTSerializationError("Duplicate key, key for unknown value already provided") @@ -650,6 +737,11 @@ def serialize(self) -> bytes: value += origin.serialize() r += ser_string(value) + for pk, pubkeys in self.musig2_participant_pubkeys.items(): + r += ser_string(ser_compact_size( + PartiallySignedOutput.PSBT_OUT_MUSIG2_PARTICIPANT_PUBKEYS) + pk) + r += ser_string(b''.join(pubkeys)) + for key, value in sorted(self.unknown.items()): r += ser_string(key) r += ser_string(value) From 9f48eb8fae323087758b65f867fcc1261e9b4cba Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Wed, 17 Apr 2024 16:02:21 +0200 Subject: [PATCH 11/45] Added python standalone implementation of MuSig2 signing, and tests --- test_utils/bip0327.py | 465 +++++++++++++++++++ test_utils/musig2.py | 843 ++++++++++++++++++++++++++++++++++ test_utils/taproot.py | 216 ++++++++- test_utils/taproot_sighash.py | 85 ++++ tests/test_musig2.py | 69 +++ 5 files changed, 1676 insertions(+), 2 deletions(-) create mode 100644 test_utils/bip0327.py create mode 100644 test_utils/musig2.py create mode 100644 test_utils/taproot_sighash.py create mode 100644 tests/test_musig2.py diff --git a/test_utils/bip0327.py b/test_utils/bip0327.py new file mode 100644 index 000000000..79149743f --- /dev/null +++ b/test_utils/bip0327.py @@ -0,0 +1,465 @@ +# from https://github.com/bitcoin/bips/blob/b3701faef2bdb98a0d7ace4eedbeefa2da4c89ed/bip-0327/reference.py +# Distributed as part of BIP-0327 under the BSD-3-Clause license + +# BIP327 reference implementation +# +# WARNING: This implementation is for demonstration purposes only and _not_ to +# be used in production environments. The code is vulnerable to timing attacks, +# for example. + +# fmt: off + +from typing import List, Optional, Tuple, NewType, NamedTuple +import hashlib +import secrets + +# +# The following helper functions were copied from the BIP-340 reference implementation: +# https://github.com/bitcoin/bips/blob/master/bip-0340/reference.py +# + +p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F +n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 + +# Points are tuples of X and Y coordinates and the point at infinity is +# represented by the None keyword. +G = (0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798, 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8) + +Point = Tuple[int, int] + +# This implementation can be sped up by storing the midstate after hashing +# tag_hash instead of rehashing it all the time. +def tagged_hash(tag: str, msg: bytes) -> bytes: + tag_hash = hashlib.sha256(tag.encode()).digest() + return hashlib.sha256(tag_hash + tag_hash + msg).digest() + +def is_infinite(P: Optional[Point]) -> bool: + return P is None + +def x(P: Point) -> int: + assert not is_infinite(P) + return P[0] + +def y(P: Point) -> int: + assert not is_infinite(P) + return P[1] + +def point_add(P1: Optional[Point], P2: Optional[Point]) -> Optional[Point]: + if P1 is None: + return P2 + if P2 is None: + return P1 + if (x(P1) == x(P2)) and (y(P1) != y(P2)): + return None + if P1 == P2: + lam = (3 * x(P1) * x(P1) * pow(2 * y(P1), p - 2, p)) % p + else: + lam = ((y(P2) - y(P1)) * pow(x(P2) - x(P1), p - 2, p)) % p + x3 = (lam * lam - x(P1) - x(P2)) % p + return (x3, (lam * (x(P1) - x3) - y(P1)) % p) + +def point_mul(P: Optional[Point], n: int) -> Optional[Point]: + R = None + for i in range(256): + if (n >> i) & 1: + R = point_add(R, P) + P = point_add(P, P) + return R + +def bytes_from_int(x: int) -> bytes: + return x.to_bytes(32, byteorder="big") + +def lift_x(b: bytes) -> Optional[Point]: + x = int_from_bytes(b) + if x >= p: + return None + y_sq = (pow(x, 3, p) + 7) % p + y = pow(y_sq, (p + 1) // 4, p) + if pow(y, 2, p) != y_sq: + return None + return (x, y if y & 1 == 0 else p-y) + +def int_from_bytes(b: bytes) -> int: + return int.from_bytes(b, byteorder="big") + +def has_even_y(P: Point) -> bool: + assert not is_infinite(P) + return y(P) % 2 == 0 + +def schnorr_verify(msg: bytes, pubkey: bytes, sig: bytes) -> bool: + if len(msg) != 32: + raise ValueError('The message must be a 32-byte array.') + if len(pubkey) != 32: + raise ValueError('The public key must be a 32-byte array.') + if len(sig) != 64: + raise ValueError('The signature must be a 64-byte array.') + P = lift_x(pubkey) + r = int_from_bytes(sig[0:32]) + s = int_from_bytes(sig[32:64]) + if (P is None) or (r >= p) or (s >= n): + return False + e = int_from_bytes(tagged_hash("BIP0340/challenge", sig[0:32] + pubkey + msg)) % n + R = point_add(point_mul(G, s), point_mul(P, n - e)) + if (R is None) or (not has_even_y(R)) or (x(R) != r): + return False + return True + +# +# End of helper functions copied from BIP-340 reference implementation. +# + +PlainPk = NewType('PlainPk', bytes) +XonlyPk = NewType('XonlyPk', bytes) + +# There are two types of exceptions that can be raised by this implementation: +# - ValueError for indicating that an input doesn't conform to some function +# precondition (e.g. an input array is the wrong length, a serialized +# representation doesn't have the correct format). +# - InvalidContributionError for indicating that a signer (or the +# aggregator) is misbehaving in the protocol. +# +# Assertions are used to (1) satisfy the type-checking system, and (2) check for +# inconvenient events that can't happen except with negligible probability (e.g. +# output of a hash function is 0) and can't be manually triggered by any +# signer. + +# This exception is raised if a party (signer or nonce aggregator) sends invalid +# values. Actual implementations should not crash when receiving invalid +# contributions. Instead, they should hold the offending party accountable. +class InvalidContributionError(Exception): + def __init__(self, signer, contrib): + self.signer = signer + # contrib is one of "pubkey", "pubnonce", "aggnonce", or "psig". + self.contrib = contrib + +infinity = None + +def xbytes(P: Point) -> bytes: + return bytes_from_int(x(P)) + +def cbytes(P: Point) -> bytes: + a = b'\x02' if has_even_y(P) else b'\x03' + return a + xbytes(P) + +def cbytes_ext(P: Optional[Point]) -> bytes: + if is_infinite(P): + return (0).to_bytes(33, byteorder='big') + assert P is not None + return cbytes(P) + +def point_negate(P: Optional[Point]) -> Optional[Point]: + if P is None: + return P + return (x(P), p - y(P)) + +def cpoint(x: bytes) -> Point: + if len(x) != 33: + raise ValueError('x is not a valid compressed point.') + P = lift_x(x[1:33]) + if P is None: + raise ValueError('x is not a valid compressed point.') + if x[0] == 2: + return P + elif x[0] == 3: + P = point_negate(P) + assert P is not None + return P + else: + raise ValueError('x is not a valid compressed point.') + +def cpoint_ext(x: bytes) -> Optional[Point]: + if x == (0).to_bytes(33, 'big'): + return None + else: + return cpoint(x) + +# Return the plain public key corresponding to a given secret key +def individual_pk(seckey: bytes) -> PlainPk: + d0 = int_from_bytes(seckey) + if not (1 <= d0 <= n - 1): + raise ValueError('The secret key must be an integer in the range 1..n-1.') + P = point_mul(G, d0) + assert P is not None + return PlainPk(cbytes(P)) + +def key_sort(pubkeys: List[PlainPk]) -> List[PlainPk]: + pubkeys.sort() + return pubkeys + +KeyAggContext = NamedTuple('KeyAggContext', [('Q', Point), + ('gacc', int), + ('tacc', int)]) + +def get_xonly_pk(keyagg_ctx: KeyAggContext) -> XonlyPk: + Q, _, _ = keyagg_ctx + return XonlyPk(xbytes(Q)) + +def key_agg(pubkeys: List[PlainPk]) -> KeyAggContext: + pk2 = get_second_key(pubkeys) + u = len(pubkeys) + Q = infinity + for i in range(u): + try: + P_i = cpoint(pubkeys[i]) + except ValueError: + raise InvalidContributionError(i, "pubkey") + a_i = key_agg_coeff_internal(pubkeys, pubkeys[i], pk2) + Q = point_add(Q, point_mul(P_i, a_i)) + # Q is not the point at infinity except with negligible probability. + assert(Q is not None) + gacc = 1 + tacc = 0 + return KeyAggContext(Q, gacc, tacc) + +def hash_keys(pubkeys: List[PlainPk]) -> bytes: + return tagged_hash('KeyAgg list', b''.join(pubkeys)) + +def get_second_key(pubkeys: List[PlainPk]) -> PlainPk: + u = len(pubkeys) + for j in range(1, u): + if pubkeys[j] != pubkeys[0]: + return pubkeys[j] + return PlainPk(b'\x00'*33) + +def key_agg_coeff(pubkeys: List[PlainPk], pk_: PlainPk) -> int: + pk2 = get_second_key(pubkeys) + return key_agg_coeff_internal(pubkeys, pk_, pk2) + +def key_agg_coeff_internal(pubkeys: List[PlainPk], pk_: PlainPk, pk2: PlainPk) -> int: + L = hash_keys(pubkeys) + if pk_ == pk2: + return 1 + return int_from_bytes(tagged_hash('KeyAgg coefficient', L + pk_)) % n + +def apply_tweak(keyagg_ctx: KeyAggContext, tweak: bytes, is_xonly: bool) -> KeyAggContext: + if len(tweak) != 32: + raise ValueError('The tweak must be a 32-byte array.') + Q, gacc, tacc = keyagg_ctx + if is_xonly and not has_even_y(Q): + g = n - 1 + else: + g = 1 + t = int_from_bytes(tweak) + if t >= n: + raise ValueError('The tweak must be less than n.') + Q_ = point_add(point_mul(Q, g), point_mul(G, t)) + if Q_ is None: + raise ValueError('The result of tweaking cannot be infinity.') + gacc_ = g * gacc % n + tacc_ = (t + g * tacc) % n + return KeyAggContext(Q_, gacc_, tacc_) + +def bytes_xor(a: bytes, b: bytes) -> bytes: + return bytes(x ^ y for x, y in zip(a, b)) + +def nonce_hash(rand: bytes, pk: PlainPk, aggpk: XonlyPk, i: int, msg_prefixed: bytes, extra_in: bytes) -> int: + buf = b'' + buf += rand + buf += len(pk).to_bytes(1, 'big') + buf += pk + buf += len(aggpk).to_bytes(1, 'big') + buf += aggpk + buf += msg_prefixed + buf += len(extra_in).to_bytes(4, 'big') + buf += extra_in + buf += i.to_bytes(1, 'big') + return int_from_bytes(tagged_hash('MuSig/nonce', buf)) + +def nonce_gen_internal(rand_: bytes, sk: Optional[bytes], pk: PlainPk, aggpk: Optional[XonlyPk], msg: Optional[bytes], extra_in: Optional[bytes]) -> Tuple[bytearray, bytes]: + if sk is not None: + rand = bytes_xor(sk, tagged_hash('MuSig/aux', rand_)) + else: + rand = rand_ + if aggpk is None: + aggpk = XonlyPk(b'') + if msg is None: + msg_prefixed = b'\x00' + else: + msg_prefixed = b'\x01' + msg_prefixed += len(msg).to_bytes(8, 'big') + msg_prefixed += msg + if extra_in is None: + extra_in = b'' + k_1 = nonce_hash(rand, pk, aggpk, 0, msg_prefixed, extra_in) % n + k_2 = nonce_hash(rand, pk, aggpk, 1, msg_prefixed, extra_in) % n + # k_1 == 0 or k_2 == 0 cannot occur except with negligible probability. + assert k_1 != 0 + assert k_2 != 0 + R_s1 = point_mul(G, k_1) + R_s2 = point_mul(G, k_2) + assert R_s1 is not None + assert R_s2 is not None + pubnonce = cbytes(R_s1) + cbytes(R_s2) + secnonce = bytearray(bytes_from_int(k_1) + bytes_from_int(k_2) + pk) + return secnonce, pubnonce + +def nonce_gen(sk: Optional[bytes], pk: PlainPk, aggpk: Optional[XonlyPk], msg: Optional[bytes], extra_in: Optional[bytes]) -> Tuple[bytearray, bytes]: + if sk is not None and len(sk) != 32: + raise ValueError('The optional byte array sk must have length 32.') + if aggpk is not None and len(aggpk) != 32: + raise ValueError('The optional byte array aggpk must have length 32.') + rand_ = secrets.token_bytes(32) + return nonce_gen_internal(rand_, sk, pk, aggpk, msg, extra_in) + +def nonce_agg(pubnonces: List[bytes]) -> bytes: + u = len(pubnonces) + aggnonce = b'' + for j in (1, 2): + R_j = infinity + for i in range(u): + try: + R_ij = cpoint(pubnonces[i][(j-1)*33:j*33]) + except ValueError: + raise InvalidContributionError(i, "pubnonce") + R_j = point_add(R_j, R_ij) + aggnonce += cbytes_ext(R_j) + return aggnonce + +SessionContext = NamedTuple('SessionContext', [('aggnonce', bytes), + ('pubkeys', List[PlainPk]), + ('tweaks', List[bytes]), + ('is_xonly', List[bool]), + ('msg', bytes)]) + +def key_agg_and_tweak(pubkeys: List[PlainPk], tweaks: List[bytes], is_xonly: List[bool]): + if len(tweaks) != len(is_xonly): + raise ValueError('The `tweaks` and `is_xonly` arrays must have the same length.') + keyagg_ctx = key_agg(pubkeys) + v = len(tweaks) + for i in range(v): + keyagg_ctx = apply_tweak(keyagg_ctx, tweaks[i], is_xonly[i]) + return keyagg_ctx + +def get_session_values(session_ctx: SessionContext) -> Tuple[Point, int, int, int, Point, int]: + (aggnonce, pubkeys, tweaks, is_xonly, msg) = session_ctx + Q, gacc, tacc = key_agg_and_tweak(pubkeys, tweaks, is_xonly) + b = int_from_bytes(tagged_hash('MuSig/noncecoef', aggnonce + xbytes(Q) + msg)) % n + try: + R_1 = cpoint_ext(aggnonce[0:33]) + R_2 = cpoint_ext(aggnonce[33:66]) + except ValueError: + # Nonce aggregator sent invalid nonces + raise InvalidContributionError(None, "aggnonce") + R_ = point_add(R_1, point_mul(R_2, b)) + R = R_ if not is_infinite(R_) else G + assert R is not None + e = int_from_bytes(tagged_hash('BIP0340/challenge', xbytes(R) + xbytes(Q) + msg)) % n + return (Q, gacc, tacc, b, R, e) + +def get_session_key_agg_coeff(session_ctx: SessionContext, P: Point) -> int: + (_, pubkeys, _, _, _) = session_ctx + pk = PlainPk(cbytes(P)) + if pk not in pubkeys: + raise ValueError('The signer\'s pubkey must be included in the list of pubkeys.') + return key_agg_coeff(pubkeys, pk) + +def sign(secnonce: bytearray, sk: bytes, session_ctx: SessionContext) -> bytes: + (Q, gacc, _, b, R, e) = get_session_values(session_ctx) + k_1_ = int_from_bytes(secnonce[0:32]) + k_2_ = int_from_bytes(secnonce[32:64]) + # Overwrite the secnonce argument with zeros such that subsequent calls of + # sign with the same secnonce raise a ValueError. + secnonce[:64] = bytearray(b'\x00'*64) + if not 0 < k_1_ < n: + raise ValueError('first secnonce value is out of range.') + if not 0 < k_2_ < n: + raise ValueError('second secnonce value is out of range.') + k_1 = k_1_ if has_even_y(R) else n - k_1_ + k_2 = k_2_ if has_even_y(R) else n - k_2_ + d_ = int_from_bytes(sk) + if not 0 < d_ < n: + raise ValueError('secret key value is out of range.') + P = point_mul(G, d_) + assert P is not None + pk = cbytes(P) + if not pk == secnonce[64:97]: + raise ValueError('Public key does not match nonce_gen argument') + a = get_session_key_agg_coeff(session_ctx, P) + g = 1 if has_even_y(Q) else n - 1 + d = g * gacc * d_ % n + s = (k_1 + b * k_2 + e * a * d) % n + psig = bytes_from_int(s) + R_s1 = point_mul(G, k_1_) + R_s2 = point_mul(G, k_2_) + assert R_s1 is not None + assert R_s2 is not None + pubnonce = cbytes(R_s1) + cbytes(R_s2) + # Optional correctness check. The result of signing should pass signature verification. + assert partial_sig_verify_internal(psig, pubnonce, pk, session_ctx) + return psig + +def det_nonce_hash(sk_: bytes, aggothernonce: bytes, aggpk: bytes, msg: bytes, i: int) -> int: + buf = b'' + buf += sk_ + buf += aggothernonce + buf += aggpk + buf += len(msg).to_bytes(8, 'big') + buf += msg + buf += i.to_bytes(1, 'big') + return int_from_bytes(tagged_hash('MuSig/deterministic/nonce', buf)) + +def deterministic_sign(sk: bytes, aggothernonce: bytes, pubkeys: List[PlainPk], tweaks: List[bytes], is_xonly: List[bool], msg: bytes, rand: Optional[bytes]) -> Tuple[bytes, bytes]: + if rand is not None: + sk_ = bytes_xor(sk, tagged_hash('MuSig/aux', rand)) + else: + sk_ = sk + aggpk = get_xonly_pk(key_agg_and_tweak(pubkeys, tweaks, is_xonly)) + + k_1 = det_nonce_hash(sk_, aggothernonce, aggpk, msg, 0) % n + k_2 = det_nonce_hash(sk_, aggothernonce, aggpk, msg, 1) % n + # k_1 == 0 or k_2 == 0 cannot occur except with negligible probability. + assert k_1 != 0 + assert k_2 != 0 + + R_s1 = point_mul(G, k_1) + R_s2 = point_mul(G, k_2) + assert R_s1 is not None + assert R_s2 is not None + pubnonce = cbytes(R_s1) + cbytes(R_s2) + secnonce = bytearray(bytes_from_int(k_1) + bytes_from_int(k_2) + individual_pk(sk)) + try: + aggnonce = nonce_agg([pubnonce, aggothernonce]) + except Exception: + raise InvalidContributionError(None, "aggothernonce") + session_ctx = SessionContext(aggnonce, pubkeys, tweaks, is_xonly, msg) + psig = sign(secnonce, sk, session_ctx) + return (pubnonce, psig) + +def partial_sig_verify(psig: bytes, pubnonces: List[bytes], pubkeys: List[PlainPk], tweaks: List[bytes], is_xonly: List[bool], msg: bytes, i: int) -> bool: + if len(pubnonces) != len(pubkeys): + raise ValueError('The `pubnonces` and `pubkeys` arrays must have the same length.') + if len(tweaks) != len(is_xonly): + raise ValueError('The `tweaks` and `is_xonly` arrays must have the same length.') + aggnonce = nonce_agg(pubnonces) + session_ctx = SessionContext(aggnonce, pubkeys, tweaks, is_xonly, msg) + return partial_sig_verify_internal(psig, pubnonces[i], pubkeys[i], session_ctx) + +def partial_sig_verify_internal(psig: bytes, pubnonce: bytes, pk: bytes, session_ctx: SessionContext) -> bool: + (Q, gacc, _, b, R, e) = get_session_values(session_ctx) + s = int_from_bytes(psig) + if s >= n: + return False + R_s1 = cpoint(pubnonce[0:33]) + R_s2 = cpoint(pubnonce[33:66]) + Re_s_ = point_add(R_s1, point_mul(R_s2, b)) + Re_s = Re_s_ if has_even_y(R) else point_negate(Re_s_) + P = cpoint(pk) + if P is None: + return False + a = get_session_key_agg_coeff(session_ctx, P) + g = 1 if has_even_y(Q) else n - 1 + g_ = g * gacc % n + return point_mul(G, s) == point_add(Re_s, point_mul(P, e * a * g_ % n)) + +def partial_sig_agg(psigs: List[bytes], session_ctx: SessionContext) -> bytes: + (Q, _, tacc, _, R, e) = get_session_values(session_ctx) + s = 0 + u = len(psigs) + for i in range(u): + s_i = int_from_bytes(psigs[i]) + if s_i >= n: + raise InvalidContributionError(i, "psig") + s = (s + s_i) % n + g = 1 if has_even_y(Q) else n - 1 + s = (s + e * g * tacc) % n + return xbytes(R) + bytes_from_int(s) diff --git a/test_utils/musig2.py b/test_utils/musig2.py new file mode 100644 index 000000000..fc0ce3544 --- /dev/null +++ b/test_utils/musig2.py @@ -0,0 +1,843 @@ +""" +This module contains a complete, minimal, standalone MuSig cosigner implementation. +It is NOT a cryptographically secure implementation, and it is only to be used for +testing purposes. + +In lack of a library for wallet policies in python, a minimal version of it for +the purpose of parsing and processing tr() descriptors is implemented here, using +embit for the the final task of compiling simple miniscript descriptors to Script. + +The main objects and methods exported in this class are: + +- PsbtMusig2Cosigner: an abstract class that represents a cosigner in MuSig2. +- HotMusig2Cosigner: an implementation of PsbtMusig2Cosigner that contains a hot + extended private key. Useful for tests. +- run_musig2_test: tests a full signing cycle for a generic list of PsbtMusig2Cosigners. +""" + + +import hashlib +import hmac +from io import BytesIO +import re +from re import Match + +from dataclasses import dataclass +import secrets +import struct +from typing import Dict, Iterable, Iterator, List, Optional, Set, Tuple, Union +from abc import ABC, abstractmethod + +import base58 + +from test_utils.taproot_sighash import SIGHASH_DEFAULT, TaprootSignatureHash + +from . import bip0327, bip0340, sha256 +from . import taproot + +from bitcoin_client.ledger_bitcoin.embit.descriptor.miniscript import Miniscript +from bitcoin_client.ledger_bitcoin.psbt import PSBT, PartiallySignedInput +from bitcoin_client.ledger_bitcoin.key import G, ExtendedKey, bytes_to_point, point_add, point_mul, point_to_bytes +from bitcoin_client.ledger_bitcoin.wallet import WalletPolicy + + +HARDENED_INDEX = 0x80000000 + + +def tapleaf_hash(script: Optional[bytes], leaf_version=b'\xC0') -> Optional[bytes]: + if script is None: + return None + return taproot.tagged_hash( + "TapLeaf", + leaf_version + taproot.ser_script(script) + ) + + +@dataclass +class PlainKeyPlaceholder: + key_index: int + num1: int + num2: int + + +@dataclass +class Musig2KeyPlaceholder: + key_indexes: List[int] + num1: int + num2: int + + +KeyPlaceholder = Union[PlainKeyPlaceholder, Musig2KeyPlaceholder] + + +def parse_placeholder(placeholder_str: str) -> KeyPlaceholder: + """Parses a placeholder string to create a KeyPlaceholder object.""" + if placeholder_str.startswith('musig'): + key_indexes_str = placeholder_str[6:placeholder_str.index( + ')/<')].split(',') + key_indexes = [int(index.strip('@')) for index in key_indexes_str] + + nums_part = placeholder_str[placeholder_str.index(')/<') + 3:-3] + num1, num2 = map(int, nums_part.split(';')) + + return Musig2KeyPlaceholder(key_indexes, num1, num2) + elif placeholder_str.startswith('@'): + parts = placeholder_str.split('/') + key_index = int(parts[0].strip('@')) + + # Remove '<' from the start and '>' from the end + nums_part = parts[1][1:-1] + num1, num2 = map(int, nums_part.split(';')) + + return PlainKeyPlaceholder(key_index, num1, num2) + else: + raise ValueError("Invalid placeholder string") + + +def extract_placeholders(desc_tmpl: str) -> List[KeyPlaceholder]: + """Extracts and parses all placeholders in a descriptor template, from left to right.""" + + pattern = r'musig\((?:@\d+,)*(?:@\d+)\)/<\d+;\d+>/\*|@\d+/<\d+;\d+>/\*' + matches = [(match.group(), match.start()) + for match in re.finditer(pattern, desc_tmpl)] + sorted_matches = sorted(matches, key=lambda x: x[1]) + return [parse_placeholder(match[0]) for match in sorted_matches] + + +def musig(pubkeys: Iterable[bytes], version_bytes: bytes) -> Tuple[str, bip0327.KeyAggContext]: + """ + Constructs the musig2 aggregated extended public key from a list of + compressed public keys, and the version bytes. + """ + + assert all(len(pk) == 33 for pk in pubkeys) + assert len(version_bytes) == 4 + + depth = b'\x00' + fingerprint = b'\x00\x00\x00\x00' + child_number = b'\x00\x00\x00\x00' + + key_agg_ctx = bip0327.key_agg(pubkeys) + Q = key_agg_ctx.Q + compressed_pubkey = ( + b'\x02' if Q[1] % 2 == 0 else b'\x03') + bip0327.get_xonly_pk(key_agg_ctx) + chaincode = bytes.fromhex( + "868087ca02a6f974c4598924c36b57762d32cb45717167e300622c7167e38965") + ext_pubkey = version_bytes + depth + fingerprint + \ + child_number + chaincode + compressed_pubkey + return base58.b58encode_check(ext_pubkey).decode(), key_agg_ctx + + +def aggregate_musig_pubkey(keys_info: Iterable[str]) -> Tuple[str, bip0327.KeyAggContext]: + """ + Constructs the musig2 aggregated extended public key from the list of keys info + of the participating keys. + """ + + pubkeys: list[bytes] = [] + versions: Set[str] = set() + for ki in keys_info: + start = ki.find(']') + xpub = ki[start + 1:] + xpub_bytes = base58.b58decode_check(xpub) + versions.add(xpub_bytes[:4]) + pubkeys.append(xpub_bytes[-33:]) + + if len(versions) > 1: + raise ValueError( + "All the extended public keys should be from the same network") + + return musig(pubkeys, versions.pop()) + + +def derive_from_key_info(key_info: str, steps: List[int]) -> str: + start = key_info.find(']') + pk = ExtendedKey.deserialize(key_info[start + 1:]) + return pk.derive_pub_path(steps).to_string() + + +def derive_plain_descriptor(desc_tmpl: str, keys_info: List[str], is_change: bool, address_index: int): + """ + Given a wallet policy, and the change/address_index combination, computes the corresponding descriptor. + It replaces /** with /<0;1>/* + It also replaces each musig() key expression with the corresponding xpub. + The resulting descriptor can be used with descriptor libraries that do not support musig or wallet policies. + """ + + desc_tmpl = desc_tmpl.replace("/**", "/<0;1>/*") + desc_tmpl = desc_tmpl.replace("*", str(address_index)) + + # Replace each with M if is_change is False, otherwise with N + def replace_m_n(match: Match[str]): + m, n = match.groups() + return m if not is_change else n + + desc_tmpl = re.sub(r'<([^;]+);([^>]+)>', replace_m_n, desc_tmpl) + + # Replace musig(...) expressions + def replace_musig(match: Match[str]): + musig_content = match.group(1) + steps = [int(x) for x in match.group(2).split("/")] + + assert len(steps) == 2 + + key_indexes = [int(i.strip('@')) for i in musig_content.split(',')] + key_infos = [keys_info[i] for i in key_indexes] + agg_xpub = aggregate_musig_pubkey(key_infos)[0] + + return derive_from_key_info(agg_xpub, steps) + + desc_tmpl = re.sub(r'musig\(([^)]+)\)/(\d+/\d+)', replace_musig, desc_tmpl) + + # Replace @i/a/b with the i-th element in keys_info, deriving the key appropriately + # to get a plain xpub + def replace_key_index(match): + index, step1, step2 = [int(x) for x in match.group(1).split('/')] + return derive_from_key_info(keys_info[index], [step1, step2]) + + desc_tmpl = re.sub(r'@(\d+/\d+/\d+)', replace_key_index, desc_tmpl) + + return desc_tmpl + + +class Tree: + """ + Recursive structure that represents a taptree, or one of its subtrees. + It can either contain a single descriptor template (if it's a tapleaf), or exactly two child Trees. + """ + + def __init__(self, content: Union[str, Tuple['Tree', 'Tree']]): + if isinstance(content, str): + self.script = content + self.left, self.right = (None, None) + else: + self.script = None + self.left, self.right = content + + @property + def is_leaf(self) -> bool: + return self.script is not None + + def __str__(self): + if self.is_leaf: + return self.script + else: + return f'{{{str(self.left)},{str(self.right)}}}' + + def placeholders(self) -> Iterator[Tuple[KeyPlaceholder, str]]: + """ + Generates an iterator over the placeholders contained in the scripts of the tree's leaf nodes. + + Yields: + Iterator[Tuple[KeyPlaceholder, str]]: An iterator over tuples containing a KeyPlaceholder and its associated script. + """ + + if self.is_leaf: + assert self.script is not None + for placeholder in extract_placeholders(self.script): + yield (placeholder, self.script) + else: + assert self.left is not None and self.right is not None + for placeholder, script in self.left.placeholders(): + yield (placeholder, script) + for placeholder, script in self.right.placeholders(): + yield (placeholder, script) + + def get_taptree_hash(self, keys_info: List[str], is_change: bool, address_index: int) -> bytes: + if self.is_leaf: + assert self.script is not None + leaf_desc = derive_plain_descriptor( + self.script, keys_info, is_change, address_index) + + s = BytesIO(leaf_desc.encode()) + desc: Miniscript = Miniscript.read_from( + s, taproot=True) + + return tapleaf_hash(desc.compile()) + + else: + assert self.left is not None and self.right is not None + left_h = self.left.get_taptree_hash( + keys_info, is_change, address_index) + right_h = self.left.get_taptree_hash( + keys_info, is_change, address_index) + if left_h <= right_h: + return taproot.tagged_hash("TapBranch", left_h + right_h) + else: + return taproot.tagged_hash("TapBranch", right_h + left_h) + + +class TrDescriptorTemplate: + """ + Represents a descriptor template for a tr(KEY) or a tr(KEY,TREE). + This is minimal implementation in order to enable iterating over the placeholders, + and compile the corresponding leaf scripts. + """ + + def __init__(self, key: KeyPlaceholder, tree=Optional[Tree]): + self.key: KeyPlaceholder = key + self.tree: Optional[Tree] = tree + + @classmethod + def from_string(cls, input_string): + parser = cls.Parser(input_string.replace("/**", "/<0;1>/*")) + return parser.parse() + + class Parser: + def __init__(self, input): + self.input = input + self.index = 0 + self.length = len(input) + + def parse(self): + if self.input.startswith('tr('): + self.consume('tr(') + key = self.parse_keyplaceholder() + tree = None + if self.peek() == ',': + self.consume(',') + tree = self.parse_tree() + self.consume(')') + return TrDescriptorTemplate(key, tree) + else: + raise Exception( + "Syntax error: Input does not start with 'tr('") + + def parse_keyplaceholder(self): + if self.peek() == '@': + self.consume('@') + key_index = self.parse_num() + self.consume('/<') + num1 = self.parse_num() + self.consume(';') + num2 = self.parse_num() + self.consume('>/*') + return PlainKeyPlaceholder(key_index, num1, num2) + elif self.input[self.index:self.index+6] == 'musig(': + self.consume('musig(') + key_indexes = self.parse_key_indexes() + self.consume(')/<') + num1 = self.parse_num() + self.consume(';') + num2 = self.parse_num() + self.consume('>/*') + return Musig2KeyPlaceholder(key_indexes, num1, num2) + else: + raise Exception("Syntax error in key placeholder") + + def parse_tree(self) -> Tree: + if self.peek() == '{': + self.consume('{') + tree1 = self.parse_tree() + self.consume(',') + tree2 = self.parse_tree() + self.consume('}') + return Tree((tree1, tree2)) + else: + return Tree(self.parse_script()) + + def parse_script(self) -> str: + start = self.index + nesting = 0 + while self.index < self.length and (nesting > 0 or self.input[self.index] not in ('}', ',', ')')): + if self.input[self.index] == '(': + nesting = nesting + 1 + elif self.input[self.index] == ')': + nesting = nesting - 1 + + self.index += 1 + return self.input[start:self.index] + + def parse_key_indexes(self): + nums = [] + self.consume('@') + nums.append(self.parse_num()) + while self.peek() == ',': + self.consume(',@') + nums.append(self.parse_num()) + return nums + + def parse_num(self): + start = self.index + while self.index < self.length and self.input[self.index].isdigit(): + self.index += 1 + return int(self.input[start:self.index]) + + def consume(self, char): + if self.input[self.index:self.index+len(char)] == char: + self.index += len(char) + else: + raise Exception( + f"Syntax error: Expected '{char}'; rest: {self.input[self.index:]}") + + def peek(self): + return self.input[self.index] if self.index < self.length else None + + def placeholders(self) -> Iterator[Tuple[KeyPlaceholder, Optional[str]]]: + """ + Generates an iterator over the placeholders contained in the template and its tree, also + yielding the corresponding leaf script descriptor (or None for the keypath placeholder). + + Yields: + Iterator[Tuple[KeyPlaceholder, Optional[str]]]: An iterator over tuples containing a KeyPlaceholder and an optional associated script. + """ + + yield (self.key, None) + + if self.tree is not None: + for placeholder, script in self.tree.placeholders(): + yield (placeholder, script) + + def get_taptree_hash(self, is_change: bool, address_index: int) -> bytes: + if self.tree is None: + raise ValueError("There is no taptree") + return self.tree.get_taptree_hash(is_change, address_index) + + +class PsbtMusig2Cosigner(ABC): + @abstractmethod + def get_participant_pubkey(self) -> bip0327.Point: + """ + This method should returns this cosigner's public key. + """ + pass + + @abstractmethod + def generate_public_nonces(self, psbt: PSBT) -> None: + """ + This method should generate public nonces and modify the given Psbt object in-place. + It should raise an exception in case of failure. + """ + pass + + @abstractmethod + def generate_partial_signatures(self, psbt: PSBT) -> None: + """ + Receives a PSBT that contains all the participants' public nonces, and adds this participant's partial signature. + It should raise an exception in case of failure. + """ + pass + + +def find_change_and_addr_index_for_musig(input_psbt: PartiallySignedInput, placeholder: Musig2KeyPlaceholder, agg_xpub: ExtendedKey): + num1, num2 = placeholder.num1, placeholder.num2 + + # Iterate through tap key origins in the input + # TODO: this might be made more precise (e.g. use the leaf_hash from the tap_bip32_paths items) + for xonly, (_, key_origin) in input_psbt.tap_bip32_paths.items(): + der_path = key_origin.path + # Check if the fingerprint matches the expected pattern and the derivation path has the correct structure + if key_origin.fingerprint == b'\x00\x00\x00\x00' and len(der_path) == 2 and der_path[0] < HARDENED_INDEX and der_path[1] < HARDENED_INDEX and (der_path[0] == num1 or der_path[0] == num2): + if xonly != agg_xpub.derive_pub_path(der_path).pubkey[1:]: + continue + + # Determine if the address is a change address and extract the address index + is_change = (der_path[0] == num2) + addr_index = int(der_path[1]) + return is_change, addr_index + + return None + + +def get_bip32_tweaks(ext_key: ExtendedKey, steps: List[int]) -> List[bytes]: + """ + Generate BIP32 tweaks for a series of derivation steps on an extended key. + + Args: + ext_key (ExtendedKey): The extended public key. + steps (List[int]): A list of derivation steps (must be unhardened). + + Returns: + List[bytes]: The list of additive tweaks for those derivation steps. + """ + + result = [] + + cur_pubkey = ext_key.pubkey + cur_chaincode = ext_key.chaincode + + for step in steps: + if step < 0 or step >= HARDENED_INDEX: + raise ValueError("Invalid unhardened derivation step") + + data = cur_pubkey + struct.pack(">L", step) + Ihmac = hmac.new(cur_chaincode, data, hashlib.sha512).digest() + Il = Ihmac[:32] + Ir = Ihmac[32:] + + result.append(Il) + + Il_int = int.from_bytes(Il, 'big') + child_pubkey_point = point_add(point_mul(G, Il_int), + bytes_to_point(cur_pubkey)) + child_pubkey = point_to_bytes(child_pubkey_point) + + cur_pubkey = child_pubkey + cur_chaincode = Ir + + return result + + +def process_placeholder( + wallet_policy: WalletPolicy, + psbt_input: PartiallySignedInput, + placeholder: Musig2KeyPlaceholder, + keyagg_ctx: bip0327.KeyAggContext, + agg_xpub: ExtendedKey, + tapleaf_desc: Optional[str], + desc_tmpl: TrDescriptorTemplate +) -> Optional[Tuple[List[bytes], List[bool], Optional[bytes], bytes]]: + """ + This method encapsulates all the precomputations that are done for a certain + wallet policy, psbt input and musig() placeholder that are common to both the + nonce generation and the partial signature generation flows. + + Returs a tuple containing: + - tweaks: a list of tweaks to be applied to the aggregate musig key + - is_xonly_tweak: a list of boolean of the same length of tweaks, specifying for + each of them if it's a plain tweak or an x-only tweak + - leaf_script: the compiled leaf script, or None for a taproot keypath spend + - aggpk_tweaked: the value of the aggregate pubkey after applying the tweaks + """ + res = find_change_and_addr_index_for_musig( + psbt_input, placeholder, agg_xpub) + if res is None: + return None + is_change, address_index = res + + leaf_script = None + if tapleaf_desc is not None: + leaf_desc = derive_plain_descriptor( + tapleaf_desc, wallet_policy.keys_info, is_change, address_index) + s = BytesIO(leaf_desc.encode()) + desc: Miniscript = Miniscript.read_from(s, taproot=True) + leaf_script = desc.compile() + + tweaks = [] + is_xonly_tweak = [] + + # Compute bip32 tweaks + bip32_steps = [ + placeholder.num2 if is_change else placeholder.num1, + address_index + ] + bip32_tweaks = get_bip32_tweaks(agg_xpub, bip32_steps) + for tweak in bip32_tweaks: + tweaks.append(tweak) + is_xonly_tweak.append(False) + + # aggregate key after the bip_32 derivations (but before the taptweak, if any) + der_key = agg_xpub.derive_pub_path(bip32_steps) + + # x-only tweak, only if spending the keypath + if tapleaf_desc is None: + t = der_key.pubkey[-32:] + if desc_tmpl.tree is not None: + t += desc_tmpl.get_taptree_hash(is_change, address_index) + tweaks.append(taproot.tagged_hash("TapTweak", t)) + is_xonly_tweak.append(True) + + keyagg_ctx = aggregate_musig_pubkey( + wallet_policy.keys_info[i] for i in placeholder.key_indexes)[1] + + for tweak, is_xonly in zip(tweaks, is_xonly_tweak): + keyagg_ctx = bip0327.apply_tweak(keyagg_ctx, tweak, is_xonly) + + aggpk_tweaked = bip0327.cbytes(keyagg_ctx.Q) + + return (tweaks, is_xonly_tweak, leaf_script, aggpk_tweaked) + + +class HotMusig2Cosigner(PsbtMusig2Cosigner): + """ + Implements a PsbtMusig2Cosigner for a given wallet policy and a private + that appears as one of the key in a musig() key expression. + """ + + def __init__(self, wallet_policy: WalletPolicy, privkey: str) -> None: + super().__init__() + + self.wallet_policy = wallet_policy + self.privkey = ExtendedKey.deserialize(privkey) + + assert self.privkey.to_string() == privkey + + self.musig_psbt_sessions: Dict[bytes, bytes] = {} + + assert self.privkey.is_private + + def compute_psbt_session_id(self, psbt: PSBT) -> bytes: + psbt.tx.rehash() + return sha256(psbt.tx.hash + self.wallet_policy.id) + + def get_participant_pubkey(self) -> bip0327.Point: + return bip0327.cpoint(self.privkey.pubkey) + + def generate_public_nonces(self, psbt: PSBT) -> None: + desc_tmpl = TrDescriptorTemplate.from_string( + self.wallet_policy.descriptor_template) + + psbt_session_id = self.compute_psbt_session_id(psbt) + + # root of all pseudorandomness for this psbt session + rand_seed = secrets.token_bytes(32) + + for placeholder_index, (placeholder, tapleaf_desc) in enumerate(desc_tmpl.placeholders()): + if not isinstance(placeholder, Musig2KeyPlaceholder): + continue + + agg_xpub_str, keyagg_ctx = aggregate_musig_pubkey( + self.wallet_policy.keys_info[i] for i in placeholder.key_indexes) + agg_xpub = ExtendedKey.deserialize(agg_xpub_str) + + for input_index, input in enumerate(psbt.inputs): + result = process_placeholder( + self.wallet_policy, input, placeholder, keyagg_ctx, agg_xpub, tapleaf_desc, desc_tmpl) + if result is None: + continue + + (_, _, leaf_script, aggpk_tweaked) = result + + rand_i_j = sha256( + rand_seed + + input_index.to_bytes(4, byteorder='big') + + placeholder_index.to_bytes(4, byteorder='big') + ) + + # secnonce: bytearray + # pubnonce: bytes + _, pubnonce = bip0327.nonce_gen_internal( + rand_=rand_i_j, + sk=None, + pk=self.privkey.pubkey, + aggpk=aggpk_tweaked, + msg=None, + extra_in=None + ) + + pubnonce_identifier = ( + self.privkey.pubkey, + aggpk_tweaked, + tapleaf_hash(leaf_script) + ) + + assert len(aggpk_tweaked) == 33 + + input.musig2_pub_nonces[pubnonce_identifier] = pubnonce + + self.musig_psbt_sessions[psbt_session_id] = rand_seed + + def generate_partial_signatures(self, psbt: PSBT) -> None: + desc_tmpl = TrDescriptorTemplate.from_string( + self.wallet_policy.descriptor_template) + + psbt_session_id = self.compute_psbt_session_id(psbt) + + # Get the session's randomness seed, while simultaneously deleting it from the open sessions + rand_seed = self.musig_psbt_sessions.pop(psbt_session_id, None) + + if rand_seed is None: + raise ValueError( + "No musig signing session for this psbt") + + for placeholder_index, (placeholder, tapleaf_desc) in enumerate(desc_tmpl.placeholders()): + if not isinstance(placeholder, Musig2KeyPlaceholder): + continue + + agg_xpub_str, keyagg_ctx = aggregate_musig_pubkey( + self.wallet_policy.keys_info[i] for i in placeholder.key_indexes) + agg_xpub = ExtendedKey.deserialize(agg_xpub_str) + + for input_index, input in enumerate(psbt.inputs): + result = process_placeholder( + self.wallet_policy, input, placeholder, keyagg_ctx, agg_xpub, tapleaf_desc, desc_tmpl) + if result is None: + continue + + (tweaks, is_xonly_tweak, leaf_script, aggpk_tweaked) = result + + rand_i_j = sha256( + rand_seed + + input_index.to_bytes(4, byteorder='big') + + placeholder_index.to_bytes(4, byteorder='big') + ) + + secnonce, pubnonce = bip0327.nonce_gen_internal( + rand_=rand_i_j, + sk=None, + pk=self.privkey.pubkey, + aggpk=aggpk_tweaked, + msg=None, + extra_in=None + ) + + pubkeys_in_musig: List[ExtendedKey] = [] + my_key_index_in_musig: Optional[int] = None + for i in placeholder.key_indexes: + k_i = self.wallet_policy.keys_info[i] + xpub_i = k_i[k_i.find(']') + 1:] + pubkeys_in_musig.append(ExtendedKey.deserialize(xpub_i)) + + if xpub_i == self.privkey.neutered().to_string(): + my_key_index_in_musig = i + + if my_key_index_in_musig is None: + raise ValueError("No internal key found in musig") + + nonces: List[bytes] = [] + for participant_key in pubkeys_in_musig: + participant_pubnonce_identifier = ( + participant_key.pubkey, + aggpk_tweaked, + tapleaf_hash(leaf_script) + ) + + if participant_key.pubkey == self.privkey.pubkey and input.musig2_pub_nonces[participant_pubnonce_identifier] != pubnonce: + raise ValueError( + f"Public nonce in psbt didn't match the expected one for cosigner {self.privkey.pubkey}") + + assert len(aggpk_tweaked) == 33 + + if participant_pubnonce_identifier in input.musig2_pub_nonces: + nonces.append( + input.musig2_pub_nonces[participant_pubnonce_identifier]) + else: + raise ValueError( + f"Missing pubnonce for pubkey {participant_key.pubkey.hex()} in psbt") + + if leaf_script is None: + sighash = TaprootSignatureHash( + txTo=psbt.tx, + spent_utxos=[ + psbt.inputs[i].witness_utxo for i in range(len(psbt.inputs))], + hash_type=input.sighash or SIGHASH_DEFAULT, + input_index=input_index, + ) + else: + sighash = TaprootSignatureHash( + txTo=psbt.tx, + spent_utxos=[ + psbt.inputs[i].witness_utxo for i in range(len(psbt.inputs))], + hash_type=input.sighash or SIGHASH_DEFAULT, + input_index=input_index, + scriptpath=True, + script=leaf_script + ) + + aggnonce = bip0327.nonce_agg(nonces) + + session_ctx = bip0327.SessionContext( + aggnonce=aggnonce, + pubkeys=[pk.pubkey for pk in pubkeys_in_musig], + tweaks=tweaks, + is_xonly=is_xonly_tweak, + msg=sighash) + + partial_sig = bip0327.sign( + secnonce, self.privkey.privkey, session_ctx) + + pubnonce_identifier = ( + self.privkey.pubkey, + aggpk_tweaked, + tapleaf_hash(leaf_script) + ) + + input.musig2_partial_sigs[pubnonce_identifier] = partial_sig + + +def run_musig2_test(wallet_policy: WalletPolicy, psbt: PSBT, cosigners: List[PsbtMusig2Cosigner], sighashes: list[bytes]): + """ + This performs the following steps: + - go through all the cosigners to let them add their pubnonce; + - go through all the cosigners to let them add their partial signature; + - aggregate the partial signatures to produce the final Schnorr signature; + - verify that the produced signature is valid for the provided sighash. + + The sighashes (one per input) are given as argument and are assumed to be correct. + """ + + if len(psbt.inputs) != len(sighashes): + raise ValueError("The length of the sighashes array is incorrect") + + for signer in cosigners: + signer.generate_public_nonces(psbt) + + for signer in cosigners: + signer.generate_partial_signatures(psbt) + + desc_tmpl = TrDescriptorTemplate.from_string( + wallet_policy.descriptor_template) + + for placeholder, tapleaf_desc in desc_tmpl.placeholders(): + if not isinstance(placeholder, Musig2KeyPlaceholder): + continue + + agg_xpub_str, keyagg_ctx = aggregate_musig_pubkey( + wallet_policy.keys_info[i] for i in placeholder.key_indexes) + agg_xpub = ExtendedKey.deserialize(agg_xpub_str) + + for input_index, input in enumerate(psbt.inputs): + result = process_placeholder( + wallet_policy, input, placeholder, keyagg_ctx, agg_xpub, tapleaf_desc, desc_tmpl) + + if result is None: + raise RuntimeError( + "Unexpected: processing the musig placeholder failed") + + (tweaks, is_xonly_tweak, leaf_script, aggpk_tweaked) = result + + assert len(aggpk_tweaked) == 33 + + pubkeys_in_musig: List[ExtendedKey] = [] + for i in placeholder.key_indexes: + k_i = wallet_policy.keys_info[i] + xpub_i = k_i[k_i.find(']') + 1:] + pubkeys_in_musig.append(ExtendedKey.deserialize(xpub_i)) + + nonces: List[bytes] = [] + for participant_key in pubkeys_in_musig: + pubnonce_identifier = ( + participant_key.pubkey, + aggpk_tweaked, + tapleaf_hash(leaf_script) + ) + + if pubnonce_identifier in input.musig2_pub_nonces: + nonces.append( + input.musig2_pub_nonces[pubnonce_identifier]) + else: + raise ValueError( + f"Missing pubnonce for pubkey {participant_key.pubkey.hex()} in psbt") + + aggnonce = bip0327.nonce_agg(nonces) + + sighash = sighashes[input_index] + + session_ctx = bip0327.SessionContext( + aggnonce=aggnonce, + pubkeys=[pk.pubkey for pk in pubkeys_in_musig], + tweaks=tweaks, + is_xonly=is_xonly_tweak, + msg=sighash) + + # collect partial signatures + psigs: List[bytes] = [] + + for participant_key in pubkeys_in_musig: + pubnonce_identifier = ( + participant_key.pubkey, + bytes(aggpk_tweaked), + tapleaf_hash(leaf_script) + ) + + if pubnonce_identifier in input.musig2_partial_sigs: + psigs.append( + input.musig2_partial_sigs[pubnonce_identifier]) + else: + raise ValueError( + f"Missing partial signature for pubkey {participant_key.pubkey.hex()} in psbt") + + sig = bip0327.partial_sig_agg(psigs, session_ctx) + + aggpk_tweaked_xonly = aggpk_tweaked[1:] + assert (bip0340.schnorr_verify(sighash, aggpk_tweaked_xonly, sig)) diff --git a/test_utils/taproot.py b/test_utils/taproot.py index 0ba25a2ae..3f84ab56e 100644 --- a/test_utils/taproot.py +++ b/test_utils/taproot.py @@ -1,14 +1,32 @@ -# from portions of BIP-0341 +# from BIP-0340 and BIP-0341 +# - https://github.com/bitcoin/bips/blob/b3701faef2bdb98a0d7ace4eedbeefa2da4c89ed/bip-0340.mediawiki # - https://github.com/bitcoin/bips/blob/b3701faef2bdb98a0d7ace4eedbeefa2da4c89ed/bip-0341.mediawiki # Distributed under the BSD-3-Clause license # fmt: off +# Set DEBUG to True to get a detailed debug output including +# intermediate values during key generation, signing, and +# verification. This is implemented via calls to the +# debug_print_vars() function. +# # If you want to print values on an individual basis, use # the pretty() function, e.g., print(pretty(foo)). import hashlib -import struct +from typing import Any, Optional, Tuple + + +DEBUG = False + +p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F +n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 +SECP256K1_ORDER = n + +# Points are tuples of X and Y coordinates and the point at infinity is +# represented by the None keyword. +G = (0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798, 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8) +Point = Tuple[int, int] # This implementation can be sped up by storing the midstate after hashing # tag_hash instead of rehashing it all the time. @@ -16,6 +34,140 @@ def tagged_hash(tag: str, msg: bytes) -> bytes: tag_hash = hashlib.sha256(tag.encode()).digest() return hashlib.sha256(tag_hash + tag_hash + msg).digest() +def is_infinite(P: Optional[Point]) -> bool: + return P is None + +def x(P: Point) -> int: + assert not is_infinite(P) + return P[0] + +def y(P: Point) -> int: + assert not is_infinite(P) + return P[1] + +def point_add(P1: Optional[Point], P2: Optional[Point]) -> Optional[Point]: + if P1 is None: + return P2 + if P2 is None: + return P1 + if (x(P1) == x(P2)) and (y(P1) != y(P2)): + return None + if P1 == P2: + lam = (3 * x(P1) * x(P1) * pow(2 * y(P1), p - 2, p)) % p + else: + lam = ((y(P2) - y(P1)) * pow(x(P2) - x(P1), p - 2, p)) % p + x3 = (lam * lam - x(P1) - x(P2)) % p + return (x3, (lam * (x(P1) - x3) - y(P1)) % p) + +def point_mul(P: Optional[Point], n: int) -> Optional[Point]: + R = None + for i in range(256): + if (n >> i) & 1: + R = point_add(R, P) + P = point_add(P, P) + return R + +def bytes_from_int(x: int) -> bytes: + return x.to_bytes(32, byteorder="big") + +def bytes_from_point(P: Point) -> bytes: + return bytes_from_int(x(P)) + +def xor_bytes(b0: bytes, b1: bytes) -> bytes: + return bytes(x ^ y for (x, y) in zip(b0, b1)) + +def lift_x(x: int) -> Optional[Point]: + if x >= p: + return None + y_sq = (pow(x, 3, p) + 7) % p + y = pow(y_sq, (p + 1) // 4, p) + if pow(y, 2, p) != y_sq: + return None + return (x, y if y & 1 == 0 else p-y) + +def int_from_bytes(b: bytes) -> int: + return int.from_bytes(b, byteorder="big") + +def hash_sha256(b: bytes) -> bytes: + return hashlib.sha256(b).digest() + +def has_even_y(P: Point) -> bool: + assert not is_infinite(P) + return y(P) % 2 == 0 + +def pubkey_gen(seckey: bytes) -> bytes: + d0 = int_from_bytes(seckey) + if not (1 <= d0 <= n - 1): + raise ValueError('The secret key must be an integer in the range 1..n-1.') + P = point_mul(G, d0) + assert P is not None + return bytes_from_point(P) + +def schnorr_sign(msg: bytes, seckey: bytes, aux_rand: bytes) -> bytes: + d0 = int_from_bytes(seckey) + if not (1 <= d0 <= n - 1): + raise ValueError('The secret key must be an integer in the range 1..n-1.') + if len(aux_rand) != 32: + raise ValueError('aux_rand must be 32 bytes instead of %i.' % len(aux_rand)) + P = point_mul(G, d0) + assert P is not None + d = d0 if has_even_y(P) else n - d0 + t = xor_bytes(bytes_from_int(d), tagged_hash("BIP0340/aux", aux_rand)) + k0 = int_from_bytes(tagged_hash("BIP0340/nonce", t + bytes_from_point(P) + msg)) % n + if k0 == 0: + raise RuntimeError('Failure. This happens only with negligible probability.') + R = point_mul(G, k0) + assert R is not None + k = n - k0 if not has_even_y(R) else k0 + e = int_from_bytes(tagged_hash("BIP0340/challenge", bytes_from_point(R) + bytes_from_point(P) + msg)) % n + sig = bytes_from_point(R) + bytes_from_int((k + e * d) % n) + debug_print_vars() + if not schnorr_verify(msg, bytes_from_point(P), sig): + raise RuntimeError('The created signature does not pass verification.') + return sig + +def schnorr_verify(msg: bytes, pubkey: bytes, sig: bytes) -> bool: + if len(pubkey) != 32: + raise ValueError('The public key must be a 32-byte array.') + if len(sig) != 64: + raise ValueError('The signature must be a 64-byte array.') + P = lift_x(int_from_bytes(pubkey)) + r = int_from_bytes(sig[0:32]) + s = int_from_bytes(sig[32:64]) + if (P is None) or (r >= p) or (s >= n): + debug_print_vars() + return False + e = int_from_bytes(tagged_hash("BIP0340/challenge", sig[0:32] + pubkey + msg)) % n + R = point_add(point_mul(G, s), point_mul(P, n - e)) + if (R is None) or (not has_even_y(R)) or (x(R) != r): + debug_print_vars() + return False + debug_print_vars() + return True + +import inspect + +def pretty(v: Any) -> Any: + if isinstance(v, bytes): + return '0x' + v.hex() + if isinstance(v, int): + return pretty(bytes_from_int(v)) + if isinstance(v, tuple): + return tuple(map(pretty, v)) + return v + +def debug_print_vars() -> None: + if DEBUG: + current_frame = inspect.currentframe() + assert current_frame is not None + frame = current_frame.f_back + assert frame is not None + print(' Variables in function ', frame.f_code.co_name, ' at line ', frame.f_lineno, ':', sep='') + for var_name, var_val in frame.f_locals.items(): + print(' ' + var_name.rjust(11, ' '), '==', pretty(var_val)) + + +import struct def ser_compact_size(l): r = b"" @@ -48,3 +200,63 @@ def ser_string(s): def ser_script(s): return ser_string(s) + + +# BIP-0341 +def taproot_tweak_pubkey(pubkey, h): + t = int_from_bytes(tagged_hash("TapTweak", pubkey + h)) + if t >= SECP256K1_ORDER: + raise ValueError + P = lift_x(int_from_bytes(pubkey)) + if P is None: + raise ValueError + Q = point_add(P, point_mul(G, t)) + return 0 if has_even_y(Q) else 1, bytes_from_int(x(Q)) + +def taproot_tweak_seckey(seckey0, h): + seckey0 = int_from_bytes(seckey0) + P = point_mul(G, seckey0) + seckey = seckey0 if has_even_y(P) else SECP256K1_ORDER - seckey0 + t = int_from_bytes(tagged_hash("TapTweak", bytes_from_int(x(P)) + h)) + if t >= SECP256K1_ORDER: + raise ValueError + return bytes_from_int((seckey + t) % SECP256K1_ORDER) + +def taproot_tree_helper(script_tree): + if isinstance(script_tree, tuple): + leaf_version, script = script_tree + h = tagged_hash("TapLeaf", bytes([leaf_version]) + ser_script(script)) + return ([((leaf_version, script), bytes())], h) + left, left_h = taproot_tree_helper(script_tree[0]) + right, right_h = taproot_tree_helper(script_tree[1]) + ret = [(l, c + right_h) for l, c in left] + [(l, c + left_h) for l, c in right] + if right_h < left_h: + left_h, right_h = right_h, left_h + return (ret, tagged_hash("TapBranch", left_h + right_h)) + +def taproot_output_script(internal_pubkey, script_tree): + """Given a internal public key and a tree of scripts, compute the output script. + script_tree is either: + - a (leaf_version, script) tuple (leaf_version is 0xc0 for [[bip-0342.mediawiki|BIP342]] scripts) + - a list of two elements, each with the same structure as script_tree itself + - None + """ + if script_tree is None: + h = bytes() + else: + _, h = taproot_tree_helper(script_tree) + _, output_pubkey = taproot_tweak_pubkey(internal_pubkey, h) + return bytes([0x51, 0x20]) + output_pubkey + + +# Tweak without tag +def tweak_pubkey(pubkey, data: bytes): + assert len(data) == 32 + t = int_from_bytes(data) + if t >= SECP256K1_ORDER: + raise ValueError + P = lift_x(int_from_bytes(pubkey)) + if P is None: + raise ValueError + Q = point_add(P, point_mul(G, t)) + return 0 if has_even_y(Q) else 1, bytes_from_int(x(Q)) diff --git a/test_utils/taproot_sighash.py b/test_utils/taproot_sighash.py new file mode 100644 index 000000000..073be97e2 --- /dev/null +++ b/test_utils/taproot_sighash.py @@ -0,0 +1,85 @@ +# Based on code from the bitcoin's functional test framework, extracted from: +# https://github.com/bitcoin/bitcoin/blob/58446e1d92c7da46da1fc48e1eb5eefe2e0748cb/test/functional/feature_taproot.py +# +# Copyright (c) 2015-2022 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying + + +import struct +from test_utils import sha256 +from test_utils.taproot import ser_string, tagged_hash + + +def BIP341_sha_prevouts(txTo): + return sha256(b"".join(i.prevout.serialize() for i in txTo.vin)) + + +def BIP341_sha_amounts(spent_utxos): + return sha256(b"".join(struct.pack(" Date: Wed, 9 Oct 2024 10:18:21 +0200 Subject: [PATCH 12/45] Add 'tweak' output parameter to bip32_CKDpub; exposed BIP341 constants --- src/crypto.c | 13 +++++++++---- src/crypto.h | 13 +++++++++++-- src/handler/lib/policy.c | 2 +- src/handler/sign_psbt.c | 2 +- src/handler/sign_psbt/sign_psbt_cache.c | 10 +++++----- 5 files changed, 27 insertions(+), 13 deletions(-) diff --git a/src/crypto.c b/src/crypto.c index 67578f5f4..5b5440269 100644 --- a/src/crypto.c +++ b/src/crypto.c @@ -42,9 +42,9 @@ #include "secp256k1.h" /* BIP0341 tags for computing the tagged hashes when tweaking public keys */ -static const uint8_t BIP0341_taptweak_tag[] = {'T', 'a', 'p', 'T', 'w', 'e', 'a', 'k'}; -static const uint8_t BIP0341_tapbranch_tag[] = {'T', 'a', 'p', 'B', 'r', 'a', 'n', 'c', 'h'}; -static const uint8_t BIP0341_tapleaf_tag[] = {'T', 'a', 'p', 'L', 'e', 'a', 'f'}; +const uint8_t BIP0341_taptweak_tag[] = {'T', 'a', 'p', 'T', 'w', 'e', 'a', 'k'}; +const uint8_t BIP0341_tapbranch_tag[] = {'T', 'a', 'p', 'B', 'r', 'a', 'n', 'c', 'h'}; +const uint8_t BIP0341_tapleaf_tag[] = {'T', 'a', 'p', 'L', 'e', 'a', 'f'}; // Copy of cx_ecfp_scalar_mult_no_throw, but without using randomization for the scalar // multiplication. Therefore, it is faster, but not safe to use on private data, as it is vulnerable @@ -90,7 +90,8 @@ static int secp256k1_point_unsafe(const uint8_t k[static 32], uint8_t out[static int bip32_CKDpub(const serialized_extended_pubkey_t *parent, uint32_t index, - serialized_extended_pubkey_t *child) { + serialized_extended_pubkey_t *child, + uint8_t *tweak) { PRINT_STACK_POINTER(); if (index >= BIP32_FIRST_HARDENED_CHILD) { @@ -115,6 +116,10 @@ int bip32_CKDpub(const serialized_extended_pubkey_t *parent, uint8_t *I_L = &I[0]; uint8_t *I_R = &I[32]; + if (tweak != NULL) { + memcpy(tweak, I_L, 32); + } + // fail if I_L is not smaller than the group order n, but the probability is < 1/2^128 int diff; if (CX_OK != cx_math_cmp_no_throw(I_L, secp256k1_n, 32, &diff) || diff >= 0) { diff --git a/src/crypto.h b/src/crypto.h index f56e24350..466824caf 100644 --- a/src/crypto.h +++ b/src/crypto.h @@ -36,18 +36,22 @@ typedef struct { * * @param[in] parent * Pointer to the extended serialized pubkey of the parent. - * @param[out] index + * @param[in] index * Index of the child to derive. It MUST be not hardened, that is, strictly less than 0x80000000. * @param[out] child * Pointer to the output struct for the child's serialized pubkey. It can equal parent, which in * that case is overwritten. + * @param[out] tweak + * If not NULL, pointer to a 32-byte array that will receive the 32-byte tweak used during the + * child key derivation. * * @return 0 if success, a negative number on failure. * */ int bip32_CKDpub(const serialized_extended_pubkey_t *parent, uint32_t index, - serialized_extended_pubkey_t *child); + serialized_extended_pubkey_t *child, + uint8_t *tweak); /** * Convenience wrapper for cx_hash_no_throw to add some data to an initialized hash context. @@ -331,6 +335,11 @@ int crypto_ecdsa_sign_sha256_hash_with_key(const uint32_t bip32_path[], uint8_t out[static MAX_DER_SIG_LEN], uint32_t *info); +// Constants defined in BIP-0341 +extern const uint8_t BIP0341_taptweak_tag[8]; +extern const uint8_t BIP0341_tapbranch_tag[9]; +extern const uint8_t BIP0341_tapleaf_tag[7]; + /** * Initializes the "tagged" SHA256 hash with the given tag, as defined by BIP-0340. * diff --git a/src/handler/lib/policy.c b/src/handler/lib/policy.c index d8ec5276a..0ce9cefaa 100644 --- a/src/handler/lib/policy.c +++ b/src/handler/lib/policy.c @@ -502,7 +502,7 @@ __attribute__((warn_unused_result)) static int get_derived_pubkey( &ext_pubkey)) { return -1; } - if (0 > bip32_CKDpub(&ext_pubkey, wdi->address_index, &ext_pubkey)) { + if (0 > bip32_CKDpub(&ext_pubkey, wdi->address_index, &ext_pubkey, NULL)) { return -1; } diff --git a/src/handler/sign_psbt.c b/src/handler/sign_psbt.c index ac050234c..f6843f7d3 100644 --- a/src/handler/sign_psbt.c +++ b/src/handler/sign_psbt.c @@ -451,7 +451,7 @@ static int read_change_and_index_from_psbt_bip32_derivation( in_out->is_change, &pubkey)) return -1; - if (0 > bip32_CKDpub(&pubkey, addr_index, &pubkey)) return -1; + if (0 > bip32_CKDpub(&pubkey, addr_index, &pubkey, NULL)) return -1; int pk_offset = is_tap ? 1 : 0; if (memcmp(pubkey.compressed_pubkey + pk_offset, bip32_derivation_pubkey, key_len) != 0) { diff --git a/src/handler/sign_psbt/sign_psbt_cache.c b/src/handler/sign_psbt/sign_psbt_cache.c index 00bf8b027..6eab0819e 100644 --- a/src/handler/sign_psbt/sign_psbt_cache.c +++ b/src/handler/sign_psbt/sign_psbt_cache.c @@ -10,15 +10,15 @@ int derive_first_step_for_pubkey(const serialized_extended_pubkey_t *base_key, // make sure a cache was provided, and the index is less than the size of the cache if (keyexpr->keyexpr_index >= MAX_CACHED_KEY_EXPRESSIONS || !cache) { // do not use the cache, derive the key directly - return bip32_CKDpub(base_key, change_step, out_pubkey); + return bip32_CKDpub(base_key, change_step, out_pubkey, NULL); } if (!cache->derived_child[keyexpr->keyexpr_index].is_child_pubkey_initialized[is_change]) { // key not in cache; compute it and store it in the cache - if (0 > - bip32_CKDpub(base_key, - change_step, - &cache->derived_child[keyexpr->keyexpr_index].child_pubkeys[is_change])) + if (0 > bip32_CKDpub(base_key, + change_step, + &cache->derived_child[keyexpr->keyexpr_index].child_pubkeys[is_change], + NULL)) return -1; cache->derived_child[keyexpr->keyexpr_index].is_child_pubkey_initialized[is_change] = true; From acc0530c23fcd0fa84f428968fae7ede3c1b0742 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Fri, 17 May 2024 17:37:32 +0200 Subject: [PATCH 13/45] Add parsing of Musig2 pubnonces and partial signatures as yielded values in sign_psbt in the python client --- bitcoin_client/ledger_bitcoin/client.py | 73 +++++++++++++++---- bitcoin_client/ledger_bitcoin/client_base.py | 53 ++++++++++++-- .../ledger_bitcoin/client_command.py | 4 + 3 files changed, 112 insertions(+), 18 deletions(-) diff --git a/bitcoin_client/ledger_bitcoin/client.py b/bitcoin_client/ledger_bitcoin/client.py index 94c22aed8..b032c3ea1 100644 --- a/bitcoin_client/ledger_bitcoin/client.py +++ b/bitcoin_client/ledger_bitcoin/client.py @@ -10,8 +10,8 @@ from .command_builder import BitcoinCommandBuilder, BitcoinInsType from .common import Chain, read_uint, read_varint -from .client_command import ClientCommandInterpreter -from .client_base import Client, TransportClient, PartialSignature +from .client_command import ClientCommandInterpreter, CCMD_YIELD_MUSIG_PARTIALSIGNATURE_TAG, CCMD_YIELD_MUSIG_PUBNONCE_TAG +from .client_base import Client, MusigPartialSignature, MusigPubNonce, SignPsbtYieldedObject, TransportClient, PartialSignature from .client_legacy import LegacyClient from .exception import DeviceException from .errors import UnknownDeviceError @@ -105,6 +105,60 @@ def _make_partial_signature(pubkey_augm: bytes, signature: bytes) -> PartialSign return PartialSignature(signature=signature, pubkey=pubkey_augm) +def _decode_signpsbt_yielded_value(res: bytes) -> Tuple[int, SignPsbtYieldedObject]: + res_buffer = BytesIO(res) + input_index_or_tag = read_varint(res_buffer) + if input_index_or_tag == CCMD_YIELD_MUSIG_PUBNONCE_TAG: + input_index = read_varint(res_buffer) + pubnonce = res_buffer.read(66) + participant_pk = res_buffer.read(33) + agg_xonlykey = res_buffer.read(32) + tapleaf_hash = res_buffer.read() + if len(tapleaf_hash) == 0: + tapleaf_hash = None + + return ( + input_index, + MusigPubNonce( + participant_pubkey=participant_pk, + agg_xonlykey=agg_xonlykey, + tapleaf_hash=tapleaf_hash, + pubnonce=pubnonce + ) + ) + elif input_index_or_tag == CCMD_YIELD_MUSIG_PARTIALSIGNATURE_TAG: + input_index = read_varint(res_buffer) + partial_signature = res_buffer.read(32) + participant_pk = res_buffer.read(33) + agg_xonlykey = res_buffer.read(32) + tapleaf_hash = res_buffer.read() + if len(tapleaf_hash) == 0: + tapleaf_hash = None + + return ( + input_index, + MusigPartialSignature( + participant_pubkey=participant_pk, + agg_xonlykey=agg_xonlykey, + tapleaf_hash=tapleaf_hash, + partial_signature=partial_signature + ) + ) + else: + # other values follow an encoding without an explicit tag, where the + # first element is the input index. All the signature types are implemented + # by the PartialSignature type (not to be confused with the musig Partial Signature). + input_index = input_index_or_tag + + pubkey_augm_len = read_uint(res_buffer, 8) + pubkey_augm = res_buffer.read(pubkey_augm_len) + + signature = res_buffer.read() + + return((input_index, _make_partial_signature(pubkey_augm, signature))) + + + class NewClient(Client): # internal use for testing: if set to True, sign_psbt will not clone the psbt before converting to psbt version 2 _no_clone_psbt: bool = False @@ -211,7 +265,7 @@ def get_wallet_address( return result - def sign_psbt(self, psbt: Union[PSBT, bytes, str], wallet: WalletPolicy, wallet_hmac: Optional[bytes]) -> List[Tuple[int, PartialSignature]]: + def sign_psbt(self, psbt: Union[PSBT, bytes, str], wallet: WalletPolicy, wallet_hmac: Optional[bytes]) -> List[Tuple[int, SignPsbtYieldedObject]]: psbt = normalize_psbt(psbt) @@ -280,17 +334,10 @@ def sign_psbt(self, psbt: Union[PSBT, bytes, str], wallet: WalletPolicy, wallet_ if any(len(x) <= 1 for x in results): raise RuntimeError("Invalid response") - results_list: List[Tuple[int, PartialSignature]] = [] + results_list: List[Tuple[int, SignPsbtYieldedObject]] = [] for res in results: - res_buffer = BytesIO(res) - input_index = read_varint(res_buffer) - - pubkey_augm_len = read_uint(res_buffer, 8) - pubkey_augm = res_buffer.read(pubkey_augm_len) - - signature = res_buffer.read() - - results_list.append((input_index, _make_partial_signature(pubkey_augm, signature))) + input_index, obj = _decode_signpsbt_yielded_value(res) + results_list.append((input_index, obj)) return results_list diff --git a/bitcoin_client/ledger_bitcoin/client_base.py b/bitcoin_client/ledger_bitcoin/client_base.py index 5130bf7ef..3e69ee14f 100644 --- a/bitcoin_client/ledger_bitcoin/client_base.py +++ b/bitcoin_client/ledger_bitcoin/client_base.py @@ -28,7 +28,8 @@ def __init__(self, sw: int, data: bytes) -> None: class TransportClient: def __init__(self, interface: Literal['hid', 'tcp'] = "tcp", *, server: str = "127.0.0.1", port: int = 9999, path: Optional[str] = None, hid: Optional[HID] = None, debug: bool = False): - self.transport = Transport('hid', path=path, hid=hid, debug=debug) if interface == 'hid' else Transport(interface, server=server, port=port, debug=debug) + self.transport = Transport('hid', path=path, hid=hid, debug=debug) if interface == 'hid' else Transport( + interface, server=server, port=port, debug=debug) def apdu_exchange( self, cla: int, ins: int, data: bytes = b"", p1: int = 0, p2: int = 0 @@ -67,18 +68,60 @@ def print_response(sw: int, data: bytes) -> None: @dataclass(frozen=True) class PartialSignature: - """Represents a partial signature returned by sign_psbt. + """Represents a partial signature returned by sign_psbt. Such objects can be added to the PSBT. It always contains a pubkey and a signature. - The pubkey + The pubkey is a compressed 33-byte for legacy and segwit Scripts, or 32-byte x-only key for taproot. + The signature is in the format it would be pushed on the scriptSig or the witness stack, therefore of + variable length, and possibly concatenated with the SIGHASH flag byte if appropriate. - The tapleaf_hash is also filled if signing a for a tapscript. + The tapleaf_hash is also filled if signing for a tapscript. + + Note: not to be confused with 'partial signature' of protocols like MuSig2; """ pubkey: bytes signature: bytes tapleaf_hash: Optional[bytes] = None +@dataclass(frozen=True) +class MusigPubNonce: + """Represents a pubnonce returned by sign_psbt during the first round of a Musig2 signing session. + + It always contains + - the participant_pubkey, a 33-byte compressed pubkey; + - agg_xonlykey, the 32-byte xonly key that is the aggregate and tweaked key present in the script; + - the 66-byte pubnonce. + + The tapleaf_hash is also filled if signing for a tapscript; `None` otherwise. + """ + participant_pubkey: bytes + agg_xonlykey: bytes + tapleaf_hash: Optional[bytes] + pubnonce: bytes + + +@dataclass(frozen=True) +class MusigPartialSignature: + """Represents a partial signature returned by sign_psbt during the second round of a Musig2 signing session. + + It always contains + - the participant_pubkey, a 33-byte compressed pubkey; + - agg_xonlykey, the 32-byte xonly key that is the aggregate and tweaked key present in the script; + - the partial_signature, the 32-byte partial signature for this participant. + + The tapleaf_hash is also filled if signing for a tapscript; `None` otherwise + """ + participant_pubkey: bytes + agg_xonlykey: bytes + tapleaf_hash: Optional[bytes] + partial_signature: bytes + + +SignPsbtYieldedObject = Union[PartialSignature, + MusigPubNonce, MusigPartialSignature] + + class Client: def __init__(self, transport_client: TransportClient, chain: Chain = Chain.MAIN, debug: bool = False) -> None: self.transport_client = transport_client @@ -218,7 +261,7 @@ def get_wallet_address( raise NotImplementedError - def sign_psbt(self, psbt: Union[PSBT, bytes, str], wallet: WalletPolicy, wallet_hmac: Optional[bytes]) -> List[Tuple[int, PartialSignature]]: + def sign_psbt(self, psbt: Union[PSBT, bytes, str], wallet: WalletPolicy, wallet_hmac: Optional[bytes]) -> List[Tuple[int, SignPsbtYieldedObject]]: """Signs a PSBT using a registered wallet (or a standard wallet that does not need registration). Signature requires explicit approval from the user. diff --git a/bitcoin_client/ledger_bitcoin/client_command.py b/bitcoin_client/ledger_bitcoin/client_command.py index 9e32a56ba..8495ec1c4 100644 --- a/bitcoin_client/ledger_bitcoin/client_command.py +++ b/bitcoin_client/ledger_bitcoin/client_command.py @@ -15,6 +15,10 @@ class ClientCommandCode(IntEnum): GET_MORE_ELEMENTS = 0xA0 +CCMD_YIELD_MUSIG_PUBNONCE_TAG = 0xFFFFFFFF +CCMD_YIELD_MUSIG_PARTIALSIGNATURE_TAG = 0xFFFFFFFE + + class ClientCommand: def execute(self, request: bytes) -> bytes: raise NotImplementedError("Subclasses should implement this method.") From 3327a8e60b1de3ebc7ce68af5e5bd9ea3fb9f941 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Wed, 9 Oct 2024 11:02:20 +0200 Subject: [PATCH 14/45] MuSig2 signing, rounds 1 and 2 --- src/commands.h | 4 + src/handler/lib/policy.c | 2 +- src/handler/lib/policy.h | 23 ++ src/handler/sign_psbt.c | 615 ++++++++++++++++++++++++++++++---- src/musig/musig.c | 459 ++++++++++++++++++++++++- src/musig/musig.h | 84 +++++ src/musig/musig_sessions.c | 64 ++++ src/musig/musig_sessions.h | 20 ++ tests/test_sign_psbt_musig.py | 162 +++++++++ 9 files changed, 1360 insertions(+), 73 deletions(-) create mode 100644 src/musig/musig_sessions.c create mode 100644 src/musig/musig_sessions.h create mode 100644 tests/test_sign_psbt_musig.py diff --git a/src/commands.h b/src/commands.h index 63b3b4d10..4de0d4910 100644 --- a/src/commands.h +++ b/src/commands.h @@ -11,3 +11,7 @@ typedef enum { GET_MASTER_FINGERPRINT = 0x05, SIGN_MESSAGE = 0x10, } command_e; + +// Tags used when yielding different objects with the YIELD client command. +#define CCMD_YIELD_MUSIG_PUBNONCE_TAG 0xffffffff +#define CCMD_YIELD_MUSIG_PARTIALSIGNATURE_TAG 0xfffffffe diff --git a/src/handler/lib/policy.c b/src/handler/lib/policy.c index 0ce9cefaa..18b527f6d 100644 --- a/src/handler/lib/policy.c +++ b/src/handler/lib/policy.c @@ -420,7 +420,7 @@ execute_processor(policy_parser_state_t *state, policy_parser_processor_t proc, // convenience function, split from get_derived_pubkey only to improve stack usage // returns -1 on error, 0 if the returned key info has no wildcard (**), 1 if it has the wildcard -__attribute__((noinline, warn_unused_result)) static int get_extended_pubkey( +__attribute__((noinline, warn_unused_result)) int get_extended_pubkey( dispatcher_context_t *dispatcher_context, const wallet_derivation_info_t *wdi, int key_index, diff --git a/src/handler/lib/policy.h b/src/handler/lib/policy.h index 6bcbc105a..25f0e564d 100644 --- a/src/handler/lib/policy.h +++ b/src/handler/lib/policy.h @@ -53,6 +53,29 @@ typedef struct { *sign_psbt_cache; // If not NULL, the cache for key derivations used during signing } wallet_derivation_info_t; +/** + * Computes the a derived compressed pubkey for one of the key of the wallet policy, + * for a given change/address_index combination. + * + * This function computes the extended public key (xpub) based on the provided + * BIP32 derivation path. It supports both standard BIP32 derivation and + * the derivation of Musig (multi-signature) keys. + * + * @param[in] dispatcher_context Pointer to the dispatcher content + * @param[in] wdi Pointer to a `wallet_derivation_info_t` struct with the details of the + * necessary details of the wallet policy, and the desired change/address_index pair. + * @param[in] key_index Index of the pubkey in the vector of keys of the wallet policy. + * @param[out] out Pointer to a `serialized_extended_pubkey_t` that will contain the requested + * extended pubkey. + * + * @return -1 on error, 0 if the returned key info has no wildcard (**), 1 if it has the wildcard. + */ +__attribute__((warn_unused_result)) int get_extended_pubkey( + dispatcher_context_t *dispatcher_context, + const wallet_derivation_info_t *wdi, + int key_index, + serialized_extended_pubkey_t *out); + /** * Computes the hash of a taptree, to be used as tweak for the internal key per BIP-0341; * The returned hash is the second value in the tuple returned by taproot_tree_helper in diff --git a/src/handler/sign_psbt.c b/src/handler/sign_psbt.c index f6843f7d3..d4a3bb26a 100644 --- a/src/handler/sign_psbt.c +++ b/src/handler/sign_psbt.c @@ -56,6 +56,8 @@ #include "../swap/swap_globals.h" #include "../swap/handle_swap_sign_transaction.h" +#include "../musig/musig.h" +#include "../musig/musig_sessions.h" // common info that applies to either the current input or the current output typedef struct { @@ -372,7 +374,6 @@ static int get_amount_scriptpubkey_from_psbt( // Convenience function to share common logic when processing all the // PSBT_{IN|OUT}_{TAP}?_BIP32_DERIVATION fields. -// TODO: not generalized for musig2 static int read_change_and_index_from_psbt_bip32_derivation( dispatcher_context_t *dc, keyexpr_info_t *keyexpr_info, @@ -443,20 +444,23 @@ static int read_change_and_index_from_psbt_bip32_derivation( return 0; } - // check that we can indeed derive the same key from the current key expression - serialized_extended_pubkey_t pubkey; - if (0 > derive_first_step_for_pubkey(&keyexpr_info->pubkey, - keyexpr_info->key_expression_ptr, - sign_psbt_cache, - in_out->is_change, - &pubkey)) - return -1; - if (0 > bip32_CKDpub(&pubkey, addr_index, &pubkey, NULL)) return -1; - - int pk_offset = is_tap ? 1 : 0; - if (memcmp(pubkey.compressed_pubkey + pk_offset, bip32_derivation_pubkey, key_len) != 0) { - return 0; - } + // TODO: safe to remove this check? It should be, since we later re-derive + // the script independently. + // // check that we can indeed derive the same key from the current key expression + // serialized_extended_pubkey_t pubkey; + // if (0 > derive_first_step_for_pubkey(&keyexpr_info->pubkey, + // keyexpr_info->key_expression_ptr, + // sign_psbt_cache, + // in_out->is_change, + // &pubkey)) + // return -1; + // if (0 > bip32_CKDpub(&pubkey, addr_index, &pubkey, NULL)) return -1; + + // int pk_offset = is_tap ? 1 : 0; + // if (memcmp(pubkey.compressed_pubkey + pk_offset, bip32_derivation_pubkey, key_len) != 0) + // { + // return 0; + // } in_out->key_expression_found = true; return 1; @@ -663,37 +667,28 @@ init_global_state(dispatcher_context_t *dc, sign_psbt_state_t *st) { return true; } -static bool __attribute__((noinline)) fill_keyexpr_info_if_internal(dispatcher_context_t *dc, - sign_psbt_state_t *st, - keyexpr_info_t *keyexpr_info) { +static bool __attribute__((noinline)) get_and_verify_key_info(dispatcher_context_t *dc, + sign_psbt_state_t *st, + uint16_t key_index, + keyexpr_info_t *keyexpr_info) { policy_map_key_info_t key_info; - { - uint8_t key_info_str[MAX_POLICY_KEY_INFO_LEN]; - - // TODO: generalize for musig: keyexpr_info->key_expression_ptr->k.key_index is wrong - LEDGER_ASSERT(keyexpr_info->key_expression_ptr->type == KEY_EXPRESSION_NORMAL, "TODO"); + uint8_t key_info_str[MAX_POLICY_KEY_INFO_LEN]; - int key_info_len = - call_get_merkle_leaf_element(dc, - st->wallet_header.keys_info_merkle_root, - st->wallet_header.n_keys, - keyexpr_info->key_expression_ptr->k.key_index, - key_info_str, - sizeof(key_info_str)); - - if (key_info_len < 0) { - SEND_SW(dc, SW_BAD_STATE); // should never happen - return false; - } + int key_info_len = call_get_merkle_leaf_element(dc, + st->wallet_header.keys_info_merkle_root, + st->wallet_header.n_keys, + key_index, + key_info_str, + sizeof(key_info_str)); + if (key_info_len < 0) { + return false; // should never happen + } - // Make a sub-buffer for the pubkey info - buffer_t key_info_buffer = buffer_create(key_info_str, key_info_len); + // Make a sub-buffer for the pubkey info + buffer_t key_info_buffer = buffer_create(key_info_str, key_info_len); - if (parse_policy_map_key_info(&key_info_buffer, &key_info, st->wallet_header.version) == - -1) { - SEND_SW(dc, SW_BAD_STATE); // should never happen - return false; - } + if (parse_policy_map_key_info(&key_info_buffer, &key_info, st->wallet_header.version) == -1) { + return false; // should never happen } uint32_t fpr = read_u32_be(key_info.master_key_fingerprint, 0); @@ -701,33 +696,62 @@ static bool __attribute__((noinline)) fill_keyexpr_info_if_internal(dispatcher_c return false; } - { - // it could be a collision on the fingerprint; we verify that we can actually generate - // the same pubkey - if (0 > get_extended_pubkey_at_path(key_info.master_key_derivation, - key_info.master_key_derivation_len, - BIP32_PUBKEY_VERSION, - &keyexpr_info->pubkey)) { - SEND_SW(dc, SW_BAD_STATE); - return false; - } - - if (memcmp(&key_info.ext_pubkey, &keyexpr_info->pubkey, sizeof(keyexpr_info->pubkey)) != - 0) { - return false; - } + // it could be a collision on the fingerprint; we verify that we can actually generate + // the same pubkey + if (0 > get_extended_pubkey_at_path(key_info.master_key_derivation, + key_info.master_key_derivation_len, + BIP32_PUBKEY_VERSION, + &keyexpr_info->pubkey)) { + return false; + } - keyexpr_info->key_derivation_length = key_info.master_key_derivation_len; - for (int i = 0; i < key_info.master_key_derivation_len; i++) { - keyexpr_info->key_derivation[i] = key_info.master_key_derivation[i]; - } + if (memcmp(&key_info.ext_pubkey, &keyexpr_info->pubkey, sizeof(keyexpr_info->pubkey)) != 0) { + return false; + } - keyexpr_info->fingerprint = read_u32_be(key_info.master_key_fingerprint, 0); + keyexpr_info->key_derivation_length = key_info.master_key_derivation_len; + for (int i = 0; i < key_info.master_key_derivation_len; i++) { + keyexpr_info->key_derivation[i] = key_info.master_key_derivation[i]; } + keyexpr_info->fingerprint = read_u32_be(key_info.master_key_fingerprint, 0); + return true; } +static bool fill_keyexpr_info_if_internal(dispatcher_context_t *dc, + sign_psbt_state_t *st, + keyexpr_info_t *keyexpr_info) { + if (keyexpr_info->key_expression_ptr->type == KEY_EXPRESSION_NORMAL) { + return get_and_verify_key_info(dc, + st, + keyexpr_info->key_expression_ptr->k.key_index, + keyexpr_info); + + } else if (keyexpr_info->key_expression_ptr->type == KEY_EXPRESSION_MUSIG) { + // iterate through the keys of the musig() placeholder to find if a key is internal + musig_aggr_key_info_t *musig_info = + r_musig_aggr_key_info(&keyexpr_info->key_expression_ptr->m.musig_info); + uint16_t *key_indexes = r_uint16(&musig_info->key_indexes); + + for (int idx_in_musig = 0; idx_in_musig < musig_info->n; idx_in_musig++) { + if (get_and_verify_key_info(dc, st, key_indexes[idx_in_musig], keyexpr_info)) { + // For musig2, we expect 0 as the fingerprint for the aggregate key, + // and its derivation length is 0 (as it's not derived from the BIP32 hierarchy) + // TODO: refactor, it's ugly to do it here + keyexpr_info->key_derivation_length = 0; + keyexpr_info->fingerprint = 0; + return true; + } + } + + return false; // no internal key found in musig placeholder + } else { + LEDGER_ASSERT(false, "Unreachable code"); + return false; + } +} + // finds the first key expression that corresponds to an internal key static bool find_first_internal_keyexpr(dispatcher_context_t *dc, sign_psbt_state_t *st, @@ -2321,6 +2345,443 @@ static bool __attribute__((noinline)) sign_sighash_schnorr_and_yield(dispatcher_ return true; } +static bool __attribute__((noinline)) yield_musig_data(dispatcher_context_t *dc, + sign_psbt_state_t *st, + unsigned int cur_input_index, + const uint8_t *data, + size_t data_len, + uint32_t tag, + const uint8_t participant_pk[static 33], + const uint8_t aggregate_pubkey[static 33], + const uint8_t *tapleaf_hash) { + LOG_PROCESSOR(__FILE__, __LINE__, __func__); + + if (st->protocol_version == 0) { + // Only support version 1 of the protocol + return false; + } + + // bytes: 1 5 varint data_len 33 33 0 or 32 + // CMD_YIELD + // + + // Yield signature + uint8_t cmd = CCMD_YIELD; + dc->add_to_response(&cmd, 1); + + uint8_t buf[9]; + + // Add tag + int tag_varint_len = varint_write(buf, 0, tag); + dc->add_to_response(buf, tag_varint_len); + + // Add input index + int input_index_varint_len = varint_write(buf, 0, cur_input_index); + dc->add_to_response(buf, input_index_varint_len); + + // Add data (pubnonce or partial signature) + dc->add_to_response(data, data_len); + + // Add participant public key + dc->add_to_response(participant_pk, 33); + + // Add aggregate public key + dc->add_to_response(aggregate_pubkey, 33); + + // Add tapleaf hash if provided + if (tapleaf_hash != NULL) { + dc->add_to_response(tapleaf_hash, 32); + } + + dc->finalize_response(SW_INTERRUPTED_EXECUTION); + + if (dc->process_interruption(dc) < 0) { + return false; + } + return true; +} + +static bool yield_musig_pubnonce(dispatcher_context_t *dc, + sign_psbt_state_t *st, + unsigned int cur_input_index, + const musig_pubnonce_t *pubnonce, + const uint8_t participant_pk[static 33], + const uint8_t aggregate_pubkey[static 33], + const uint8_t *tapleaf_hash) { + return yield_musig_data(dc, + st, + cur_input_index, + (const uint8_t *) pubnonce, + sizeof(musig_pubnonce_t), + CCMD_YIELD_MUSIG_PUBNONCE_TAG, + participant_pk, + aggregate_pubkey, + tapleaf_hash); +} + +static bool yield_musig_partial_signature(dispatcher_context_t *dc, + sign_psbt_state_t *st, + unsigned int cur_input_index, + const uint8_t psig[static 32], + const uint8_t participant_pk[static 33], + const uint8_t aggregate_pubkey[static 33], + const uint8_t *tapleaf_hash) { + return yield_musig_data(dc, + st, + cur_input_index, + psig, + 32, + CCMD_YIELD_MUSIG_PARTIALSIGNATURE_TAG, + participant_pk, + aggregate_pubkey, + tapleaf_hash); +} + +static bool __attribute__((noinline)) sign_sighash_musig_and_yield(dispatcher_context_t *dc, + sign_psbt_state_t *st, + keyexpr_info_t *keyexpr_info, + input_info_t *input, + unsigned int cur_input_index, + uint8_t sighash[static 32]) { + LOG_PROCESSOR(__FILE__, __LINE__, __func__); + + if (st->wallet_policy_map->type != TOKEN_TR) { + SEND_SW(dc, SW_BAD_STATE); // should never happen + return false; + } + + const policy_node_tr_t *tr_policy = (policy_node_tr_t *) st->wallet_policy_map; + + // plan: + // 1) compute aggregate pubkey + // 2) compute musig2 tweaks + // 3) compute taproot tweak (if keypath spend) + // if my pubnonce is in the psbt: + // 5) generate and yield pubnonce + // else: + // 6) generate and yield partial signature + + // 1) compute aggregate pubkey + + // TODO: we should compute the aggregate pubkey just once for the placeholder, instead of + // repeating for each input + wallet_derivation_info_t wdi = {.n_keys = st->wallet_header.n_keys, + .wallet_version = st->wallet_header.version, + .keys_merkle_root = st->wallet_header.keys_info_merkle_root, + .change = input->in_out.is_change, + .address_index = input->in_out.address_index}; + + // TODO: code duplication with policy.c::get_derived_pubkey; worth extracting a common method? + + serialized_extended_pubkey_t ext_pubkey; + + const policy_node_keyexpr_t *key_expr = keyexpr_info->key_expression_ptr; + musig_aggr_key_info_t *musig_info = r_musig_aggr_key_info(&key_expr->m.musig_info); + uint16_t *key_indexes = r_uint16(&musig_info->key_indexes); + plain_pk_t keys[MAX_PUBKEYS_PER_MUSIG]; + for (int i = 0; i < musig_info->n; i++) { + // we use ext_pubkey as a temporary variable; will overwrite later + if (0 > get_extended_pubkey(dc, &wdi, key_indexes[i], &ext_pubkey)) { + return -1; + } + memcpy(keys[i], ext_pubkey.compressed_pubkey, sizeof(ext_pubkey.compressed_pubkey)); + } + + musig_keyagg_context_t musig_ctx; + musig_key_agg(keys, musig_info->n, &musig_ctx); + + // compute the aggregated extended pubkey + memset(&ext_pubkey, 0, sizeof(ext_pubkey)); + write_u32_be(ext_pubkey.version, 0, BIP32_PUBKEY_VERSION); + + ext_pubkey.compressed_pubkey[0] = (musig_ctx.Q.y[31] % 2 == 0) ? 2 : 3; + memcpy(&ext_pubkey.compressed_pubkey[1], musig_ctx.Q.x, sizeof(musig_ctx.Q.x)); + memcpy(&ext_pubkey.chain_code, BIP_MUSIG_CHAINCODE, sizeof(BIP_MUSIG_CHAINCODE)); + + // 2) compute musig2 tweaks + // We always have exactly 2 BIP32 tweaks in wallet policies; if the musig is in the keypath + // spend, we also have an x-only taptweak with the taproot tree hash (or BIP-86/BIP-386 style if + // there is no taproot tree). + + uint32_t change_step = input->in_out.is_change ? keyexpr_info->key_expression_ptr->num_second + : keyexpr_info->key_expression_ptr->num_first; + uint32_t addr_index_step = input->in_out.address_index; + + // in wallet policies, we always have at least two bip32-tweaks, and we might have + // one x-only tweak per BIP-0341 (if spending from the keypath). + uint8_t tweaks[3][32]; + uint8_t *tweaks_ptrs[3] = {tweaks[0], tweaks[1], tweaks[2]}; + bool is_xonly[] = {false, false, true}; + size_t n_tweaks = 2; // might be changed to 3 below + + serialized_extended_pubkey_t agg_key_tweaked; + if (0 > bip32_CKDpub(&ext_pubkey, change_step, &agg_key_tweaked, tweaks[0])) { + SEND_SW(dc, SW_BAD_STATE); // should never happen + return false; + } + if (0 > bip32_CKDpub(&agg_key_tweaked, addr_index_step, &agg_key_tweaked, tweaks[1])) { + SEND_SW(dc, SW_BAD_STATE); // should never happen + return false; + } + + // 3) compute taproot tweak (if keypath spend) + memset(tweaks[2], 0, 32); + if (!keyexpr_info->is_tapscript) { + n_tweaks = 3; + + crypto_tr_tagged_hash( + BIP0341_taptweak_tag, + sizeof(BIP0341_taptweak_tag), + agg_key_tweaked.compressed_pubkey + 1, // xonly key, after BIP-32 tweaks + 32, + input->taptree_hash, + // BIP-86 compliant tweak if there's no taptree, otherwise use the taptree hash + isnull_policy_node_tree(&tr_policy->tree) ? 0 : 32, + tweaks[2]); + + // also apply the taptweak to agg_key_tweaked + + uint8_t parity = 0; + crypto_tr_tweak_pubkey(agg_key_tweaked.compressed_pubkey + 1, + input->taptree_hash, + isnull_policy_node_tree(&tr_policy->tree) ? 0 : 32, + &parity, + agg_key_tweaked.compressed_pubkey + 1); + agg_key_tweaked.compressed_pubkey[0] = 0x02 + parity; + } + + // we will no longer use the other fields of the extended pubkey, so we zero them for sanity + memset(agg_key_tweaked.chain_code, 0, sizeof(agg_key_tweaked.chain_code)); + memset(agg_key_tweaked.child_number, 0, sizeof(agg_key_tweaked.child_number)); + agg_key_tweaked.depth = 0; + memset(agg_key_tweaked.parent_fingerprint, 0, sizeof(agg_key_tweaked.parent_fingerprint)); + memset(agg_key_tweaked.version, 0, sizeof(agg_key_tweaked.version)); + + // Compute musig_my_psbt_id. It is the psbt key that this signer uses to find pubnonces and + // partial signatures (PSBT_IN_MUSIG2_PUB_NONCE and PSBT_IN_MUSIG2_PARTIAL_SIG fields). The + // length is either 33+33 (keypath spend), or 33+33+32 bytes (tapscript spend). It's the + // concatenation of: + // - the 33-byte compressed pubkey of this participant + // - the 33-byte aggregate compressed pubkey (after all the tweaks) + // - (tapscript only) the 32-byte tapleaf hash + uint8_t musig_my_psbt_id_key[1 + 33 + 33 + 32]; + musig_my_psbt_id_key[0] = PSBT_IN_MUSIG2_PUB_NONCE; + + uint8_t *musig_my_psbt_id = musig_my_psbt_id_key + 1; + size_t psbt_id_len = keyexpr_info->is_tapscript ? 33 + 33 + 32 : 33 + 33; + memcpy(musig_my_psbt_id, keyexpr_info->pubkey.compressed_pubkey, 33); + memcpy(musig_my_psbt_id + 33, agg_key_tweaked.compressed_pubkey, 33); + if (keyexpr_info->is_tapscript) { + memcpy(musig_my_psbt_id + 33 + 33, keyexpr_info->tapleaf_hash, 32); + } + + // compute psbt session id + uint8_t psbt_session_id[32]; + // TODO: for now we use simply a hash that depends on the keys of the wallet policy; this is not + // good enough. It should be a hash that depends on: + // - the wallet policy id + // - the tx being signed + // - the input index + // - the index of the placeholder we're signing for + memcpy(psbt_session_id, st->wallet_header.keys_info_merkle_root, sizeof(psbt_session_id)); + + // 4) check if my pubnonce is in the psbt + musig_pubnonce_t my_pubnonce; + if (sizeof(musig_pubnonce_t) != call_get_merkleized_map_value(dc, + &input->in_out.map, + musig_my_psbt_id_key, + 1 + psbt_id_len, + my_pubnonce.raw, + sizeof(musig_pubnonce_t))) { + // 5) generate and yield pubnonce + + // if an existing session for psbt_session_id exists, delete it + if (musigsession_pop(psbt_session_id, NULL)) { + // We wouldn't expect this: probably the client sent the same psbt for + // round 1 twice, without adding the pubnonces to the psbt after the first round. + // We delete the old session and start a fresh one, but we print a + // warning if in debug mode. + PRINTF("Session with the same id already existing\n"); + } + + musig_session_t psbt_session; + memcpy(psbt_session.id, psbt_session_id, sizeof(psbt_session_id)); + + // TODO: the "session" should be initialized once for all the (inputs, placeholder) pairs; + // this is wrong! + musigsession_init_randomness(&psbt_session); + + uint8_t rand_i_j[32]; + compute_rand_i_j(psbt_session.rand_root, + cur_input_index, + keyexpr_info->cur_index, + rand_i_j); + + musig_secnonce_t secnonce; + musig_pubnonce_t pubnonce; + if (0 > musig_nonce_gen(rand_i_j, + keyexpr_info->pubkey.compressed_pubkey, + agg_key_tweaked.compressed_pubkey + 1, + &secnonce, + &pubnonce)) { + PRINTF("MuSig2 nonce generation failed\n"); + SEND_SW(dc, SW_BAD_STATE); // should never happen + return false; + } + + if (!yield_musig_pubnonce(dc, + st, + cur_input_index, + &pubnonce, + keyexpr_info->pubkey.compressed_pubkey, + agg_key_tweaked.compressed_pubkey, + keyexpr_info->is_tapscript ? keyexpr_info->tapleaf_hash : NULL)) { + PRINTF("Failed yielding MuSig2 pubnonce\n"); + SEND_SW(dc, SW_BAD_STATE); // should never happen + return false; + } + + // TODO: wrong if we have multiple inputs! + musigsession_store(psbt_session_id, &psbt_session); + } else { + // 6) generate and yield partial signature + musig_session_t psbt_session; + // get and delete the musig session from permanent storage + if (!musigsession_pop(psbt_session_id, &psbt_session)) { + // The PSBT contains a partial nonce, but we do not have the corresponding psbt session + // in storage. Either it was deleted, or the pubnonces were not real. Either way, we + // cannot continue. + PRINTF("Missing MuSig2 session\n"); + SEND_SW(dc, SW_BAD_STATE); + return false; + } + + musig_pubnonce_t nonces[MAX_PUBKEYS_PER_MUSIG]; + + for (int i = 0; i < musig_info->n; i++) { + uint8_t musig_ith_psbt_id_key[1 + 33 + 33 + 32]; + uint8_t *musig_ith_psbt_id = musig_ith_psbt_id_key + 1; + // copy from musig_my_psbt_id_key, but replace the corresponding pubkey + memcpy(musig_ith_psbt_id_key, musig_my_psbt_id_key, sizeof(musig_my_psbt_id_key)); + memcpy(musig_ith_psbt_id, keys[i], sizeof(plain_pk_t)); + + // TODO: could avoid fetching again our own pubnonce + if (sizeof(musig_pubnonce_t) != + call_get_merkleized_map_value(dc, + &input->in_out.map, + musig_ith_psbt_id_key, + 1 + psbt_id_len, + nonces[i].raw, + sizeof(musig_pubnonce_t))) { + PRINTF("Missing or incorrect pubnonce for a MuSig2 cosigner\n"); + SEND_SW(dc, SW_INCORRECT_DATA); + return false; + } + } + + // compute aggregate nonce + musig_pubnonce_t aggnonce; + int res = musig_nonce_agg(nonces, musig_info->n, &aggnonce); + if (res < 0) { + PRINTF("Musig aggregation failed; disruptive signer has index %d\n", -res); + SEND_SW(dc, SW_INCORRECT_DATA); + } + + // recompute secnonce from psbt_session randomness + uint8_t rand_i_j[32]; + compute_rand_i_j(psbt_session.rand_root, + cur_input_index, + keyexpr_info->cur_index, + rand_i_j); + + musig_secnonce_t secnonce; + musig_pubnonce_t pubnonce; + + if (0 > musig_nonce_gen(rand_i_j, + keyexpr_info->pubkey.compressed_pubkey, + agg_key_tweaked.compressed_pubkey + 1, + &secnonce, + &pubnonce)) { + PRINTF("MuSig2 nonce generation failed\n"); + SEND_SW(dc, SW_BAD_STATE); // should never happen + return false; + } + + // derive secret key + + cx_ecfp_private_key_t private_key = {0}; + uint8_t psig[32]; + bool err = false; + do { // block executed once, only to allow safely breaking out on error + + // derive secret key + uint32_t sign_path[MAX_BIP32_PATH_STEPS]; + + // TODO: wrong, this should be based on the internal key derivation length + for (int i = 0; i < keyexpr_info->key_derivation_length; i++) { + sign_path[i] = keyexpr_info->key_derivation[i]; + } + sign_path[keyexpr_info->key_derivation_length] = change_step; + sign_path[keyexpr_info->key_derivation_length + 1] = addr_index_step; + + int sign_path_len = keyexpr_info->key_derivation_length + 2; + + // TODO: hardcoded for the current tests. Fix this! + sign_path[0] = 0x80000000 + 44; + sign_path[1] = 0x80000000 + 1; + sign_path[2] = 0x80000000 + 0; + sign_path_len = 3; + + if (bip32_derive_init_privkey_256(CX_CURVE_256K1, + sign_path, + sign_path_len, + &private_key, + NULL) != CX_OK) { + err = true; + break; + } + + // Create partial signature + musig_session_context_t musig_session_context = {.aggnonce = &aggnonce, + .n_keys = musig_info->n, + .pubkeys = keys, + .n_tweaks = n_tweaks, + .tweaks = tweaks_ptrs, + .is_xonly = is_xonly, + .msg = sighash, + .msg_len = 32}; + + if (0 > musig_sign(&secnonce, private_key.d, &musig_session_context, psig)) { + PRINTF("Musig2 signature failed\n"); + err = true; + break; + } + } while (false); + + explicit_bzero(&private_key, sizeof(private_key)); + + if (err) { + PRINTF("Partial signature generation failed\n"); + return false; + } + + if (!yield_musig_partial_signature( + dc, + st, + cur_input_index, + psig, + keyexpr_info->pubkey.compressed_pubkey, + agg_key_tweaked.compressed_pubkey, + keyexpr_info->is_tapscript ? keyexpr_info->tapleaf_hash : NULL)) { + PRINTF("Failed yielding MuSig2 partial signature\n"); + SEND_SW(dc, SW_BAD_STATE); // should never happen + return false; + } + } + + return true; +} + static bool __attribute__((noinline)) compute_segwit_hashes(dispatcher_context_t *dc, sign_psbt_state_t *st, segwit_hashes_t *hashes) { { @@ -2475,6 +2936,8 @@ static bool __attribute__((noinline)) sign_transaction_input(dispatcher_context_ // Sign as segwit input iff it has a witness utxo if (!input->has_witnessUtxo) { + LEDGER_ASSERT(keyexpr_info->key_expression_ptr->type == KEY_EXPRESSION_NORMAL, + "Only plain key expressions are valid for legacy inputs"); // sign legacy P2PKH or P2SH // sign_non_witness(non_witness_utxo.vout[psbt.tx.input_[i].prevout.n].scriptPubKey, i) @@ -2555,6 +3018,8 @@ static bool __attribute__((noinline)) sign_transaction_input(dispatcher_context_ int segwit_version = get_policy_segwit_version(st->wallet_policy_map); uint8_t sighash[32]; if (segwit_version == 0) { + LEDGER_ASSERT(keyexpr_info->key_expression_ptr->type == KEY_EXPRESSION_NORMAL, + "Only plain key expressions are valid for SegwitV0 inputs"); if (!input->has_sighash_type) { // segwitv0 inputs default to SIGHASH_ALL input->sighash_type = SIGHASH_ALL; @@ -2606,13 +3071,25 @@ static bool __attribute__((noinline)) sign_transaction_input(dispatcher_context_ } } - if (!sign_sighash_schnorr_and_yield(dc, - st, - keyexpr_info, - input, - cur_input_index, - sighash)) - return false; + if (keyexpr_info->key_expression_ptr->type == KEY_EXPRESSION_NORMAL) { + if (!sign_sighash_schnorr_and_yield(dc, + st, + keyexpr_info, + input, + cur_input_index, + sighash)) + return false; + } else if (keyexpr_info->key_expression_ptr->type == KEY_EXPRESSION_MUSIG) { + if (!sign_sighash_musig_and_yield(dc, + st, + keyexpr_info, + input, + cur_input_index, + sighash)) + return false; + } else { + LEDGER_ASSERT(false, "Unreachable"); + } } else { SEND_SW(dc, SW_BAD_STATE); // can't happen diff --git a/src/musig/musig.c b/src/musig/musig.c index b4ffb4432..752e54e45 100644 --- a/src/musig/musig.c +++ b/src/musig/musig.c @@ -1,5 +1,7 @@ #include +#include "cx_errors.h" + #include "musig.h" #include "../crypto.h" @@ -9,6 +11,12 @@ static const uint8_t BIP0327_keyagg_coeff_tag[] = {'K', 'e', 'y', 'A', 'g', 'g', ' ', 'c', 'o', 'e', 'f', 'f', 'i', 'c', 'i', 'e', 'n', 't'}; static const uint8_t BIP0327_keyagg_list_tag[] = {'K', 'e', 'y', 'A', 'g', 'g', ' ', 'l', 'i', 's', 't'}; +static const uint8_t BIP0327_nonce_tag[] = {'M', 'u', 'S', 'i', 'g', '/', 'n', 'o', 'n', 'c', 'e'}; +static const uint8_t BIP0327_noncecoef_tag[] = + {'M', 'u', 'S', 'i', 'g', '/', 'n', 'o', 'n', 'c', 'e', 'c', 'o', 'e', 'f'}; + +static const uint8_t BIP0340_challenge_tag[] = + {'B', 'I', 'P', '0', '3', '4', '0', '/', 'c', 'h', 'a', 'l', 'l', 'e', 'n', 'g', 'e'}; static inline bool is_point_infinite(const point_t *P) { return P->prefix == 0; @@ -18,7 +26,9 @@ static inline void set_point_infinite(point_t *P) { memset(P->raw, 0, sizeof(point_t)); } -static int point_add(const point_t *P1, const point_t *P2, point_t *out) { +#define G ((const point_t *) secp256k1_generator) + +static cx_err_t point_add(const point_t *P1, const point_t *P2, point_t *out) { if (is_point_infinite(P1)) { memmove(out->raw, P2->raw, sizeof(point_t)); return CX_OK; @@ -31,7 +41,30 @@ static int point_add(const point_t *P1, const point_t *P2, point_t *out) { memset(out->raw, 0, sizeof(point_t)); return CX_OK; } - return cx_ecfp_add_point_no_throw(CX_CURVE_SECP256K1, out->raw, P1->raw, P2->raw); + + cx_err_t res = cx_ecfp_add_point_no_throw(CX_CURVE_SECP256K1, out->raw, P1->raw, P2->raw); + if (res == CX_EC_INFINITE_POINT) { + set_point_infinite(out); + return CX_OK; + } + + return res; +} + +static cx_err_t point_mul(const point_t *P, const uint8_t scalar[static 32], point_t *out) { + if (is_point_infinite(P)) { + set_point_infinite(out); + return CX_OK; + } + point_t Q; // result + memcpy(&Q, P, sizeof(point_t)); + cx_err_t res = cx_ecfp_scalar_mult_no_throw(CX_CURVE_SECP256K1, Q.raw, scalar, 32); + if (res == CX_EC_INFINITE_POINT) { + set_point_infinite(out); + return CX_OK; + } + memcpy(out, &Q, sizeof(point_t)); + return res; } // out can be equal to P @@ -48,6 +81,12 @@ static int point_negate(const point_t *P, point_t *out) { return 0; } +static bool has_even_y(const point_t *P) { + LEDGER_ASSERT(!is_point_infinite(P), "has_even_y called with an infinite point"); + + return P->y[31] % 2 == 0; +} + static int cpoint(const uint8_t x[33], point_t *out) { crypto_tr_lift_x(&x[1], out->raw); if (is_point_infinite(out)) { @@ -67,6 +106,25 @@ static int cpoint(const uint8_t x[33], point_t *out) { } } +static bool is_array_zero(const uint8_t buffer[], size_t buffer_len) { + uint8_t acc = 0; + for (size_t i = 0; i < buffer_len; i++) { + acc |= buffer[i]; + } + return acc == 0; +} + +int cpoint_ext(const uint8_t x[static 33], point_t *out) { + // Check if the point is at infinity (all bytes zero) + if (is_array_zero(x, 33)) { + set_point_infinite(out); + return 0; + } + + // Otherwise, handle as a regular compressed point + return cpoint(x, out); +} + static void musig_get_second_key(const plain_pk_t pubkeys[], size_t n_keys, plain_pk_t out) { for (size_t i = 0; i < n_keys; i++) { if (memcmp(pubkeys[0], pubkeys[i], sizeof(plain_pk_t)) != 0) { @@ -114,6 +172,16 @@ static void musig_key_agg_coeff_internal(const plain_pk_t pubkeys[], } } +static void musig_key_agg_coeff(const plain_pk_t pubkeys[], + size_t n_keys, + const plain_pk_t pk_, + uint8_t out[static CX_SHA256_SIZE]) { + plain_pk_t pk2; + musig_get_second_key(pubkeys, n_keys, pk2); + + musig_key_agg_coeff_internal(pubkeys, n_keys, pk_, pk2, out); +} + int musig_key_agg(const plain_pk_t pubkeys[], size_t n_keys, musig_keyagg_context_t *ctx) { plain_pk_t pk2; musig_get_second_key(pubkeys, n_keys, pk2); @@ -132,7 +200,7 @@ int musig_key_agg(const plain_pk_t pubkeys[], size_t n_keys, musig_keyagg_contex musig_key_agg_coeff_internal(pubkeys, n_keys, pubkeys[i], pk2, a_i); // set P := a_i * P_i - if (CX_OK != cx_ecfp_scalar_mult_no_throw(CX_CURVE_SECP256K1, P.raw, a_i, 32)) { + if (CX_OK != point_mul(&P, a_i, &P)) { PRINTF("Scalar multiplication failed in musig_key_agg\n"); return -1; } @@ -144,3 +212,388 @@ int musig_key_agg(const plain_pk_t pubkeys[], size_t n_keys, musig_keyagg_contex ctx->gacc[31] = 1; return 0; } + +static void musig_nonce_hash(const uint8_t *rand, + const plain_pk_t pk, + const xonly_pk_t aggpk, + uint8_t i, + const uint8_t *msg_prefixed, + size_t msg_prefixed_len, + const uint8_t *extra_in, + size_t extra_in_len, + uint8_t out[static CX_SHA256_SIZE]) { + cx_sha256_t hash_context; + crypto_tr_tagged_hash_init(&hash_context, BIP0327_nonce_tag, sizeof(BIP0327_nonce_tag)); + + // rand + crypto_hash_update(&hash_context.header, rand, 32); + + // len(pk) + pk + crypto_hash_update_u8(&hash_context.header, sizeof(plain_pk_t)); + crypto_hash_update(&hash_context.header, pk, sizeof(plain_pk_t)); + + // len(aggpk) + aggpk + crypto_hash_update_u8(&hash_context.header, sizeof(xonly_pk_t)); + crypto_hash_update(&hash_context.header, aggpk, sizeof(xonly_pk_t)); + + // msg_prefixed + crypto_hash_update(&hash_context.header, msg_prefixed, msg_prefixed_len); + + // len(extra_in) (4 bytes) + extra_in + crypto_hash_update_u32(&hash_context.header, extra_in_len); + if (extra_in_len > 0) { + crypto_hash_update(&hash_context.header, extra_in, extra_in_len); + } + + crypto_hash_update_u8(&hash_context.header, i); + + crypto_hash_digest(&hash_context.header, out, CX_SHA256_SIZE); +} + +// same as nonce_gen_internal from the reference, removing the optional arguments sk, msg and +// extra_in, and making aggpk compulsory +int musig_nonce_gen(const uint8_t rand[32], + const plain_pk_t pk, + const xonly_pk_t aggpk, + musig_secnonce_t *secnonce, + musig_pubnonce_t *pubnonce) { + uint8_t msg[] = {0x00}; + + musig_nonce_hash(rand, pk, aggpk, 0, msg, 1, NULL, 0, secnonce->k_1); + if (CX_OK != cx_math_modm_no_throw(secnonce->k_1, 32, secp256k1_n, 32)) return -1; + musig_nonce_hash(rand, pk, aggpk, 1, msg, 1, NULL, 0, secnonce->k_2); + if (CX_OK != cx_math_modm_no_throw(secnonce->k_2, 32, secp256k1_n, 32)) return -1; + + memcpy(secnonce->pk, pk, 33); + + point_t R_s1, R_s2; + + if (CX_OK != point_mul(G, secnonce->k_1, &R_s1)) return -1; + if (CX_OK != point_mul(G, secnonce->k_2, &R_s2)) return -1; + + if (0 > crypto_get_compressed_pubkey(R_s1.raw, pubnonce->R_s1)) return -1; + if (0 > crypto_get_compressed_pubkey(R_s2.raw, pubnonce->R_s2)) return -1; + + return 0; +} + +int musig_nonce_agg(const musig_pubnonce_t pubnonces[], size_t n_keys, musig_pubnonce_t *out) { + for (size_t j = 1; j <= 2; j++) { + point_t R_j; + set_point_infinite(&R_j); + for (size_t i = 0; i < n_keys; i++) { + point_t R_ij; + if (0 > cpoint(&pubnonces[i].raw[(j - 1) * 33], &R_ij)) { + PRINTF("Musig2 nonce aggregation: invalid contribution from cosigner %d\n", i); + return -i - 1; + } + point_add(&R_j, &R_ij, &R_j); + } + + if (is_point_infinite(&R_j)) { + memset(&out->raw[(j - 1) * 33], 0, 33); + } else { + crypto_get_compressed_pubkey(R_j.raw, &out->raw[(j - 1) * 33]); + } + } + return 0; +} + +static int apply_tweak(musig_keyagg_context_t *ctx, const uint8_t tweak[static 32], bool is_xonly) { + if (tweak == NULL || ctx == NULL) { + return -1; + } + + uint8_t g[32]; + memset(g, 0, 31); + g[31] = 1; // g = 1 + + if (is_xonly && !has_even_y(&ctx->Q)) { + // g = n - 1 + if (CX_OK != cx_math_sub_no_throw(g, secp256k1_n, g, 32)) { + return -1; + }; + } + + int diff; + if (CX_OK != cx_math_cmp_no_throw(tweak, secp256k1_n, 32, &diff)) { + return -1; + } + if (diff >= 0) { + PRINTF("The tweak must be less than n\n"); + return -1; + } + + // compute Q * g (in place) + + if (point_mul(&ctx->Q, g, &ctx->Q) != CX_OK) { + return -1; + } + + point_t T; // compute T = tweak * G + if (point_mul(G, tweak, &T) != CX_OK) { + return -1; + } + + // compute the resulting tweaked point g * Q + tweak * G + point_add(&ctx->Q, &T, &ctx->Q); + if (is_point_infinite(&ctx->Q)) { + PRINTF("The result of tweaking cannot be infinity\n"); + return -1; + } + + // gacc := g * gacc % n + if (CX_OK != cx_math_multm_no_throw(ctx->gacc, g, ctx->gacc, secp256k1_n, 32)) { + return -1; + } + + // tacc := (g * tacc + t) % n + if (CX_OK != cx_math_multm_no_throw(ctx->tacc, g, ctx->tacc, secp256k1_n, 32)) { + return -1; + } + if (CX_OK != cx_math_addm_no_throw(ctx->tacc, ctx->tacc, tweak, secp256k1_n, 32)) { + return -1; + } + + return 0; +} + +static int musig_get_session_values(const musig_session_context_t *session_ctx, + point_t *Q, + uint8_t gacc[static 32], + uint8_t tacc[static 32], + uint8_t b[static 32], + point_t *R, + uint8_t e[static 32]) { + cx_sha256_t hash_context; + + // Perform key aggregation and tweaking + musig_keyagg_context_t keyagg_ctx; + musig_key_agg(session_ctx->pubkeys, session_ctx->n_keys, &keyagg_ctx); + for (size_t i = 0; i < session_ctx->n_tweaks; i++) { + if (0 > apply_tweak(&keyagg_ctx, session_ctx->tweaks[i], session_ctx->is_xonly[i])) { + return -1; + }; + } + + // Copy Q, gacc, tacc from keyagg_ctx + memcpy(Q, &keyagg_ctx.Q, sizeof(point_t)); + memcpy(gacc, keyagg_ctx.gacc, 32); + memcpy(tacc, keyagg_ctx.tacc, 32); + + // Calculate b + crypto_tr_tagged_hash_init(&hash_context, BIP0327_noncecoef_tag, sizeof(BIP0327_noncecoef_tag)); + crypto_hash_update(&hash_context.header, session_ctx->aggnonce->raw, 66); + crypto_hash_update(&hash_context.header, Q->x, 32); + crypto_hash_update(&hash_context.header, session_ctx->msg, session_ctx->msg_len); + crypto_hash_digest(&hash_context.header, b, 32); + + // Calculate R + point_t R_1, R_2; + if (0 > cpoint_ext(session_ctx->aggnonce->R_s1, &R_1)) { + return -1; + }; + if (0 > cpoint_ext(session_ctx->aggnonce->R_s2, &R_2)) { + return -1; + }; + + // R2 := b*R2 + if (point_mul(&R_2, b, &R_2) != CX_OK) { + return -1; + } + + if (CX_OK != point_add(&R_1, &R_2, R)) { + return -1; + }; + if (is_point_infinite(R)) { + memcpy(R->raw, G, sizeof(point_t)); + } + + // Calculate e + crypto_tr_tagged_hash_init(&hash_context, BIP0340_challenge_tag, sizeof(BIP0340_challenge_tag)); + crypto_hash_update(&hash_context.header, R->x, 32); + crypto_hash_update(&hash_context.header, Q->x, 32); + crypto_hash_update(&hash_context.header, session_ctx->msg, session_ctx->msg_len); + crypto_hash_digest(&hash_context.header, e, 32); + return 0; +} + +int musig_get_session_key_agg_coeff(const musig_session_context_t *session_ctx, + const point_t *P, + uint8_t out[static 32]) { + // Convert point to compressed public key + plain_pk_t pk; + crypto_get_compressed_pubkey(P->raw, pk); + + // Check if pk is in pubkeys + bool found = false; + for (size_t i = 0; i < session_ctx->n_keys; i++) { + if (memcmp(pk, session_ctx->pubkeys[i], sizeof(plain_pk_t)) == 0) { + found = true; + break; + } + } + if (!found) { + return -1; // Public key not found in the list of pubkeys + } + + musig_key_agg_coeff(session_ctx->pubkeys, session_ctx->n_keys, pk, out); + return 0; +} + +int musig_sign(musig_secnonce_t *secnonce, + const uint8_t sk[static 32], + const musig_session_context_t *session_ctx, + uint8_t psig[static 32]) { + point_t Q; + uint8_t gacc[32]; + uint8_t tacc[32]; + uint8_t b[32]; + point_t R; + uint8_t e[32]; + + int diff; + + if (0 > musig_get_session_values(session_ctx, &Q, gacc, tacc, b, &R, e)) { + return -1; + } + + uint8_t k_1[32]; + uint8_t k_2[32]; + memcpy(k_1, secnonce->k_1, 32); + memcpy(k_2, secnonce->k_2, 32); + + // paranoia: since reusing nonces is catastrophic, we make sure that they are zeroed out and + // work with a local copy instead + explicit_bzero(secnonce->k_1, sizeof(secnonce->k_1)); + explicit_bzero(secnonce->k_2, sizeof(secnonce->k_2)); + + if (CX_OK != cx_math_cmp_no_throw(k_1, secp256k1_n, 32, &diff)) { + return -1; + } + if (is_array_zero(k_1, sizeof(k_1)) || diff >= 0) { + PRINTF("first secnonce value is out of range\n"); + return -1; + } + if (CX_OK != cx_math_cmp_no_throw(k_2, secp256k1_n, 32, &diff)) { + return -1; + } + if (is_array_zero(k_2, sizeof(k_2)) || diff >= 0) { + PRINTF("second secnonce value is out of range\n"); + return -1; + } + + if (!has_even_y(&R)) { + if (CX_OK != cx_math_sub_no_throw(k_1, secp256k1_n, k_1, 32)) { + return -1; + }; + if (CX_OK != cx_math_sub_no_throw(k_2, secp256k1_n, k_2, 32)) { + return -1; + }; + } + + if (CX_OK != cx_math_cmp_no_throw(sk, secp256k1_n, 32, &diff)) { + return -1; + } + if (is_array_zero(sk, 32) || diff >= 0) { + PRINTF("secret key value is out of range\n"); + return -1; + } + + bool err = false; + + // Put together all the variables that we want to always zero out before returning. + // As an excess of safety, we put here any variable that is (directly or indirectly) derived + // from the secret during the computation of the signature + struct { + uint8_t d[32]; + point_t P; + uint8_t ead[32]; + uint8_t s[32]; + } secrets; + + do { // executed only once, to allow for an easy way to break out of the block + // P = d_ * G + if (point_mul(G, sk, &secrets.P) != CX_OK) { + err = true; + break; + } + + plain_pk_t pk; + crypto_get_compressed_pubkey(secrets.P.raw, pk); + + if (memcmp(pk, secnonce->pk, 33) != 0) { + err = true; + PRINTF("Public key does not match nonce_gen argument\n"); + break; + } + + uint8_t a[32]; + if (0 > musig_get_session_key_agg_coeff(session_ctx, &secrets.P, a)) { + err = true; + break; + } + + // g = 1 if has_even_y(Q) else n - 1 + uint8_t g[32]; + memset(g, 0, 31); + g[31] = 1; // g = 1 + if (!has_even_y(&Q)) { + // g = n - 1 + if (CX_OK != cx_math_sub_no_throw(g, secp256k1_n, g, 32)) { + err = true; + break; + }; + } + + // d_ in the reference implementation is just sk + // d = g * gacc % n + if (CX_OK != cx_math_multm_no_throw(secrets.d, g, gacc, secp256k1_n, 32)) { + err = true; + break; + } + // d = g * gacc * d_ % n + if (CX_OK != cx_math_multm_no_throw(secrets.d, secrets.d, sk, secp256k1_n, 32)) { + err = true; + break; + } + + uint8_t bk_2[32]; // b * k_2 + if (CX_OK != cx_math_multm_no_throw(bk_2, b, k_2, secp256k1_n, 32)) { + err = true; + break; + } + + // e * a * d + if (CX_OK != cx_math_multm_no_throw(secrets.ead, e, a, secp256k1_n, 32)) { + err = true; + break; + } + if (CX_OK != cx_math_multm_no_throw(secrets.ead, secrets.ead, secrets.d, secp256k1_n, 32)) { + err = true; + break; + } + + // s = k_1 + b * k_2 + e * a * d + memcpy(secrets.s, k_1, 32); + if (CX_OK != cx_math_addm_no_throw(secrets.s, secrets.s, bk_2, secp256k1_n, 32)) { + err = true; + break; + } + if (CX_OK != cx_math_addm_no_throw(secrets.s, secrets.s, secrets.ead, secp256k1_n, 32)) { + err = true; + break; + } + + memcpy(psig, secrets.s, 32); + } while (false); + + // make sure to zero out any variable derived from secrets before returning + explicit_bzero(&secrets, sizeof(secrets)); + + if (err) { + return -1; + } + + return 0; +} diff --git a/src/musig/musig.h b/src/musig/musig.h index d17f89b67..1a70334a7 100644 --- a/src/musig/musig.h +++ b/src/musig/musig.h @@ -3,6 +3,8 @@ #include #include +#define MUSIG_PUBNONCE_SIZE 66 + // TODO: rename once BIP number is assigned static uint8_t BIP_MUSIG_CHAINCODE[32] = { 0x86, 0x80, 0x87, 0xCA, 0x02, 0xA6, 0xF9, 0x74, 0xC4, 0x59, 0x89, 0x24, 0xC3, 0x6B, 0x57, 0x76, @@ -30,6 +32,33 @@ typedef struct musig_keyagg_context_s { uint8_t tacc[32]; } musig_keyagg_context_t; +typedef struct musig_secnonce_s { + uint8_t k_1[32]; + uint8_t k_2[32]; + uint8_t pk[33]; +} musig_secnonce_t; + +typedef struct musig_pubnonce_s { + union { + struct { + uint8_t R_s1[33]; + uint8_t R_s2[33]; + }; + uint8_t raw[66]; + }; +} musig_pubnonce_t; + +typedef struct musig_session_context_s { + musig_pubnonce_t *aggnonce; + size_t n_keys; + plain_pk_t *pubkeys; + size_t n_tweaks; + uint8_t **tweaks; + bool *is_xonly; + uint8_t *msg; + size_t msg_len; +} musig_session_context_t; + /** * Computes the KeyAgg Context per BIP-0327. * @@ -43,3 +72,58 @@ typedef struct musig_keyagg_context_s { * @return 0 on success, a negative number in case of error. */ int musig_key_agg(const plain_pk_t pubkeys[], size_t n_keys, musig_keyagg_context_t *ctx); + +/** + * Generates secret and public nonces (round 1 of MuSig per BIP-0327). + * + * @param[in] rand + * The randomness to use. + * @param[in] pk + * The 33-byte public key of the signer. + * @param[in] aggpk + * The 32-byte x-only aggregate public key. + * @param[out] secnonce + * Pointer to receive the secret nonce. + * @param[out] pubnonce + * Pointer to receive the public nonce. + * + * @return 0 on success, a negative number in case of error. + */ +int musig_nonce_gen(const uint8_t rand[32], + const plain_pk_t pk, + const xonly_pk_t aggpk, + musig_secnonce_t *secnonce, + musig_pubnonce_t *pubnonce); + +/** + * Generates the aggregate nonce (nonce_agg in the reference implementation). + * + * @param[in] rand + * A list of musig_pubnonce_t, the pubnonces of all the participants. + * @param[in] n_keys + * Number of pubkeys. + * @param[out] out + * Pointer to receive the aggregate nonce. + * + * @return 0 on success, a negative number in case of error. On error, `-i - 1` is returned if the + * nonce provided by the cosigner with index `i` is invalid, in order to allow blaming for a + * disruptive signer. + */ +int musig_nonce_agg(const musig_pubnonce_t pubnonces[], size_t n_keys, musig_pubnonce_t *out); + +/** + * Computes the partial signature (round 2 of MuSig per BIP-0327). + * + * @param[in] secnonce + * The secret nonce. + * @param[in] session_ctx + * The session context. + * @param[out] psig + * Pointer to receive the partial signature. + * + * @return 0 on success, a negative number in case of error. + */ +int musig_sign(musig_secnonce_t *secnonce, + const uint8_t sk[static 32], + const musig_session_context_t *session_ctx, + uint8_t psig[static 32]); diff --git a/src/musig/musig_sessions.c b/src/musig/musig_sessions.c new file mode 100644 index 000000000..4d38117b7 --- /dev/null +++ b/src/musig/musig_sessions.c @@ -0,0 +1,64 @@ +#include + +#include "cx.h" + +#include "musig_sessions.h" +#include "../crypto.h" + +// TODO: persist in NVRAM instead +musig_session_t musig_sessions[MAX_N_MUSIG_SESSIONS]; + +bool musigsession_pop(uint8_t psbt_session_id[static 32], musig_session_t *out) { + for (int i = 0; i < MAX_N_MUSIG_SESSIONS; i++) { + if (memcmp(psbt_session_id, musig_sessions[i].id, 32) == 0) { + if (out != NULL) { + memcpy(out, &musig_sessions[i], sizeof(musig_session_t)); + } + explicit_bzero(&musig_sessions[i], sizeof(musig_session_t)); + return true; + } + } + return false; +} + +static bool is_all_zeros(const uint8_t *array, size_t size) { + for (size_t i = 0; i < size; ++i) { + if (array[i] != 0) { + return false; + } + } + return true; +} + +void musigsession_init_randomness(musig_session_t *session) { + cx_get_random_bytes(session->rand_root, 32); +} + +void musigsession_store(uint8_t psbt_session_id[static 32], const musig_session_t *session) { + // make sure that no session with the same id exists; delete it otherwise + musigsession_pop(psbt_session_id, NULL); + + int i; + for (i = 0; i < MAX_N_MUSIG_SESSIONS; i++) { + if (is_all_zeros((uint8_t *) &musig_sessions[i], sizeof(musig_session_t))) { + break; + } + } + if (i >= MAX_N_MUSIG_SESSIONS) { + // no free slot found, delete the first by default + // TODO: should we use a LIFO structure? Could add a counter to musig_session_t + i = 0; + } + // no free slot; replace the first slot + explicit_bzero(&musig_sessions[i], sizeof(musig_session_t)); + memcpy(&musig_sessions[i], session, sizeof(musig_session_t)); +} + +void compute_rand_i_j(const uint8_t rand_root[static 32], int i, int j, uint8_t out[static 32]) { + cx_sha256_t hash_context; + cx_sha256_init(&hash_context); + crypto_hash_update(&hash_context.header, rand_root, CX_SHA256_SIZE); + crypto_hash_update_u32(&hash_context.header, (uint32_t) i); + crypto_hash_update_u32(&hash_context.header, (uint32_t) j); + crypto_hash_digest(&hash_context.header, out, 32); +} diff --git a/src/musig/musig_sessions.h b/src/musig/musig_sessions.h new file mode 100644 index 000000000..b708d6fb9 --- /dev/null +++ b/src/musig/musig_sessions.h @@ -0,0 +1,20 @@ +#pragma once + +#include +#include "musig.h" + +#define MAX_N_MUSIG_SESSIONS 8 + +typedef struct { + uint8_t id[32]; + uint8_t rand_root[32]; +} musig_session_t; + +extern musig_session_t musig_sessions[MAX_N_MUSIG_SESSIONS]; + +// TODO: docs +bool musigsession_pop(uint8_t psbt_session_id[static 32], musig_session_t *out); +void musigsession_init_randomness(musig_session_t *session); +void musigsession_store(uint8_t psbt_session_id[static 32], const musig_session_t *session); + +void compute_rand_i_j(const uint8_t rand_root[static 32], int i, int j, uint8_t out[static 32]); diff --git a/tests/test_sign_psbt_musig.py b/tests/test_sign_psbt_musig.py new file mode 100644 index 000000000..1baf41443 --- /dev/null +++ b/tests/test_sign_psbt_musig.py @@ -0,0 +1,162 @@ + +from pathlib import Path + +from hashlib import sha256 +import hmac + + +from ledger_bitcoin.client_base import Client, MusigPartialSignature, MusigPubNonce +from ledger_bitcoin.key import ExtendedKey +from ledger_bitcoin.psbt import PSBT +from ragger.navigator import Navigator +from ragger.firmware import Firmware + +from ledger_bitcoin.wallet import WalletPolicy +from ragger_bitcoin import RaggerClient +from test_utils import SpeculosGlobals, bip0327 +from test_utils.musig2 import HotMusig2Cosigner, Musig2KeyPlaceholder, PsbtMusig2Cosigner, TrDescriptorTemplate, run_musig2_test +from .instructions import * + +tests_root: Path = Path(__file__).parent + + +# for now, we assume that there's a single internal musig placeholder, with a single internal key +class LedgerMusig2Cosigner(PsbtMusig2Cosigner): + """ + Implements a PsbtMusig2Cosigner that uses a BitcoinClient + """ + + def __init__(self, client: Client, wallet_policy: WalletPolicy, wallet_hmac: bytes) -> None: + super().__init__() + + self.client = client + self.wallet_policy = wallet_policy + self.wallet_hmac = wallet_hmac + + self.fingerprint = client.get_master_fingerprint() + + desc_tmpl = TrDescriptorTemplate.from_string( + wallet_policy.descriptor_template) + + self.pubkey = None + for _, (placeholder, _) in enumerate(desc_tmpl.placeholders()): + if not isinstance(placeholder, Musig2KeyPlaceholder): + continue + + for i in placeholder.key_indexes: + key_info = self.wallet_policy.keys_info[i] + if key_info[0] == "[" and key_info[1:9] == self.fingerprint.hex(): + xpub = key_info[key_info.find(']') + 1:] + self.pubkey = ExtendedKey.deserialize(xpub) + break + + if self.pubkey is not None: + break + + if self.pubkey is None: + raise ValueError("no musig with an internal key in wallet policy") + + def get_participant_pubkey(self) -> bip0327.Point: + return bip0327.cpoint(self.pubkey.pubkey) + + def generate_public_nonces(self, psbt: PSBT) -> None: + print("PSBT before nonce generation:", psbt.serialize()) + res = self.client.sign_psbt(psbt, self.wallet_policy, self.wallet_hmac) + print("Pubnonces:", res) + for (input_index, yielded) in res: + if isinstance(yielded, MusigPubNonce): + psbt_key = ( + yielded.participant_pubkey, + yielded.aggregate_pubkey, + yielded.tapleaf_hash + ) + print("Adding pubnonce to psbt for Ledger input", input_index) + print("Key:", psbt_key) + print("Value:", yielded.pubnonce) + + assert len(yielded.aggregate_pubkey) == 33 + + psbt.inputs[input_index].musig2_pub_nonces[psbt_key] = yielded.pubnonce + + def generate_partial_signatures(self, psbt: PSBT) -> None: + print("PSBT before partial signature generation:", psbt.serialize()) + res = self.client.sign_psbt(psbt, self.wallet_policy, self.wallet_hmac) + print("Ledger result of second round:", res) + for (input_index, yielded) in res: + if isinstance(yielded, MusigPartialSignature): + psbt_key = ( + yielded.participant_pubkey, + yielded.aggregate_pubkey, + yielded.tapleaf_hash + ) + + print("Adding partial signature to psbt for Ledger input", input_index) + print("Key:", psbt_key) + print("Value:", yielded.partial_signature) + + psbt.inputs[input_index].musig2_partial_sigs[psbt_key] = yielded.partial_signature + elif isinstance(yielded, MusigPubNonce): + raise ValueError("Expected partial signatures, got a pubnonce") + + +def test_sign_psbt_musig2_keypath(client: RaggerClient, speculos_globals: SpeculosGlobals): + cosigner_1_xpub = "[f5acc2fd/44'/1'/0']tpubDCwYjpDhUdPGP5rS3wgNg13mTrrjBuG8V9VpWbyptX6TRPbNoZVXsoVUSkCjmQ8jJycjuDKBb9eataSymXakTTaGifxR6kmVsfFehH1ZgJT" + + cosigner_2_xpriv = "tprv8gFWbQBTLFhbX3EK3cS7LmenwE3JjXbD9kN9yXfq7LcBm81RSf8vPGPqGPjZSeX41LX9ZN14St3z8YxW48aq5Yhr9pQZVAyuBthfi6quTCf" + cosigner_2_xpub = "tpubDCwYjpDhUdPGQWG6wG6hkBJuWFZEtrn7j3xwG3i8XcQabcGC53xWZm1hSXrUPFS5UvZ3QhdPSjXWNfWmFGTioARHuG5J7XguEjgg7p8PxAm" + + wallet_policy = WalletPolicy( + name="Musig for my ears", + descriptor_template="tr(musig(@0,@1)/**)", + keys_info=[cosigner_1_xpub, cosigner_2_xpub] + ) + wallet_hmac = hmac.new( + speculos_globals.wallet_registration_key, wallet_policy.id, sha256).digest() + + psbt_b64 = "cHNidP8BAIACAAAAAWbcwfJ78yV/+Jn0waX9pBWhDp2pZCm0GuTEXe2wXcP2AQAAAAD9////AQAAAAAAAAAARGpCVGhpcyBpbnB1dHMgaGFzIHR3byBwdWJrZXlzIGJ1dCB5b3Ugb25seSBzZWUgb25lLiAjbXBjZ2FuZyByZXZlbmdlAAAAAAABASuf/gQAAAAAACJRIPSL0RqGcuiQxWUrpyqc9CJwAk7i1Wk1p+YZWmGpB5tmIRbGANErPozSP7sjGM7KD11/WcKOe0InwGoEZz9MPQ7Bxg0AAAAAAAAAAAADAAAAAAA=" + psbt = PSBT() + psbt.deserialize(psbt_b64) + + sighashes = [ + bytes.fromhex( + "f3f6d4ae955af42665667ccff4edc9244d9143ada53ba26aee036258e0ffeda9") + ] + + signer_1 = LedgerMusig2Cosigner(client, wallet_policy, wallet_hmac) + signer_2 = HotMusig2Cosigner(wallet_policy, cosigner_2_xpriv) + + run_musig2_test(wallet_policy, psbt, [signer_1, signer_2], sighashes) + + +def test_sign_psbt_musig2_scriptpath(client: RaggerClient, speculos_globals: SpeculosGlobals): + cosigner_1_xpub = "[f5acc2fd/44'/1'/0']tpubDCwYjpDhUdPGP5rS3wgNg13mTrrjBuG8V9VpWbyptX6TRPbNoZVXsoVUSkCjmQ8jJycjuDKBb9eataSymXakTTaGifxR6kmVsfFehH1ZgJT" + + cosigner_2_xpriv = "tprv8gFWbQBTLFhbX3EK3cS7LmenwE3JjXbD9kN9yXfq7LcBm81RSf8vPGPqGPjZSeX41LX9ZN14St3z8YxW48aq5Yhr9pQZVAyuBthfi6quTCf" + cosigner_2_xpub = ExtendedKey.deserialize( + cosigner_2_xpriv).neutered().to_string() + + wallet_policy = WalletPolicy( + name="Musig2 in the scriptpath", + descriptor_template="tr(@0/**,pk(musig(@1,@2)/**))", + keys_info=[ + "tpubD6NzVbkrYhZ4WLczPJWReQycCJdd6YVWXubbVUFnJ5KgU5MDQrD998ZJLSmaB7GVcCnJSDWprxmrGkJ6SvgQC6QAffVpqSvonXmeizXcrkN", + cosigner_1_xpub, + cosigner_2_xpub + ] + ) + wallet_hmac = hmac.new( + speculos_globals.wallet_registration_key, wallet_policy.id, sha256).digest() + + psbt_b64 = "cHNidP8BAFoCAAAAAeyfHxrwzXffQqF9egw6KMS7RwCLP4rW95dxtXUKYJGFAQAAAAD9////AQAAAAAAAAAAHmocTXVzaWcyLiBOb3cgZXZlbiBpbiBTY3JpcHRzLgAAAAAAAQErOTAAAAAAAAAiUSDZqQIMWvfc0h2w2z6+0vTt0z1YoUHA6JHynopzSe3hgiIVwethFsEeXf/x51pIczoAIsj9RoVePIBTyk/rOMW8B6uIIyDGANErPozSP7sjGM7KD11/WcKOe0InwGoEZz9MPQ7BxqzAIRbGANErPozSP7sjGM7KD11/WcKOe0InwGoEZz9MPQ7Bxi0BkW61VIaT9Qaz/k0SzoZ1UBsjkrXzPqXQbCbBjbNZP/kAAAAAAAAAAAMAAAABFyDrYRbBHl3/8edaSHM6ACLI/UaFXjyAU8pP6zjFvAeriAEYIJFutVSGk/UGs/5NEs6GdVAbI5K18z6l0GwmwY2zWT/5AAA=" + psbt = PSBT() + psbt.deserialize(psbt_b64) + + sighashes = [ + bytes.fromhex( + "ba6d1d859dbc471999fff1fc5b8740fdacadd64a10c8d62de76e39a1c8dcd835") + ] + + signer_1 = LedgerMusig2Cosigner(client, wallet_policy, wallet_hmac) + signer_2 = HotMusig2Cosigner(wallet_policy, cosigner_2_xpriv) + + run_musig2_test(wallet_policy, psbt, [signer_1, signer_2], sighashes) From 41be6f1e4beb90b36fcc4ba3fee40d0256a844d7 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Wed, 9 Oct 2024 11:37:44 +0200 Subject: [PATCH 15/45] Update musig() specs, and fix psbt processing - musig() now sorts the keys, as per the BIP draft - correctly compute fingerprint for musig() aggregate key - added both the aggregate and the internal key in keyexpr_info_t struct - fixed psbt parsing logic to detect change/addr_index for musig - updated musig tests --- bitcoin_client/ledger_bitcoin/client.py | 11 +- bitcoin_client/ledger_bitcoin/client_base.py | 10 +- src/handler/lib/policy.c | 12 ++ src/handler/sign_psbt.c | 163 ++++++++++++++----- test_utils/musig2.py | 27 ++- tests/test_get_wallet_address.py | 4 +- tests/test_musig2.py | 8 +- tests/test_sign_psbt_musig.py | 8 +- 8 files changed, 178 insertions(+), 65 deletions(-) diff --git a/bitcoin_client/ledger_bitcoin/client.py b/bitcoin_client/ledger_bitcoin/client.py index b032c3ea1..89279df36 100644 --- a/bitcoin_client/ledger_bitcoin/client.py +++ b/bitcoin_client/ledger_bitcoin/client.py @@ -44,7 +44,8 @@ def parse_stream_to_map(f: BufferedReader) -> Mapping[bytes, bytes]: def aggr_xpub(pubkeys: List[bytes], chain: Chain) -> str: BIP_MUSIG_CHAINCODE = bytes.fromhex( "868087ca02a6f974c4598924c36b57762d32cb45717167e300622c7167e38965") - ctx = key_agg(pubkeys) + # sort the pubkeys prior to aggregation + ctx = key_agg(list(sorted(pubkeys))) compressed_pubkey = cbytes(ctx.Q) # Serialize according to BIP-32 @@ -112,7 +113,7 @@ def _decode_signpsbt_yielded_value(res: bytes) -> Tuple[int, SignPsbtYieldedObje input_index = read_varint(res_buffer) pubnonce = res_buffer.read(66) participant_pk = res_buffer.read(33) - agg_xonlykey = res_buffer.read(32) + aggregate_pubkey = res_buffer.read(33) tapleaf_hash = res_buffer.read() if len(tapleaf_hash) == 0: tapleaf_hash = None @@ -121,7 +122,7 @@ def _decode_signpsbt_yielded_value(res: bytes) -> Tuple[int, SignPsbtYieldedObje input_index, MusigPubNonce( participant_pubkey=participant_pk, - agg_xonlykey=agg_xonlykey, + aggregate_pubkey=aggregate_pubkey, tapleaf_hash=tapleaf_hash, pubnonce=pubnonce ) @@ -130,7 +131,7 @@ def _decode_signpsbt_yielded_value(res: bytes) -> Tuple[int, SignPsbtYieldedObje input_index = read_varint(res_buffer) partial_signature = res_buffer.read(32) participant_pk = res_buffer.read(33) - agg_xonlykey = res_buffer.read(32) + aggregate_pubkey = res_buffer.read(33) tapleaf_hash = res_buffer.read() if len(tapleaf_hash) == 0: tapleaf_hash = None @@ -139,7 +140,7 @@ def _decode_signpsbt_yielded_value(res: bytes) -> Tuple[int, SignPsbtYieldedObje input_index, MusigPartialSignature( participant_pubkey=participant_pk, - agg_xonlykey=agg_xonlykey, + aggregate_pubkey=aggregate_pubkey, tapleaf_hash=tapleaf_hash, partial_signature=partial_signature ) diff --git a/bitcoin_client/ledger_bitcoin/client_base.py b/bitcoin_client/ledger_bitcoin/client_base.py index 3e69ee14f..d7b9461db 100644 --- a/bitcoin_client/ledger_bitcoin/client_base.py +++ b/bitcoin_client/ledger_bitcoin/client_base.py @@ -90,13 +90,14 @@ class MusigPubNonce: It always contains - the participant_pubkey, a 33-byte compressed pubkey; - - agg_xonlykey, the 32-byte xonly key that is the aggregate and tweaked key present in the script; + - aggregate_pubkey, the 33-byte compressed pubkey key that is the aggregate of all the participant + pubkeys, with the necessary tweaks; its x-only version is the key present in the Script; - the 66-byte pubnonce. The tapleaf_hash is also filled if signing for a tapscript; `None` otherwise. """ participant_pubkey: bytes - agg_xonlykey: bytes + aggregate_pubkey: bytes tapleaf_hash: Optional[bytes] pubnonce: bytes @@ -107,13 +108,14 @@ class MusigPartialSignature: It always contains - the participant_pubkey, a 33-byte compressed pubkey; - - agg_xonlykey, the 32-byte xonly key that is the aggregate and tweaked key present in the script; + - aggregate_pubkey, the 33-byte compressed pubkey key that is the aggregate of all the participant + pubkeys, with the necessary tweaks; its x-only version is the key present in the Script; - the partial_signature, the 32-byte partial signature for this participant. The tapleaf_hash is also filled if signing for a tapscript; `None` otherwise """ participant_pubkey: bytes - agg_xonlykey: bytes + aggregate_pubkey: bytes tapleaf_hash: Optional[bytes] partial_signature: bytes diff --git a/src/handler/lib/policy.c b/src/handler/lib/policy.c index 18b527f6d..ea9b66201 100644 --- a/src/handler/lib/policy.c +++ b/src/handler/lib/policy.c @@ -479,6 +479,18 @@ __attribute__((warn_unused_result)) static int get_derived_pubkey( memcpy(keys[i], ext_pubkey.compressed_pubkey, sizeof(ext_pubkey.compressed_pubkey)); } + // sort the keys in ascending order using bubble sort + for (int i = 0; i < musig_info->n; i++) { + for (int j = 0; j < musig_info->n - 1; j++) { + if (memcmp(keys[j], keys[j + 1], sizeof(plain_pk_t)) > 0) { + uint8_t tmp[sizeof(plain_pk_t)]; + memcpy(tmp, keys[j], sizeof(plain_pk_t)); + memcpy(keys[j], keys[j + 1], sizeof(plain_pk_t)); + memcpy(keys[j + 1], tmp, sizeof(plain_pk_t)); + } + } + } + musig_keyagg_context_t musig_ctx; musig_key_agg(keys, musig_info->n, &musig_ctx); diff --git a/src/handler/sign_psbt.c b/src/handler/sign_psbt.c index d4a3bb26a..1a3946d39 100644 --- a/src/handler/sign_psbt.c +++ b/src/handler/sign_psbt.c @@ -112,9 +112,25 @@ typedef struct { policy_node_keyexpr_t *key_expression_ptr; int cur_index; uint32_t fingerprint; - uint8_t key_derivation_length; + + // info about the internal key of this key expression + // used at signing time to derive the correct key uint32_t key_derivation[MAX_BIP32_PATH_STEPS]; + uint8_t key_derivation_length; + + // same as key_derivation_length for internal key + // expressions; 0 for musig, as the key derivation in + // the PSBT use the aggregate key as the root + // used to identify the correct change/address_index from the psbt + uint8_t psbt_root_key_derivation_length; + + // the root pubkey of this key expression serialized_extended_pubkey_t pubkey; + // the pubkey of the internal key of this key expression. + // same as `pubkey` for simple key expressions, but it's the actual + // internal key for musig key expressions + serialized_extended_pubkey_t internal_pubkey; + bool is_tapscript; // true if signing with a BIP342 tapleaf script path spend uint8_t tapleaf_hash[32]; // only used for tapscripts } keyexpr_info_t; @@ -423,8 +439,8 @@ static int read_change_and_index_from_psbt_bip32_derivation( // we use it to detect whether the current input is change or not, // and store its address index if (fpt_der[0] == keyexpr_info->fingerprint && - der_len == keyexpr_info->key_derivation_length + 2) { - for (int i = 0; i < keyexpr_info->key_derivation_length; i++) { + der_len == keyexpr_info->psbt_root_key_derivation_length + 2) { + for (int i = 0; i < keyexpr_info->psbt_root_key_derivation_length; i++) { if (keyexpr_info->key_derivation[i] != fpt_der[1 + i]) { return 0; } @@ -691,6 +707,16 @@ static bool __attribute__((noinline)) get_and_verify_key_info(dispatcher_context return false; // should never happen } + keyexpr_info->key_derivation_length = key_info.master_key_derivation_len; + for (int i = 0; i < key_info.master_key_derivation_len; i++) { + keyexpr_info->key_derivation[i] = key_info.master_key_derivation[i]; + } + + keyexpr_info->fingerprint = read_u32_be(key_info.master_key_fingerprint, 0); + + memcpy(&keyexpr_info->pubkey, &key_info.ext_pubkey, sizeof(serialized_extended_pubkey_t)); + + // the rest of the function verifies if the key is indeed internal, if it has our fingerprint uint32_t fpr = read_u32_be(key_info.master_key_fingerprint, 0); if (fpr != st->master_key_fingerprint) { return false; @@ -698,54 +724,104 @@ static bool __attribute__((noinline)) get_and_verify_key_info(dispatcher_context // it could be a collision on the fingerprint; we verify that we can actually generate // the same pubkey + serialized_extended_pubkey_t derived_pubkey; if (0 > get_extended_pubkey_at_path(key_info.master_key_derivation, key_info.master_key_derivation_len, BIP32_PUBKEY_VERSION, - &keyexpr_info->pubkey)) { + &derived_pubkey)) { return false; } - if (memcmp(&key_info.ext_pubkey, &keyexpr_info->pubkey, sizeof(keyexpr_info->pubkey)) != 0) { + if (memcmp(&key_info.ext_pubkey, &derived_pubkey, sizeof(derived_pubkey)) != 0) { return false; } - keyexpr_info->key_derivation_length = key_info.master_key_derivation_len; - for (int i = 0; i < key_info.master_key_derivation_len; i++) { - keyexpr_info->key_derivation[i] = key_info.master_key_derivation[i]; - } - - keyexpr_info->fingerprint = read_u32_be(key_info.master_key_fingerprint, 0); - return true; } static bool fill_keyexpr_info_if_internal(dispatcher_context_t *dc, sign_psbt_state_t *st, keyexpr_info_t *keyexpr_info) { - if (keyexpr_info->key_expression_ptr->type == KEY_EXPRESSION_NORMAL) { - return get_and_verify_key_info(dc, - st, - keyexpr_info->key_expression_ptr->k.key_index, - keyexpr_info); + keyexpr_info_t tmp_keyexpr_info; + // preserve the fields that are already computed outside of this function + memcpy(&tmp_keyexpr_info, keyexpr_info, sizeof(keyexpr_info_t)); + if (keyexpr_info->key_expression_ptr->type == KEY_EXPRESSION_NORMAL) { + bool result = get_and_verify_key_info(dc, + st, + keyexpr_info->key_expression_ptr->k.key_index, + &tmp_keyexpr_info); + if (result) { + memcpy(keyexpr_info, &tmp_keyexpr_info, sizeof(keyexpr_info_t)); + memcpy(&keyexpr_info->internal_pubkey, + &keyexpr_info->pubkey, + sizeof(serialized_extended_pubkey_t)); + keyexpr_info->psbt_root_key_derivation_length = keyexpr_info->key_derivation_length; + } + return result; } else if (keyexpr_info->key_expression_ptr->type == KEY_EXPRESSION_MUSIG) { // iterate through the keys of the musig() placeholder to find if a key is internal musig_aggr_key_info_t *musig_info = r_musig_aggr_key_info(&keyexpr_info->key_expression_ptr->m.musig_info); uint16_t *key_indexes = r_uint16(&musig_info->key_indexes); + bool has_internal_key = false; + + // collect the keys of the musig, and fill the info related to the internal key (if any) + uint8_t keys[MAX_PUBKEYS_PER_MUSIG][33]; for (int idx_in_musig = 0; idx_in_musig < musig_info->n; idx_in_musig++) { - if (get_and_verify_key_info(dc, st, key_indexes[idx_in_musig], keyexpr_info)) { - // For musig2, we expect 0 as the fingerprint for the aggregate key, - // and its derivation length is 0 (as it's not derived from the BIP32 hierarchy) - // TODO: refactor, it's ugly to do it here - keyexpr_info->key_derivation_length = 0; - keyexpr_info->fingerprint = 0; - return true; + if (get_and_verify_key_info(dc, st, key_indexes[idx_in_musig], &tmp_keyexpr_info)) { + memcpy(keyexpr_info->key_derivation, + tmp_keyexpr_info.key_derivation, + sizeof(tmp_keyexpr_info.key_derivation)); + keyexpr_info->key_derivation_length = tmp_keyexpr_info.key_derivation_length; + + // keep track of the actual internal key of this key expression + memcpy(&keyexpr_info->internal_pubkey, + &tmp_keyexpr_info.pubkey, + sizeof(serialized_extended_pubkey_t)); + + has_internal_key = true; } + + memcpy(keys[idx_in_musig], tmp_keyexpr_info.pubkey.compressed_pubkey, 33); + } + + if (has_internal_key) { + keyexpr_info->psbt_root_key_derivation_length = 0; + + // sort the keys in ascending order using bubble sort + for (int i = 0; i < musig_info->n; i++) { + for (int j = 0; j < musig_info->n - 1; j++) { + if (memcmp(keys[j], keys[j + 1], sizeof(plain_pk_t)) > 0) { + uint8_t tmp[sizeof(plain_pk_t)]; + memcpy(tmp, keys[j], sizeof(plain_pk_t)); + memcpy(keys[j], keys[j + 1], sizeof(plain_pk_t)); + memcpy(keys[j + 1], tmp, sizeof(plain_pk_t)); + } + } + } + + musig_keyagg_context_t musig_ctx; + musig_key_agg(keys, musig_info->n, &musig_ctx); + + // compute the aggregated extended pubkey + memset(&keyexpr_info->pubkey, 0, sizeof(keyexpr_info->pubkey)); + write_u32_be(keyexpr_info->pubkey.version, 0, BIP32_PUBKEY_VERSION); + + keyexpr_info->pubkey.compressed_pubkey[0] = (musig_ctx.Q.y[31] % 2 == 0) ? 2 : 3; + memcpy(&keyexpr_info->pubkey.compressed_pubkey[1], + musig_ctx.Q.x, + sizeof(musig_ctx.Q.x)); + memcpy(&keyexpr_info->pubkey.chain_code, + BIP_MUSIG_CHAINCODE, + sizeof(BIP_MUSIG_CHAINCODE)); + + keyexpr_info->fingerprint = + crypto_get_key_fingerprint(keyexpr_info->pubkey.compressed_pubkey); } - return false; // no internal key found in musig placeholder + return has_internal_key; // no internal key found in musig placeholder } else { LEDGER_ASSERT(false, "Unreachable code"); return false; @@ -2487,6 +2563,18 @@ static bool __attribute__((noinline)) sign_sighash_musig_and_yield(dispatcher_co memcpy(keys[i], ext_pubkey.compressed_pubkey, sizeof(ext_pubkey.compressed_pubkey)); } + // sort the keys in ascending order using bubble sort + for (int i = 0; i < musig_info->n; i++) { + for (int j = 0; j < musig_info->n - 1; j++) { + if (memcmp(keys[j], keys[j + 1], sizeof(plain_pk_t)) > 0) { + uint8_t tmp[sizeof(plain_pk_t)]; + memcpy(tmp, keys[j], sizeof(plain_pk_t)); + memcpy(keys[j], keys[j + 1], sizeof(plain_pk_t)); + memcpy(keys[j + 1], tmp, sizeof(plain_pk_t)); + } + } + } + musig_keyagg_context_t musig_ctx; musig_key_agg(keys, musig_info->n, &musig_ctx); @@ -2569,7 +2657,7 @@ static bool __attribute__((noinline)) sign_sighash_musig_and_yield(dispatcher_co uint8_t *musig_my_psbt_id = musig_my_psbt_id_key + 1; size_t psbt_id_len = keyexpr_info->is_tapscript ? 33 + 33 + 32 : 33 + 33; - memcpy(musig_my_psbt_id, keyexpr_info->pubkey.compressed_pubkey, 33); + memcpy(musig_my_psbt_id, keyexpr_info->internal_pubkey.compressed_pubkey, 33); memcpy(musig_my_psbt_id + 33, agg_key_tweaked.compressed_pubkey, 33); if (keyexpr_info->is_tapscript) { memcpy(musig_my_psbt_id + 33 + 33, keyexpr_info->tapleaf_hash, 32); @@ -2620,7 +2708,7 @@ static bool __attribute__((noinline)) sign_sighash_musig_and_yield(dispatcher_co musig_secnonce_t secnonce; musig_pubnonce_t pubnonce; if (0 > musig_nonce_gen(rand_i_j, - keyexpr_info->pubkey.compressed_pubkey, + keyexpr_info->internal_pubkey.compressed_pubkey, agg_key_tweaked.compressed_pubkey + 1, &secnonce, &pubnonce)) { @@ -2633,7 +2721,7 @@ static bool __attribute__((noinline)) sign_sighash_musig_and_yield(dispatcher_co st, cur_input_index, &pubnonce, - keyexpr_info->pubkey.compressed_pubkey, + keyexpr_info->internal_pubkey.compressed_pubkey, agg_key_tweaked.compressed_pubkey, keyexpr_info->is_tapscript ? keyexpr_info->tapleaf_hash : NULL)) { PRINTF("Failed yielding MuSig2 pubnonce\n"); @@ -2698,7 +2786,7 @@ static bool __attribute__((noinline)) sign_sighash_musig_and_yield(dispatcher_co musig_pubnonce_t pubnonce; if (0 > musig_nonce_gen(rand_i_j, - keyexpr_info->pubkey.compressed_pubkey, + keyexpr_info->internal_pubkey.compressed_pubkey, agg_key_tweaked.compressed_pubkey + 1, &secnonce, &pubnonce)) { @@ -2717,20 +2805,10 @@ static bool __attribute__((noinline)) sign_sighash_musig_and_yield(dispatcher_co // derive secret key uint32_t sign_path[MAX_BIP32_PATH_STEPS]; - // TODO: wrong, this should be based on the internal key derivation length for (int i = 0; i < keyexpr_info->key_derivation_length; i++) { sign_path[i] = keyexpr_info->key_derivation[i]; } - sign_path[keyexpr_info->key_derivation_length] = change_step; - sign_path[keyexpr_info->key_derivation_length + 1] = addr_index_step; - - int sign_path_len = keyexpr_info->key_derivation_length + 2; - - // TODO: hardcoded for the current tests. Fix this! - sign_path[0] = 0x80000000 + 44; - sign_path[1] = 0x80000000 + 1; - sign_path[2] = 0x80000000 + 0; - sign_path_len = 3; + int sign_path_len = keyexpr_info->key_derivation_length; if (bip32_derive_init_privkey_256(CX_CURVE_256K1, sign_path, @@ -2770,7 +2848,7 @@ static bool __attribute__((noinline)) sign_sighash_musig_and_yield(dispatcher_co st, cur_input_index, psig, - keyexpr_info->pubkey.compressed_pubkey, + keyexpr_info->internal_pubkey.compressed_pubkey, agg_key_tweaked.compressed_pubkey, keyexpr_info->is_tapscript ? keyexpr_info->tapleaf_hash : NULL)) { PRINTF("Failed yielding MuSig2 partial signature\n"); @@ -3213,8 +3291,9 @@ sign_transaction(dispatcher_context_t *dc, &input, tapleaf_ptr, &keyexpr_info, - sign_psbt_cache)) + sign_psbt_cache)) { return false; + } if (!sign_transaction_input(dc, st, diff --git a/test_utils/musig2.py b/test_utils/musig2.py index fc0ce3544..5f9a98bc1 100644 --- a/test_utils/musig2.py +++ b/test_utils/musig2.py @@ -32,7 +32,7 @@ from test_utils.taproot_sighash import SIGHASH_DEFAULT, TaprootSignatureHash -from . import bip0327, bip0340, sha256 +from . import bip0327, bip0340, hash160, sha256 from . import taproot from bitcoin_client.ledger_bitcoin.embit.descriptor.miniscript import Miniscript @@ -104,9 +104,9 @@ def extract_placeholders(desc_tmpl: str) -> List[KeyPlaceholder]: return [parse_placeholder(match[0]) for match in sorted_matches] -def musig(pubkeys: Iterable[bytes], version_bytes: bytes) -> Tuple[str, bip0327.KeyAggContext]: +def unsorted_musig(pubkeys: Iterable[bytes], version_bytes: bytes) -> Tuple[str, bip0327.KeyAggContext]: """ - Constructs the musig2 aggregated extended public key from a list of + Constructs the musig2 aggregated extended public key from an unsorted list of compressed public keys, and the version bytes. """ @@ -128,6 +128,15 @@ def musig(pubkeys: Iterable[bytes], version_bytes: bytes) -> Tuple[str, bip0327. return base58.b58encode_check(ext_pubkey).decode(), key_agg_ctx +def musig(pubkeys: Iterable[bytes], version_bytes: bytes) -> Tuple[str, bip0327.KeyAggContext]: + """ + Constructs the musig2 aggregated extended public key from a list of compressed public keys, + and the version bytes. The keys are sorted, as required by the `the musig()` key expression + in descriptors. + """ + return unsorted_musig(sorted(pubkeys), version_bytes) + + def aggregate_musig_pubkey(keys_info: Iterable[str]) -> Tuple[str, bip0327.KeyAggContext]: """ Constructs the musig2 aggregated extended public key from the list of keys info @@ -422,12 +431,14 @@ def generate_partial_signatures(self, psbt: PSBT) -> None: def find_change_and_addr_index_for_musig(input_psbt: PartiallySignedInput, placeholder: Musig2KeyPlaceholder, agg_xpub: ExtendedKey): num1, num2 = placeholder.num1, placeholder.num2 + agg_xpub_fingerprint = hash160(agg_xpub.pubkey)[0:4] + # Iterate through tap key origins in the input # TODO: this might be made more precise (e.g. use the leaf_hash from the tap_bip32_paths items) for xonly, (_, key_origin) in input_psbt.tap_bip32_paths.items(): der_path = key_origin.path # Check if the fingerprint matches the expected pattern and the derivation path has the correct structure - if key_origin.fingerprint == b'\x00\x00\x00\x00' and len(der_path) == 2 and der_path[0] < HARDENED_INDEX and der_path[1] < HARDENED_INDEX and (der_path[0] == num1 or der_path[0] == num2): + if key_origin.fingerprint == agg_xpub_fingerprint and len(der_path) == 2 and der_path[0] < HARDENED_INDEX and der_path[1] < HARDENED_INDEX and (der_path[0] == num1 or der_path[0] == num2): if xonly != agg_xpub.derive_pub_path(der_path).pubkey[1:]: continue @@ -684,6 +695,10 @@ def generate_partial_signatures(self, psbt: PSBT) -> None: if my_key_index_in_musig is None: raise ValueError("No internal key found in musig") + # sort the keys in ascending order + pubkeys_in_musig = list( + sorted(pubkeys_in_musig, key=lambda x: x.pubkey)) + nonces: List[bytes] = [] for participant_key in pubkeys_in_musig: participant_pubnonce_identifier = ( @@ -794,6 +809,10 @@ def run_musig2_test(wallet_policy: WalletPolicy, psbt: PSBT, cosigners: List[Psb xpub_i = k_i[k_i.find(']') + 1:] pubkeys_in_musig.append(ExtendedKey.deserialize(xpub_i)) + # sort the keys in ascending order + pubkeys_in_musig = list( + sorted(pubkeys_in_musig, key=lambda x: x.pubkey)) + nonces: List[bytes] = [] for participant_key in pubkeys_in_musig: pubnonce_identifier = ( diff --git a/tests/test_get_wallet_address.py b/tests/test_get_wallet_address.py index 5aae1d5c3..36df27d9a 100644 --- a/tests/test_get_wallet_address.py +++ b/tests/test_get_wallet_address.py @@ -317,7 +317,7 @@ def test_get_wallet_address_tr_musig_keypath(client: RaggerClient): "05b7b4bccd3188effc24de8fd67e83231d8486772800884db0d81bad19f2be3e") res = client.get_wallet_address(wallet, wallet_hmac, 0, 3, False) - assert res == "tb1p7j9azx5xwt5fp3t99wnj4885yfcqynhz645ntflxr9dxr2g8ndnq32xa2m" + assert res == "tb1pc87la0ksvw4pfq6qc3gn9en33kx7s9rx4c4epy578kfjsdjv6mks7u7dgn" def test_get_wallet_address_tr_musig_scriptpath(client: RaggerClient): @@ -335,7 +335,7 @@ def test_get_wallet_address_tr_musig_scriptpath(client: RaggerClient): "b22397b717949ede59c3c9f31c987acda098471211f754b6633c87054c1efb51") res = client.get_wallet_address(wallet, wallet_hmac, 0, 3, False) - assert res == "tb1pmx5syrz67lwdy8dsmvlta5h5ahfn6k9pg8qw3y0jn698xj0duxpq3k94zq" + assert res == "tb1pa423acwcjc8jgt36muavyun8e2hz3t5qwptsr3wr8afmdfk3wchswf9ntp" def test_get_wallet_address_large_addr_index(client: RaggerClient): diff --git a/tests/test_musig2.py b/tests/test_musig2.py index d4c5b8862..2cc80aaba 100644 --- a/tests/test_musig2.py +++ b/tests/test_musig2.py @@ -20,13 +20,13 @@ def test_musig2_hotsigner_keypath(): keys_info=[cosigner_1_xpub, cosigner_2_xpub] ) - psbt_b64 = "cHNidP8BAIACAAAAAWbcwfJ78yV/+Jn0waX9pBWhDp2pZCm0GuTEXe2wXcP2AQAAAAD9////AQAAAAAAAAAARGpCVGhpcyBpbnB1dHMgaGFzIHR3byBwdWJrZXlzIGJ1dCB5b3Ugb25seSBzZWUgb25lLiAjbXBjZ2FuZyByZXZlbmdlAAAAAAABASuf/gQAAAAAACJRIPSL0RqGcuiQxWUrpyqc9CJwAk7i1Wk1p+YZWmGpB5tmIRbGANErPozSP7sjGM7KD11/WcKOe0InwGoEZz9MPQ7Bxg0AAAAAAAAAAAADAAAAAAA=" + psbt_b64 = "cHNidP8BAIACAAAAAdF2HhQ2XCgTpd3Sel7VkS5FvESbwo1rgeuG4tBt9GICAAAAAAD9////AQAAAAAAAAAARGpCVGhpcyBpbnB1dHMgaGFzIHR3byBwdWJrZXlzIGJ1dCB5b3Ugb25seSBzZWUgb25lLiAjbXBjZ2FuZyByZXZlbmdlAAAAAAABASuf/gQAAAAAACJRIMH9/r7QY6oUg0DEUTLmcY2N6BRmriuQkp49kyg2TNbtIRaQZkYWUCCfi7xZsFr10WFcUPX3nBiNe+dC/ZMiUvaPDA0AW4+8kwAAAAADAAAAAAA=" psbt = PSBT() psbt.deserialize(psbt_b64) sighashes = [ bytes.fromhex( - "f3f6d4ae955af42665667ccff4edc9244d9143ada53ba26aee036258e0ffeda9") + "a3aeecb6c236b4a7e72c95fa138250d449b97a75c573f8ab612356279ff64046") ] signer_1 = HotMusig2Cosigner(wallet_policy, cosigner_1_xpriv) @@ -54,13 +54,13 @@ def test_musig2_hotsigner_scriptpath(): ] ) - psbt_b64 = "cHNidP8BAFoCAAAAAeyfHxrwzXffQqF9egw6KMS7RwCLP4rW95dxtXUKYJGFAQAAAAD9////AQAAAAAAAAAAHmocTXVzaWcyLiBOb3cgZXZlbiBpbiBTY3JpcHRzLgAAAAAAAQErOTAAAAAAAAAiUSDZqQIMWvfc0h2w2z6+0vTt0z1YoUHA6JHynopzSe3hgiIVwethFsEeXf/x51pIczoAIsj9RoVePIBTyk/rOMW8B6uIIyDGANErPozSP7sjGM7KD11/WcKOe0InwGoEZz9MPQ7BxqzAIRbGANErPozSP7sjGM7KD11/WcKOe0InwGoEZz9MPQ7Bxi0BkW61VIaT9Qaz/k0SzoZ1UBsjkrXzPqXQbCbBjbNZP/kAAAAAAAAAAAMAAAABFyDrYRbBHl3/8edaSHM6ACLI/UaFXjyAU8pP6zjFvAeriAEYIJFutVSGk/UGs/5NEs6GdVAbI5K18z6l0GwmwY2zWT/5AAA=" + psbt_b64 = "cHNidP8BAFoCAAAAAdOnEESfpXpBe9X59Q4jxz1u9E4Wovn2bkAuuyqUUY0mAAAAAAD9////AQAAAAAAAAAAHmocTXVzaWcyLiBOb3cgZXZlbiBpbiBTY3JpcHRzLgAAAAAAAQErOTAAAAAAAAAiUSDtVR7h2JYPJC463zrCcmfKriiugHBXAcXDP1O2ptF2LyIVwethFsEeXf/x51pIczoAIsj9RoVePIBTyk/rOMW8B6uIIyCQZkYWUCCfi7xZsFr10WFcUPX3nBiNe+dC/ZMiUvaPDKzAIRaQZkYWUCCfi7xZsFr10WFcUPX3nBiNe+dC/ZMiUvaPDC0BuYMCXh1wIlpyBMdMaCFPSwOeOyvhqg+FJ+fOMoWlJsRbj7yTAAAAAAMAAAABFyDrYRbBHl3/8edaSHM6ACLI/UaFXjyAU8pP6zjFvAeriAEYILmDAl4dcCJacgTHTGghT0sDnjsr4aoPhSfnzjKFpSbEAAA=" psbt = PSBT() psbt.deserialize(psbt_b64) sighashes = [ bytes.fromhex( - "ba6d1d859dbc471999fff1fc5b8740fdacadd64a10c8d62de76e39a1c8dcd835") + "28f86cd95c144ed4a877701ae7166867e8805b654c43d9f44da45d7b0070c313") ] signer_1 = HotMusig2Cosigner(wallet_policy, cosigner_1_xpriv) diff --git a/tests/test_sign_psbt_musig.py b/tests/test_sign_psbt_musig.py index 1baf41443..eaff3b320 100644 --- a/tests/test_sign_psbt_musig.py +++ b/tests/test_sign_psbt_musig.py @@ -113,13 +113,13 @@ def test_sign_psbt_musig2_keypath(client: RaggerClient, speculos_globals: Specul wallet_hmac = hmac.new( speculos_globals.wallet_registration_key, wallet_policy.id, sha256).digest() - psbt_b64 = "cHNidP8BAIACAAAAAWbcwfJ78yV/+Jn0waX9pBWhDp2pZCm0GuTEXe2wXcP2AQAAAAD9////AQAAAAAAAAAARGpCVGhpcyBpbnB1dHMgaGFzIHR3byBwdWJrZXlzIGJ1dCB5b3Ugb25seSBzZWUgb25lLiAjbXBjZ2FuZyByZXZlbmdlAAAAAAABASuf/gQAAAAAACJRIPSL0RqGcuiQxWUrpyqc9CJwAk7i1Wk1p+YZWmGpB5tmIRbGANErPozSP7sjGM7KD11/WcKOe0InwGoEZz9MPQ7Bxg0AAAAAAAAAAAADAAAAAAA=" + psbt_b64 = "cHNidP8BAIACAAAAAdF2HhQ2XCgTpd3Sel7VkS5FvESbwo1rgeuG4tBt9GICAAAAAAD9////AQAAAAAAAAAARGpCVGhpcyBpbnB1dHMgaGFzIHR3byBwdWJrZXlzIGJ1dCB5b3Ugb25seSBzZWUgb25lLiAjbXBjZ2FuZyByZXZlbmdlAAAAAAABASuf/gQAAAAAACJRIMH9/r7QY6oUg0DEUTLmcY2N6BRmriuQkp49kyg2TNbtIRaQZkYWUCCfi7xZsFr10WFcUPX3nBiNe+dC/ZMiUvaPDA0AW4+8kwAAAAADAAAAAAA=" psbt = PSBT() psbt.deserialize(psbt_b64) sighashes = [ bytes.fromhex( - "f3f6d4ae955af42665667ccff4edc9244d9143ada53ba26aee036258e0ffeda9") + "a3aeecb6c236b4a7e72c95fa138250d449b97a75c573f8ab612356279ff64046") ] signer_1 = LedgerMusig2Cosigner(client, wallet_policy, wallet_hmac) @@ -147,13 +147,13 @@ def test_sign_psbt_musig2_scriptpath(client: RaggerClient, speculos_globals: Spe wallet_hmac = hmac.new( speculos_globals.wallet_registration_key, wallet_policy.id, sha256).digest() - psbt_b64 = "cHNidP8BAFoCAAAAAeyfHxrwzXffQqF9egw6KMS7RwCLP4rW95dxtXUKYJGFAQAAAAD9////AQAAAAAAAAAAHmocTXVzaWcyLiBOb3cgZXZlbiBpbiBTY3JpcHRzLgAAAAAAAQErOTAAAAAAAAAiUSDZqQIMWvfc0h2w2z6+0vTt0z1YoUHA6JHynopzSe3hgiIVwethFsEeXf/x51pIczoAIsj9RoVePIBTyk/rOMW8B6uIIyDGANErPozSP7sjGM7KD11/WcKOe0InwGoEZz9MPQ7BxqzAIRbGANErPozSP7sjGM7KD11/WcKOe0InwGoEZz9MPQ7Bxi0BkW61VIaT9Qaz/k0SzoZ1UBsjkrXzPqXQbCbBjbNZP/kAAAAAAAAAAAMAAAABFyDrYRbBHl3/8edaSHM6ACLI/UaFXjyAU8pP6zjFvAeriAEYIJFutVSGk/UGs/5NEs6GdVAbI5K18z6l0GwmwY2zWT/5AAA=" + psbt_b64 = "cHNidP8BAFoCAAAAAdOnEESfpXpBe9X59Q4jxz1u9E4Wovn2bkAuuyqUUY0mAAAAAAD9////AQAAAAAAAAAAHmocTXVzaWcyLiBOb3cgZXZlbiBpbiBTY3JpcHRzLgAAAAAAAQErOTAAAAAAAAAiUSDtVR7h2JYPJC463zrCcmfKriiugHBXAcXDP1O2ptF2LyIVwethFsEeXf/x51pIczoAIsj9RoVePIBTyk/rOMW8B6uIIyCQZkYWUCCfi7xZsFr10WFcUPX3nBiNe+dC/ZMiUvaPDKzAIRaQZkYWUCCfi7xZsFr10WFcUPX3nBiNe+dC/ZMiUvaPDC0BuYMCXh1wIlpyBMdMaCFPSwOeOyvhqg+FJ+fOMoWlJsRbj7yTAAAAAAMAAAABFyDrYRbBHl3/8edaSHM6ACLI/UaFXjyAU8pP6zjFvAeriAEYILmDAl4dcCJacgTHTGghT0sDnjsr4aoPhSfnzjKFpSbEAAA=" psbt = PSBT() psbt.deserialize(psbt_b64) sighashes = [ bytes.fromhex( - "ba6d1d859dbc471999fff1fc5b8740fdacadd64a10c8d62de76e39a1c8dcd835") + "28f86cd95c144ed4a877701ae7166867e8805b654c43d9f44da45d7b0070c313") ] signer_1 = LedgerMusig2Cosigner(client, wallet_policy, wallet_hmac) From c15335cdf5386c9baa1bd2fb3d71be4ea3a0b32a Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Mon, 15 Jul 2024 17:46:42 +0200 Subject: [PATCH 16/45] Fix psbt-level musig signing session logic --- src/handler/sign_psbt.c | 110 +++++++++++++++++++++++-------------- src/musig/musig_sessions.h | 12 ++++ 2 files changed, 82 insertions(+), 40 deletions(-) diff --git a/src/handler/sign_psbt.c b/src/handler/sign_psbt.c index 1a3946d39..068a90dea 100644 --- a/src/handler/sign_psbt.c +++ b/src/handler/sign_psbt.c @@ -135,14 +135,20 @@ typedef struct { uint8_t tapleaf_hash[32]; // only used for tapscripts } keyexpr_info_t; -// Cache for partial hashes during segwit signing (avoid quadratic hashing for segwit transactions) -typedef struct { +// Cache for partial hashes during signing (avoid quadratic hashing for segwit transactions) +typedef struct tx_hashes_s { uint8_t sha_prevouts[32]; uint8_t sha_amounts[32]; uint8_t sha_scriptpubkeys[32]; uint8_t sha_sequences[32]; uint8_t sha_outputs[32]; -} segwit_hashes_t; +} tx_hashes_t; + +// the signing state for the current transaction; it does not contain any per-input state +typedef struct signing_state_s { + tx_hashes_t tx_hashes; + musig_signing_state_t musig; +} signing_state_t; // We cache the first 2 external outputs; that's needed for the swap checks // Moreover, this helps the code for the simplified UX for transactions that @@ -1893,7 +1899,7 @@ static bool __attribute__((noinline)) compute_sighash_legacy(dispatcher_context_ static bool __attribute__((noinline)) compute_sighash_segwitv0(dispatcher_context_t *dc, sign_psbt_state_t *st, - segwit_hashes_t *hashes, + tx_hashes_t *hashes, input_info_t *input, unsigned int cur_input_index, uint8_t sighash[static 32]) { @@ -2078,7 +2084,7 @@ static bool __attribute__((noinline)) compute_sighash_segwitv0(dispatcher_contex static bool __attribute__((noinline)) compute_sighash_segwitv1(dispatcher_context_t *dc, sign_psbt_state_t *st, - segwit_hashes_t *hashes, + tx_hashes_t *hashes, input_info_t *input, unsigned int cur_input_index, keyexpr_info_t *keyexpr_info, @@ -2515,6 +2521,7 @@ static bool yield_musig_partial_signature(dispatcher_context_t *dc, static bool __attribute__((noinline)) sign_sighash_musig_and_yield(dispatcher_context_t *dc, sign_psbt_state_t *st, + signing_state_t *signing_state, keyexpr_info_t *keyexpr_info, input_info_t *input, unsigned int cur_input_index, @@ -2663,14 +2670,23 @@ static bool __attribute__((noinline)) sign_sighash_musig_and_yield(dispatcher_co memcpy(musig_my_psbt_id + 33 + 33, keyexpr_info->tapleaf_hash, 32); } - // compute psbt session id + // The psbt_session_id identifies the musig signing session for the entire (psbt, wallet_policy) + // pair, in both rounds 1 and 2 of the protocol; it is the same for all the musig placeholders + // in the policy (if more than one), and it is the same for all the inputs in the psbt. By + // making the hash depend on both the wallet policy and the transaction hashes, we make sure + // that an accidental collision is impossible, allowing for independent, parallel MuSig2 signing + // sessions for different transactions or wallet policies. + // Malicious collisions are not a concern, as they would only result in a signing failure (since + // the nonces would be incorrectly regenerated during round 2 of MuSig2). uint8_t psbt_session_id[32]; - // TODO: for now we use simply a hash that depends on the keys of the wallet policy; this is not - // good enough. It should be a hash that depends on: - // - the wallet policy id - // - the tx being signed - // - the input index - // - the index of the placeholder we're signing for + crypto_tr_tagged_hash( + (uint8_t[]){'P', 's', 'b', 't', 'S', 'e', 's', 's', 'i', 'o', 'n', 'I', 'd'}, + 13, + st->wallet_header.keys_info_merkle_root, // TODO: wallet policy id would be more precise + 32, + (uint8_t *) &signing_state->tx_hashes, + sizeof(signing_state->tx_hashes), + psbt_session_id); memcpy(psbt_session_id, st->wallet_header.keys_info_merkle_root, sizeof(psbt_session_id)); // 4) check if my pubnonce is in the psbt @@ -2692,15 +2708,14 @@ static bool __attribute__((noinline)) sign_sighash_musig_and_yield(dispatcher_co PRINTF("Session with the same id already existing\n"); } - musig_session_t psbt_session; - memcpy(psbt_session.id, psbt_session_id, sizeof(psbt_session_id)); - - // TODO: the "session" should be initialized once for all the (inputs, placeholder) pairs; - // this is wrong! - musigsession_init_randomness(&psbt_session); + if (memcmp(signing_state->musig.round1.id, psbt_session_id, sizeof(psbt_session_id)) != 0) { + // first input/placeholder pair using this session: initialize the session + memcpy(signing_state->musig.round1.id, psbt_session_id, sizeof(psbt_session_id)); + musigsession_init_randomness(&signing_state->musig.round1); + } uint8_t rand_i_j[32]; - compute_rand_i_j(psbt_session.rand_root, + compute_rand_i_j(signing_state->musig.round1.rand_root, cur_input_index, keyexpr_info->cur_index, rand_i_j); @@ -2728,20 +2743,19 @@ static bool __attribute__((noinline)) sign_sighash_musig_and_yield(dispatcher_co SEND_SW(dc, SW_BAD_STATE); // should never happen return false; } - - // TODO: wrong if we have multiple inputs! - musigsession_store(psbt_session_id, &psbt_session); } else { // 6) generate and yield partial signature - musig_session_t psbt_session; - // get and delete the musig session from permanent storage - if (!musigsession_pop(psbt_session_id, &psbt_session)) { - // The PSBT contains a partial nonce, but we do not have the corresponding psbt session - // in storage. Either it was deleted, or the pubnonces were not real. Either way, we - // cannot continue. - PRINTF("Missing MuSig2 session\n"); - SEND_SW(dc, SW_BAD_STATE); - return false; + // If the session is not already initialized, we pop it from persistent storage + if (memcmp(signing_state->musig.round2.id, psbt_session_id, sizeof(psbt_session_id)) != 0) { + // get and delete the musig session from permanent storage + if (!musigsession_pop(psbt_session_id, &signing_state->musig.round2)) { + // The PSBT contains a partial nonce, but we do not have the corresponding psbt + // session in storage. Either it was deleted, or the pubnonces were not real. Either + // way, we cannot continue. + PRINTF("Missing MuSig2 session\n"); + SEND_SW(dc, SW_BAD_STATE); + return false; + } } musig_pubnonce_t nonces[MAX_PUBKEYS_PER_MUSIG]; @@ -2777,7 +2791,7 @@ static bool __attribute__((noinline)) sign_sighash_musig_and_yield(dispatcher_co // recompute secnonce from psbt_session randomness uint8_t rand_i_j[32]; - compute_rand_i_j(psbt_session.rand_root, + compute_rand_i_j(signing_state->musig.round2.rand_root, cur_input_index, keyexpr_info->cur_index, rand_i_j); @@ -2861,7 +2875,7 @@ static bool __attribute__((noinline)) sign_sighash_musig_and_yield(dispatcher_co } static bool __attribute__((noinline)) -compute_segwit_hashes(dispatcher_context_t *dc, sign_psbt_state_t *st, segwit_hashes_t *hashes) { +compute_tx_hashes(dispatcher_context_t *dc, sign_psbt_state_t *st, tx_hashes_t *hashes) { { // compute sha_prevouts and sha_sequences cx_sha256_t sha_prevouts_context, sha_sequences_context; @@ -2990,7 +3004,7 @@ compute_segwit_hashes(dispatcher_context_t *dc, sign_psbt_state_t *st, segwit_ha static bool __attribute__((noinline)) sign_transaction_input(dispatcher_context_t *dc, sign_psbt_state_t *st, sign_psbt_cache_t *sign_psbt_cache, - segwit_hashes_t *hashes, + signing_state_t *signing_state, keyexpr_info_t *keyexpr_info, input_info_t *input, unsigned int cur_input_index) { @@ -3103,7 +3117,12 @@ static bool __attribute__((noinline)) sign_transaction_input(dispatcher_context_ input->sighash_type = SIGHASH_ALL; } - if (!compute_sighash_segwitv0(dc, st, hashes, input, cur_input_index, sighash)) + if (!compute_sighash_segwitv0(dc, + st, + &signing_state->tx_hashes, + input, + cur_input_index, + sighash)) return false; if (!sign_sighash_ecdsa_and_yield(dc, @@ -3121,7 +3140,7 @@ static bool __attribute__((noinline)) sign_transaction_input(dispatcher_context_ if (!compute_sighash_segwitv1(dc, st, - hashes, + &signing_state->tx_hashes, input, cur_input_index, keyexpr_info, @@ -3160,6 +3179,7 @@ static bool __attribute__((noinline)) sign_transaction_input(dispatcher_context_ } else if (keyexpr_info->key_expression_ptr->type == KEY_EXPRESSION_MUSIG) { if (!sign_sighash_musig_and_yield(dc, st, + signing_state, keyexpr_info, input, cur_input_index, @@ -3228,13 +3248,13 @@ sign_transaction(dispatcher_context_t *dc, int key_expression_index = 0; - segwit_hashes_t hashes; + signing_state_t signing_state; // compute all the tx-wide hashes // while this is redundant for legacy transactions, we do it here in order to // avoid doing it in places that have more stack limitations - if (!compute_segwit_hashes(dc, st, &hashes)) { - // we do not send a status word, since compute_segwit_hashes already does it on failure + if (!compute_tx_hashes(dc, st, &signing_state.tx_hashes)) { + // we do not send a status word, since compute_tx_hashes already does it on failure return false; } @@ -3298,7 +3318,7 @@ sign_transaction(dispatcher_context_t *dc, if (!sign_transaction_input(dc, st, sign_psbt_cache, - &hashes, + &signing_state, &keyexpr_info, &input, i)) { @@ -3312,6 +3332,16 @@ sign_transaction(dispatcher_context_t *dc, ++key_expression_index; } + // MuSig2: if there is an active session at the end of round 1, we move it to persistent + // storage. It is important that this is only done at the very end of the signing process. + uint8_t acc = 0; + for (size_t i = 0; i < sizeof(signing_state.musig.round1); i++) { + acc |= signing_state.musig.round1.id[i]; + } + if (acc != 0) { + musigsession_store(signing_state.musig.round1.id, &signing_state.musig.round1); + } + return true; } diff --git a/src/musig/musig_sessions.h b/src/musig/musig_sessions.h index b708d6fb9..8236693c9 100644 --- a/src/musig/musig_sessions.h +++ b/src/musig/musig_sessions.h @@ -3,13 +3,25 @@ #include #include "musig.h" +// the maximum number of musig sessions that are stored in permanent memory #define MAX_N_MUSIG_SESSIONS 8 +// TODO: rename to musig_psbt_session_t to avoid confusion with musig_session_context_t typedef struct { uint8_t id[32]; uint8_t rand_root[32]; } musig_session_t; +// volatile state for musig signing +typedef struct musig_signing_state_s { + // a session created during round 1; if signing completes (and in no other case), it is moved to + // the persistent storage + musig_session_t round1; + // a session that was removed from the persistent storage before any partial signature is + // returned during round 2. It is deleted at the end of signing, and must _never_ be used again. + musig_session_t round2; +} musig_signing_state_t; + extern musig_session_t musig_sessions[MAX_N_MUSIG_SESSIONS]; // TODO: docs From b0e02d0fa9b4b87c5c460e145c4b7e59f37dbc67 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Mon, 15 Jul 2024 17:54:09 +0200 Subject: [PATCH 17/45] Modularize and extract the musig session handling from sign_psbt.c --- src/handler/sign_psbt.c | 72 ++++++++++++---------------- src/musig/musig_sessions.c | 98 +++++++++++++++++++++++++++++--------- src/musig/musig_sessions.h | 79 ++++++++++++++++++++++++------ 3 files changed, 171 insertions(+), 78 deletions(-) diff --git a/src/handler/sign_psbt.c b/src/handler/sign_psbt.c index 068a90dea..6efecb36d 100644 --- a/src/handler/sign_psbt.c +++ b/src/handler/sign_psbt.c @@ -2697,28 +2697,23 @@ static bool __attribute__((noinline)) sign_sighash_musig_and_yield(dispatcher_co 1 + psbt_id_len, my_pubnonce.raw, sizeof(musig_pubnonce_t))) { - // 5) generate and yield pubnonce - - // if an existing session for psbt_session_id exists, delete it - if (musigsession_pop(psbt_session_id, NULL)) { - // We wouldn't expect this: probably the client sent the same psbt for - // round 1 twice, without adding the pubnonces to the psbt after the first round. - // We delete the old session and start a fresh one, but we print a - // warning if in debug mode. - PRINTF("Session with the same id already existing\n"); + /** + * Round 1 of the MuSig2 protocol + **/ + + const musig_psbt_session_t *psbt_session = + musigsession_round1_initialize(psbt_session_id, &signing_state->musig); + if (psbt_session == NULL) { + // This should never happen + PRINTF("Unexpected: failed to initialize MuSig2 round 1\n"); + SEND_SW(dc, SW_BAD_STATE); + return false; } - if (memcmp(signing_state->musig.round1.id, psbt_session_id, sizeof(psbt_session_id)) != 0) { - // first input/placeholder pair using this session: initialize the session - memcpy(signing_state->musig.round1.id, psbt_session_id, sizeof(psbt_session_id)); - musigsession_init_randomness(&signing_state->musig.round1); - } + // 5) generate and yield pubnonce uint8_t rand_i_j[32]; - compute_rand_i_j(signing_state->musig.round1.rand_root, - cur_input_index, - keyexpr_info->cur_index, - rand_i_j); + compute_rand_i_j(psbt_session, cur_input_index, keyexpr_info->cur_index, rand_i_j); musig_secnonce_t secnonce; musig_pubnonce_t pubnonce; @@ -2744,20 +2739,24 @@ static bool __attribute__((noinline)) sign_sighash_musig_and_yield(dispatcher_co return false; } } else { - // 6) generate and yield partial signature - // If the session is not already initialized, we pop it from persistent storage - if (memcmp(signing_state->musig.round2.id, psbt_session_id, sizeof(psbt_session_id)) != 0) { - // get and delete the musig session from permanent storage - if (!musigsession_pop(psbt_session_id, &signing_state->musig.round2)) { - // The PSBT contains a partial nonce, but we do not have the corresponding psbt - // session in storage. Either it was deleted, or the pubnonces were not real. Either - // way, we cannot continue. - PRINTF("Missing MuSig2 session\n"); - SEND_SW(dc, SW_BAD_STATE); - return false; - } + /** + * Round 2 of the MuSig2 protocol + **/ + + const musig_psbt_session_t *psbt_session = + musigsession_round2_initialize(psbt_session_id, &signing_state->musig); + + if (psbt_session == NULL) { + // The PSBT contains a partial nonce, but we do not have the corresponding psbt + // session in storage. Either it was deleted, or the pubnonces were not real. Either + // way, we cannot continue. + PRINTF("Missing MuSig2 session\n"); + SEND_SW(dc, SW_BAD_STATE); + return false; } + // 6) generate and yield partial signature + musig_pubnonce_t nonces[MAX_PUBKEYS_PER_MUSIG]; for (int i = 0; i < musig_info->n; i++) { @@ -2791,10 +2790,7 @@ static bool __attribute__((noinline)) sign_sighash_musig_and_yield(dispatcher_co // recompute secnonce from psbt_session randomness uint8_t rand_i_j[32]; - compute_rand_i_j(signing_state->musig.round2.rand_root, - cur_input_index, - keyexpr_info->cur_index, - rand_i_j); + compute_rand_i_j(psbt_session, cur_input_index, keyexpr_info->cur_index, rand_i_j); musig_secnonce_t secnonce; musig_pubnonce_t pubnonce; @@ -3334,13 +3330,7 @@ sign_transaction(dispatcher_context_t *dc, // MuSig2: if there is an active session at the end of round 1, we move it to persistent // storage. It is important that this is only done at the very end of the signing process. - uint8_t acc = 0; - for (size_t i = 0; i < sizeof(signing_state.musig.round1); i++) { - acc |= signing_state.musig.round1.id[i]; - } - if (acc != 0) { - musigsession_store(signing_state.musig.round1.id, &signing_state.musig.round1); - } + musigsession_commit(&signing_state.musig); return true; } diff --git a/src/musig/musig_sessions.c b/src/musig/musig_sessions.c index 4d38117b7..be8e714fe 100644 --- a/src/musig/musig_sessions.c +++ b/src/musig/musig_sessions.c @@ -6,20 +6,7 @@ #include "../crypto.h" // TODO: persist in NVRAM instead -musig_session_t musig_sessions[MAX_N_MUSIG_SESSIONS]; - -bool musigsession_pop(uint8_t psbt_session_id[static 32], musig_session_t *out) { - for (int i = 0; i < MAX_N_MUSIG_SESSIONS; i++) { - if (memcmp(psbt_session_id, musig_sessions[i].id, 32) == 0) { - if (out != NULL) { - memcpy(out, &musig_sessions[i], sizeof(musig_session_t)); - } - explicit_bzero(&musig_sessions[i], sizeof(musig_session_t)); - return true; - } - } - return false; -} +musig_psbt_session_t musig_sessions[MAX_N_MUSIG_SESSIONS]; static bool is_all_zeros(const uint8_t *array, size_t size) { for (size_t i = 0; i < size; ++i) { @@ -30,35 +17,100 @@ static bool is_all_zeros(const uint8_t *array, size_t size) { return true; } -void musigsession_init_randomness(musig_session_t *session) { - cx_get_random_bytes(session->rand_root, 32); +static bool musigsession_pop(const uint8_t psbt_session_id[static 32], musig_psbt_session_t *out) { + for (int i = 0; i < MAX_N_MUSIG_SESSIONS; i++) { + if (memcmp(psbt_session_id, musig_sessions[i]._id, 32) == 0) { + if (out != NULL) { + memcpy(out, &musig_sessions[i], sizeof(musig_psbt_session_t)); + } + explicit_bzero(&musig_sessions[i], sizeof(musig_psbt_session_t)); + return true; + } + } + return false; } -void musigsession_store(uint8_t psbt_session_id[static 32], const musig_session_t *session) { +static void musigsession_init_randomness(musig_psbt_session_t *session) { + cx_get_random_bytes(session->_rand_root, 32); +} + +static void musigsession_store(const uint8_t psbt_session_id[static 32], + const musig_psbt_session_t *session) { // make sure that no session with the same id exists; delete it otherwise musigsession_pop(psbt_session_id, NULL); int i; for (i = 0; i < MAX_N_MUSIG_SESSIONS; i++) { - if (is_all_zeros((uint8_t *) &musig_sessions[i], sizeof(musig_session_t))) { + if (is_all_zeros((uint8_t *) &musig_sessions[i], sizeof(musig_psbt_session_t))) { break; } } if (i >= MAX_N_MUSIG_SESSIONS) { // no free slot found, delete the first by default - // TODO: should we use a LIFO structure? Could add a counter to musig_session_t + // TODO: should we use a LIFO structure? Could add a counter to musig_psbt_session_t i = 0; } // no free slot; replace the first slot - explicit_bzero(&musig_sessions[i], sizeof(musig_session_t)); - memcpy(&musig_sessions[i], session, sizeof(musig_session_t)); + explicit_bzero(&musig_sessions[i], sizeof(musig_psbt_session_t)); + memcpy(&musig_sessions[i], session, sizeof(musig_psbt_session_t)); } -void compute_rand_i_j(const uint8_t rand_root[static 32], int i, int j, uint8_t out[static 32]) { +void compute_rand_i_j(const musig_psbt_session_t *psbt_session, + int i, + int j, + uint8_t out[static 32]) { cx_sha256_t hash_context; cx_sha256_init(&hash_context); - crypto_hash_update(&hash_context.header, rand_root, CX_SHA256_SIZE); + crypto_hash_update(&hash_context.header, psbt_session->_rand_root, CX_SHA256_SIZE); crypto_hash_update_u32(&hash_context.header, (uint32_t) i); crypto_hash_update_u32(&hash_context.header, (uint32_t) j); crypto_hash_digest(&hash_context.header, out, 32); } + +const musig_psbt_session_t *musigsession_round1_initialize( + uint8_t psbt_session_id[static 32], + musig_signing_state_t *musig_signing_state) { + // if an existing session for psbt_session_id exists, delete it + if (musigsession_pop(psbt_session_id, NULL)) { + // We wouldn't expect this: probably the client sent the same psbt for + // round 1 twice, without adding the pubnonces to the psbt after the first round. + // We delete the old session and start a fresh one, but we print a + // warning if in debug mode. + PRINTF("Session with the same id already existing\n"); + } + + if (memcmp(musig_signing_state->_round1._id, psbt_session_id, 32) != 0) { + // first input/placeholder pair using this session: initialize the session + memcpy(musig_signing_state->_round1._id, psbt_session_id, 32); + musigsession_init_randomness(&musig_signing_state->_round1); + } + + return &musig_signing_state->_round1; +} + +const musig_psbt_session_t *musigsession_round2_initialize( + uint8_t psbt_session_id[static 32], + musig_signing_state_t *musig_signing_state) { + if (memcmp(musig_signing_state->_round2._id, psbt_session_id, 32) != 0) { + // get and delete the musig session from permanent storage + if (!musigsession_pop(psbt_session_id, &musig_signing_state->_round2)) { + // The PSBT contains a partial nonce, but we do not have the corresponding psbt + // session in storage. Either it was deleted, or the pubnonces were not real. Either + // way, we cannot continue. + PRINTF("Missing MuSig2 session\n"); + return NULL; + } + } + + return &musig_signing_state->_round2; +} + +void musigsession_commit(musig_signing_state_t *musig_signing_state) { + uint8_t acc = 0; + for (size_t i = 0; i < sizeof(musig_signing_state->_round1); i++) { + acc |= musig_signing_state->_round1._id[i]; + } + if (acc != 0) { + musigsession_store(musig_signing_state->_round1._id, &musig_signing_state->_round1); + } +} diff --git a/src/musig/musig_sessions.h b/src/musig/musig_sessions.h index 8236693c9..0ef41cb0e 100644 --- a/src/musig/musig_sessions.h +++ b/src/musig/musig_sessions.h @@ -6,27 +6,78 @@ // the maximum number of musig sessions that are stored in permanent memory #define MAX_N_MUSIG_SESSIONS 8 -// TODO: rename to musig_psbt_session_t to avoid confusion with musig_session_context_t -typedef struct { - uint8_t id[32]; - uint8_t rand_root[32]; -} musig_session_t; +// state of a musig_psbt_session. Members are private and must not be accessed directly by any +// code outside of musig_sessions.c. +typedef struct musig_psbt_session_s { + uint8_t _id[32]; + uint8_t _rand_root[32]; +} musig_psbt_session_t; -// volatile state for musig signing +// volatile state for musig signing. Members are private and must not be accessed directly by any +// code outside of musig_sessions.c. typedef struct musig_signing_state_s { // a session created during round 1; if signing completes (and in no other case), it is moved to // the persistent storage - musig_session_t round1; + musig_psbt_session_t _round1; // a session that was removed from the persistent storage before any partial signature is // returned during round 2. It is deleted at the end of signing, and must _never_ be used again. - musig_session_t round2; + musig_psbt_session_t _round2; } musig_signing_state_t; -extern musig_session_t musig_sessions[MAX_N_MUSIG_SESSIONS]; +/** + * Given a musig psbt session, computes the synthetic randomness for a given + * (input_index, placeholder_index) pair. + */ +void compute_rand_i_j(const musig_psbt_session_t *psbt_session, + int input_index, + int placeholder_index, + uint8_t out[static 32]); -// TODO: docs -bool musigsession_pop(uint8_t psbt_session_id[static 32], musig_session_t *out); -void musigsession_init_randomness(musig_session_t *session); -void musigsession_store(uint8_t psbt_session_id[static 32], const musig_session_t *session); +/** + * Handles the creation of a new musig psbt session into the volatile memory, or its retrieval (if + * the session already exists). + * It must be called when starting MuSig2 round 1 for a fixed input/placeholder pair, during the + * signing process. + * + * @param[in] psbt_session_id + * Pointer to the musig psbt session id. + * @param[in] musig_signing_state + * Pointer to the musig signing state. + * + * @return a musig_psbt_session_t on success, NULL on failure. + */ +__attribute__((warn_unused_result)) const musig_psbt_session_t *musigsession_round1_initialize( + uint8_t psbt_session_id[static 32], + musig_signing_state_t *musig_signing_state); -void compute_rand_i_j(const uint8_t rand_root[static 32], int i, int j, uint8_t out[static 32]); +/** + * Handles the retrieval of a musig psbt session from volatile memory (if it exists already) or its + * retrieval from the persistent memory otherwise. The session is guaranteed to be deleted from the + * persistent memory prior to returning. + * It must be called when starting MuSig2 round 2 for a fixed input/placeholder pair, during the + * signing process. + * + * @param[in] psbt_session_id + * Pointer to the musig psbt session id. + * @param[in] musig_signing_state + * Pointer to the musig signing state. + * + * @return a musig_psbt_session_t on success, NULL on failure. + */ +__attribute__((warn_unused_result)) const musig_psbt_session_t *musigsession_round2_initialize( + uint8_t psbt_session_id[static 32], + musig_signing_state_t *musig_signing_state); + +/** + * If a session produced in round 1 is active in volatile memory, it is stored in the persistent + * memory. + * This must be called at the end of a successful signing flow, after all the public nonces have + * been returned to the client. It must _not_ be called if any error occurs, or if the signing + * process is aborted for any reason. + * + * @param[in] psbt_session_id + * Pointer to the musig psbt session id. + * @param[in] musig_signing_state + * Pointer to the musig signing state. + */ +void musigsession_commit(musig_signing_state_t *musig_signing_state); From 1b3061b69a46d3047a04e098bb2b01b56aa733b9 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Thu, 30 May 2024 17:32:14 +0200 Subject: [PATCH 18/45] Persistent storage for musig psbt signing sessions --- src/musig/musig_sessions.c | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/src/musig/musig_sessions.c b/src/musig/musig_sessions.c index be8e714fe..174e111b3 100644 --- a/src/musig/musig_sessions.c +++ b/src/musig/musig_sessions.c @@ -5,8 +5,15 @@ #include "musig_sessions.h" #include "../crypto.h" -// TODO: persist in NVRAM instead -musig_psbt_session_t musig_sessions[MAX_N_MUSIG_SESSIONS]; +typedef struct { + // Aligning by 4 is necessary due to platform limitations. + // Aligning by 64 further guarantees that each session occupies exactly + // a single NVRAM page, minimizing the number of writes. + __attribute__((aligned(64))) musig_psbt_session_t sessions[MAX_N_MUSIG_SESSIONS]; +} musig_persistent_storage_t; + +const musig_persistent_storage_t N_musig_storage_real; +#define N_musig_storage (*(const volatile musig_persistent_storage_t *) PIC(&N_musig_storage_real)) static bool is_all_zeros(const uint8_t *array, size_t size) { for (size_t i = 0; i < size; ++i) { @@ -19,11 +26,17 @@ static bool is_all_zeros(const uint8_t *array, size_t size) { static bool musigsession_pop(const uint8_t psbt_session_id[static 32], musig_psbt_session_t *out) { for (int i = 0; i < MAX_N_MUSIG_SESSIONS; i++) { - if (memcmp(psbt_session_id, musig_sessions[i]._id, 32) == 0) { + if (memcmp(psbt_session_id, (const void *) N_musig_storage.sessions[i]._id, 32) == 0) { if (out != NULL) { - memcpy(out, &musig_sessions[i], sizeof(musig_psbt_session_t)); + memcpy(out, + (const void *) &N_musig_storage.sessions[i], + sizeof(musig_psbt_session_t)); } - explicit_bzero(&musig_sessions[i], sizeof(musig_psbt_session_t)); + uint8_t zeros[sizeof(musig_psbt_session_t)] = {0}; + nvm_write((void *) &N_musig_storage.sessions[i], + (void *) zeros, + sizeof(musig_psbt_session_t)); + return true; } } @@ -31,6 +44,8 @@ static bool musigsession_pop(const uint8_t psbt_session_id[static 32], musig_psb } static void musigsession_init_randomness(musig_psbt_session_t *session) { + // it is extremely important that the randomness is initialized with a cryptographically strong + // random number generator cx_get_random_bytes(session->_rand_root, 32); } @@ -41,7 +56,7 @@ static void musigsession_store(const uint8_t psbt_session_id[static 32], int i; for (i = 0; i < MAX_N_MUSIG_SESSIONS; i++) { - if (is_all_zeros((uint8_t *) &musig_sessions[i], sizeof(musig_psbt_session_t))) { + if (is_all_zeros((uint8_t *) &N_musig_storage.sessions[i], sizeof(musig_psbt_session_t))) { break; } } @@ -50,9 +65,10 @@ static void musigsession_store(const uint8_t psbt_session_id[static 32], // TODO: should we use a LIFO structure? Could add a counter to musig_psbt_session_t i = 0; } - // no free slot; replace the first slot - explicit_bzero(&musig_sessions[i], sizeof(musig_psbt_session_t)); - memcpy(&musig_sessions[i], session, sizeof(musig_psbt_session_t)); + // replace the chosen slot + nvm_write((void *) &N_musig_storage.sessions[i], + (void *) session, + sizeof(musig_psbt_session_t)); } void compute_rand_i_j(const musig_psbt_session_t *psbt_session, From 27c95dcfe064a1521bf0933597388a32bef6f807 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Fri, 31 May 2024 14:17:53 +0200 Subject: [PATCH 19/45] Add ragger navigation to musig sign_psbt tests --- tests/test_sign_psbt_musig.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/tests/test_sign_psbt_musig.py b/tests/test_sign_psbt_musig.py index eaff3b320..bde7ac2a7 100644 --- a/tests/test_sign_psbt_musig.py +++ b/tests/test_sign_psbt_musig.py @@ -3,6 +3,7 @@ from hashlib import sha256 import hmac +from typing import Optional from ledger_bitcoin.client_base import Client, MusigPartialSignature, MusigPubNonce @@ -26,13 +27,18 @@ class LedgerMusig2Cosigner(PsbtMusig2Cosigner): Implements a PsbtMusig2Cosigner that uses a BitcoinClient """ - def __init__(self, client: Client, wallet_policy: WalletPolicy, wallet_hmac: bytes) -> None: + def __init__(self, client: Client, wallet_policy: WalletPolicy, wallet_hmac: bytes, *, navigator: Optional[Navigator] = None, + testname: str = "", instructions: Instructions = None) -> None: super().__init__() self.client = client self.wallet_policy = wallet_policy self.wallet_hmac = wallet_hmac + self.navigator = navigator + self.testname = testname + self.instructions = instructions + self.fingerprint = client.get_master_fingerprint() desc_tmpl = TrDescriptorTemplate.from_string( @@ -61,7 +67,8 @@ def get_participant_pubkey(self) -> bip0327.Point: def generate_public_nonces(self, psbt: PSBT) -> None: print("PSBT before nonce generation:", psbt.serialize()) - res = self.client.sign_psbt(psbt, self.wallet_policy, self.wallet_hmac) + res = self.client.sign_psbt( + psbt, self.wallet_policy, self.wallet_hmac, navigator=self.navigator, testname=self.testname, instructions=self.instructions) print("Pubnonces:", res) for (input_index, yielded) in res: if isinstance(yielded, MusigPubNonce): @@ -80,7 +87,8 @@ def generate_public_nonces(self, psbt: PSBT) -> None: def generate_partial_signatures(self, psbt: PSBT) -> None: print("PSBT before partial signature generation:", psbt.serialize()) - res = self.client.sign_psbt(psbt, self.wallet_policy, self.wallet_hmac) + res = self.client.sign_psbt( + psbt, self.wallet_policy, self.wallet_hmac, navigator=self.navigator, testname=self.testname, instructions=self.instructions) print("Ledger result of second round:", res) for (input_index, yielded) in res: if isinstance(yielded, MusigPartialSignature): @@ -99,7 +107,7 @@ def generate_partial_signatures(self, psbt: PSBT) -> None: raise ValueError("Expected partial signatures, got a pubnonce") -def test_sign_psbt_musig2_keypath(client: RaggerClient, speculos_globals: SpeculosGlobals): +def test_sign_psbt_musig2_keypath(navigator: Navigator, firmware: Firmware, client: RaggerClient, test_name: str, speculos_globals: SpeculosGlobals): cosigner_1_xpub = "[f5acc2fd/44'/1'/0']tpubDCwYjpDhUdPGP5rS3wgNg13mTrrjBuG8V9VpWbyptX6TRPbNoZVXsoVUSkCjmQ8jJycjuDKBb9eataSymXakTTaGifxR6kmVsfFehH1ZgJT" cosigner_2_xpriv = "tprv8gFWbQBTLFhbX3EK3cS7LmenwE3JjXbD9kN9yXfq7LcBm81RSf8vPGPqGPjZSeX41LX9ZN14St3z8YxW48aq5Yhr9pQZVAyuBthfi6quTCf" @@ -122,13 +130,14 @@ def test_sign_psbt_musig2_keypath(client: RaggerClient, speculos_globals: Specul "a3aeecb6c236b4a7e72c95fa138250d449b97a75c573f8ab612356279ff64046") ] - signer_1 = LedgerMusig2Cosigner(client, wallet_policy, wallet_hmac) + signer_1 = LedgerMusig2Cosigner(client, wallet_policy, wallet_hmac, + navigator=navigator, instructions=sign_psbt_instruction_approve(firmware, save_screenshot=False, has_spend_from_wallet=True, has_feewarning=True), testname=test_name) signer_2 = HotMusig2Cosigner(wallet_policy, cosigner_2_xpriv) run_musig2_test(wallet_policy, psbt, [signer_1, signer_2], sighashes) -def test_sign_psbt_musig2_scriptpath(client: RaggerClient, speculos_globals: SpeculosGlobals): +def test_sign_psbt_musig2_scriptpath(navigator: Navigator, firmware: Firmware, client: RaggerClient, test_name: str, speculos_globals: SpeculosGlobals): cosigner_1_xpub = "[f5acc2fd/44'/1'/0']tpubDCwYjpDhUdPGP5rS3wgNg13mTrrjBuG8V9VpWbyptX6TRPbNoZVXsoVUSkCjmQ8jJycjuDKBb9eataSymXakTTaGifxR6kmVsfFehH1ZgJT" cosigner_2_xpriv = "tprv8gFWbQBTLFhbX3EK3cS7LmenwE3JjXbD9kN9yXfq7LcBm81RSf8vPGPqGPjZSeX41LX9ZN14St3z8YxW48aq5Yhr9pQZVAyuBthfi6quTCf" @@ -156,7 +165,8 @@ def test_sign_psbt_musig2_scriptpath(client: RaggerClient, speculos_globals: Spe "28f86cd95c144ed4a877701ae7166867e8805b654c43d9f44da45d7b0070c313") ] - signer_1 = LedgerMusig2Cosigner(client, wallet_policy, wallet_hmac) + signer_1 = LedgerMusig2Cosigner(client, wallet_policy, wallet_hmac, + navigator=navigator, instructions=sign_psbt_instruction_approve(firmware, save_screenshot=False, has_spend_from_wallet=True), testname=test_name) signer_2 = HotMusig2Cosigner(wallet_policy, cosigner_2_xpriv) run_musig2_test(wallet_policy, psbt, [signer_1, signer_2], sighashes) From 16261f546215010da8fe2266119bc5608ceee67e Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Mon, 15 Jul 2024 18:03:05 +0200 Subject: [PATCH 20/45] Update sanity checks for musig key expressions --- src/handler/lib/policy.c | 89 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 86 insertions(+), 3 deletions(-) diff --git a/src/handler/lib/policy.c b/src/handler/lib/policy.c index ea9b66201..3ea11a5d4 100644 --- a/src/handler/lib/policy.c +++ b/src/handler/lib/policy.c @@ -1897,6 +1897,19 @@ static int is_taptree_miniscript_sane(const policy_node_tree_t *taptree) { return 0; } +// sort an array of uint16_t in place using bubble sort +static void sort_uint16_array(uint16_t *array, size_t n) { + for (size_t i = 0; i < n; i++) { + for (size_t j = i + 1; j < n; j++) { + if (array[i] > array[j]) { + uint16_t tmp = array[i]; + array[i] = array[j]; + array[j] = tmp; + } + } + } +} + int is_policy_sane(dispatcher_context_t *dispatcher_context, const policy_node_t *policy, int wallet_version, @@ -1961,7 +1974,30 @@ int is_policy_sane(dispatcher_context_t *dispatcher_context, return WITH_ERROR(-1, "Unexpected error while counting key expressions"); } - // The following loop computationally very inefficient (quadratic in the number of + // for each MuSig key expression, checks that the key indices are all distinct + for (int i = 0; i < n_key_expressions; i++) { + policy_node_keyexpr_t *kp_i; + if (0 > get_keyexpr_by_index(policy, i, NULL, &kp_i)) { + return WITH_ERROR(-1, "Unexpected error retrieving key expressions from the policy"); + } + if (kp_i->type == KEY_EXPRESSION_MUSIG) { + musig_aggr_key_info_t *musig_info_i = r_musig_aggr_key_info(&kp_i->m.musig_info); + uint16_t *key_indexes_i = r_uint16(&musig_info_i->key_indexes); + + uint16_t key_indexes_i_sorted[MAX_PUBKEYS_PER_MUSIG]; + memcpy(key_indexes_i_sorted, key_indexes_i, musig_info_i->n * sizeof(uint16_t)); + + // sort the arrays + sort_uint16_array(key_indexes_i_sorted, musig_info_i->n); + for (int j = 0; j < musig_info_i->n - 1; j++) { + if (key_indexes_i_sorted[j] == key_indexes_i_sorted[j + 1]) { + return WITH_ERROR(-1, "Repeated key in musig key expression"); + } + } + } + } + + // The following loop is computationally very inefficient (quadratic in the number of // key expressions), but more efficient solutions likely require a substantial amount of RAM // (proportional to the number of key expressions). Instead, this only requires stack depth // proportional to the depth of the wallet policy's abstract syntax tree. @@ -1978,13 +2014,60 @@ int is_policy_sane(dispatcher_context_t *dispatcher_context, "Unexpected error retrieving key expressions from the policy"); } - // key expressions for the same key must have disjoint derivation options - if (kp_i->k.key_index == kp_j->k.key_index) { + if ((kp_i->type == KEY_EXPRESSION_NORMAL && kp_j->type == KEY_EXPRESSION_MUSIG) || + (kp_i->type == KEY_EXPRESSION_MUSIG && kp_j->type == KEY_EXPRESSION_NORMAL)) { + // if one is a key and the other is a musig, there's nothing else to check + continue; + } else if (kp_i->type == KEY_EXPRESSION_NORMAL && kp_j->type == KEY_EXPRESSION_NORMAL) { + // key expressions for the same key must have disjoint derivation options + if (kp_i->k.key_index == kp_j->k.key_index) { + if (kp_i->num_first == kp_j->num_first || kp_i->num_first == kp_j->num_second || + kp_i->num_second == kp_j->num_first || + kp_i->num_second == kp_j->num_second) { + return WITH_ERROR( + -1, + "Key expressions with repeated derivations in miniscript"); + } + } + } else if (kp_i->type == KEY_EXPRESSION_MUSIG && kp_j->type == KEY_EXPRESSION_MUSIG) { + musig_aggr_key_info_t *musig_info_i = r_musig_aggr_key_info(&kp_i->m.musig_info); + uint16_t *key_indexes_i = r_uint16(&musig_info_i->key_indexes); + musig_aggr_key_info_t *musig_info_j = r_musig_aggr_key_info(&kp_j->m.musig_info); + uint16_t *key_indexes_j = r_uint16(&musig_info_j->key_indexes); + // if two musigs have exactly the same set of keys, then the derivation options must + // be disjoint + + // make sure that there is no repeated key in the first musig + + if (musig_info_i->n != musig_info_j->n) { + continue; // cannot be the same set if the size is different + } + + uint16_t key_indexes_i_sorted[MAX_PUBKEYS_PER_MUSIG]; + uint16_t key_indexes_j_sorted[MAX_PUBKEYS_PER_MUSIG]; + memcpy(key_indexes_i_sorted, key_indexes_i, musig_info_i->n * sizeof(uint16_t)); + memcpy(key_indexes_j_sorted, key_indexes_j, musig_info_j->n * sizeof(uint16_t)); + + // sort the arrays + sort_uint16_array(key_indexes_i_sorted, musig_info_i->n); + sort_uint16_array(key_indexes_j_sorted, musig_info_j->n); + + if (memcmp(key_indexes_i_sorted, + key_indexes_j_sorted, + musig_info_i->n * sizeof(uint16_t)) != 0) { + continue; // different set of keys + } + + // same set of keys; therefore, we need to check that the derivation options are + // disjoint if (kp_i->num_first == kp_j->num_first || kp_i->num_first == kp_j->num_second || kp_i->num_second == kp_j->num_first || kp_i->num_second == kp_j->num_second) { return WITH_ERROR(-1, "Key expressions with repeated derivations in miniscript"); } + + } else { + LEDGER_ASSERT(false, "Unexpected key expression type"); } } } From 5b51ab8ce24d815c2075fd1358b37958bdb7e0e7 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Mon, 3 Jun 2024 14:05:48 +0200 Subject: [PATCH 21/45] Add architecture docs for MuSig2 --- doc/musig.md | 87 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 doc/musig.md diff --git a/doc/musig.md b/doc/musig.md new file mode 100644 index 000000000..f3356aa89 --- /dev/null +++ b/doc/musig.md @@ -0,0 +1,87 @@ +# MuSig2 + +The Ledger Bitcoin app supports wallet policies with `musig()` key expressions. + +MuSig2 is a 2-round multi-signature scheme compatible with the public keys and signatures used in taproot transactions. The implementation is compliant with [BIP-0327](https://github.com/bitcoin/bips/blob/master/bip-0327.mediawiki). + +## Specs + +`musig()` key expressions are supported for all taproot policies, including taproot keypaths and miniscript. + +- At most 16 keys are allowed in the musig expression; performance limitations, however, might apply in practice. +- At most 8 parallel MuSig signing sessions are supported, due to the need to persist state in the device's memory. +- Only `musig(...)/**` or `musig(...)//*` key expressions are supported; the public keys must be xpubs aggregated without any further derivation. Schemes where each pubkey is derived prior to aggregation (for example descriptors similar to `musig(xpub1/<0;1>/*,xpub2/<0;1>/*,...)`) are not supported. + +## State minimization + +This section describes implementation details that allow to minimize the amount of statefor each MuSig2 signing session, allowing secure support for multiple parallel MuSig2 on embedded device with limited storage. + +### Introduction + +BIP-0327 discusses at length the necessity to keep some state during a signing session. However, a "signing session" in BIP-0327 only refers to the production of a single signature. + +In the typical signing flow of a wallet, it's more logical to consider a _session_ at the level of an entire transaction. All transaction inputs are likely obtained from the same [descriptor containing musig()](https://github.com/bitcoin/bips/pull/1540), with the signer producing the pubnonce/signature for all the inputs at once. + +Therefore, in the flow of BIP-0327, you would expect at least _one MuSig2 signing session per input_ to be active at the same time. In the context of hardware signing device support, that's somewhat problematic: it would require to persist state for an unbounded number of signing sessions, for example for a wallet that received a large number of small UTXOs. Persistent storage is often a scarce resource in embedded signing devices, and a naive approach would likely impose a maximum limit on the number of inputs of the transactions, depending on the hardware limitations. + +This document describes an approach that is compatible with and builds on top of BIP-0327 to define a _psbt-level session_ with only a small amount of state persisted on the device. Each psbt-level session allows to manage in parallel all the MuSig2 sessions involved in signing a transaction (typically, at least one for each input). Each psbt-level session only requires 64 bytes of storage for the entire transaction, regardless of the amount of inputs. + +### Signing flow with synthetic randomness + +#### Synthetic generation of BIP-0327 state + +This section presents the core idea, while the next section makes it more precise in the context of signing devices. + +In BIP-0327, the internal state that is kept by the signing device is essentially the *secnonce*, which in turn is computed from a random number _rand'_, and optionally from other parameters of _NonceGen_ which depend on the transaction being signed. + +The core idea for state minimization is to compute a global random `rand_root`; then, for the *i*-th input and for the *j*-th `musig()` key that the device is signing for in the [wallet policy](https://github.com/bitcoin/bips/pull/1389), one defines the *rand'* in _NonceGen_ as: + +$\qquad rand_{i,j} = SHA256(rand\_root || i || j)$ + +In the concatenation, a fixed-length encoding of $i$ and $j$ is used in order to avoid collisions. That is used as the *rand'* value in the *NonceGen* algorithm for that input/KEY pair. + +The *j* parameter allows to handle wallet policies that contain more than one `musig()` key expression involving the signing device. + +#### Signing flow in detail + +This section describes the handling of the psbt-level sessions, plugging on top of the default signing flow of BIP-0327. + +We assume that the signing device handles a single psbt-level session; this can be generalized to multiple parallel psbt-level sessions, where each session computes and stores a different `rand_root`. + +In the following, a _session_ always refers to the psbt-level signing session; it contains `rand_root`, and possibly any other auxiliary data that the device wishes to save while signing is in progress. + +The term *persistent memory* refers to secure storage that is not wiped out when the device is turned off. The term *volatile memory* refers to the working memory available while the device is involved in the signing process. In Ledger signing devices, the persistent storage is flash memory, and the volatile memory is the RAM of the app. Both are contained in the Secure Element. + +**Phase 1: pubnonce generation:** A PSBT is sent to the signing device, and it does not contain any pubnonce. +- If a session already exists, it is deleted from the persistent memory. +- A new session is created in volatile memory. +- The device produces a fresh random number $rand\_root$, and saves it in the current session. +- The device generates the randomness for the $i$-th input and for the $j$-th key as: $rand_{i,j} = SHA256(rand\_root || i || j)$. +- Compute each *(secnonce, pubnonce)* as per the `NonceGen` algorithm. +- At completion (after all the pubnonces are returned), the session secret $rand\_root$ is copied into the persistent memory. + +**Phase 2: partial signature generation:** A PSBT containing all the pubnonces is sent to the device. +- *A copy of the session is stored in the volatile memory, and the session is deleted from the persistent memory*. +- For each input/musig-key pair $(i, j)$: + - Recompute the pubnonce/secnonce pair using `NonceGen` with the synthetic randomness $rand_{i,j}$ as above. + - Verify that the pubnonce contained in the PSBT matches the one synthetically recomputed. + - Continue the signing flow as per BIP-0327, generating the partial signature. + +### Security considerations +#### State reuse avoidance +Storing the session in persistent memory only at the end of Phase 1, and deleting it before beginning Phase 2 simplifies auditing and making sure that there is no reuse of state across signing sessions. + +#### Security of synthetic randomness + +Generating $rand_{i, j}$ synthetically is not a problem, since the $rand\_root$ value is kept secret and never leaves the device. This ensures that all the values produced for different $i$ and $j$ not predictable for an attacker. + +#### Malleability of the PSBT +If the optional parameters are passed to the _NonceGen_ function, they will depend on the transaction data present in the PSBT. Therefore, there is no guarantee that they will be unchanged the next time the PSBT is provided. + +However, that does not constitute a security risk, as those parameters are only used as additional sources of entropy in _NonceGen_. A malicious software wallet can't affect the _secnonce_/_pubnonce_ pairs in any predictable way. Changing any of the parameters used in _NonceGen_ would cause a failure during Phase 2, as the recomputed _pubnonce_ would not match the one in the psbt. + +### Generalization to multiple PSBT signing sessions + +The approach described above assumes that no attempt to sign a PSBT containing for a wallet policy containing `musig()` keys is initiated while a session is already in progress. + +It is possible to generalize this to an arbitrary number of parallel signing sessions. Each session could be identified by a `psbt_session_id` computed by hashing together the transaction hashes, \ No newline at end of file From 3418b6041ee8653c09baeb5d3494b28dc3588500 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Mon, 3 Jun 2024 14:07:36 +0200 Subject: [PATCH 22/45] Reference musig docs in musig session module --- src/musig/musig_sessions.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/musig/musig_sessions.h b/src/musig/musig_sessions.h index 0ef41cb0e..0cda21301 100644 --- a/src/musig/musig_sessions.h +++ b/src/musig/musig_sessions.h @@ -3,6 +3,11 @@ #include #include "musig.h" +/** + * This module encapsulates the logic to manage the psbt-level MuSig2 sessions. See the + * documentation in docs/musig.md for more information. + */ + // the maximum number of musig sessions that are stored in permanent memory #define MAX_N_MUSIG_SESSIONS 8 From 8e131012cf732a49c4aa9cdd6dfe97e1b83a2d5f Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Tue, 4 Jun 2024 10:10:25 +0200 Subject: [PATCH 23/45] Add const qualifiers, and asserts guarding against overflows --- src/handler/lib/policy.c | 22 ++++++++++-------- src/handler/sign_psbt.c | 49 +++++++++++++++++++++++----------------- 2 files changed, 40 insertions(+), 31 deletions(-) diff --git a/src/handler/lib/policy.c b/src/handler/lib/policy.c index 3ea11a5d4..a1285f4c9 100644 --- a/src/handler/lib/policy.c +++ b/src/handler/lib/policy.c @@ -468,8 +468,8 @@ __attribute__((warn_unused_result)) static int get_derived_pubkey( return -1; } } else if (key_expr->type == KEY_EXPRESSION_MUSIG) { - musig_aggr_key_info_t *musig_info = r_musig_aggr_key_info(&key_expr->m.musig_info); - uint16_t *key_indexes = r_uint16(&musig_info->key_indexes); + const musig_aggr_key_info_t *musig_info = r_musig_aggr_key_info(&key_expr->m.musig_info); + const uint16_t *key_indexes = r_uint16(&musig_info->key_indexes); plain_pk_t keys[MAX_PUBKEYS_PER_MUSIG]; for (int i = 0; i < musig_info->n; i++) { // we use ext_pubkey as a temporary variable; will overwrite later @@ -1772,9 +1772,9 @@ int count_distinct_keys_info(const policy_node_t *policy) { if (key_expression_ptr->type == KEY_EXPRESSION_NORMAL) { ret = MAX(ret, key_expression_ptr->k.key_index + 1); } else if (key_expression_ptr->type == KEY_EXPRESSION_MUSIG) { - musig_aggr_key_info_t *musig_info = + const musig_aggr_key_info_t *musig_info = r_musig_aggr_key_info(&key_expression_ptr->m.musig_info); - uint16_t *key_indexes = r_uint16(&musig_info->key_indexes); + const uint16_t *key_indexes = r_uint16(&musig_info->key_indexes); for (int i = 0; i < musig_info->n; i++) { ret = MAX(ret, key_indexes[i] + 1); } @@ -1981,8 +1981,8 @@ int is_policy_sane(dispatcher_context_t *dispatcher_context, return WITH_ERROR(-1, "Unexpected error retrieving key expressions from the policy"); } if (kp_i->type == KEY_EXPRESSION_MUSIG) { - musig_aggr_key_info_t *musig_info_i = r_musig_aggr_key_info(&kp_i->m.musig_info); - uint16_t *key_indexes_i = r_uint16(&musig_info_i->key_indexes); + const musig_aggr_key_info_t *musig_info_i = r_musig_aggr_key_info(&kp_i->m.musig_info); + const uint16_t *key_indexes_i = r_uint16(&musig_info_i->key_indexes); uint16_t key_indexes_i_sorted[MAX_PUBKEYS_PER_MUSIG]; memcpy(key_indexes_i_sorted, key_indexes_i, musig_info_i->n * sizeof(uint16_t)); @@ -2030,10 +2030,12 @@ int is_policy_sane(dispatcher_context_t *dispatcher_context, } } } else if (kp_i->type == KEY_EXPRESSION_MUSIG && kp_j->type == KEY_EXPRESSION_MUSIG) { - musig_aggr_key_info_t *musig_info_i = r_musig_aggr_key_info(&kp_i->m.musig_info); - uint16_t *key_indexes_i = r_uint16(&musig_info_i->key_indexes); - musig_aggr_key_info_t *musig_info_j = r_musig_aggr_key_info(&kp_j->m.musig_info); - uint16_t *key_indexes_j = r_uint16(&musig_info_j->key_indexes); + const musig_aggr_key_info_t *musig_info_i = + r_musig_aggr_key_info(&kp_i->m.musig_info); + const uint16_t *key_indexes_i = r_uint16(&musig_info_i->key_indexes); + const musig_aggr_key_info_t *musig_info_j = + r_musig_aggr_key_info(&kp_j->m.musig_info); + const uint16_t *key_indexes_j = r_uint16(&musig_info_j->key_indexes); // if two musigs have exactly the same set of keys, then the derivation options must // be disjoint diff --git a/src/handler/sign_psbt.c b/src/handler/sign_psbt.c index 6efecb36d..6fb3de7df 100644 --- a/src/handler/sign_psbt.c +++ b/src/handler/sign_psbt.c @@ -398,7 +398,7 @@ static int get_amount_scriptpubkey_from_psbt( // PSBT_{IN|OUT}_{TAP}?_BIP32_DERIVATION fields. static int read_change_and_index_from_psbt_bip32_derivation( dispatcher_context_t *dc, - keyexpr_info_t *keyexpr_info, + const keyexpr_info_t *keyexpr_info, in_out_info_t *in_out, sign_psbt_cache_t *sign_psbt_cache, int psbt_key_type, @@ -767,14 +767,17 @@ static bool fill_keyexpr_info_if_internal(dispatcher_context_t *dc, return result; } else if (keyexpr_info->key_expression_ptr->type == KEY_EXPRESSION_MUSIG) { // iterate through the keys of the musig() placeholder to find if a key is internal - musig_aggr_key_info_t *musig_info = + const musig_aggr_key_info_t *musig_info = r_musig_aggr_key_info(&keyexpr_info->key_expression_ptr->m.musig_info); - uint16_t *key_indexes = r_uint16(&musig_info->key_indexes); + const uint16_t *key_indexes = r_uint16(&musig_info->key_indexes); bool has_internal_key = false; // collect the keys of the musig, and fill the info related to the internal key (if any) uint8_t keys[MAX_PUBKEYS_PER_MUSIG][33]; + + LEDGER_ASSERT(musig_info->n <= MAX_PUBKEYS_PER_MUSIG, "Too many keys in musig placeholder"); + for (int idx_in_musig = 0; idx_in_musig < musig_info->n; idx_in_musig++) { if (get_and_verify_key_info(dc, st, key_indexes[idx_in_musig], &tmp_keyexpr_info)) { memcpy(keyexpr_info->key_derivation, @@ -1899,7 +1902,7 @@ static bool __attribute__((noinline)) compute_sighash_legacy(dispatcher_context_ static bool __attribute__((noinline)) compute_sighash_segwitv0(dispatcher_context_t *dc, sign_psbt_state_t *st, - tx_hashes_t *hashes, + const tx_hashes_t *hashes, input_info_t *input, unsigned int cur_input_index, uint8_t sighash[static 32]) { @@ -2084,10 +2087,10 @@ static bool __attribute__((noinline)) compute_sighash_segwitv0(dispatcher_contex static bool __attribute__((noinline)) compute_sighash_segwitv1(dispatcher_context_t *dc, sign_psbt_state_t *st, - tx_hashes_t *hashes, + const tx_hashes_t *hashes, input_info_t *input, unsigned int cur_input_index, - keyexpr_info_t *keyexpr_info, + const keyexpr_info_t *keyexpr_info, uint8_t sighash[static 32]) { LOG_PROCESSOR(__FILE__, __LINE__, __func__); @@ -2260,12 +2263,13 @@ static bool __attribute__((noinline)) yield_signature(dispatcher_context_t *dc, return true; } -static bool __attribute__((noinline)) sign_sighash_ecdsa_and_yield(dispatcher_context_t *dc, - sign_psbt_state_t *st, - keyexpr_info_t *keyexpr_info, - input_info_t *input, - unsigned int cur_input_index, - uint8_t sighash[static 32]) { +static bool __attribute__((noinline)) +sign_sighash_ecdsa_and_yield(dispatcher_context_t *dc, + sign_psbt_state_t *st, + const keyexpr_info_t *keyexpr_info, + input_info_t *input, + unsigned int cur_input_index, + uint8_t sighash[static 32]) { LOG_PROCESSOR(__FILE__, __LINE__, __func__); uint32_t sign_path[MAX_BIP32_PATH_STEPS]; @@ -2519,13 +2523,14 @@ static bool yield_musig_partial_signature(dispatcher_context_t *dc, tapleaf_hash); } -static bool __attribute__((noinline)) sign_sighash_musig_and_yield(dispatcher_context_t *dc, - sign_psbt_state_t *st, - signing_state_t *signing_state, - keyexpr_info_t *keyexpr_info, - input_info_t *input, - unsigned int cur_input_index, - uint8_t sighash[static 32]) { +static bool __attribute__((noinline)) +sign_sighash_musig_and_yield(dispatcher_context_t *dc, + sign_psbt_state_t *st, + signing_state_t *signing_state, + const keyexpr_info_t *keyexpr_info, + const input_info_t *input, + unsigned int cur_input_index, + uint8_t sighash[static 32]) { LOG_PROCESSOR(__FILE__, __LINE__, __func__); if (st->wallet_policy_map->type != TOKEN_TR) { @@ -2559,9 +2564,11 @@ static bool __attribute__((noinline)) sign_sighash_musig_and_yield(dispatcher_co serialized_extended_pubkey_t ext_pubkey; const policy_node_keyexpr_t *key_expr = keyexpr_info->key_expression_ptr; - musig_aggr_key_info_t *musig_info = r_musig_aggr_key_info(&key_expr->m.musig_info); - uint16_t *key_indexes = r_uint16(&musig_info->key_indexes); + const musig_aggr_key_info_t *musig_info = r_musig_aggr_key_info(&key_expr->m.musig_info); + const uint16_t *key_indexes = r_uint16(&musig_info->key_indexes); plain_pk_t keys[MAX_PUBKEYS_PER_MUSIG]; + + LEDGER_ASSERT(musig_info->n <= MAX_PUBKEYS_PER_MUSIG, "Too many keys in musig key expression"); for (int i = 0; i < musig_info->n; i++) { // we use ext_pubkey as a temporary variable; will overwrite later if (0 > get_extended_pubkey(dc, &wdi, key_indexes[i], &ext_pubkey)) { From ad377fbba5cd1b60145a5e558479bbc165d78df4 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Tue, 4 Jun 2024 15:03:37 +0200 Subject: [PATCH 24/45] Expose new types in python client --- bitcoin_client/ledger_bitcoin/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bitcoin_client/ledger_bitcoin/__init__.py b/bitcoin_client/ledger_bitcoin/__init__.py index 4c4bd82be..5777dfb37 100644 --- a/bitcoin_client/ledger_bitcoin/__init__.py +++ b/bitcoin_client/ledger_bitcoin/__init__.py @@ -1,7 +1,7 @@ """Ledger Nano Bitcoin app client""" -from .client_base import Client, TransportClient, PartialSignature +from .client_base import Client, TransportClient, PartialSignature, MusigPubNonce, MusigPartialSignature, SignPsbtYieldedObject from .client import createClient from .common import Chain @@ -13,6 +13,9 @@ "Client", "TransportClient", "PartialSignature", + "MusigPubNonce", + "MusigPartialSignature", + "SignPsbtYieldedObject", "createClient", "Chain", "AddressType", From 38a9e80f2c85cc27d02497ffae76d73917776bdd Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Wed, 9 Oct 2024 14:37:09 +0200 Subject: [PATCH 25/45] Update BIP_MUSIG_CHAINCODE ==> BIP_328_CHAINCODE --- bitcoin_client/ledger_bitcoin/client.py | 4 ++-- src/handler/lib/policy.c | 2 +- src/handler/sign_psbt.c | 6 ++---- src/musig/musig.h | 3 +-- 4 files changed, 6 insertions(+), 9 deletions(-) diff --git a/bitcoin_client/ledger_bitcoin/client.py b/bitcoin_client/ledger_bitcoin/client.py index 89279df36..6fe983152 100644 --- a/bitcoin_client/ledger_bitcoin/client.py +++ b/bitcoin_client/ledger_bitcoin/client.py @@ -42,7 +42,7 @@ def parse_stream_to_map(f: BufferedReader) -> Mapping[bytes, bytes]: def aggr_xpub(pubkeys: List[bytes], chain: Chain) -> str: - BIP_MUSIG_CHAINCODE = bytes.fromhex( + BIP_328_CHAINCODE = bytes.fromhex( "868087ca02a6f974c4598924c36b57762d32cb45717167e300622c7167e38965") # sort the pubkeys prior to aggregation ctx = key_agg(list(sorted(pubkeys))) @@ -59,7 +59,7 @@ def aggr_xpub(pubkeys: List[bytes], chain: Chain) -> str: b'\x00', # depth b'\x00\x00\x00\x00', # parent fingerprint b'\x00\x00\x00\x00', # child number - BIP_MUSIG_CHAINCODE, + BIP_328_CHAINCODE, compressed_pubkey ])) diff --git a/src/handler/lib/policy.c b/src/handler/lib/policy.c index a1285f4c9..a889cc898 100644 --- a/src/handler/lib/policy.c +++ b/src/handler/lib/policy.c @@ -500,7 +500,7 @@ __attribute__((warn_unused_result)) static int get_derived_pubkey( ext_pubkey.compressed_pubkey[0] = (musig_ctx.Q.y[31] % 2 == 0) ? 2 : 3; memcpy(&ext_pubkey.compressed_pubkey[1], musig_ctx.Q.x, sizeof(musig_ctx.Q.x)); - memcpy(&ext_pubkey.chain_code, BIP_MUSIG_CHAINCODE, sizeof(BIP_MUSIG_CHAINCODE)); + memcpy(&ext_pubkey.chain_code, BIP_328_CHAINCODE, sizeof(BIP_328_CHAINCODE)); } else { LEDGER_ASSERT(false, "Unreachable code"); } diff --git a/src/handler/sign_psbt.c b/src/handler/sign_psbt.c index 6fb3de7df..80f2ec5d2 100644 --- a/src/handler/sign_psbt.c +++ b/src/handler/sign_psbt.c @@ -822,9 +822,7 @@ static bool fill_keyexpr_info_if_internal(dispatcher_context_t *dc, memcpy(&keyexpr_info->pubkey.compressed_pubkey[1], musig_ctx.Q.x, sizeof(musig_ctx.Q.x)); - memcpy(&keyexpr_info->pubkey.chain_code, - BIP_MUSIG_CHAINCODE, - sizeof(BIP_MUSIG_CHAINCODE)); + memcpy(&keyexpr_info->pubkey.chain_code, BIP_328_CHAINCODE, sizeof(BIP_328_CHAINCODE)); keyexpr_info->fingerprint = crypto_get_key_fingerprint(keyexpr_info->pubkey.compressed_pubkey); @@ -2598,7 +2596,7 @@ sign_sighash_musig_and_yield(dispatcher_context_t *dc, ext_pubkey.compressed_pubkey[0] = (musig_ctx.Q.y[31] % 2 == 0) ? 2 : 3; memcpy(&ext_pubkey.compressed_pubkey[1], musig_ctx.Q.x, sizeof(musig_ctx.Q.x)); - memcpy(&ext_pubkey.chain_code, BIP_MUSIG_CHAINCODE, sizeof(BIP_MUSIG_CHAINCODE)); + memcpy(&ext_pubkey.chain_code, BIP_328_CHAINCODE, sizeof(BIP_328_CHAINCODE)); // 2) compute musig2 tweaks // We always have exactly 2 BIP32 tweaks in wallet policies; if the musig is in the keypath diff --git a/src/musig/musig.h b/src/musig/musig.h index 1a70334a7..ed28d3b77 100644 --- a/src/musig/musig.h +++ b/src/musig/musig.h @@ -5,8 +5,7 @@ #define MUSIG_PUBNONCE_SIZE 66 -// TODO: rename once BIP number is assigned -static uint8_t BIP_MUSIG_CHAINCODE[32] = { +static uint8_t BIP_328_CHAINCODE[32] = { 0x86, 0x80, 0x87, 0xCA, 0x02, 0xA6, 0xF9, 0x74, 0xC4, 0x59, 0x89, 0x24, 0xC3, 0x6B, 0x57, 0x76, 0x2D, 0x32, 0xCB, 0x45, 0x71, 0x71, 0x67, 0xE3, 0x00, 0x62, 0x2C, 0x71, 0x67, 0xE3, 0x89, 0x65}; From aea89fd0cd8365276774d32e99e07062b85e1da2 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Mon, 4 Nov 2024 16:22:10 +0000 Subject: [PATCH 26/45] Fix read_change_and_index_from_psbt_bip32_derivation incorrectly aborting prematurely for some PSBTs. Added explanatory comment --- src/handler/sign_psbt.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/handler/sign_psbt.c b/src/handler/sign_psbt.c index 80f2ec5d2..ae2b77018 100644 --- a/src/handler/sign_psbt.c +++ b/src/handler/sign_psbt.c @@ -396,6 +396,9 @@ static int get_amount_scriptpubkey_from_psbt( // Convenience function to share common logic when processing all the // PSBT_{IN|OUT}_{TAP}?_BIP32_DERIVATION fields. +// Note: This function must return -1 only on errors (causing signing to abort). +// It should return 1 if a derivation that makes sense for this input/output is found. +// It should return 0 otherwise (no match found, but continue the signing flow). static int read_change_and_index_from_psbt_bip32_derivation( dispatcher_context_t *dc, const keyexpr_info_t *keyexpr_info, @@ -438,7 +441,7 @@ static int read_change_and_index_from_psbt_bip32_derivation( if (der_len < 2 || der_len > MAX_BIP32_PATH_STEPS) { PRINTF("BIP32_DERIVATION path too long\n"); - return -1; + return 0; } // if this derivation path matches the internal key expression, From 13abab865e90f318fa517a2aab626b8f9365e70c Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Wed, 6 Nov 2024 08:32:15 +0000 Subject: [PATCH 27/45] Removed unused argument; deleted commented out check The check doesn't serve a security purpose, since direct derivation of each input/change script is used to check if they are internal. --- src/handler/sign_psbt.c | 30 ++---------------------------- 1 file changed, 2 insertions(+), 28 deletions(-) diff --git a/src/handler/sign_psbt.c b/src/handler/sign_psbt.c index ae2b77018..c09637aa4 100644 --- a/src/handler/sign_psbt.c +++ b/src/handler/sign_psbt.c @@ -403,7 +403,6 @@ static int read_change_and_index_from_psbt_bip32_derivation( dispatcher_context_t *dc, const keyexpr_info_t *keyexpr_info, in_out_info_t *in_out, - sign_psbt_cache_t *sign_psbt_cache, int psbt_key_type, buffer_t *data, const merkleized_map_commitment_t *map_commitment, @@ -469,24 +468,6 @@ static int read_change_and_index_from_psbt_bip32_derivation( return 0; } - // TODO: safe to remove this check? It should be, since we later re-derive - // the script independently. - // // check that we can indeed derive the same key from the current key expression - // serialized_extended_pubkey_t pubkey; - // if (0 > derive_first_step_for_pubkey(&keyexpr_info->pubkey, - // keyexpr_info->key_expression_ptr, - // sign_psbt_cache, - // in_out->is_change, - // &pubkey)) - // return -1; - // if (0 > bip32_CKDpub(&pubkey, addr_index, &pubkey, NULL)) return -1; - - // int pk_offset = is_tap ? 1 : 0; - // if (memcmp(pubkey.compressed_pubkey + pk_offset, bip32_derivation_pubkey, key_len) != 0) - // { - // return 0; - // } - in_out->key_expression_found = true; return 1; } @@ -876,7 +857,6 @@ static bool find_first_internal_keyexpr(dispatcher_context_t *dc, typedef struct { keyexpr_info_t *keyexpr_info; input_info_t *input; - sign_psbt_cache_t *sign_psbt_cache; } input_keys_callback_data_t; /** @@ -906,7 +886,6 @@ static void input_keys_callback(dispatcher_context_t *dc, if (0 > read_change_and_index_from_psbt_bip32_derivation(dc, callback_data->keyexpr_info, &callback_data->input->in_out, - callback_data->sign_psbt_cache, key_type, data, map_commitment, @@ -936,9 +915,7 @@ preprocess_inputs(dispatcher_context_t *dc, input_info_t input; memset(&input, 0, sizeof(input)); - input_keys_callback_data_t callback_data = {.input = &input, - .keyexpr_info = &keyexpr_info, - .sign_psbt_cache = sign_psbt_cache}; + input_keys_callback_data_t callback_data = {.input = &input, .keyexpr_info = &keyexpr_info}; int res = call_get_merkleized_map_with_callback( dc, (void *) &callback_data, @@ -1141,7 +1118,6 @@ preprocess_inputs(dispatcher_context_t *dc, typedef struct { keyexpr_info_t *keyexpr_info; output_info_t *output; - sign_psbt_cache_t *sign_psbt_cache; } output_keys_callback_data_t; /** @@ -1163,7 +1139,6 @@ static void output_keys_callback(dispatcher_context_t *dc, if (0 > read_change_and_index_from_psbt_bip32_derivation(dc, callback_data->keyexpr_info, &callback_data->output->in_out, - callback_data->sign_psbt_cache, key_type, data, map_commitment, @@ -1203,8 +1178,7 @@ preprocess_outputs(dispatcher_context_t *dc, memset(&output, 0, sizeof(output)); output_keys_callback_data_t callback_data = {.output = &output, - .keyexpr_info = &keyexpr_info, - .sign_psbt_cache = sign_psbt_cache}; + .keyexpr_info = &keyexpr_info}; int res = call_get_merkleized_map_with_callback( dc, (void *) &callback_data, From 5fcdcdf80754be4c6c039109ef09f3ee8b6ef7f4 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Tue, 5 Nov 2024 11:40:31 +0000 Subject: [PATCH 28/45] Generalized count_internal_keys in the test suite to count_internal_key_placeholders, with musig placeholder support --- test_utils/__init__.py | 39 +++++++++++++++++++++++++++++------- tests/test_e2e_miniscript.py | 4 ++-- tests/test_e2e_multisig.py | 4 ++-- tests/test_e2e_tapscripts.py | 4 ++-- 4 files changed, 38 insertions(+), 13 deletions(-) diff --git a/test_utils/__init__.py b/test_utils/__init__.py index f7867407a..013d9e2e3 100644 --- a/test_utils/__init__.py +++ b/test_utils/__init__.py @@ -1,10 +1,12 @@ +import re + import hashlib from typing import Literal, Union from mnemonic import Mnemonic from bip32 import BIP32 -from bitcoin_client.ledger_bitcoin.wallet import WalletPolicy +from bitcoin_client.ledger_bitcoin.wallet import WalletPolicy, WalletType from .slip21 import Slip21Node @@ -89,14 +91,16 @@ def get_internal_xpub(seed: str, path: str) -> str: return bip32.get_xpub_from_path(f"m/{path}") if path else bip32.get_xpub_from_path("m") -def count_internal_keys(seed: str, network: Union[Literal['main'], Literal['test']], wallet_policy: WalletPolicy) -> int: - """Count how many of the keys in wallet_policy are indeed internal""" +def count_internal_key_placeholders(seed: str, network: Union[Literal['main'], Literal['test']], wallet_policy: WalletPolicy) -> int: + """Count how many of the key placeholders in wallet_policy are indeed internal. + musig() placeholders are counted as many times as there are internal keys in them.""" bip32 = BIP32.from_seed(seed, network) master_key_fingerprint = hash160(bip32.pubkey)[0:4] - count = 0 + is_key_internal = [] for key_index, key_info in enumerate(wallet_policy.keys_info): + is_this_key_internal = False if "]" in key_info: key_orig_end_pos = key_info.index("]") fpr = key_info[1:9] @@ -110,8 +114,29 @@ def count_internal_keys(seed: str, network: Union[Literal['main'], Literal['test if fpr == master_key_fingerprint.hex(): computed_xpub = get_internal_xpub(seed, path) if computed_xpub == xpub: - # there could be multiple placeholders using the same key; we must count all of them - count += wallet_policy.descriptor_template.count( - f"@{key_index}/") + is_this_key_internal = True + is_key_internal.append(is_this_key_internal) + + # enumerate all the key placeholders + # for simplicity, we look for all the following patterns using regular expressions: + # - Simple keys: @/ (always with additional derivations, hence the final '/') + # - Musig expressions: musig(@k1, @k2, ...) + + count = 0 + + simple_key_placeholders = re.findall( + r'@(\d+)/', wallet_policy.descriptor_template) + # for each match, count it if the corresponding key is internal + for key_index in simple_key_placeholders: + if is_key_internal[int(key_index)]: + count += 1 + + if wallet_policy.version != WalletType.WALLET_POLICY_V1: # no musig in V1 policies + musig_key_placeholders = re.findall( + r'musig\(([^)]*)\)', wallet_policy.descriptor_template) + for musig_expr in musig_key_placeholders: + musig_keys_indices = [int(k[1:]) for k in musig_expr.split(",")] + # We count each musig placeholder as many times are there are internal keys in it + count += sum(int(is_key_internal[k]) for k in musig_keys_indices) return count diff --git a/tests/test_e2e_miniscript.py b/tests/test_e2e_miniscript.py index ab6be201a..3470421f4 100644 --- a/tests/test_e2e_miniscript.py +++ b/tests/test_e2e_miniscript.py @@ -11,7 +11,7 @@ from ledger_bitcoin.psbt import PSBT from ledger_bitcoin.wallet import WalletPolicy -from test_utils import SpeculosGlobals, get_internal_xpub, count_internal_keys +from test_utils import SpeculosGlobals, get_internal_xpub, count_internal_key_placeholders from ragger_bitcoin import RaggerClient from ragger_bitcoin.ragger_instructions import Instructions @@ -106,7 +106,7 @@ def run_test_e2e(navigator: Navigator, client: RaggerClient, wallet_policy: Wall instructions=instructions_sign_psbt, testname=f"{test_name}_sign") - n_internal_keys = count_internal_keys(speculos_globals.seed, "test", wallet_policy) + n_internal_keys = count_internal_key_placeholders(speculos_globals.seed, "test", wallet_policy) assert len(hww_sigs) == n_internal_keys * len(psbt.inputs) # should be true as long as all inputs are internal for i, part_sig in hww_sigs: diff --git a/tests/test_e2e_multisig.py b/tests/test_e2e_multisig.py index f0a35fddf..094c29e1a 100644 --- a/tests/test_e2e_multisig.py +++ b/tests/test_e2e_multisig.py @@ -11,7 +11,7 @@ from ledger_bitcoin.psbt import PSBT from ledger_bitcoin.wallet import WalletPolicy -from test_utils import SpeculosGlobals, get_internal_xpub, count_internal_keys +from test_utils import SpeculosGlobals, get_internal_xpub, count_internal_key_placeholders from speculos.client import SpeculosClient @@ -112,7 +112,7 @@ def run_test(navigator: Navigator, client: RaggerClient, wallet_policy: WalletPo instructions=instructions_sign_psbt, testname=f"{test_name}_sign") - n_internal_keys = count_internal_keys( + n_internal_keys = count_internal_key_placeholders( speculos_globals.seed, "test", wallet_policy) # should be true as long as all inputs are internal assert len(hww_sigs) == n_internal_keys * len(psbt.inputs) diff --git a/tests/test_e2e_tapscripts.py b/tests/test_e2e_tapscripts.py index 1ef95a31a..1b813f2ee 100644 --- a/tests/test_e2e_tapscripts.py +++ b/tests/test_e2e_tapscripts.py @@ -13,7 +13,7 @@ from ledger_bitcoin.psbt import PSBT from ledger_bitcoin.wallet import WalletPolicy -from test_utils import SpeculosGlobals, get_internal_xpub, count_internal_keys +from test_utils import SpeculosGlobals, get_internal_xpub, count_internal_key_placeholders from ragger_bitcoin import RaggerClient from ragger_bitcoin.ragger_instructions import Instructions @@ -119,7 +119,7 @@ def run_test_e2e(navigator: Navigator, client: RaggerClient, wallet_policy: Wall signed_psbt_hww_b64 = psbt.serialize() - n_internal_keys = count_internal_keys(speculos_globals.seed, "test", wallet_policy) + n_internal_keys = count_internal_key_placeholders(speculos_globals.seed, "test", wallet_policy) assert len(hww_sigs) == n_internal_keys * len(psbt.inputs) # should be true as long as all inputs are internal # ==> sign it with bitcoin-core From 1192d105a1402ae4dab7077cf8b12ca7fb8671ea Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Tue, 5 Nov 2024 14:26:13 +0000 Subject: [PATCH 29/45] Support BIP-389 multipath descriptors in get_descriptor --- bitcoin_client/ledger_bitcoin/wallet.py | 35 +++++++++++++++++++------ 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/bitcoin_client/ledger_bitcoin/wallet.py b/bitcoin_client/ledger_bitcoin/wallet.py index d6ccf4390..326d7e52f 100644 --- a/bitcoin_client/ledger_bitcoin/wallet.py +++ b/bitcoin_client/ledger_bitcoin/wallet.py @@ -1,13 +1,14 @@ import re from enum import IntEnum -from typing import List +from typing import List, Union from hashlib import sha256 from .common import serialize_str, AddressType, write_varint from .merkle import MerkleTree, element_hash + class WalletType(IntEnum): WALLET_POLICY_V1 = 1 WALLET_POLICY_V2 = 2 @@ -58,34 +59,52 @@ def n_keys(self) -> int: return len(self.keys_info) def serialize(self) -> bytes: - keys_info_hashes = map(lambda k: element_hash(k.encode()), self.keys_info) + keys_info_hashes = map( + lambda k: element_hash(k.encode()), self.keys_info) - descriptor_template_sha256 = sha256(self.descriptor_template.encode()).digest() + descriptor_template_sha256 = sha256( + self.descriptor_template.encode()).digest() return b"".join([ super().serialize(), write_varint(len(self.descriptor_template.encode())), - self.descriptor_template.encode() if self.version == WalletType.WALLET_POLICY_V1 else descriptor_template_sha256, + self.descriptor_template.encode( + ) if self.version == WalletType.WALLET_POLICY_V1 else descriptor_template_sha256, write_varint(len(self.keys_info)), MerkleTree(keys_info_hashes).root ]) - def get_descriptor(self, change: bool) -> str: + def get_descriptor(self, change: Union[bool, None]) -> str: + """ + Generates a descriptor string based on the wallet's descriptor template and keys. + Args: + change (bool | None): Indicates whether the descriptor is for a change address. + - If None, returns the BIP-389 multipath address for both the receive and change address. + - If True, the descriptor is for a change address. + - If False, the descriptor is for a non-change address. + Returns: + str: The generated descriptor. + """ + desc = self.descriptor_template for i in reversed(range(self.n_keys)): key = self.keys_info[i] desc = desc.replace(f"@{i}", key) # in V1, /** is part of the key; in V2, it's part of the policy map. This handles either - desc = desc.replace("/**", f"/{1 if change else 0}/*") + if change is not None: + desc = desc.replace("/**", f"/{1 if change else 0}/*") + else: + desc = desc.replace("/**", f"/<0;1>/*") if self.version == WalletType.WALLET_POLICY_V2: # V2, the / syntax is supported. Replace with M if not change, or with N if change - regex = r"/<(\d+);(\d+)>" - desc = re.sub(regex, "/\\2" if change else "/\\1", desc) + if change is not None: + desc = re.sub(r"/<(\d+);(\d+)>", "/\\2" if change else "/\\1", desc) return desc + class MultisigWallet(WalletPolicy): def __init__(self, name: str, address_type: AddressType, threshold: int, keys_info: List[str], sorted: bool = True, version: WalletType = WalletType.WALLET_POLICY_V2) -> None: n_keys = len(keys_info) From 6c8fb7e867004e06099c9e0411563ca0f8ed3820 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Tue, 5 Nov 2024 17:02:59 +0000 Subject: [PATCH 30/45] Updated e2e tests to use deterministic xprivs in bitcoin-core --- tests/conftest.py | 107 +++++++++++++++++++++++------------ tests/test_e2e_miniscript.py | 38 +++++++++---- tests/test_e2e_multisig.py | 16 +++--- tests/test_e2e_tapscripts.py | 13 +++-- 4 files changed, 114 insertions(+), 60 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index cffd00cf6..23693c417 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,32 +1,32 @@ -import sys -import os -absolute_path = os.path.dirname(os.path.abspath(__file__)) -relative_bitcoin_path = ('../bitcoin_client') -absolute_bitcoin_client_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../') -sys.path.append(os.path.join(absolute_path, relative_bitcoin_path)) +import sys # noqa: E402 +import os # noqa: E402 -import random -from typing import Tuple - -from test_utils.fixtures import * -from test_utils.authproxy import AuthServiceProxy, JSONRPCException -from test_utils import segwit_addr - -import shutil -import subprocess -from time import sleep -from decimal import Decimal -from pathlib import Path +absolute_path = os.path.dirname(os.path.abspath(__file__)) # noqa: E402 +relative_bitcoin_path = ('../bitcoin_client') # noqa: E402 +absolute_bitcoin_client_path = os.path.join( + os.path.dirname(os.path.abspath(__file__)), '../') # noqa: E402 +sys.path.append(os.path.join(absolute_path, relative_bitcoin_path)) # noqa: E402 -from ledger_bitcoin import Chain -from ledger_bitcoin.common import sha256 +from ragger_bitcoin import createRaggerClient, RaggerClient +from ragger.backend import RaisePolicy +from ragger.backend.interface import BackendInterface +from ragger.conftest import configuration import ledger_bitcoin._base58 as base58 +from ledger_bitcoin.common import sha256 +from ledger_bitcoin import Chain +from pathlib import Path +from decimal import Decimal +from time import sleep +import subprocess +import shutil +from test_utils import segwit_addr +from test_utils.authproxy import AuthServiceProxy, JSONRPCException +from test_utils.fixtures import * +from typing import Tuple +import random +from bip32 import BIP32 -from ragger.conftest import configuration -from ragger.backend.interface import BackendInterface -from ragger.backend import RaisePolicy -from ragger_bitcoin import createRaggerClient, RaggerClient ########################### ### CONFIGURATION START ### @@ -105,7 +105,8 @@ def run_bitcoind(): bitcoind = os.getenv("BITCOIND", "/bitcoin/bin/bitcoind") - shutil.copy(os.path.join(os.path.dirname(__file__), "bitcoin.conf"), BITCOIN_DIRNAME) + shutil.copy(os.path.join(os.path.dirname(__file__), + "bitcoin.conf"), BITCOIN_DIRNAME) subprocess.Popen([bitcoind, f"--datadir={BITCOIN_DIRNAME}"]) # Make sure the node is ready, and generate some initial blocks @@ -165,29 +166,63 @@ def get_unique_wallet_name() -> str: return result +def get_pseudorandom_keypair(wallet_name: str) -> Tuple[str, str]: + """ + Generates a tpub and tpriv deterministically from the wallet name + Used in tests to have deterministic wallets in bitcoin-core instances. + """ + + bip32 = BIP32.from_seed(wallet_name.encode(), network="test") + + xpub = bip32.get_xpub_from_path("m") + xpriv = bip32.get_xpriv_from_path("m") + + return xpub, xpriv + + def create_new_wallet() -> Tuple[str, str]: """Creates a new descriptor-enabled wallet in bitcoin-core. Each new wallet has an increasing counter as - part of it's name in order to avoid conflicts. Returns the wallet name and the xpub (dropping the key origin + part of it's name in order to avoid conflicts. Returns the wallet name and the xpub (with no key origin information).""" wallet_name = get_unique_wallet_name() - # TODO: derive seed from wallet_count, and use it to create a descriptor wallet (how?) - # this would help to have repeatable tests, generating always the same seeds - get_rpc().createwallet(wallet_name=wallet_name, descriptors=True) - wallet_rpc = get_wallet_rpc(wallet_name) - - all_descriptors = wallet_rpc.listdescriptors()["descriptors"] - descriptor: str = next(filter(lambda d: d["desc"].startswith( - "pkh") and "/0/*" in d["desc"], all_descriptors))["desc"] - core_xpub_orig = descriptor[descriptor.index("(")+1: descriptor.index("/0/*")] - core_xpub = core_xpub_orig[core_xpub_orig.find("]") + 1:] + core_xpub, _ = get_pseudorandom_keypair(wallet_name) return wallet_name, core_xpub +def recompute_checksum(rpc: AuthServiceProxy, descriptor: str) -> str: + # remove "#" and everything after it, if present + if '#' in descriptor: + descriptor = descriptor[:descriptor.index('#')] + descriptor_info = rpc.getdescriptorinfo(descriptor) + return descriptor + '#' + descriptor_info["checksum"] + + +def import_descriptors_with_privkeys(core_wallet_name: str, receive_desc: str, change_desc: str): + wallet = get_wallet_rpc(core_wallet_name) + wallet_xpub, wallet_xpriv = get_pseudorandom_keypair(core_wallet_name) + + assert wallet_xpub in receive_desc and wallet_xpub in change_desc + + import_desc = [{ + "desc": recompute_checksum(wallet, receive_desc.replace(wallet_xpub, wallet_xpriv)), + "active": True, + "internal": False, + "timestamp": "now" + }, { + "desc": recompute_checksum(wallet, change_desc.replace(wallet_xpub, wallet_xpriv)), + "active": True, + "internal": True, + "timestamp": "now" + }] + import_res = wallet.importdescriptors(import_desc) + assert import_res[0]["success"] and import_res[1]["success"] + + def generate_blocks(n): return get_rpc().generatetoaddress(n, btc_addr) diff --git a/tests/test_e2e_miniscript.py b/tests/test_e2e_miniscript.py index 3470421f4..aa5b34856 100644 --- a/tests/test_e2e_miniscript.py +++ b/tests/test_e2e_miniscript.py @@ -1,6 +1,6 @@ import pytest -from typing import List, Union +from typing import List import hmac from hashlib import sha256 @@ -15,13 +15,13 @@ from ragger_bitcoin import RaggerClient from ragger_bitcoin.ragger_instructions import Instructions -from ragger.navigator import Navigator, NavInsID +from ragger.navigator import Navigator from ragger.firmware import Firmware from ragger.error import ExceptionRAPDU from .instructions import e2e_register_wallet_instruction, e2e_sign_psbt_instruction -from .conftest import create_new_wallet, generate_blocks, get_unique_wallet_name, get_wallet_rpc, testnet_to_regtest_addr as T +from .conftest import create_new_wallet, generate_blocks, get_unique_wallet_name, get_wallet_rpc, import_descriptors_with_privkeys, testnet_to_regtest_addr as T from .conftest import AuthServiceProxy @@ -34,11 +34,13 @@ def run_test_e2e(navigator: Navigator, client: RaggerClient, wallet_policy: Wall assert wallet_id == wallet_policy.id assert hmac.compare_digest( - hmac.new(speculos_globals.wallet_registration_key, wallet_id, sha256).digest(), + hmac.new(speculos_globals.wallet_registration_key, + wallet_id, sha256).digest(), wallet_hmac, ) - address_hww = client.get_wallet_address(wallet_policy, wallet_hmac, 0, 3, False) + address_hww = client.get_wallet_address( + wallet_policy, wallet_hmac, 0, 3, False) # ==> verify the address matches what bitcoin-core computes receive_descriptor = wallet_policy.get_descriptor(change=False) @@ -91,7 +93,8 @@ def run_test_e2e(navigator: Navigator, client: RaggerClient, wallet_policy: Wall out_address: Decimal("0.01") }, options={ - "changePosition": 1 # We need a fixed position to be able to know how to navigate in the flows + # We need a fixed position to be able to know how to navigate in the flows + "changePosition": 1 } ) @@ -106,19 +109,27 @@ def run_test_e2e(navigator: Navigator, client: RaggerClient, wallet_policy: Wall instructions=instructions_sign_psbt, testname=f"{test_name}_sign") - n_internal_keys = count_internal_key_placeholders(speculos_globals.seed, "test", wallet_policy) - assert len(hww_sigs) == n_internal_keys * len(psbt.inputs) # should be true as long as all inputs are internal + n_internal_keys = count_internal_key_placeholders( + speculos_globals.seed, "test", wallet_policy) + # should be true as long as all inputs are internal + assert len(hww_sigs) == n_internal_keys * len(psbt.inputs) for i, part_sig in hww_sigs: psbt.inputs[i].partial_sigs[part_sig.pubkey] = part_sig.signature signed_psbt_hww_b64 = psbt.serialize() + # ==> import descriptor for each bitcoin-core wallet + for core_wallet_name in core_wallet_names: + import_descriptors_with_privkeys( + core_wallet_name, receive_descriptor_chk, change_descriptor_chk) + # ==> sign it with bitcoin-core partial_psbts = [signed_psbt_hww_b64] for core_wallet_name in core_wallet_names: - partial_psbts.append(get_wallet_rpc(core_wallet_name).walletprocesspsbt(psbt_b64)["psbt"]) + partial_psbts.append(get_wallet_rpc( + core_wallet_name).walletprocesspsbt(psbt_b64)["psbt"]) # ==> finalize the psbt, extract tx and broadcast combined_psbt = rpc.combinepsbt(partial_psbts) @@ -404,15 +415,18 @@ def test_invalid_miniscript(navigator: Navigator, firmware: Firmware, client: Ra run_test_invalid(client, "wsh(wsh(pkh(@0/**)))", [internal_xpub_orig]) # sh(wsh(...)) is meaningful with valid miniscript, but current implementation of miniscript assumes wsh(...) - run_test_invalid(client, "sh(wsh(or_d(pk(@0/**),pkh(@1/**))))", [internal_xpub_orig, core_xpub_orig1]) + run_test_invalid(client, "sh(wsh(or_d(pk(@0/**),pkh(@1/**))))", + [internal_xpub_orig, core_xpub_orig1]) # tr must be top-level run_test_invalid(client, "wsh(tr(pk(@0/**)))", [internal_xpub_orig]) run_test_invalid(client, "sh(tr(pk(@0/**)))", [internal_xpub_orig]) # valid miniscript must be inside wsh() - run_test_invalid(client, "or_d(pk(@0/**),pkh(@1/**))", [internal_xpub_orig, core_xpub_orig1]) - run_test_invalid(client, "sh(or_d(pk(@0/**),pkh(@1/**)))", [internal_xpub_orig, core_xpub_orig1]) + run_test_invalid(client, "or_d(pk(@0/**),pkh(@1/**))", + [internal_xpub_orig, core_xpub_orig1]) + run_test_invalid(client, "sh(or_d(pk(@0/**),pkh(@1/**)))", + [internal_xpub_orig, core_xpub_orig1]) # sortedmulti is not valid miniscript, can only be used as a descriptor inside sh or wsh run_test_invalid(client, "wsh(or_d(pk(@0/**),sortedmulti(3,@1/**,@2/**,@3/**,@4/**,@5/**)))", diff --git a/tests/test_e2e_multisig.py b/tests/test_e2e_multisig.py index 094c29e1a..d1738c4f0 100644 --- a/tests/test_e2e_multisig.py +++ b/tests/test_e2e_multisig.py @@ -1,28 +1,25 @@ import pytest -from typing import List, Union +from typing import List import hmac from hashlib import sha256 from decimal import Decimal -from ledger_bitcoin import Client, MultisigWallet, AddressType -from ledger_bitcoin.client_base import TransportClient +from ledger_bitcoin import MultisigWallet, AddressType from ledger_bitcoin.psbt import PSBT from ledger_bitcoin.wallet import WalletPolicy from test_utils import SpeculosGlobals, get_internal_xpub, count_internal_key_placeholders -from speculos.client import SpeculosClient - from ragger_bitcoin import RaggerClient from ragger_bitcoin.ragger_instructions import Instructions -from ragger.navigator import Navigator, NavInsID +from ragger.navigator import Navigator from ragger.firmware import Firmware from .instructions import e2e_register_wallet_instruction, e2e_sign_psbt_instruction -from .conftest import create_new_wallet, generate_blocks, get_unique_wallet_name, get_wallet_rpc, testnet_to_regtest_addr as T +from .conftest import create_new_wallet, generate_blocks, get_unique_wallet_name, get_wallet_rpc, import_descriptors_with_privkeys, testnet_to_regtest_addr as T from .conftest import AuthServiceProxy @@ -122,6 +119,11 @@ def run_test(navigator: Navigator, client: RaggerClient, wallet_policy: WalletPo signed_psbt_hww_b64 = psbt.serialize() + # ==> import descriptor for each bitcoin-core wallet + for core_wallet_name in core_wallet_names: + import_descriptors_with_privkeys( + core_wallet_name, receive_descriptor_chk, change_descriptor_chk) + # ==> sign it with bitcoin-core partial_psbts = [signed_psbt_hww_b64] diff --git a/tests/test_e2e_tapscripts.py b/tests/test_e2e_tapscripts.py index 1b813f2ee..23f27f1eb 100644 --- a/tests/test_e2e_tapscripts.py +++ b/tests/test_e2e_tapscripts.py @@ -1,13 +1,11 @@ import pytest -from typing import List, Union +from typing import List import hmac from hashlib import sha256 from decimal import Decimal -from ledger_bitcoin import Client -from ledger_bitcoin.client_base import TransportClient from ledger_bitcoin.exception.errors import IncorrectDataError, NotSupportedError from ledger_bitcoin.exception.device_exception import DeviceException from ledger_bitcoin.psbt import PSBT @@ -17,12 +15,12 @@ from ragger_bitcoin import RaggerClient from ragger_bitcoin.ragger_instructions import Instructions -from ragger.navigator import Navigator, NavInsID +from ragger.navigator import Navigator from ragger.firmware import Firmware from ragger.error import ExceptionRAPDU from .instructions import e2e_register_wallet_instruction, e2e_sign_psbt_instruction -from .conftest import AuthServiceProxy +from .conftest import AuthServiceProxy, import_descriptors_with_privkeys from .conftest import create_new_wallet, generate_blocks, get_unique_wallet_name, get_wallet_rpc, testnet_to_regtest_addr as T @@ -122,6 +120,11 @@ def run_test_e2e(navigator: Navigator, client: RaggerClient, wallet_policy: Wall n_internal_keys = count_internal_key_placeholders(speculos_globals.seed, "test", wallet_policy) assert len(hww_sigs) == n_internal_keys * len(psbt.inputs) # should be true as long as all inputs are internal + # ==> import descriptor for each bitcoin-core wallet + for core_wallet_name in core_wallet_names: + import_descriptors_with_privkeys( + core_wallet_name, receive_descriptor_chk, change_descriptor_chk) + # ==> sign it with bitcoin-core partial_psbts = [signed_psbt_hww_b64] From 41898234ff76179e6d3a6f19909912ee33967dec Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Wed, 6 Nov 2024 17:07:45 +0000 Subject: [PATCH 31/45] Refactor code of is_policy_sane for clarity; improved comments --- src/handler/lib/policy.c | 104 ++++++++++++++++++++------------------- 1 file changed, 53 insertions(+), 51 deletions(-) diff --git a/src/handler/lib/policy.c b/src/handler/lib/policy.c index a889cc898..7af8280f0 100644 --- a/src/handler/lib/policy.c +++ b/src/handler/lib/policy.c @@ -1910,6 +1910,48 @@ static void sort_uint16_array(uint16_t *array, size_t n) { } } +static bool are_key_placeholders_identical(const policy_node_keyexpr_t *kp1, + const policy_node_keyexpr_t *kp2) { + if (kp1->type != kp2->type) { + return false; + } + if (kp1->type == KEY_EXPRESSION_NORMAL && kp2->type == KEY_EXPRESSION_NORMAL) { + return kp1->k.key_index == kp2->k.key_index; + } else if (kp1->type == KEY_EXPRESSION_MUSIG && kp2->type == KEY_EXPRESSION_MUSIG) { + const musig_aggr_key_info_t *musig_info_i = r_musig_aggr_key_info(&kp1->m.musig_info); + const uint16_t *key_indexes_i = r_uint16(&musig_info_i->key_indexes); + const musig_aggr_key_info_t *musig_info_j = r_musig_aggr_key_info(&kp2->m.musig_info); + const uint16_t *key_indexes_j = r_uint16(&musig_info_j->key_indexes); + + // two musig key expressions have identical placeholders if and only if they have + // exactly the same set of key indexes + + if (musig_info_i->n != musig_info_j->n) { + return false; // cannot be the same set if the size is different + } + + uint16_t key_indexes_i_sorted[MAX_PUBKEYS_PER_MUSIG]; + uint16_t key_indexes_j_sorted[MAX_PUBKEYS_PER_MUSIG]; + memcpy(key_indexes_i_sorted, key_indexes_i, musig_info_i->n * sizeof(uint16_t)); + memcpy(key_indexes_j_sorted, key_indexes_j, musig_info_j->n * sizeof(uint16_t)); + + // sort the arrays + sort_uint16_array(key_indexes_i_sorted, musig_info_i->n); + sort_uint16_array(key_indexes_j_sorted, musig_info_j->n); + + if (memcmp(key_indexes_i_sorted, + key_indexes_j_sorted, + musig_info_i->n * sizeof(uint16_t)) != 0) { + return false; // different set of keys + } + return true; + } else { + LEDGER_ASSERT(false, "Unknown key expression type"); + return false; + } + LEDGER_ASSERT(false, "Unreachable code"); +} + int is_policy_sane(dispatcher_context_t *dispatcher_context, const policy_node_t *policy, int wallet_version, @@ -1997,10 +2039,10 @@ int is_policy_sane(dispatcher_context_t *dispatcher_context, } } - // The following loop is computationally very inefficient (quadratic in the number of - // key expressions), but more efficient solutions likely require a substantial amount of RAM - // (proportional to the number of key expressions). Instead, this only requires stack depth - // proportional to the depth of the wallet policy's abstract syntax tree. + // The following loop is computationally very inefficient, but more efficient solutions likely + // require a substantial amount of RAM and/or more complex code. + // As it's unlikely that the number of keys in a wallet policy will be large enough for this to, + // matther, we rather keep the code as simple as possible. for (int i = 0; i < n_key_expressions - 1; i++) { // no point in running this for the last key expression policy_node_keyexpr_t *kp_i; @@ -2014,12 +2056,13 @@ int is_policy_sane(dispatcher_context_t *dispatcher_context, "Unexpected error retrieving key expressions from the policy"); } - if ((kp_i->type == KEY_EXPRESSION_NORMAL && kp_j->type == KEY_EXPRESSION_MUSIG) || - (kp_i->type == KEY_EXPRESSION_MUSIG && kp_j->type == KEY_EXPRESSION_NORMAL)) { - // if one is a key and the other is a musig, there's nothing else to check - continue; - } else if (kp_i->type == KEY_EXPRESSION_NORMAL && kp_j->type == KEY_EXPRESSION_NORMAL) { - // key expressions for the same key must have disjoint derivation options + // There is nothing to check for two placeholders that are not identical. + // If they are identical, we make sure that the derivations are disjoint, as per + // BIP-388. Note that this means that we do not enforce that _all_ the keys in different + // musig placeholders are disjoint, as long as they are not exactly the same set of + // keys. Similarly, a key used in a normal placeholder could also be part of the set of + // keys in a musig placeholder. + if (are_key_placeholders_identical(kp_i, kp_j)) { if (kp_i->k.key_index == kp_j->k.key_index) { if (kp_i->num_first == kp_j->num_first || kp_i->num_first == kp_j->num_second || kp_i->num_second == kp_j->num_first || @@ -2029,47 +2072,6 @@ int is_policy_sane(dispatcher_context_t *dispatcher_context, "Key expressions with repeated derivations in miniscript"); } } - } else if (kp_i->type == KEY_EXPRESSION_MUSIG && kp_j->type == KEY_EXPRESSION_MUSIG) { - const musig_aggr_key_info_t *musig_info_i = - r_musig_aggr_key_info(&kp_i->m.musig_info); - const uint16_t *key_indexes_i = r_uint16(&musig_info_i->key_indexes); - const musig_aggr_key_info_t *musig_info_j = - r_musig_aggr_key_info(&kp_j->m.musig_info); - const uint16_t *key_indexes_j = r_uint16(&musig_info_j->key_indexes); - // if two musigs have exactly the same set of keys, then the derivation options must - // be disjoint - - // make sure that there is no repeated key in the first musig - - if (musig_info_i->n != musig_info_j->n) { - continue; // cannot be the same set if the size is different - } - - uint16_t key_indexes_i_sorted[MAX_PUBKEYS_PER_MUSIG]; - uint16_t key_indexes_j_sorted[MAX_PUBKEYS_PER_MUSIG]; - memcpy(key_indexes_i_sorted, key_indexes_i, musig_info_i->n * sizeof(uint16_t)); - memcpy(key_indexes_j_sorted, key_indexes_j, musig_info_j->n * sizeof(uint16_t)); - - // sort the arrays - sort_uint16_array(key_indexes_i_sorted, musig_info_i->n); - sort_uint16_array(key_indexes_j_sorted, musig_info_j->n); - - if (memcmp(key_indexes_i_sorted, - key_indexes_j_sorted, - musig_info_i->n * sizeof(uint16_t)) != 0) { - continue; // different set of keys - } - - // same set of keys; therefore, we need to check that the derivation options are - // disjoint - if (kp_i->num_first == kp_j->num_first || kp_i->num_first == kp_j->num_second || - kp_i->num_second == kp_j->num_first || kp_i->num_second == kp_j->num_second) { - return WITH_ERROR(-1, - "Key expressions with repeated derivations in miniscript"); - } - - } else { - LEDGER_ASSERT(false, "Unexpected key expression type"); } } } From 712bfa703d3369b1aae6d063af50dd8a7e36552b Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Tue, 5 Nov 2024 18:29:43 +0000 Subject: [PATCH 32/45] [WIP] Musig2 e2e tests --- tests/test_e2e_musig2.py | 385 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 385 insertions(+) create mode 100644 tests/test_e2e_musig2.py diff --git a/tests/test_e2e_musig2.py b/tests/test_e2e_musig2.py new file mode 100644 index 000000000..1cda4d8e4 --- /dev/null +++ b/tests/test_e2e_musig2.py @@ -0,0 +1,385 @@ +import pytest + +from typing import Dict, List, Tuple + +import hmac +from hashlib import sha256 +from decimal import Decimal + +from ledger_bitcoin.exception.errors import IncorrectDataError, NotSupportedError +from ledger_bitcoin.exception.device_exception import DeviceException +from ledger_bitcoin.psbt import PSBT +from ledger_bitcoin.wallet import WalletPolicy +from ledger_bitcoin import MusigPubNonce, MusigPartialSignature, PartialSignature, SignPsbtYieldedObject + +from test_utils import SpeculosGlobals, bip0327, get_internal_xpub, count_internal_key_placeholders +from test_utils.musig2 import PsbtMusig2Cosigner + +from ragger_bitcoin import RaggerClient +from ragger_bitcoin.ragger_instructions import Instructions +from ragger.navigator import Navigator +from ragger.firmware import Firmware +from ragger.error import ExceptionRAPDU + +from .instructions import e2e_register_wallet_instruction, e2e_sign_psbt_instruction + +from .conftest import AuthServiceProxy, create_new_wallet, generate_blocks, get_unique_wallet_name, get_wallet_rpc, import_descriptors_with_privkeys, testnet_to_regtest_addr as T + + +class BitcoinCoreMusig2Cosigner(PsbtMusig2Cosigner): + """ + Implements a PsbtMusig2Cosigner for a given wallet policy using bitcoin-core. + """ + + def __init__(self, wallet_policy: WalletPolicy, rpc) -> None: + super().__init__() + + self.wallet_policy = wallet_policy + self.rpc = rpc + + self.musig_psbt_sessions: Dict[bytes, bytes] = {} + + def compute_psbt_session_id(self, psbt: PSBT) -> bytes: + psbt.tx.rehash() + return sha256(psbt.tx.hash + self.wallet_policy.id) + + def get_participant_pubkey(self) -> bip0327.Point: + raise NotImplementedError() + + def generate_public_nonces(self, psbt: PSBT) -> None: + raise NotImplementedError() + + def generate_partial_signatures(self, psbt: PSBT) -> None: + raise NotImplementedError() + + +def run_test_e2e_musig2(navigator: Navigator, client: RaggerClient, wallet_policy: WalletPolicy, core_wallet_names: List[str], rpc: AuthServiceProxy, rpc_test_wallet: AuthServiceProxy, speculos_globals: SpeculosGlobals, + instructions_register_wallet: Instructions, + instructions_sign_psbt: Instructions, test_name: str): + # TODO: delete + def printb(*args): + print('\033[94m', end='') + print(*args) + print('\033[0m', end='') + + wallet_id, wallet_hmac = client.register_wallet(wallet_policy, navigator, + instructions=instructions_register_wallet, testname=f"{test_name}_register") + + assert wallet_id == wallet_policy.id + + assert hmac.compare_digest( + hmac.new(speculos_globals.wallet_registration_key, + wallet_id, sha256).digest(), + wallet_hmac, + ) + + # TODO: reenable registration above and delete this + wallet_hmac = hmac.new(speculos_globals.wallet_registration_key, + wallet_policy.id, sha256).digest() + + address_hww = client.get_wallet_address( + wallet_policy, wallet_hmac, 0, 3, False) + + # ==> verify the address matches what bitcoin-core computes + receive_descriptor = wallet_policy.get_descriptor(change=False) + receive_descriptor_info = rpc.getdescriptorinfo(receive_descriptor) + # bitcoin-core adds the checksum, and requires it for other calls + receive_descriptor_chk: str = receive_descriptor_info["descriptor"] + address_core = rpc.deriveaddresses(receive_descriptor_chk, [3, 3])[0] + + assert T(address_hww) == address_core + + # also get the change descriptor for later + change_descriptor = wallet_policy.get_descriptor(change=True) + change_descriptor_info = rpc.getdescriptorinfo(change_descriptor) + change_descriptor_chk: str = change_descriptor_info["descriptor"] + + printb("Receive descriptor:", receive_descriptor_chk) # TODO: remove + printb("Change descriptor:", change_descriptor_chk) # TODO: remove + + # ==> import wallet in bitcoin-core + + new_core_wallet_name = get_unique_wallet_name() + rpc.createwallet( + wallet_name=new_core_wallet_name, + disable_private_keys=True, + descriptors=True, + ) + core_wallet_rpc = get_wallet_rpc(new_core_wallet_name) + + core_wallet_rpc.importdescriptors([{ + "desc": receive_descriptor_chk, + "active": True, + "internal": False, + "timestamp": "now" + }, { + "desc": change_descriptor_chk, + "active": True, + "internal": True, + "timestamp": "now" + }]) + + # ==> fund the wallet and get prevout info + + rpc_test_wallet.sendtoaddress(T(address_hww), "0.1") + generate_blocks(1) + + assert core_wallet_rpc.getwalletinfo()["balance"] == Decimal("0.1") + + # ==> prepare a psbt spending from the wallet + + out_address = rpc_test_wallet.getnewaddress() + + result = core_wallet_rpc.walletcreatefundedpsbt( + outputs={ + out_address: Decimal("0.01") + }, + options={ + # We need a fixed position to be able to know how to navigate in the flows + "changePosition": 1 + } + ) + + # ==> import descriptor for each bitcoin-core wallet + for core_wallet_name in core_wallet_names: + import_descriptors_with_privkeys( + core_wallet_name, receive_descriptor_chk, change_descriptor_chk) + + psbt_b64 = result["psbt"] + + printb("PSBT before the first round:") + printb(psbt_b64) + + # Round 1: get nonces + + # ==> get nonce from the hww + + n_internal_keys = count_internal_key_placeholders( + speculos_globals.seed, "test", wallet_policy) + + psbt = PSBT() + psbt.deserialize(psbt_b64) + + hww_yielded: List[Tuple[int, SignPsbtYieldedObject]] = client.sign_psbt(psbt, wallet_policy, wallet_hmac, navigator, + instructions=instructions_sign_psbt, + testname=f"{test_name}_sign") + + printb("SignPsbt yielded:", hww_yielded) + for (input_index, yielded) in hww_yielded: + if isinstance(yielded, MusigPubNonce): + printb(f"Yielded MusigPubNonce for input {input_index}:") + printb(yielded.participant_pubkey.hex(), yielded.aggregate_pubkey.hex( + ), None if yielded.tapleaf_hash is None else yielded.tapleaf_hash.hex()) + psbt_key = ( + yielded.participant_pubkey, + yielded.aggregate_pubkey, + yielded.tapleaf_hash + ) + + assert len(yielded.aggregate_pubkey) == 33 + + psbt.inputs[input_index].musig2_pub_nonces[psbt_key] = yielded.pubnonce + elif isinstance(yielded, PartialSignature): + # depending on the policy, a PartialSignature might be returned + pass + else: + # We don't expect a MusigPartialSignature here + raise ValueError( + f"sign_psbt yielded an unexpected object for input {input_index}:", yielded) + + # should be true as long as all inputs are internal + assert len(hww_yielded) == n_internal_keys * len(psbt.inputs) + + signed_psbt_hww_b64 = psbt.serialize() + + printb("PSBT after the first round for the hww:", signed_psbt_hww_b64) + + # ==> Process it with bitcoin-core to get the musig pubnonces + partial_psbts = [signed_psbt_hww_b64] + + # partial_psbts = [] + + for core_wallet_name in core_wallet_names: + printb("Processing for:", core_wallet_name) + psbt_res = get_wallet_rpc( + core_wallet_name).walletprocesspsbt(psbt_b64)["psbt"] + printb("PSBT processed by core:") + printb(psbt_res) + partial_psbts.append(psbt_res) + + combined_psbt = rpc.combinepsbt(partial_psbts) + + # Round 2: get Musig Partial Signatures + + printb(wallet_policy.get_descriptor(None)) + + # TODO: should now do the second round + printb("PSBT after the first round:", combined_psbt) + + printb("Starting round 2") + + psbt = PSBT() + psbt.deserialize(combined_psbt) + + hww_yielded: List[Tuple[int, SignPsbtYieldedObject]] = client.sign_psbt(psbt, wallet_policy, wallet_hmac, navigator, + instructions=instructions_sign_psbt, + testname=f"{test_name}_sign") + + printb("SignPsbt yielded:", hww_yielded) + for (input_index, yielded) in hww_yielded: + if isinstance(yielded, MusigPartialSignature): + psbt_key = ( + yielded.participant_pubkey, + yielded.aggregate_pubkey, + yielded.tapleaf_hash + ) + + assert len(yielded.aggregate_pubkey) == 33 + + psbt.inputs[input_index].musig2_partial_sigs[psbt_key] = yielded.partial_signature + elif isinstance(yielded, PartialSignature): + # depending on the policy, a PartialSignature might be returned + pass + else: + # We don't expect a MusigPubNonce here, we should be in the second round + raise ValueError( + f"sign_psbt yielded an unexpected object for input {input_index}:", yielded) + + # should be true as long as all inputs are internal + assert len(hww_yielded) == n_internal_keys * len(psbt.inputs) + + signed_psbt_hww_b64 = psbt.serialize() + + printb("PSBT after the second round for the hww:", signed_psbt_hww_b64) + + # ==> Get Musig partial signatures with each bitcoin-core wallet + + partial_psbts = [signed_psbt_hww_b64] + for core_wallet_name in core_wallet_names: + printb("Processing for:", core_wallet_name) + psbt_res = get_wallet_rpc( + core_wallet_name).walletprocesspsbt(combined_psbt)["psbt"] + printb("PSBT processed by core:") + printb(psbt_res) + partial_psbts.append(psbt_res) + + # ==> finalize the psbt, extract tx and broadcast + combined_psbt = rpc.combinepsbt(partial_psbts) + result = rpc.finalizepsbt(combined_psbt) + + assert result["complete"] == True + rawtx = result["hex"] + + # make sure the transaction is valid by broadcasting it (would fail if rejected) + rpc.sendrawtransaction(rawtx) + + +def run_test_invalid(client: RaggerClient, descriptor_template: str, keys_info: List[str]): + wallet_policy = WalletPolicy( + name="Invalid wallet", + descriptor_template=descriptor_template, + keys_info=keys_info) + + with pytest.raises(ExceptionRAPDU) as e: + client.register_wallet(wallet_policy) + assert DeviceException.exc.get(e.value.status) == IncorrectDataError or DeviceException.exc.get( + e.value.status) == NotSupportedError + + +def test_e2e_musig2_keypath(navigator: Navigator, firmware: Firmware, client: RaggerClient, + test_name: str, rpc, rpc_test_wallet, speculos_globals: SpeculosGlobals): + path = "48'/1'/0'/2'" + internal_xpub = get_internal_xpub(speculos_globals.seed, path) + + core_wallet_name, core_xpub_orig = create_new_wallet() + wallet_policy = WalletPolicy( + name="Musig 2 my ears", + descriptor_template="tr(musig(@0,@1)/**)", + keys_info=[ + f"[{speculos_globals.master_key_fingerprint.hex()}/{path}]{internal_xpub}", + f"{core_xpub_orig}", + ]) + + run_test_e2e_musig2(navigator, client, wallet_policy, [core_wallet_name], rpc, rpc_test_wallet, speculos_globals, + e2e_register_wallet_instruction(firmware, wallet_policy.n_keys), e2e_sign_psbt_instruction(firmware), test_name) + + +def test_e2e_musig2_scriptpath(navigator: Navigator, firmware: Firmware, client: RaggerClient, + test_name: str, rpc, rpc_test_wallet, speculos_globals: SpeculosGlobals): + path = "48'/1'/0'/2'" + internal_xpub = get_internal_xpub(speculos_globals.seed, path) + + core_wallet_name, core_xpub_orig = create_new_wallet() + + # In this policy, the keypath is unspendable + + wallet_policy = WalletPolicy( + name="Musig 2 my ears", + descriptor_template="tr(@0/**,pk(musig(@1,@2)/**))", + keys_info=[ + "tpubD6NzVbkrYhZ4WLczPJWReQycCJdd6YVWXubbVUFnJ5KgU5MDQrD998ZJLSmaB7GVcCnJSDWprxmrGkJ6SvgQC6QAffVpqSvonXmeizXcrkN", + f"[{speculos_globals.master_key_fingerprint.hex()}/{path}]{internal_xpub}", + f"{core_xpub_orig}", + ]) + + run_test_e2e_musig2(navigator, client, wallet_policy, [core_wallet_name], rpc, rpc_test_wallet, speculos_globals, + e2e_register_wallet_instruction(firmware, wallet_policy.n_keys), e2e_sign_psbt_instruction(firmware), test_name) + + +def test_e2e_musig2_3of3keypath_decaying_scriptpath(navigator: Navigator, firmware: Firmware, client: RaggerClient, + test_name: str, rpc, rpc_test_wallet, speculos_globals: SpeculosGlobals): + path = "48'/1'/0'/2'" + internal_xpub = get_internal_xpub(speculos_globals.seed, path) + + core_wallet_name_1, core_xpub_orig_1 = create_new_wallet() + core_wallet_name_2, core_xpub_orig_2 = create_new_wallet() + + wallet_policy = WalletPolicy( + name="3-of-3-to-2-of-3", + descriptor_template="tr(musig(@0,@1,@2)/**,and_v(v:multi_a(2,@0/**,@1/**,@2/**),older(12960)))", + keys_info=[ + f"{core_xpub_orig_1}", + f"[{speculos_globals.master_key_fingerprint.hex()}/{path}]{internal_xpub}", + f"{core_xpub_orig_2}", + ]) + + run_test_e2e_musig2(navigator, client, wallet_policy, [core_wallet_name_1, core_wallet_name_2], rpc, rpc_test_wallet, speculos_globals, + e2e_register_wallet_instruction(firmware, wallet_policy.n_keys), e2e_sign_psbt_instruction(firmware), test_name) + + +def test_e2e_musig_invalid(client: RaggerClient, speculos_globals: SpeculosGlobals): + path = "48'/1'/0'/2'" + text_xpub_1 = "tpubDCwYjpDhUdPGP5rS3wgNg13mTrrjBuG8V9VpWbyptX6TRPbNoZVXsoVUSkCjmQ8jJycjuDKBb9eataSymXakTTaGifxR6kmVsfFehH1ZgJT" + internal_xpub = get_internal_xpub(speculos_globals.seed, path) + internal_xpub_orig = f"[{speculos_globals.master_key_fingerprint.hex()}/{path}]{internal_xpub}" + + # Some of these tests are for script syntax that is not currently supported in wallet policies. + # Still worth adding the tests, as they should stay invalid even if such syntax is supported in the future. + + two_keys = [internal_xpub_orig, text_xpub_1] + + # no musig solo + run_test_invalid(client, "tr(musig(@0)/**))", [internal_xpub_orig]) + + # Invalid per BIP-390 + run_test_invalid(client, "pk(musig(@0,@1)/**)", two_keys) + run_test_invalid(client, "pkh(musig(@0,@1)/**)", two_keys) + run_test_invalid(client, "wpkh(musig(@0,@1)/**)", two_keys) + run_test_invalid(client, "combo(musig(@0,@1)/**)", two_keys) + run_test_invalid(client, "sh(wpkh(musig(@0,@1)/**))", two_keys) + run_test_invalid(client, "sh(wsh(musig(@0,@1)/**))", two_keys) + run_test_invalid(client, "wsh(musig(@0,@1)/**)", two_keys) + run_test_invalid(client, "sh(musig(@0,@1)/**)", two_keys) + run_test_invalid(client, "sh(musig(@0/**,@1/**)/**)", two_keys) + + # nonsensical + run_test_invalid(client, "musig(@0,@1)/**", two_keys) + + # Invalid per BIP-388 + run_test_invalid(client, "tr(musig(@0,@0,@1)/**))", two_keys) + run_test_invalid( # repeated musig() placeholders + client, "tr(musig(@0,@1)/**,pk(musig(@1,@0)/**))", two_keys) + run_test_invalid(client, "tr(musig(@0,@1))", two_keys) + + # supported in BIP-390, not in BIP-388 + run_test_invalid(client, "tr(musig(@0/**,@1/**))", two_keys) From 34f27463471f280b0b8b5eb529ce15bd40f102e6 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Thu, 7 Nov 2024 15:26:32 +0000 Subject: [PATCH 33/45] [CI] Use custom image for bitcoin from achow101's branch with MuSig2 support --- .github/workflows/build_and_functional_tests.yml | 2 +- tests/conftest.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build_and_functional_tests.yml b/.github/workflows/build_and_functional_tests.yml index 7e57e5c65..785c546a5 100644 --- a/.github/workflows/build_and_functional_tests.yml +++ b/.github/workflows/build_and_functional_tests.yml @@ -31,4 +31,4 @@ jobs: uses: LedgerHQ/ledger-app-workflows/.github/workflows/reusable_ragger_tests.yml@v1 with: download_app_binaries_artifact: "compiled_app_binaries" - + container_image: "ghcr.io/ledgerhq/app-bitcoin-new/speculos-bitcoin-musig2:latest" diff --git a/tests/conftest.py b/tests/conftest.py index 23693c417..ab40a51e7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -103,7 +103,7 @@ def run_bitcoind(): # Run bitcoind in a separate folder os.makedirs(BITCOIN_DIRNAME, exist_ok=True) - bitcoind = os.getenv("BITCOIND", "/bitcoin/bin/bitcoind") + bitcoind = os.getenv("BITCOIND", "bitcoind") shutil.copy(os.path.join(os.path.dirname(__file__), "bitcoin.conf"), BITCOIN_DIRNAME) From ce066d7ee9442244d074aacd620ed771933cf233 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Tue, 12 Nov 2024 15:44:58 +0000 Subject: [PATCH 34/45] Reduce maximum supported number of keys in musig to 5 --- src/common/wallet.h | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/common/wallet.h b/src/common/wallet.h index 84db3ecf4..e50584805 100644 --- a/src/common/wallet.h +++ b/src/common/wallet.h @@ -21,7 +21,10 @@ // The maximum number of keys supported in a musig() key expression // It is basically unlimited in theory, but we need to set a practical limit. -#define MAX_PUBKEYS_PER_MUSIG MAX_PUBKEYS_PER_MULTISIG +// The implementation of MuSig2 requires quite a few large arrays (for example, the pubnonces are +// 66 bytes each, and there is one for each cosigner), therefore we keep this quite small. +// Increasing this might require optimizing the memory management in the MuSig2 implementation. +#define MAX_PUBKEYS_PER_MUSIG 5 #define WALLET_POLICY_VERSION_V1 1 // the legacy version of the first release #define WALLET_POLICY_VERSION_V2 2 // the current full version From ac4a48c564e60d33bf036b7df8c84d2dc7f4d71f Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Mon, 18 Nov 2024 14:10:22 +0000 Subject: [PATCH 35/45] Move sign_psbt_cache to global space to reduce stack usage --- src/handler/sign_psbt.c | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/handler/sign_psbt.c b/src/handler/sign_psbt.c index c09637aa4..0686ab998 100644 --- a/src/handler/sign_psbt.c +++ b/src/handler/sign_psbt.c @@ -3317,6 +3317,13 @@ sign_transaction(dispatcher_context_t *dc, return true; } +// We declare this in the global space in order to use less stack space, since BOLOS enforces on +// some device a 8kb stack limit. +// Once this is resolved in BOLOS, we should move this to the function scope to avoid unnecessarily +// reserving RAM that can only be used for the signing flow (which, at time of writing, is the most +// RAM-intensive operation command of the app). +sign_psbt_cache_t G_sign_psbt_cache; + void handler_sign_psbt(dispatcher_context_t *dc, uint8_t protocol_version) { LOG_PROCESSOR(__FILE__, __LINE__, __func__); @@ -3328,8 +3335,8 @@ void handler_sign_psbt(dispatcher_context_t *dc, uint8_t protocol_version) { // read APDU inputs, intialize global state and read global PSBT map if (!init_global_state(dc, &st)) return; - sign_psbt_cache_t cache; - init_sign_psbt_cache(&cache); + sign_psbt_cache_t *cache = &G_sign_psbt_cache; + init_sign_psbt_cache(cache); // bitmap to keep track of which inputs are internal uint8_t internal_inputs[BITVECTOR_REAL_SIZE(MAX_N_INPUTS_CAN_SIGN)]; @@ -3347,14 +3354,14 @@ void handler_sign_psbt(dispatcher_context_t *dc, uint8_t protocol_version) { * - detect internal inputs that should be signed, and if there are external inputs or unusual * sighashes */ - if (!preprocess_inputs(dc, &st, &cache, internal_inputs)) return; + if (!preprocess_inputs(dc, &st, cache, internal_inputs)) return; /** OUTPUTS VERIFICATION FLOW * * For each output, check if it's a change address. * Check if it's an acceptable output. */ - if (!preprocess_outputs(dc, &st, &cache, internal_outputs)) return; + if (!preprocess_outputs(dc, &st, cache, internal_outputs)) return; if (G_swap_state.called_from_swap) { /** SWAP CHECKS @@ -3380,7 +3387,7 @@ void handler_sign_psbt(dispatcher_context_t *dc, uint8_t protocol_version) { * For each internal key expression, and for each internal input, sign using the * appropriate algorithm. */ - int sign_result = sign_transaction(dc, &st, &cache, internal_inputs); + int sign_result = sign_transaction(dc, &st, cache, internal_inputs); if (!G_swap_state.called_from_swap) { ui_post_processing_confirm_transaction(dc, sign_result); From 9645d2577223e1f1856f9195abd55c6756bc9e4b Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Mon, 18 Nov 2024 15:40:23 +0000 Subject: [PATCH 36/45] Add array of all internal key expressions in sign_psbt_state_t --- src/constants.h | 7 +++ src/error_codes.h | 3 + src/handler/sign_psbt.c | 118 +++++++++++++++++++++++----------------- 3 files changed, 78 insertions(+), 50 deletions(-) diff --git a/src/constants.h b/src/constants.h index 4fd326a10..cd4f88aa1 100644 --- a/src/constants.h +++ b/src/constants.h @@ -51,6 +51,13 @@ */ #define MAX_N_OUTPUTS_CAN_SIGN 512 +/** + * Maximum supported number of internal key expressions in a wallet policy. + * A key expression is internal if we can sign for it (either as an individual key, + * or as part of a MuSig key expression). + */ +#define MAX_INTERNAL_KEY_EXPRESSIONS 8 + // SIGHASH flags #define SIGHASH_DEFAULT 0x00000000 #define SIGHASH_ALL 0x00000001 diff --git a/src/error_codes.h b/src/error_codes.h index feb283670..9782a491f 100644 --- a/src/error_codes.h +++ b/src/error_codes.h @@ -56,6 +56,9 @@ // The redeem Script in the PSBT is incorrect. #define EC_SIGN_PSBT_MISMATCHING_REDEEM_SCRIPT 0x000b +// The wallet policy has too many internal keys. +#define EC_SIGN_PSBT_WALLET_POLICY_TOO_MANY_INTERNAL_KEYS 0x000c + /** * Swap */ diff --git a/src/handler/sign_psbt.c b/src/handler/sign_psbt.c index 0686ab998..d90d6ed8e 100644 --- a/src/handler/sign_psbt.c +++ b/src/handler/sign_psbt.c @@ -110,7 +110,8 @@ typedef struct { typedef struct { policy_node_keyexpr_t *key_expression_ptr; - int cur_index; + // index of this key expression in the descriptor template, in parsing order + int index; uint32_t fingerprint; // info about the internal key of this key expression @@ -160,6 +161,9 @@ typedef struct { uint32_t tx_version; uint32_t locktime; + unsigned int n_internal_key_expressions; + keyexpr_info_t internal_key_expressions[MAX_INTERNAL_KEY_EXPRESSIONS]; + unsigned int n_inputs; uint8_t inputs_root[32]; // merkle root of the vector of input maps commitments unsigned int n_outputs; @@ -819,41 +823,6 @@ static bool fill_keyexpr_info_if_internal(dispatcher_context_t *dc, } } -// finds the first key expression that corresponds to an internal key -static bool find_first_internal_keyexpr(dispatcher_context_t *dc, - sign_psbt_state_t *st, - keyexpr_info_t *keyexpr_info) { - keyexpr_info->cur_index = 0; - - // find and parse our registered key info in the wallet - while (true) { - int n_key_expressions = get_keyexpr_by_index(st->wallet_policy_map, - keyexpr_info->cur_index, - NULL, - &keyexpr_info->key_expression_ptr); - if (n_key_expressions < 0) { - SEND_SW(dc, SW_BAD_STATE); // should never happen - return false; - } - - if (keyexpr_info->cur_index >= n_key_expressions) { - // all keys have been processed - break; - } - - if (fill_keyexpr_info_if_internal(dc, st, keyexpr_info)) { - return true; - } - - // Not an internal key, move on - ++keyexpr_info->cur_index; - } - - PRINTF("No internal key found in wallet policy"); - SEND_SW_EC(dc, SW_INCORRECT_DATA, EC_SIGN_PSBT_WALLET_POLICY_HAS_NO_INTERNAL_KEY); - return false; -} - typedef struct { keyexpr_info_t *keyexpr_info; input_info_t *input; @@ -896,6 +865,58 @@ static void input_keys_callback(dispatcher_context_t *dc, } } +static bool fill_internal_key_expressions(dispatcher_context_t *dc, sign_psbt_state_t *st) { + size_t cur_index = 0; + + st->n_internal_key_expressions = 0; + memset(st->internal_key_expressions, 0, sizeof(st->internal_key_expressions)); + + // find and parse our registered key info in the wallet + keyexpr_info_t keyexpr_info; + while (true) { + int n_key_expressions = get_keyexpr_by_index(st->wallet_policy_map, + cur_index, + NULL, + &keyexpr_info.key_expression_ptr); + if (n_key_expressions < 0) { + SEND_SW(dc, SW_BAD_STATE); // should never happen + return false; + } + + if (cur_index >= (size_t) n_key_expressions) { + // all keys have been processed + break; + } + + if (fill_keyexpr_info_if_internal(dc, st, &keyexpr_info)) { + if (st->n_internal_key_expressions >= MAX_INTERNAL_KEY_EXPRESSIONS) { + PRINTF("Too many internal key expressions. The maximum supported is %d\n", + MAX_INTERNAL_KEY_EXPRESSIONS); + SEND_SW_EC(dc, SW_NOT_SUPPORTED, EC_SIGN_PSBT_WALLET_POLICY_TOO_MANY_INTERNAL_KEYS); + return false; + } + + // store this key info, as it's internal + memcpy(&st->internal_key_expressions[st->n_internal_key_expressions], + &keyexpr_info, + sizeof(keyexpr_info_t)); + ++st->n_internal_key_expressions; + keyexpr_info.index = 0; + } + + // Not an internal key, move on + ++cur_index; + } + + if (st->n_internal_key_expressions == 0) { + PRINTF("No internal key found in wallet policy"); + SEND_SW_EC(dc, SW_INCORRECT_DATA, EC_SIGN_PSBT_WALLET_POLICY_HAS_NO_INTERNAL_KEY); + return false; + } + + return true; +} + static bool __attribute__((noinline)) preprocess_inputs(dispatcher_context_t *dc, sign_psbt_state_t *st, @@ -905,17 +926,17 @@ preprocess_inputs(dispatcher_context_t *dc, memset(internal_inputs, 0, BITVECTOR_REAL_SIZE(MAX_N_INPUTS_CAN_SIGN)); - keyexpr_info_t keyexpr_info; - memset(&keyexpr_info, 0, sizeof(keyexpr_info)); - - if (!find_first_internal_keyexpr(dc, st, &keyexpr_info)) return false; + if (!fill_internal_key_expressions(dc, st)) return false; // process each input for (unsigned int cur_input_index = 0; cur_input_index < st->n_inputs; cur_input_index++) { input_info_t input; memset(&input, 0, sizeof(input)); - input_keys_callback_data_t callback_data = {.input = &input, .keyexpr_info = &keyexpr_info}; + input_keys_callback_data_t callback_data = { + .input = &input, + // it doesn't matter which key expression we use here + .keyexpr_info = &st->internal_key_expressions[0]}; int res = call_get_merkleized_map_with_callback( dc, (void *) &callback_data, @@ -1162,11 +1183,6 @@ preprocess_outputs(dispatcher_context_t *dc, LOG_PROCESSOR(__FILE__, __LINE__, __func__); - keyexpr_info_t keyexpr_info; - memset(&keyexpr_info, 0, sizeof(keyexpr_info)); - - if (!find_first_internal_keyexpr(dc, st, &keyexpr_info)) return false; - memset(&st->outputs, 0, sizeof(st->outputs)); // the counter used when showing outputs to the user, which ignores change outputs @@ -1177,8 +1193,10 @@ preprocess_outputs(dispatcher_context_t *dc, output_info_t output; memset(&output, 0, sizeof(output)); - output_keys_callback_data_t callback_data = {.output = &output, - .keyexpr_info = &keyexpr_info}; + output_keys_callback_data_t callback_data = { + .output = &output, + // any internal key expression is good here + .keyexpr_info = &st->internal_key_expressions[0]}; int res = call_get_merkleized_map_with_callback( dc, (void *) &callback_data, @@ -2695,7 +2713,7 @@ sign_sighash_musig_and_yield(dispatcher_context_t *dc, // 5) generate and yield pubnonce uint8_t rand_i_j[32]; - compute_rand_i_j(psbt_session, cur_input_index, keyexpr_info->cur_index, rand_i_j); + compute_rand_i_j(psbt_session, cur_input_index, keyexpr_info->index, rand_i_j); musig_secnonce_t secnonce; musig_pubnonce_t pubnonce; @@ -2772,7 +2790,7 @@ sign_sighash_musig_and_yield(dispatcher_context_t *dc, // recompute secnonce from psbt_session randomness uint8_t rand_i_j[32]; - compute_rand_i_j(psbt_session, cur_input_index, keyexpr_info->cur_index, rand_i_j); + compute_rand_i_j(psbt_session, cur_input_index, keyexpr_info->index, rand_i_j); musig_secnonce_t secnonce; musig_pubnonce_t pubnonce; From 6184d44feca297007e599e50c6f05d81ecfd8063 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Mon, 18 Nov 2024 17:29:34 +0000 Subject: [PATCH 37/45] Refactor input_keys_callback output_keys_callback to match against all the internal key expressions, instead of only the first --- src/handler/sign_psbt.c | 291 ++++++++++++++++++++++------------------ 1 file changed, 164 insertions(+), 127 deletions(-) diff --git a/src/handler/sign_psbt.c b/src/handler/sign_psbt.c index d90d6ed8e..f8001021b 100644 --- a/src/handler/sign_psbt.c +++ b/src/handler/sign_psbt.c @@ -114,6 +114,10 @@ typedef struct { int index; uint32_t fingerprint; + // we only sign for keys expressions for which we find a matching key derivation in the PSBT, + // at least for one of the inputs + bool to_sign; + // info about the internal key of this key expression // used at signing time to derive the correct key uint32_t key_derivation[MAX_BIP32_PATH_STEPS]; @@ -132,8 +136,10 @@ typedef struct { // internal key for musig key expressions serialized_extended_pubkey_t internal_pubkey; - bool is_tapscript; // true if signing with a BIP342 tapleaf script path spend - uint8_t tapleaf_hash[32]; // only used for tapscripts + bool is_tapscript; // true if signing with a BIP342 tapleaf script path spend + // only used for tapscripts + const policy_node_t *tapleaf_ptr; + uint8_t tapleaf_hash[32]; } keyexpr_info_t; // Cache for partial hashes during signing (avoid quadratic hashing for segwit transactions) @@ -398,19 +404,24 @@ static int get_amount_scriptpubkey_from_psbt( NULL); } -// Convenience function to share common logic when processing all the -// PSBT_{IN|OUT}_{TAP}?_BIP32_DERIVATION fields. +typedef struct { + uint32_t fingerprint; + size_t derivation_len; + uint32_t key_origin[MAX_BIP32_PATH_STEPS]; +} derivation_info_t; + +// Convenience function to share common logic when parsing the +// PSBT_{IN|OUT}_{TAP}?_BIP32_DERIVATION fields from inputs or outputs. // Note: This function must return -1 only on errors (causing signing to abort). -// It should return 1 if a derivation that makes sense for this input/output is found. -// It should return 0 otherwise (no match found, but continue the signing flow). +// It returns 1 if a that might match the wallet policy is found. +// It returns 0 otherwise (not a match, but continue the signing flow). static int read_change_and_index_from_psbt_bip32_derivation( dispatcher_context_t *dc, - const keyexpr_info_t *keyexpr_info, - in_out_info_t *in_out, int psbt_key_type, buffer_t *data, const merkleized_map_commitment_t *map_commitment, - int index) { + int index, + derivation_info_t *derivation_info) { uint8_t bip32_derivation_pubkey[33]; bool is_tap = psbt_key_type == PSBT_IN_TAP_BIP32_DERIVATION || @@ -423,7 +434,6 @@ static int read_change_and_index_from_psbt_bip32_derivation( || buffer_can_read(data, 1) // ...but should not be able to read more ) { PRINTF("Unexpected pubkey length\n"); - in_out->unexpected_pubkey_error = true; return -1; } @@ -447,35 +457,34 @@ static int read_change_and_index_from_psbt_bip32_derivation( return 0; } - // if this derivation path matches the internal key expression, - // we use it to detect whether the current input is change or not, - // and store its address index - if (fpt_der[0] == keyexpr_info->fingerprint && - der_len == keyexpr_info->psbt_root_key_derivation_length + 2) { - for (int i = 0; i < keyexpr_info->psbt_root_key_derivation_length; i++) { - if (keyexpr_info->key_derivation[i] != fpt_der[1 + i]) { - return 0; - } - } + derivation_info->fingerprint = fpt_der[0]; + for (int i = 0; i < der_len; i++) { + derivation_info->key_origin[i] = fpt_der[i + 1]; + } + derivation_info->derivation_len = der_len; - uint32_t change_step = fpt_der[1 + der_len - 2]; - uint32_t addr_index = fpt_der[1 + der_len - 1]; + return 1; +} - // check if the 'change' derivation step is indeed coherent with key expression - if (change_step == keyexpr_info->key_expression_ptr->num_first) { - in_out->is_change = false; - in_out->address_index = addr_index; - } else if (change_step == keyexpr_info->key_expression_ptr->num_second) { - in_out->is_change = true; - in_out->address_index = addr_index; - } else { - return 0; +bool is_keyexpr_compatible_with_derivation_info(const keyexpr_info_t *keyexpr_info, + const derivation_info_t *derivation_info) { + if (keyexpr_info->fingerprint != derivation_info->fingerprint) { + return false; + } + if (keyexpr_info->psbt_root_key_derivation_length + 2 != derivation_info->derivation_len) { + return false; + } + for (int i = 0; i < keyexpr_info->psbt_root_key_derivation_length; i++) { + if (keyexpr_info->key_derivation[i] != derivation_info->key_origin[i]) { + return false; } - - in_out->key_expression_found = true; - return 1; } - return 0; + uint32_t change_step = derivation_info->key_origin[derivation_info->derivation_len - 2]; + if (change_step != keyexpr_info->key_expression_ptr->num_first && + change_step != keyexpr_info->key_expression_ptr->num_second) { + return false; + } + return true; } /** @@ -824,7 +833,7 @@ static bool fill_keyexpr_info_if_internal(dispatcher_context_t *dc, } typedef struct { - keyexpr_info_t *keyexpr_info; + sign_psbt_state_t *state; input_info_t *input; } input_keys_callback_data_t; @@ -835,7 +844,7 @@ typedef struct { static void input_keys_callback(dispatcher_context_t *dc, input_keys_callback_data_t *callback_data, const merkleized_map_commitment_t *map_commitment, - int i, + int index, buffer_t *data) { size_t data_len = data->size - data->offset; if (data_len >= 1) { @@ -849,17 +858,39 @@ static void input_keys_callback(dispatcher_context_t *dc, callback_data->input->has_redeemScript = true; } else if (key_type == PSBT_IN_SIGHASH_TYPE) { callback_data->input->has_sighash_type = true; - } else if ((key_type == PSBT_IN_BIP32_DERIVATION || - key_type == PSBT_IN_TAP_BIP32_DERIVATION) && - !callback_data->input->in_out.key_expression_found) { - if (0 > read_change_and_index_from_psbt_bip32_derivation(dc, - callback_data->keyexpr_info, - &callback_data->input->in_out, - key_type, - data, - map_commitment, - i)) { + } else if (key_type == PSBT_IN_BIP32_DERIVATION || + key_type == PSBT_IN_TAP_BIP32_DERIVATION) { + derivation_info_t derivation_info; + int res = read_change_and_index_from_psbt_bip32_derivation(dc, + key_type, + data, + map_commitment, + index, + &derivation_info); + if (res < 0) { + // there was an error; we keep track of it so an error SW is sent later callback_data->input->in_out.unexpected_pubkey_error = true; + } else if (res == 0) { + // nothing to do + } else if (res == 1) { + in_out_info_t *in_out = &callback_data->input->in_out; + for (size_t i = 0; i < callback_data->state->n_internal_key_expressions; i++) { + keyexpr_info_t *key_expr = &callback_data->state->internal_key_expressions[i]; + if (is_keyexpr_compatible_with_derivation_info(key_expr, &derivation_info)) { + key_expr->to_sign = true; + + bool is_change = + key_expr->key_expression_ptr->num_second == + derivation_info.key_origin[derivation_info.derivation_len - 2]; + + in_out->key_expression_found = true; + in_out->is_change = is_change; + in_out->address_index = + derivation_info.key_origin[derivation_info.derivation_len - 1]; + } + } + } else { + LEDGER_ASSERT(false, "Unreachable code"); } } } @@ -873,11 +904,20 @@ static bool fill_internal_key_expressions(dispatcher_context_t *dc, sign_psbt_st // find and parse our registered key info in the wallet keyexpr_info_t keyexpr_info; + memset(&keyexpr_info, 0, sizeof(keyexpr_info_t)); while (true) { + keyexpr_info.index = cur_index; + const policy_node_t *tapleaf_ptr = NULL; int n_key_expressions = get_keyexpr_by_index(st->wallet_policy_map, cur_index, - NULL, + &tapleaf_ptr, &keyexpr_info.key_expression_ptr); + if (tapleaf_ptr != NULL) { + // get_keyexpr_by_index returns the pointer to the tapleaf only if the key being + // spent is indeed in a tapleaf + keyexpr_info.tapleaf_ptr = tapleaf_ptr; + keyexpr_info.is_tapscript = true; + } if (n_key_expressions < 0) { SEND_SW(dc, SW_BAD_STATE); // should never happen return false; @@ -901,7 +941,6 @@ static bool fill_internal_key_expressions(dispatcher_context_t *dc, sign_psbt_st &keyexpr_info, sizeof(keyexpr_info_t)); ++st->n_internal_key_expressions; - keyexpr_info.index = 0; } // Not an internal key, move on @@ -933,10 +972,7 @@ preprocess_inputs(dispatcher_context_t *dc, input_info_t input; memset(&input, 0, sizeof(input)); - input_keys_callback_data_t callback_data = { - .input = &input, - // it doesn't matter which key expression we use here - .keyexpr_info = &st->internal_key_expressions[0]}; + input_keys_callback_data_t callback_data = {.input = &input, .state = st}; int res = call_get_merkleized_map_with_callback( dc, (void *) &callback_data, @@ -1137,7 +1173,7 @@ preprocess_inputs(dispatcher_context_t *dc, } typedef struct { - keyexpr_info_t *keyexpr_info; + sign_psbt_state_t *state; output_info_t *output; } output_keys_callback_data_t; @@ -1148,7 +1184,7 @@ typedef struct { static void output_keys_callback(dispatcher_context_t *dc, output_keys_callback_data_t *callback_data, const merkleized_map_commitment_t *map_commitment, - int i, + int index, buffer_t *data) { size_t data_len = data->size - data->offset; if (data_len >= 1) { @@ -1157,14 +1193,36 @@ static void output_keys_callback(dispatcher_context_t *dc, if ((key_type == PSBT_OUT_BIP32_DERIVATION || key_type == PSBT_OUT_TAP_BIP32_DERIVATION) && !callback_data->output->in_out.key_expression_found) { - if (0 > read_change_and_index_from_psbt_bip32_derivation(dc, - callback_data->keyexpr_info, - &callback_data->output->in_out, - key_type, - data, - map_commitment, - i)) { + derivation_info_t derivation_info; + int res = read_change_and_index_from_psbt_bip32_derivation(dc, + key_type, + data, + map_commitment, + index, + &derivation_info); + if (res < 0) { + // there was an error; we keep track of it so an error SW is sent later callback_data->output->in_out.unexpected_pubkey_error = true; + } else if (res == 1) { + in_out_info_t *in_out = &callback_data->output->in_out; + for (size_t i = 0; i < callback_data->state->n_internal_key_expressions; i++) { + const keyexpr_info_t *key_expr = + &callback_data->state->internal_key_expressions[i]; + if (is_keyexpr_compatible_with_derivation_info(key_expr, &derivation_info)) { + bool is_change = + key_expr->key_expression_ptr->num_second == + derivation_info.key_origin[derivation_info.derivation_len - 2]; + + in_out->key_expression_found = true; + in_out->is_change = is_change; + in_out->address_index = + derivation_info.key_origin[derivation_info.derivation_len - 1]; + // unlike for inputs, where we want to keep track of all the key expressions + // we want to sign for, here we only care about finding the relevant info + // for this output. Therefore, we're done as soon as we have a match. + break; + } + } } } } @@ -1193,10 +1251,7 @@ preprocess_outputs(dispatcher_context_t *dc, output_info_t output; memset(&output, 0, sizeof(output)); - output_keys_callback_data_t callback_data = { - .output = &output, - // any internal key expression is good here - .keyexpr_info = &st->internal_key_expressions[0]}; + output_keys_callback_data_t callback_data = {.output = &output, .state = st}; int res = call_get_merkleized_map_with_callback( dc, (void *) &callback_data, @@ -3255,74 +3310,56 @@ sign_transaction(dispatcher_context_t *dc, } // Iterate over all the key expressions that correspond to keys owned by us - while (true) { - keyexpr_info_t keyexpr_info; - memset(&keyexpr_info, 0, sizeof(keyexpr_info)); - - const policy_node_t *tapleaf_ptr = NULL; - int n_key_expressions = get_keyexpr_by_index(st->wallet_policy_map, - key_expression_index, - &tapleaf_ptr, - &keyexpr_info.key_expression_ptr); - - if (n_key_expressions < 0) { - SEND_SW(dc, SW_BAD_STATE); // should never happen - return false; - } - - if (key_expression_index >= n_key_expressions) { - // all key expressions were processed - break; + for (size_t i_keyexpr = 0; i_keyexpr < st->n_internal_key_expressions; i_keyexpr++) { + keyexpr_info_t *keyexpr_info = &st->internal_key_expressions[i_keyexpr]; + if (!keyexpr_info->to_sign) { + continue; } - if (tapleaf_ptr != NULL) { - // get_keyexpr_by_index returns the pointer to the tapleaf only if the key being - // spent is indeed in a tapleaf - keyexpr_info.is_tapscript = true; + if (!fill_keyexpr_info_if_internal(dc, st, keyexpr_info) == true) { + continue; } - if (fill_keyexpr_info_if_internal(dc, st, &keyexpr_info) == true) { - for (unsigned int i = 0; i < st->n_inputs; i++) - if (bitvector_get(internal_inputs, i)) { - input_info_t input; - memset(&input, 0, sizeof(input)); - - input_keys_callback_data_t callback_data = {.input = &input, - .keyexpr_info = &keyexpr_info}; - int res = call_get_merkleized_map_with_callback( - dc, - (void *) &callback_data, - st->inputs_root, - st->n_inputs, - i, - (merkle_tree_elements_callback_t) input_keys_callback, - &input.in_out.map); - if (res < 0) { - SEND_SW(dc, SW_INCORRECT_DATA); - return false; - } + for (unsigned int i = 0; i < st->n_inputs; i++) { + if (bitvector_get(internal_inputs, i)) { + input_info_t input; + memset(&input, 0, sizeof(input)); - if (tapleaf_ptr != NULL && !fill_taproot_keyexpr_info(dc, - st, - &input, - tapleaf_ptr, - &keyexpr_info, - sign_psbt_cache)) { - return false; - } + input_keys_callback_data_t callback_data = {.input = &input, .state = st}; + int res = call_get_merkleized_map_with_callback( + dc, + (void *) &callback_data, + st->inputs_root, + st->n_inputs, + i, + (merkle_tree_elements_callback_t) input_keys_callback, + &input.in_out.map); + if (res < 0) { + SEND_SW(dc, SW_INCORRECT_DATA); + return false; + } + if (keyexpr_info->tapleaf_ptr != NULL && + !fill_taproot_keyexpr_info(dc, + st, + &input, + keyexpr_info->tapleaf_ptr, + keyexpr_info, + sign_psbt_cache)) { + return false; + } - if (!sign_transaction_input(dc, - st, - sign_psbt_cache, - &signing_state, - &keyexpr_info, - &input, - i)) { - // we do not send a status word, since sign_transaction_input - // already does it on failure - return false; - } + if (!sign_transaction_input(dc, + st, + sign_psbt_cache, + &signing_state, + keyexpr_info, + &input, + i)) { + // we do not send a status word, since sign_transaction_input + // already does it on failure + return false; } + } } ++key_expression_index; From c5a3b9240511da4fe9e71440e61d18a55d5f5f8d Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Tue, 19 Nov 2024 14:33:41 +0000 Subject: [PATCH 38/45] Added test for incomplete matching of BIP32 derivation paths in policies with multiple internal keys --- tests/test_sign_psbt.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/test_sign_psbt.py b/tests/test_sign_psbt.py index 1a9296e1a..2336155e5 100644 --- a/tests/test_sign_psbt.py +++ b/tests/test_sign_psbt.py @@ -964,3 +964,26 @@ def test_sign_psbt_against_wrong_tapleaf_hash(navigator: Navigator, firmware: Fi "9e646fb9e0452460944e49f8869f9ab9da90820da5d53841af9e1271a3bcf9ce") assert part_sig_2.tapleaf_hash == bytes.fromhex( "5b82996fe6cf1bf43868511181c64c965b7bf5f16f007acec52dede5288f9225") + + +def test_sign_psbt_multiple_derivation_paths(navigator: Navigator, firmware: Firmware, client: RaggerClient, test_name: str): + # A previous implementation of the app incompletely checked the derivation paths of keys in certain + # transactions when multiple internal in the policy; that wasn't detected in other tests, so this + # was added in order to avoid regressions. + wallet = WalletPolicy( + name="Cold storage", + descriptor_template="wsh(or_d(multi(4,@0/<0;1>/*,@1/<0;1>/*,@2/<0;1>/*,@3/<0;1>/*),and_v(v:thresh(3,pkh(@0/<2;3>/*),a:pkh(@1/<2;3>/*),a:pkh(@2/<2;3>/*),a:pkh(@3/<2;3>/*)),older(65535))))", + keys_info=["[f5acc2fd/48'/1'/0'/2']tpubDFAqEGNyad35aBCKUAXbQGDjdVhNueno5ZZVEn3sQbW5ci457gLR7HyTmHBg93oourBssgUxuWz1jX5uhc1qaqFo9VsybY1J5FuedLfm4dK", 'tpubDE7NQymr4AFtewpAsWtnreyq9ghkzQBXpCZjWLFVRAvnbf7vya2eMTvT2fPapNqL8SuVvLQdbUbMfWLVDCZKnsEBqp6UK93QEzL8Ck23AwF', + 'tpubDF4kujkh5dAhC1pFgBToZybXdvJFXXGX4BWdDxWqP7EUpG8gxkfMQeDjGPDnTr9e4NrkFmDM1ocav3Jz6x79CRZbxGr9dzFokJLuvDDnyRh', 'tpubDD3ULTdBbyuMMMs8BCsJKgZgEnZjjbsbtV6ig3xtkQnaSc1gu9kNhmDDEW49HoLzDNA4y2TMqRzj4BugrrtcpXkjoHSoMVhJwfZLUFmv6yn'] + ) + wallet_hmac = bytes.fromhex( + "8a0e67be3697449e4d1b19d6aaec634ce747cbcf35287887588028c9da250ab3") + + psbt_b64 = "cHNidP8BAIkBAAAAAVrwzTKgg6tMc9v7Q/I8V4WAgNcjaR/75ec1yAnDtAtKCQAAAAAAAAAAAogTAAAAAAAAIlEgs/VEmdPtA5hQyskAYxHdgZk6wHPbDqNn99T+SToVXkKHEwAAAAAAACIAIIOSU1QNZGmYffGgJdIDQ9Ba/o7Zw2XAYL8wxvqmYq1tAAAAAAABAP2qAgIAAAACi2Zf4OfqcC9dP65eJYTdm2lEN3xrnoEYNkv/hkQqOWYTAAAAUH9xQ+dl/v00udlaANFBQ8e8ZWi3c/8Z0+0VpGehUw6m+yXOnVtzCPM7aeSUm5QDs4ouBwzvGEwrHIOfJSApchGgqu0M+c6UDXq2s6RX1mHKAAAAABoOiW2ZTQbNg34JFFvnTHKomMgn83CJhxG7mIJ3naqVCAAAAFDB+Dkn1WRZaoy+4uHRa+OvMG/0njULECR32KQwLveX/e8envK98kFzGeZ7f3QRkTjFrNWwSMTpQdRQdhO/7Og6qIRCmBJklYV5Keo6+aRcnAAAAAAKvZcHBAAAAAAiACBUAxjw2HG6OrfLFbYssfGGedd7uQ+zRhDpUy9lVZgmv1RO9wEAAAAAIgAgROs//J4l9zteFJQLgPfThvlQ/EaW7zamDjUa3Igq+Hb+tocCAAAAACIAIJikAWfDfFJz8dDGRvcZ5wT3y1Rxzho0Od3mllEPlYHlg7sgAwAAAAAiACBKVGjcCkkC2NxgguZGk9rzzqAG8KBY5MzTFfm+vVslpmLu8gEAAAAAIgAgr00MjwnaUMATFIQXZuu42pFvDEw0gMQKjkCRRCCnwi/1HSQAAAAAACIAIGYb/o9UFORFY2ROJKcziKQglXIsJdPWagIspZ3IiT1UOzm1AAAAAAAiACDh0X20Ps51dozZHB3Fs5kY/UwQzayX3D5uW75jT0I0SiF1yAQAAAAAIgAgk2tug44aCowkvN3eHI++I/v09t1lg07puohUJaitMnN16CEDAAAAACIAIKbGDEP0Qq+vkN6BPg7+h5h35z69yxPiTLW6dDx0BGuNECcAAAAAAAAiACAF42YWI29NGW9kDAYPsBXblMbaRLXPydreRe16JcPvfAAAAAABASsQJwAAAAAAACIAIAXjZhYjb00Zb2QMBg+wFduUxtpEtc/J2t5F7Xolw+98AQX9AgFUIQMZ97fwu0jrNC0PAYtW3F2DKuKwotSdPQhAI5aJjIkX3iECgXFEyxMHM5/kW0j5cAhcvppwm0iVNC0Fe3lvaRephgghA7XkdUGcyWun5uDUQByg2S2bqORWXDxuK2KKYQ+PIGdmIQPlrYVplvzvvMn4/1grtQ6JaDh+heyYF/mFMSiAnIkpXFSuc2R2qRSj/+wHoZz/UbEtXd4ziK5a50dPZ4isa3apFP7rXJfetE6jrh2H1/pnvTTS4pioiKxsk2t2qRSBEa8aKbmTOe0oiDjtmteZdh0Hc4isbJNrdqkUZxd8DR1rcAF9hUGikKJCV3yzJ3uIrGyTU4gD//8AsmgiBgMHoiONlif9tR7i5AaLjW2skP3hhmCjInLZCdyGslZGLxz1rML9MAAAgAEAAIAAAACAAgAAgAMAAAAjHAAAIgYDGfe38LtI6zQtDwGLVtxdgyrisKLUnT0IQCOWiYyJF94c9azC/TAAAIABAACAAAAAgAIAAIABAAAAIxwAAAAAAQH9AgFUIQMnUfMLFKU8CycQ/P/sETMZCn9wNbEesbMjJ+irdAJ6UiEDXbLtNSdbxJcL/1BHSWYgzkA5Kinbr72+LimjkF/OsOchAoX2huZIot+kK9BtmV0RiBtHwfnzVL1x7mCa4rnZMd0yIQJ1muTjPOn7M/bYI4dks3IwvMZrYU425ZvyAh6eijv6s1Suc2R2qRTCnxOxFN6CD/IfE+1XHCgYhDq03oisa3apFNcA73/Xw7BQhuriZLhj0mhNcRy5iKxsk2t2qRSsaw8/5TNVxKr+CdTk/HOCByPjMIisbJNrdqkUcvQ/cBCs1WYpeF3pqAauVo+5lUyIrGyTU4gD//8AsmgiAgLc23+KOzv1nhLHL/chcb9HPs+LFIwEixuyLe6M7RAtJhz1rML9MAAAgAEAAIAAAACAAgAAgAMAAAA2IAAAIgIDJ1HzCxSlPAsnEPz/7BEzGQp/cDWxHrGzIyfoq3QCelIc9azC/TAAAIABAACAAAAAgAIAAIABAAAANiAAAAA=" + + result = client.sign_psbt(psbt_b64, wallet, wallet_hmac, navigator, + instructions=sign_psbt_instruction_approve( + firmware, has_spend_from_wallet=True, save_screenshot=False), + testname=test_name) + + assert len(result) == 2 From 1d25a27440017b93420e37eb611d89c71a2406d5 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Tue, 19 Nov 2024 15:08:45 +0000 Subject: [PATCH 39/45] Test that only paths for which a key is present are indeed signed --- tests/test_sign_psbt.py | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/tests/test_sign_psbt.py b/tests/test_sign_psbt.py index 2336155e5..f37237ff6 100644 --- a/tests/test_sign_psbt.py +++ b/tests/test_sign_psbt.py @@ -987,3 +987,44 @@ def test_sign_psbt_multiple_derivation_paths(navigator: Navigator, firmware: Fir testname=test_name) assert len(result) == 2 + + +def test_sign_psbt_multiple_derivation_paths(navigator: Navigator, firmware: Firmware, client: RaggerClient, test_name: str): + # A previous implementation of the app incompletely checked the derivation paths of keys in certain + # transactions when multiple internal in the policy; that wasn't detected in other tests, so this + # was added in order to avoid regressions. + wallet = WalletPolicy( + name="Cold storage", + descriptor_template="wsh(or_d(multi(4,@0/<0;1>/*,@1/<0;1>/*,@2/<0;1>/*,@3/<0;1>/*),and_v(v:thresh(3,pkh(@0/<2;3>/*),a:pkh(@1/<2;3>/*),a:pkh(@2/<2;3>/*),a:pkh(@3/<2;3>/*)),older(65535))))", + keys_info=["[f5acc2fd/48'/1'/0'/2']tpubDFAqEGNyad35aBCKUAXbQGDjdVhNueno5ZZVEn3sQbW5ci457gLR7HyTmHBg93oourBssgUxuWz1jX5uhc1qaqFo9VsybY1J5FuedLfm4dK", 'tpubDE7NQymr4AFtewpAsWtnreyq9ghkzQBXpCZjWLFVRAvnbf7vya2eMTvT2fPapNqL8SuVvLQdbUbMfWLVDCZKnsEBqp6UK93QEzL8Ck23AwF', + 'tpubDF4kujkh5dAhC1pFgBToZybXdvJFXXGX4BWdDxWqP7EUpG8gxkfMQeDjGPDnTr9e4NrkFmDM1ocav3Jz6x79CRZbxGr9dzFokJLuvDDnyRh', 'tpubDD3ULTdBbyuMMMs8BCsJKgZgEnZjjbsbtV6ig3xtkQnaSc1gu9kNhmDDEW49HoLzDNA4y2TMqRzj4BugrrtcpXkjoHSoMVhJwfZLUFmv6yn'] + ) + wallet_hmac = bytes.fromhex( + "8a0e67be3697449e4d1b19d6aaec634ce747cbcf35287887588028c9da250ab3") + + psbt_b64 = "cHNidP8BAIkBAAAAAVrwzTKgg6tMc9v7Q/I8V4WAgNcjaR/75ec1yAnDtAtKCQAAAAAAAAAAAogTAAAAAAAAIlEgs/VEmdPtA5hQyskAYxHdgZk6wHPbDqNn99T+SToVXkKHEwAAAAAAACIAIIOSU1QNZGmYffGgJdIDQ9Ba/o7Zw2XAYL8wxvqmYq1tAAAAAAABAP2qAgIAAAACi2Zf4OfqcC9dP65eJYTdm2lEN3xrnoEYNkv/hkQqOWYTAAAAUH9xQ+dl/v00udlaANFBQ8e8ZWi3c/8Z0+0VpGehUw6m+yXOnVtzCPM7aeSUm5QDs4ouBwzvGEwrHIOfJSApchGgqu0M+c6UDXq2s6RX1mHKAAAAABoOiW2ZTQbNg34JFFvnTHKomMgn83CJhxG7mIJ3naqVCAAAAFDB+Dkn1WRZaoy+4uHRa+OvMG/0njULECR32KQwLveX/e8envK98kFzGeZ7f3QRkTjFrNWwSMTpQdRQdhO/7Og6qIRCmBJklYV5Keo6+aRcnAAAAAAKvZcHBAAAAAAiACBUAxjw2HG6OrfLFbYssfGGedd7uQ+zRhDpUy9lVZgmv1RO9wEAAAAAIgAgROs//J4l9zteFJQLgPfThvlQ/EaW7zamDjUa3Igq+Hb+tocCAAAAACIAIJikAWfDfFJz8dDGRvcZ5wT3y1Rxzho0Od3mllEPlYHlg7sgAwAAAAAiACBKVGjcCkkC2NxgguZGk9rzzqAG8KBY5MzTFfm+vVslpmLu8gEAAAAAIgAgr00MjwnaUMATFIQXZuu42pFvDEw0gMQKjkCRRCCnwi/1HSQAAAAAACIAIGYb/o9UFORFY2ROJKcziKQglXIsJdPWagIspZ3IiT1UOzm1AAAAAAAiACDh0X20Ps51dozZHB3Fs5kY/UwQzayX3D5uW75jT0I0SiF1yAQAAAAAIgAgk2tug44aCowkvN3eHI++I/v09t1lg07puohUJaitMnN16CEDAAAAACIAIKbGDEP0Qq+vkN6BPg7+h5h35z69yxPiTLW6dDx0BGuNECcAAAAAAAAiACAF42YWI29NGW9kDAYPsBXblMbaRLXPydreRe16JcPvfAAAAAABASsQJwAAAAAAACIAIAXjZhYjb00Zb2QMBg+wFduUxtpEtc/J2t5F7Xolw+98AQX9AgFUIQMZ97fwu0jrNC0PAYtW3F2DKuKwotSdPQhAI5aJjIkX3iECgXFEyxMHM5/kW0j5cAhcvppwm0iVNC0Fe3lvaRephgghA7XkdUGcyWun5uDUQByg2S2bqORWXDxuK2KKYQ+PIGdmIQPlrYVplvzvvMn4/1grtQ6JaDh+heyYF/mFMSiAnIkpXFSuc2R2qRSj/+wHoZz/UbEtXd4ziK5a50dPZ4isa3apFP7rXJfetE6jrh2H1/pnvTTS4pioiKxsk2t2qRSBEa8aKbmTOe0oiDjtmteZdh0Hc4isbJNrdqkUZxd8DR1rcAF9hUGikKJCV3yzJ3uIrGyTU4gD//8AsmgiBgMHoiONlif9tR7i5AaLjW2skP3hhmCjInLZCdyGslZGLxz1rML9MAAAgAEAAIAAAACAAgAAgAMAAAAjHAAAIgYDGfe38LtI6zQtDwGLVtxdgyrisKLUnT0IQCOWiYyJF94c9azC/TAAAIABAACAAAAAgAIAAIABAAAAIxwAAAAAAQH9AgFUIQMnUfMLFKU8CycQ/P/sETMZCn9wNbEesbMjJ+irdAJ6UiEDXbLtNSdbxJcL/1BHSWYgzkA5Kinbr72+LimjkF/OsOchAoX2huZIot+kK9BtmV0RiBtHwfnzVL1x7mCa4rnZMd0yIQJ1muTjPOn7M/bYI4dks3IwvMZrYU425ZvyAh6eijv6s1Suc2R2qRTCnxOxFN6CD/IfE+1XHCgYhDq03oisa3apFNcA73/Xw7BQhuriZLhj0mhNcRy5iKxsk2t2qRSsaw8/5TNVxKr+CdTk/HOCByPjMIisbJNrdqkUcvQ/cBCs1WYpeF3pqAauVo+5lUyIrGyTU4gD//8AsmgiAgLc23+KOzv1nhLHL/chcb9HPs+LFIwEixuyLe6M7RAtJhz1rML9MAAAgAEAAIAAAACAAgAAgAMAAAA2IAAAIgIDJ1HzCxSlPAsnEPz/7BEzGQp/cDWxHrGzIyfoq3QCelIc9azC/TAAAIABAACAAAAAgAIAAIABAAAANiAAAAA=" + psbt = PSBT() + psbt.deserialize(psbt_b64) + + assert len(psbt.inputs) == 1 + + result = client.sign_psbt(psbt, wallet, wallet_hmac, navigator, + instructions=sign_psbt_instruction_approve( + firmware, has_spend_from_wallet=True, save_screenshot=False), + testname=test_name) + + assert len(result) == 2 + + # Removing all the PSBT_IN_BIP32_DERIVATION fields for that don't end in /<0;1>/*, the app should + # no longer sign for those keys (therefore we only expect one signature) + for input in psbt.inputs: + for pk, key_orig in list(input.hd_keypaths.items()): + if key_orig.path[-2] not in [0, 1]: + del input.hd_keypaths[pk] + + result = client.sign_psbt(psbt, wallet, wallet_hmac, navigator, + instructions=sign_psbt_instruction_approve( + firmware, has_spend_from_wallet=True, save_screenshot=False), + testname=test_name) + + assert len(result) == 1 From 6d858b4bc7542e7915f52d989ccdb84c9c87fcff Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Tue, 19 Nov 2024 16:40:45 +0000 Subject: [PATCH 40/45] Fixup: musig e2e tests --- tests/test_e2e_musig2.py | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/tests/test_e2e_musig2.py b/tests/test_e2e_musig2.py index 1cda4d8e4..8e402e9c2 100644 --- a/tests/test_e2e_musig2.py +++ b/tests/test_e2e_musig2.py @@ -26,33 +26,6 @@ from .conftest import AuthServiceProxy, create_new_wallet, generate_blocks, get_unique_wallet_name, get_wallet_rpc, import_descriptors_with_privkeys, testnet_to_regtest_addr as T -class BitcoinCoreMusig2Cosigner(PsbtMusig2Cosigner): - """ - Implements a PsbtMusig2Cosigner for a given wallet policy using bitcoin-core. - """ - - def __init__(self, wallet_policy: WalletPolicy, rpc) -> None: - super().__init__() - - self.wallet_policy = wallet_policy - self.rpc = rpc - - self.musig_psbt_sessions: Dict[bytes, bytes] = {} - - def compute_psbt_session_id(self, psbt: PSBT) -> bytes: - psbt.tx.rehash() - return sha256(psbt.tx.hash + self.wallet_policy.id) - - def get_participant_pubkey(self) -> bip0327.Point: - raise NotImplementedError() - - def generate_public_nonces(self, psbt: PSBT) -> None: - raise NotImplementedError() - - def generate_partial_signatures(self, psbt: PSBT) -> None: - raise NotImplementedError() - - def run_test_e2e_musig2(navigator: Navigator, client: RaggerClient, wallet_policy: WalletPolicy, core_wallet_names: List[str], rpc: AuthServiceProxy, rpc_test_wallet: AuthServiceProxy, speculos_globals: SpeculosGlobals, instructions_register_wallet: Instructions, instructions_sign_psbt: Instructions, test_name: str): @@ -73,10 +46,6 @@ def printb(*args): wallet_hmac, ) - # TODO: reenable registration above and delete this - wallet_hmac = hmac.new(speculos_globals.wallet_registration_key, - wallet_policy.id, sha256).digest() - address_hww = client.get_wallet_address( wallet_policy, wallet_hmac, 0, 3, False) From 3f556d8f52cb4c4e41421e6a7992ec220e046201 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Wed, 20 Nov 2024 11:17:17 +0000 Subject: [PATCH 41/45] Detect if the PSBT has at least a PSBT_IN_MUSIG2_PUB_NONCE field It will be needed in order to enable silently participating to Round 1 of the MuSig2 protocol, which should only happen if no such fields are found (which would indicate that Round 1 was already executed). --- src/handler/sign_psbt.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/handler/sign_psbt.c b/src/handler/sign_psbt.c index f8001021b..664edbb1d 100644 --- a/src/handler/sign_psbt.c +++ b/src/handler/sign_psbt.c @@ -182,6 +182,9 @@ typedef struct { unsigned int n_external_inputs; unsigned int n_external_outputs; + // set to true if at least a PSBT_IN_MUSIG2_PUB_NONCES field is present in the PSBT + bool has_musig2_pub_nonces; + // aggregate info on outputs struct { uint64_t total_amount; // amount of all the outputs (external + change) @@ -892,6 +895,8 @@ static void input_keys_callback(dispatcher_context_t *dc, } else { LEDGER_ASSERT(false, "Unreachable code"); } + } else if (key_type == PSBT_IN_MUSIG2_PUB_NONCE) { + callback_data->state->has_musig2_pub_nonces = true; } } } From b6ce98c3d5735727dac99b4c0903254138a92135 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Thu, 21 Nov 2024 10:46:44 +0000 Subject: [PATCH 42/45] Moved MuSig2 Round 1 out of the signing flow. Allow it without user confirmation. The first round of MuSig does not involve access to the private keys and does not complete authorizing a transaction. Therefore, it is safe to do it without user confirmation, which allows software wallets to possibly do it in background. In cases when the other cosigners are online, this allows to get a single-sig user experience, as the user would only have to plug the device once, and confirm a single action. --- src/handler/sign_psbt.c | 753 ++++++++++++++++++++++++--------------- test_utils/__init__.py | 15 +- tests/test_e2e_musig2.py | 45 ++- 3 files changed, 507 insertions(+), 306 deletions(-) diff --git a/src/handler/sign_psbt.c b/src/handler/sign_psbt.c index 664edbb1d..d5b5f2b5e 100644 --- a/src/handler/sign_psbt.c +++ b/src/handler/sign_psbt.c @@ -182,7 +182,7 @@ typedef struct { unsigned int n_external_inputs; unsigned int n_external_outputs; - // set to true if at least a PSBT_IN_MUSIG2_PUB_NONCES field is present in the PSBT + // set to true if at least a PSBT_IN_MUSIG2_PUB_NONCE field is present in the PSBT bool has_musig2_pub_nonces; // aggregate info on outputs @@ -948,7 +948,6 @@ static bool fill_internal_key_expressions(dispatcher_context_t *dc, sign_psbt_st ++st->n_internal_key_expressions; } - // Not an internal key, move on ++cur_index; } @@ -2540,50 +2539,22 @@ static bool __attribute__((noinline)) yield_musig_data(dispatcher_context_t *dc, return true; } -static bool yield_musig_pubnonce(dispatcher_context_t *dc, - sign_psbt_state_t *st, - unsigned int cur_input_index, - const musig_pubnonce_t *pubnonce, - const uint8_t participant_pk[static 33], - const uint8_t aggregate_pubkey[static 33], - const uint8_t *tapleaf_hash) { - return yield_musig_data(dc, - st, - cur_input_index, - (const uint8_t *) pubnonce, - sizeof(musig_pubnonce_t), - CCMD_YIELD_MUSIG_PUBNONCE_TAG, - participant_pk, - aggregate_pubkey, - tapleaf_hash); -} - -static bool yield_musig_partial_signature(dispatcher_context_t *dc, - sign_psbt_state_t *st, - unsigned int cur_input_index, - const uint8_t psig[static 32], - const uint8_t participant_pk[static 33], - const uint8_t aggregate_pubkey[static 33], - const uint8_t *tapleaf_hash) { - return yield_musig_data(dc, - st, - cur_input_index, - psig, - 32, - CCMD_YIELD_MUSIG_PARTIALSIGNATURE_TAG, - participant_pk, - aggregate_pubkey, - tapleaf_hash); -} - -static bool __attribute__((noinline)) -sign_sighash_musig_and_yield(dispatcher_context_t *dc, - sign_psbt_state_t *st, - signing_state_t *signing_state, - const keyexpr_info_t *keyexpr_info, - const input_info_t *input, - unsigned int cur_input_index, - uint8_t sighash[static 32]) { +// Struct to hold the info computed for a given input in either of the two rounds +typedef struct { + plain_pk_t keys[MAX_PUBKEYS_PER_MUSIG]; + serialized_extended_pubkey_t agg_key_tweaked; + uint8_t psbt_session_id[32]; + uint8_t tweaks[3][32]; // 2 or three tweaks + size_t n_tweaks; + bool is_xonly[3]; // 2 or 3 elements +} musig_per_input_info_t; + +static bool compute_musig_per_input_info(dispatcher_context_t *dc, + sign_psbt_state_t *st, + signing_state_t *signing_state, + const input_info_t *input, + const keyexpr_info_t *keyexpr_info, + musig_per_input_info_t *out) { LOG_PROCESSOR(__FILE__, __LINE__, __func__); if (st->wallet_policy_map->type != TOKEN_TR) { @@ -2597,20 +2568,14 @@ sign_sighash_musig_and_yield(dispatcher_context_t *dc, // 1) compute aggregate pubkey // 2) compute musig2 tweaks // 3) compute taproot tweak (if keypath spend) - // if my pubnonce is in the psbt: - // 5) generate and yield pubnonce - // else: - // 6) generate and yield partial signature + // 4) compute the psbt_session_id that identifies the psbt-level signing session - // 1) compute aggregate pubkey - - // TODO: we should compute the aggregate pubkey just once for the placeholder, instead of - // repeating for each input wallet_derivation_info_t wdi = {.n_keys = st->wallet_header.n_keys, .wallet_version = st->wallet_header.version, .keys_merkle_root = st->wallet_header.keys_info_merkle_root, .change = input->in_out.is_change, - .address_index = input->in_out.address_index}; + .address_index = input->in_out.address_index, + .sign_psbt_cache = NULL}; // TODO: code duplication with policy.c::get_derived_pubkey; worth extracting a common method? @@ -2619,7 +2584,6 @@ sign_sighash_musig_and_yield(dispatcher_context_t *dc, const policy_node_keyexpr_t *key_expr = keyexpr_info->key_expression_ptr; const musig_aggr_key_info_t *musig_info = r_musig_aggr_key_info(&key_expr->m.musig_info); const uint16_t *key_indexes = r_uint16(&musig_info->key_indexes); - plain_pk_t keys[MAX_PUBKEYS_PER_MUSIG]; LEDGER_ASSERT(musig_info->n <= MAX_PUBKEYS_PER_MUSIG, "Too many keys in musig key expression"); for (int i = 0; i < musig_info->n; i++) { @@ -2627,23 +2591,23 @@ sign_sighash_musig_and_yield(dispatcher_context_t *dc, if (0 > get_extended_pubkey(dc, &wdi, key_indexes[i], &ext_pubkey)) { return -1; } - memcpy(keys[i], ext_pubkey.compressed_pubkey, sizeof(ext_pubkey.compressed_pubkey)); + memcpy(out->keys[i], ext_pubkey.compressed_pubkey, sizeof(ext_pubkey.compressed_pubkey)); } // sort the keys in ascending order using bubble sort for (int i = 0; i < musig_info->n; i++) { for (int j = 0; j < musig_info->n - 1; j++) { - if (memcmp(keys[j], keys[j + 1], sizeof(plain_pk_t)) > 0) { + if (memcmp(out->keys[j], out->keys[j + 1], sizeof(plain_pk_t)) > 0) { uint8_t tmp[sizeof(plain_pk_t)]; - memcpy(tmp, keys[j], sizeof(plain_pk_t)); - memcpy(keys[j], keys[j + 1], sizeof(plain_pk_t)); - memcpy(keys[j + 1], tmp, sizeof(plain_pk_t)); + memcpy(tmp, out->keys[j], sizeof(plain_pk_t)); + memcpy(out->keys[j], out->keys[j + 1], sizeof(plain_pk_t)); + memcpy(out->keys[j + 1], tmp, sizeof(plain_pk_t)); } } } musig_keyagg_context_t musig_ctx; - musig_key_agg(keys, musig_info->n, &musig_ctx); + musig_key_agg(out->keys, musig_info->n, &musig_ctx); // compute the aggregated extended pubkey memset(&ext_pubkey, 0, sizeof(ext_pubkey)); @@ -2664,71 +2628,58 @@ sign_sighash_musig_and_yield(dispatcher_context_t *dc, // in wallet policies, we always have at least two bip32-tweaks, and we might have // one x-only tweak per BIP-0341 (if spending from the keypath). - uint8_t tweaks[3][32]; - uint8_t *tweaks_ptrs[3] = {tweaks[0], tweaks[1], tweaks[2]}; - bool is_xonly[] = {false, false, true}; - size_t n_tweaks = 2; // might be changed to 3 below + out->is_xonly[0] = false; + out->is_xonly[1] = false; + out->n_tweaks = 2; // might be changed to 3 below - serialized_extended_pubkey_t agg_key_tweaked; - if (0 > bip32_CKDpub(&ext_pubkey, change_step, &agg_key_tweaked, tweaks[0])) { + if (0 > bip32_CKDpub(&ext_pubkey, change_step, &out->agg_key_tweaked, out->tweaks[0])) { SEND_SW(dc, SW_BAD_STATE); // should never happen return false; } - if (0 > bip32_CKDpub(&agg_key_tweaked, addr_index_step, &agg_key_tweaked, tweaks[1])) { + + if (0 > bip32_CKDpub(&out->agg_key_tweaked, + addr_index_step, + &out->agg_key_tweaked, + out->tweaks[1])) { SEND_SW(dc, SW_BAD_STATE); // should never happen return false; } // 3) compute taproot tweak (if keypath spend) - memset(tweaks[2], 0, 32); + memset(out->tweaks[2], 0, 32); if (!keyexpr_info->is_tapscript) { - n_tweaks = 3; + out->n_tweaks = 3; + out->is_xonly[2] = true; crypto_tr_tagged_hash( BIP0341_taptweak_tag, sizeof(BIP0341_taptweak_tag), - agg_key_tweaked.compressed_pubkey + 1, // xonly key, after BIP-32 tweaks + out->agg_key_tweaked.compressed_pubkey + 1, // xonly key, after BIP-32 tweaks 32, input->taptree_hash, // BIP-86 compliant tweak if there's no taptree, otherwise use the taptree hash isnull_policy_node_tree(&tr_policy->tree) ? 0 : 32, - tweaks[2]); + out->tweaks[2]); // also apply the taptweak to agg_key_tweaked uint8_t parity = 0; - crypto_tr_tweak_pubkey(agg_key_tweaked.compressed_pubkey + 1, + crypto_tr_tweak_pubkey(out->agg_key_tweaked.compressed_pubkey + 1, input->taptree_hash, isnull_policy_node_tree(&tr_policy->tree) ? 0 : 32, &parity, - agg_key_tweaked.compressed_pubkey + 1); - agg_key_tweaked.compressed_pubkey[0] = 0x02 + parity; + out->agg_key_tweaked.compressed_pubkey + 1); + out->agg_key_tweaked.compressed_pubkey[0] = 0x02 + parity; } // we will no longer use the other fields of the extended pubkey, so we zero them for sanity - memset(agg_key_tweaked.chain_code, 0, sizeof(agg_key_tweaked.chain_code)); - memset(agg_key_tweaked.child_number, 0, sizeof(agg_key_tweaked.child_number)); - agg_key_tweaked.depth = 0; - memset(agg_key_tweaked.parent_fingerprint, 0, sizeof(agg_key_tweaked.parent_fingerprint)); - memset(agg_key_tweaked.version, 0, sizeof(agg_key_tweaked.version)); - - // Compute musig_my_psbt_id. It is the psbt key that this signer uses to find pubnonces and - // partial signatures (PSBT_IN_MUSIG2_PUB_NONCE and PSBT_IN_MUSIG2_PARTIAL_SIG fields). The - // length is either 33+33 (keypath spend), or 33+33+32 bytes (tapscript spend). It's the - // concatenation of: - // - the 33-byte compressed pubkey of this participant - // - the 33-byte aggregate compressed pubkey (after all the tweaks) - // - (tapscript only) the 32-byte tapleaf hash - uint8_t musig_my_psbt_id_key[1 + 33 + 33 + 32]; - musig_my_psbt_id_key[0] = PSBT_IN_MUSIG2_PUB_NONCE; - - uint8_t *musig_my_psbt_id = musig_my_psbt_id_key + 1; - size_t psbt_id_len = keyexpr_info->is_tapscript ? 33 + 33 + 32 : 33 + 33; - memcpy(musig_my_psbt_id, keyexpr_info->internal_pubkey.compressed_pubkey, 33); - memcpy(musig_my_psbt_id + 33, agg_key_tweaked.compressed_pubkey, 33); - if (keyexpr_info->is_tapscript) { - memcpy(musig_my_psbt_id + 33 + 33, keyexpr_info->tapleaf_hash, 32); - } + memset(out->agg_key_tweaked.chain_code, 0, sizeof(out->agg_key_tweaked.chain_code)); + memset(out->agg_key_tweaked.child_number, 0, sizeof(out->agg_key_tweaked.child_number)); + out->agg_key_tweaked.depth = 0; + memset(out->agg_key_tweaked.parent_fingerprint, + 0, + sizeof(out->agg_key_tweaked.parent_fingerprint)); + memset(out->agg_key_tweaked.version, 0, sizeof(out->agg_key_tweaked.version)); // The psbt_session_id identifies the musig signing session for the entire (psbt, wallet_policy) // pair, in both rounds 1 and 2 of the protocol; it is the same for all the musig placeholders @@ -2738,18 +2689,92 @@ sign_sighash_musig_and_yield(dispatcher_context_t *dc, // sessions for different transactions or wallet policies. // Malicious collisions are not a concern, as they would only result in a signing failure (since // the nonces would be incorrectly regenerated during round 2 of MuSig2). - uint8_t psbt_session_id[32]; crypto_tr_tagged_hash( (uint8_t[]){'P', 's', 'b', 't', 'S', 'e', 's', 's', 'i', 'o', 'n', 'I', 'd'}, 13, st->wallet_header.keys_info_merkle_root, // TODO: wallet policy id would be more precise 32, (uint8_t *) &signing_state->tx_hashes, - sizeof(signing_state->tx_hashes), - psbt_session_id); - memcpy(psbt_session_id, st->wallet_header.keys_info_merkle_root, sizeof(psbt_session_id)); + sizeof(tx_hashes_t), + out->psbt_session_id); + + return true; +} + +static bool yield_musig_pubnonce(dispatcher_context_t *dc, + sign_psbt_state_t *st, + unsigned int cur_input_index, + const musig_pubnonce_t *pubnonce, + const uint8_t participant_pk[static 33], + const uint8_t aggregate_pubkey[static 33], + const uint8_t *tapleaf_hash) { + return yield_musig_data(dc, + st, + cur_input_index, + (const uint8_t *) pubnonce, + sizeof(musig_pubnonce_t), + CCMD_YIELD_MUSIG_PUBNONCE_TAG, + participant_pk, + aggregate_pubkey, + tapleaf_hash); +} + +static bool yield_musig_partial_signature(dispatcher_context_t *dc, + sign_psbt_state_t *st, + unsigned int cur_input_index, + const uint8_t psig[static 32], + const uint8_t participant_pk[static 33], + const uint8_t aggregate_pubkey[static 33], + const uint8_t *tapleaf_hash) { + return yield_musig_data(dc, + st, + cur_input_index, + psig, + 32, + CCMD_YIELD_MUSIG_PARTIALSIGNATURE_TAG, + participant_pk, + aggregate_pubkey, + tapleaf_hash); +} + +static bool __attribute__((noinline)) +sign_sighash_musig_and_yield(dispatcher_context_t *dc, + sign_psbt_state_t *st, + signing_state_t *signing_state, + const keyexpr_info_t *keyexpr_info, + const input_info_t *input, + unsigned int cur_input_index, + uint8_t sighash[static 32]) { + LOG_PROCESSOR(__FILE__, __LINE__, __func__); + + musig_per_input_info_t musig_per_input_info; + if (!compute_musig_per_input_info(dc, + st, + signing_state, + input, + keyexpr_info, + &musig_per_input_info)) { + return false; + } // 4) check if my pubnonce is in the psbt + // Compute musig_my_psbt_id. It is the psbt key that this signer uses to find pubnonces and + // partial signatures (PSBT_IN_MUSIG2_PUB_NONCE and PSBT_IN_MUSIG2_PARTIAL_SIG fields). The + // length is either 33+33 (keypath spend), or 33+33+32 bytes (tapscript spend). It's the + // concatenation of: + // - the 33-byte compressed pubkey of this participant + // - the 33-byte aggregate compressed pubkey (after all the tweaks) + // - (tapscript only) the 32-byte tapleaf hash + uint8_t musig_my_psbt_id_key[1 + 33 + 33 + 32]; + musig_my_psbt_id_key[0] = PSBT_IN_MUSIG2_PUB_NONCE; + + uint8_t *musig_my_psbt_id = musig_my_psbt_id_key + 1; + size_t psbt_id_len = keyexpr_info->is_tapscript ? 33 + 33 + 32 : 33 + 33; + memcpy(musig_my_psbt_id, keyexpr_info->internal_pubkey.compressed_pubkey, 33); + memcpy(musig_my_psbt_id + 33, musig_per_input_info.agg_key_tweaked.compressed_pubkey, 33); + if (keyexpr_info->is_tapscript) { + memcpy(musig_my_psbt_id + 33 + 33, keyexpr_info->tapleaf_hash, 32); + } musig_pubnonce_t my_pubnonce; if (sizeof(musig_pubnonce_t) != call_get_merkleized_map_value(dc, &input->in_out.map, @@ -2757,174 +2782,141 @@ sign_sighash_musig_and_yield(dispatcher_context_t *dc, 1 + psbt_id_len, my_pubnonce.raw, sizeof(musig_pubnonce_t))) { - /** - * Round 1 of the MuSig2 protocol - **/ - - const musig_psbt_session_t *psbt_session = - musigsession_round1_initialize(psbt_session_id, &signing_state->musig); - if (psbt_session == NULL) { - // This should never happen - PRINTF("Unexpected: failed to initialize MuSig2 round 1\n"); - SEND_SW(dc, SW_BAD_STATE); - return false; - } - - // 5) generate and yield pubnonce - - uint8_t rand_i_j[32]; - compute_rand_i_j(psbt_session, cur_input_index, keyexpr_info->index, rand_i_j); - - musig_secnonce_t secnonce; - musig_pubnonce_t pubnonce; - if (0 > musig_nonce_gen(rand_i_j, - keyexpr_info->internal_pubkey.compressed_pubkey, - agg_key_tweaked.compressed_pubkey + 1, - &secnonce, - &pubnonce)) { - PRINTF("MuSig2 nonce generation failed\n"); - SEND_SW(dc, SW_BAD_STATE); // should never happen - return false; - } + PRINTF("Missing pubnonce in PSBT\n"); + SEND_SW(dc, SW_BAD_STATE); + return false; + } + /** + * Round 2 of the MuSig2 protocol + **/ - if (!yield_musig_pubnonce(dc, - st, - cur_input_index, - &pubnonce, - keyexpr_info->internal_pubkey.compressed_pubkey, - agg_key_tweaked.compressed_pubkey, - keyexpr_info->is_tapscript ? keyexpr_info->tapleaf_hash : NULL)) { - PRINTF("Failed yielding MuSig2 pubnonce\n"); - SEND_SW(dc, SW_BAD_STATE); // should never happen - return false; - } - } else { - /** - * Round 2 of the MuSig2 protocol - **/ - - const musig_psbt_session_t *psbt_session = - musigsession_round2_initialize(psbt_session_id, &signing_state->musig); - - if (psbt_session == NULL) { - // The PSBT contains a partial nonce, but we do not have the corresponding psbt - // session in storage. Either it was deleted, or the pubnonces were not real. Either - // way, we cannot continue. - PRINTF("Missing MuSig2 session\n"); - SEND_SW(dc, SW_BAD_STATE); - return false; - } + const musig_psbt_session_t *psbt_session = + musigsession_round2_initialize(musig_per_input_info.psbt_session_id, &signing_state->musig); - // 6) generate and yield partial signature + if (psbt_session == NULL) { + // The PSBT contains a partial nonce, but we do not have the corresponding psbt + // session in storage. Either it was deleted, or the pubnonces were not real. Either + // way, we cannot continue. + PRINTF("Missing MuSig2 session\n"); + SEND_SW(dc, SW_BAD_STATE); + return false; + } - musig_pubnonce_t nonces[MAX_PUBKEYS_PER_MUSIG]; + // 6) generate and yield partial signature - for (int i = 0; i < musig_info->n; i++) { - uint8_t musig_ith_psbt_id_key[1 + 33 + 33 + 32]; - uint8_t *musig_ith_psbt_id = musig_ith_psbt_id_key + 1; - // copy from musig_my_psbt_id_key, but replace the corresponding pubkey - memcpy(musig_ith_psbt_id_key, musig_my_psbt_id_key, sizeof(musig_my_psbt_id_key)); - memcpy(musig_ith_psbt_id, keys[i], sizeof(plain_pk_t)); + const policy_node_keyexpr_t *key_expr = keyexpr_info->key_expression_ptr; + const musig_aggr_key_info_t *musig_info = r_musig_aggr_key_info(&key_expr->m.musig_info); - // TODO: could avoid fetching again our own pubnonce - if (sizeof(musig_pubnonce_t) != - call_get_merkleized_map_value(dc, - &input->in_out.map, - musig_ith_psbt_id_key, - 1 + psbt_id_len, - nonces[i].raw, - sizeof(musig_pubnonce_t))) { - PRINTF("Missing or incorrect pubnonce for a MuSig2 cosigner\n"); - SEND_SW(dc, SW_INCORRECT_DATA); - return false; - } - } + musig_pubnonce_t nonces[MAX_PUBKEYS_PER_MUSIG]; - // compute aggregate nonce - musig_pubnonce_t aggnonce; - int res = musig_nonce_agg(nonces, musig_info->n, &aggnonce); - if (res < 0) { - PRINTF("Musig aggregation failed; disruptive signer has index %d\n", -res); + for (int i = 0; i < musig_info->n; i++) { + uint8_t musig_ith_psbt_id_key[1 + 33 + 33 + 32]; + uint8_t *musig_ith_psbt_id = musig_ith_psbt_id_key + 1; + // copy from musig_my_psbt_id_key, but replace the corresponding pubkey + memcpy(musig_ith_psbt_id_key, musig_my_psbt_id_key, sizeof(musig_my_psbt_id_key)); + memcpy(musig_ith_psbt_id, musig_per_input_info.keys[i], sizeof(plain_pk_t)); + + if (sizeof(musig_pubnonce_t) != call_get_merkleized_map_value(dc, + &input->in_out.map, + musig_ith_psbt_id_key, + 1 + psbt_id_len, + nonces[i].raw, + sizeof(musig_pubnonce_t))) { + PRINTF("Missing or incorrect pubnonce for a MuSig2 cosigner\n"); SEND_SW(dc, SW_INCORRECT_DATA); + return false; } + } - // recompute secnonce from psbt_session randomness - uint8_t rand_i_j[32]; - compute_rand_i_j(psbt_session, cur_input_index, keyexpr_info->index, rand_i_j); - - musig_secnonce_t secnonce; - musig_pubnonce_t pubnonce; + // compute aggregate nonce + musig_pubnonce_t aggnonce; + int res = musig_nonce_agg(nonces, musig_info->n, &aggnonce); + if (res < 0) { + PRINTF("Musig aggregation failed; disruptive signer has index %d\n", -res); + SEND_SW(dc, SW_INCORRECT_DATA); + } - if (0 > musig_nonce_gen(rand_i_j, - keyexpr_info->internal_pubkey.compressed_pubkey, - agg_key_tweaked.compressed_pubkey + 1, - &secnonce, - &pubnonce)) { - PRINTF("MuSig2 nonce generation failed\n"); - SEND_SW(dc, SW_BAD_STATE); // should never happen - return false; - } + // recompute secnonce from psbt_session randomness + uint8_t rand_i_j[32]; + compute_rand_i_j(psbt_session, cur_input_index, keyexpr_info->index, rand_i_j); - // derive secret key + musig_secnonce_t secnonce; + musig_pubnonce_t pubnonce; - cx_ecfp_private_key_t private_key = {0}; - uint8_t psig[32]; - bool err = false; - do { // block executed once, only to allow safely breaking out on error + if (0 > musig_nonce_gen(rand_i_j, + keyexpr_info->internal_pubkey.compressed_pubkey, + musig_per_input_info.agg_key_tweaked.compressed_pubkey + 1, + &secnonce, + &pubnonce)) { + PRINTF("MuSig2 nonce generation failed\n"); + SEND_SW(dc, SW_BAD_STATE); // should never happen + return false; + } - // derive secret key - uint32_t sign_path[MAX_BIP32_PATH_STEPS]; + // derive secret key - for (int i = 0; i < keyexpr_info->key_derivation_length; i++) { - sign_path[i] = keyexpr_info->key_derivation[i]; - } - int sign_path_len = keyexpr_info->key_derivation_length; - - if (bip32_derive_init_privkey_256(CX_CURVE_256K1, - sign_path, - sign_path_len, - &private_key, - NULL) != CX_OK) { - err = true; - break; - } + cx_ecfp_private_key_t private_key = {0}; + uint8_t psig[32]; + bool err = false; + do { // block executed once, only to allow safely breaking out on error - // Create partial signature - musig_session_context_t musig_session_context = {.aggnonce = &aggnonce, - .n_keys = musig_info->n, - .pubkeys = keys, - .n_tweaks = n_tweaks, - .tweaks = tweaks_ptrs, - .is_xonly = is_xonly, - .msg = sighash, - .msg_len = 32}; - - if (0 > musig_sign(&secnonce, private_key.d, &musig_session_context, psig)) { - PRINTF("Musig2 signature failed\n"); - err = true; - break; - } - } while (false); + // derive secret key + uint32_t sign_path[MAX_BIP32_PATH_STEPS]; - explicit_bzero(&private_key, sizeof(private_key)); + for (int i = 0; i < keyexpr_info->key_derivation_length; i++) { + sign_path[i] = keyexpr_info->key_derivation[i]; + } + int sign_path_len = keyexpr_info->key_derivation_length; - if (err) { - PRINTF("Partial signature generation failed\n"); - return false; + if (bip32_derive_init_privkey_256(CX_CURVE_256K1, + sign_path, + sign_path_len, + &private_key, + NULL) != CX_OK) { + err = true; + break; } - if (!yield_musig_partial_signature( - dc, - st, - cur_input_index, - psig, - keyexpr_info->internal_pubkey.compressed_pubkey, - agg_key_tweaked.compressed_pubkey, - keyexpr_info->is_tapscript ? keyexpr_info->tapleaf_hash : NULL)) { - PRINTF("Failed yielding MuSig2 partial signature\n"); - SEND_SW(dc, SW_BAD_STATE); // should never happen - return false; + // Create partial signature + uint8_t *tweaks_ptrs[3] = { + musig_per_input_info.tweaks[0], + musig_per_input_info.tweaks[1], + musig_per_input_info.tweaks[2] // the last element is ignored if n_tweaks == 2 + }; + musig_session_context_t musig_session_context = {.aggnonce = &aggnonce, + .n_keys = musig_info->n, + .pubkeys = musig_per_input_info.keys, + .n_tweaks = musig_per_input_info.n_tweaks, + .tweaks = tweaks_ptrs, + .is_xonly = musig_per_input_info.is_xonly, + .msg = sighash, + .msg_len = 32}; + + if (0 > musig_sign(&secnonce, private_key.d, &musig_session_context, psig)) { + PRINTF("Musig2 signature failed\n"); + err = true; + break; } + } while (false); + + explicit_bzero(&private_key, sizeof(private_key)); + + if (err) { + PRINTF("Partial signature generation failed\n"); + return false; + } + + if (!yield_musig_partial_signature( + dc, + st, + cur_input_index, + psig, + keyexpr_info->internal_pubkey.compressed_pubkey, + musig_per_input_info.agg_key_tweaked.compressed_pubkey, + keyexpr_info->is_tapscript ? keyexpr_info->tapleaf_hash : NULL)) { + PRINTF("Failed yielding MuSig2 partial signature\n"); + SEND_SW(dc, SW_BAD_STATE); // should never happen + return false; } return true; @@ -3295,25 +3287,163 @@ fill_taproot_keyexpr_info(dispatcher_context_t *dc, return true; } +static bool produce_and_yield_pubnonce(dispatcher_context_t *dc, + sign_psbt_state_t *st, + signing_state_t *signing_state, + const keyexpr_info_t *keyexpr_info, + const input_info_t *input, + unsigned int cur_input_index) { + LOG_PROCESSOR(__FILE__, __LINE__, __func__); + + musig_per_input_info_t musig_per_input_info; + if (!compute_musig_per_input_info(dc, + st, + signing_state, + input, + keyexpr_info, + &musig_per_input_info)) { + return false; + } + + /** + * Round 1 of the MuSig2 protocol + **/ + + const musig_psbt_session_t *psbt_session = + musigsession_round1_initialize(musig_per_input_info.psbt_session_id, &signing_state->musig); + if (psbt_session == NULL) { + // This should never happen + PRINTF("Unexpected: failed to initialize MuSig2 round 1\n"); + SEND_SW(dc, SW_BAD_STATE); + return false; + } + + // 5) generate and yield pubnonce + + uint8_t rand_i_j[32]; + compute_rand_i_j(psbt_session, cur_input_index, keyexpr_info->index, rand_i_j); + + musig_secnonce_t secnonce; + musig_pubnonce_t pubnonce; + if (0 > musig_nonce_gen(rand_i_j, + keyexpr_info->internal_pubkey.compressed_pubkey, + musig_per_input_info.agg_key_tweaked.compressed_pubkey + 1, + &secnonce, + &pubnonce)) { + PRINTF("MuSig2 nonce generation failed\n"); + SEND_SW(dc, SW_BAD_STATE); // should never happen + return false; + } + + if (!yield_musig_pubnonce(dc, + st, + cur_input_index, + &pubnonce, + keyexpr_info->internal_pubkey.compressed_pubkey, + musig_per_input_info.agg_key_tweaked.compressed_pubkey, + keyexpr_info->is_tapscript ? keyexpr_info->tapleaf_hash : NULL)) { + PRINTF("Failed yielding MuSig2 pubnonce\n"); + SEND_SW(dc, SW_BAD_STATE); // should never happen + return false; + } + + return true; +} + +static bool __attribute__((noinline)) produce_musig2_pubnonces( + dispatcher_context_t *dc, + sign_psbt_state_t *st, + signing_state_t *signing_state, + sign_psbt_cache_t *sign_psbt_cache, + const uint8_t internal_inputs[static BITVECTOR_REAL_SIZE(MAX_N_INPUTS_CAN_SIGN)]) { + LOG_PROCESSOR(__FILE__, __LINE__, __func__); + + if (st->wallet_policy_map->type != TOKEN_TR) { + return true; // nothing to do + } + + // Iterate over all the key expressions that correspond to keys owned by us + for (size_t i_keyexpr = 0; i_keyexpr < st->n_internal_key_expressions; i_keyexpr++) { + keyexpr_info_t *keyexpr_info = &st->internal_key_expressions[i_keyexpr]; + if (!keyexpr_info->to_sign || + keyexpr_info->key_expression_ptr->type != KEY_EXPRESSION_MUSIG) { + continue; + } + + if (!fill_keyexpr_info_if_internal(dc, st, keyexpr_info) == true) { + continue; + } + + for (unsigned int i = 0; i < st->n_inputs; i++) { + if (bitvector_get(internal_inputs, i)) { + input_info_t input; + memset(&input, 0, sizeof(input)); + + input_keys_callback_data_t callback_data = {.input = &input, .state = st}; + int res = call_get_merkleized_map_with_callback( + dc, + (void *) &callback_data, + st->inputs_root, + st->n_inputs, + i, + (merkle_tree_elements_callback_t) input_keys_callback, + &input.in_out.map); + if (res < 0) { + SEND_SW(dc, SW_INCORRECT_DATA); + return false; + } + + // TODO: code duplication with sign_transaction_input + if (keyexpr_info->tapleaf_ptr != NULL) { + if (!fill_taproot_keyexpr_info(dc, + st, + &input, + keyexpr_info->tapleaf_ptr, + keyexpr_info, + sign_psbt_cache)) { + return false; + } + } + + policy_node_tr_t *policy = (policy_node_tr_t *) st->wallet_policy_map; + if (!isnull_policy_node_tree(&policy->tree)) { + if (0 > compute_taptree_hash( + dc, + &(wallet_derivation_info_t){ + .address_index = input.in_out.address_index, + .change = input.in_out.is_change ? 1 : 0, + .keys_merkle_root = st->wallet_header.keys_info_merkle_root, + .n_keys = st->wallet_header.n_keys, + .wallet_version = st->wallet_header.version, + .sign_psbt_cache = sign_psbt_cache}, + r_policy_node_tree(&policy->tree), + input.taptree_hash)) { + PRINTF("Error while computing taptree hash\n"); + SEND_SW(dc, SW_BAD_STATE); + return false; + } + } + + if (!produce_and_yield_pubnonce(dc, st, signing_state, keyexpr_info, &input, i)) { + return false; + } + } + } + } + + return true; +} + static bool __attribute__((noinline)) sign_transaction(dispatcher_context_t *dc, sign_psbt_state_t *st, sign_psbt_cache_t *sign_psbt_cache, + signing_state_t *signing_state, const uint8_t internal_inputs[static BITVECTOR_REAL_SIZE(MAX_N_INPUTS_CAN_SIGN)]) { LOG_PROCESSOR(__FILE__, __LINE__, __func__); int key_expression_index = 0; - signing_state_t signing_state; - - // compute all the tx-wide hashes - // while this is redundant for legacy transactions, we do it here in order to - // avoid doing it in places that have more stack limitations - if (!compute_tx_hashes(dc, st, &signing_state.tx_hashes)) { - // we do not send a status word, since compute_tx_hashes already does it on failure - return false; - } - // Iterate over all the key expressions that correspond to keys owned by us for (size_t i_keyexpr = 0; i_keyexpr < st->n_internal_key_expressions; i_keyexpr++) { keyexpr_info_t *keyexpr_info = &st->internal_key_expressions[i_keyexpr]; @@ -3356,7 +3486,7 @@ sign_transaction(dispatcher_context_t *dc, if (!sign_transaction_input(dc, st, sign_psbt_cache, - &signing_state, + signing_state, keyexpr_info, &input, i)) { @@ -3370,10 +3500,6 @@ sign_transaction(dispatcher_context_t *dc, ++key_expression_index; } - // MuSig2: if there is an active session at the end of round 1, we move it to persistent - // storage. It is important that this is only done at the very end of the signing process. - musigsession_commit(&signing_state.musig); - return true; } @@ -3423,44 +3549,79 @@ void handler_sign_psbt(dispatcher_context_t *dc, uint8_t protocol_version) { */ if (!preprocess_outputs(dc, &st, cache, internal_outputs)) return; - if (G_swap_state.called_from_swap) { - /** SWAP CHECKS - * - * If called from the exchange app, perform the necessary additional checks. - */ - - // During swaps, the user approval was already obtained in the exchange app - if (!execute_swap_checks(dc, &st)) return; - } else { - /** TRANSACTION CONFIRMATION - * - * Display each non-change output, and transaction fees, and acquire user confirmation, - */ - if (!display_transaction(dc, &st, internal_outputs)) return; + // check if we're only executing the MuSig2 Round 1 + bool only_signing_for_musig = true; + for (size_t i = 0; i < st.n_internal_key_expressions; i++) { + if (st.internal_key_expressions[i].to_sign && + st.internal_key_expressions[i].key_expression_ptr->type != KEY_EXPRESSION_MUSIG) { + // at least one of the key expressions we're signing for is not a MuSig + only_signing_for_musig = false; + } } - // Signing always takes some time, so we rather not wait before showing the spinner - io_show_processing_screen(); - - /** SIGNING FLOW - * - * For each internal key expression, and for each internal input, sign using the - * appropriate algorithm. - */ - int sign_result = sign_transaction(dc, &st, cache, internal_inputs); + signing_state_t signing_state; + memset(&signing_state, 0, sizeof(signing_state)); - if (!G_swap_state.called_from_swap) { - ui_post_processing_confirm_transaction(dc, sign_result); + // compute all the tx-wide hashes + if (!compute_tx_hashes(dc, &st, &signing_state.tx_hashes)) { + return; } - if (!sign_result) { - return; + if (!st.has_musig2_pub_nonces) { + // We execute the first round of MuSig for any musig2 key expression.produce the pubnonces; + // this does not involve the private keys, therefore we can do it without user confirmation + + if (!produce_musig2_pubnonces(dc, &st, &signing_state, cache, internal_inputs)) { + return; + } } - // Only if called from swap, the app should terminate after sending the response - if (G_swap_state.called_from_swap) { - G_swap_state.should_exit = true; + // we execute the signing flow only if we're producing any signature + // (or any MuSig partial signature) + if (!only_signing_for_musig || st.has_musig2_pub_nonces) { + if (G_swap_state.called_from_swap) { + /** SWAP CHECKS + * + * If called from the exchange app, perform the necessary additional checks. + */ + + // During swaps, the user approval was already obtained in the exchange app + if (!execute_swap_checks(dc, &st)) return; + } else { + /** TRANSACTION CONFIRMATION + * + * Display each non-change output, and transaction fees, and acquire user confirmation, + */ + if (!display_transaction(dc, &st, internal_outputs)) return; + } + + // Signing always takes some time, so we rather not wait before showing the spinner + io_show_processing_screen(); + + /** SIGNING FLOW + * + * For each internal key expression, and for each internal input, sign using the + * appropriate algorithm. + */ + int sign_result = sign_transaction(dc, &st, cache, &signing_state, internal_inputs); + + if (!G_swap_state.called_from_swap) { + ui_post_processing_confirm_transaction(dc, sign_result); + } + + if (!sign_result) { + return; + } + + // Only if called from swap, the app should terminate after sending the response + if (G_swap_state.called_from_swap) { + G_swap_state.should_exit = true; + } } + // MuSig2: if there is an active session at the end of round 1, we move it to persistent + // storage. It is important that this is only done at the very end of the signing process. + musigsession_commit(&signing_state.musig); + SEND_SW(dc, SW_OK); } diff --git a/test_utils/__init__.py b/test_utils/__init__.py index 013d9e2e3..d0bb333f4 100644 --- a/test_utils/__init__.py +++ b/test_utils/__init__.py @@ -91,7 +91,7 @@ def get_internal_xpub(seed: str, path: str) -> str: return bip32.get_xpub_from_path(f"m/{path}") if path else bip32.get_xpub_from_path("m") -def count_internal_key_placeholders(seed: str, network: Union[Literal['main'], Literal['test']], wallet_policy: WalletPolicy) -> int: +def count_internal_key_placeholders(seed: str, network: Union[Literal['main'], Literal['test']], wallet_policy: WalletPolicy, *, only_musig=False) -> int: """Count how many of the key placeholders in wallet_policy are indeed internal. musig() placeholders are counted as many times as there are internal keys in them.""" @@ -124,12 +124,13 @@ def count_internal_key_placeholders(seed: str, network: Union[Literal['main'], L count = 0 - simple_key_placeholders = re.findall( - r'@(\d+)/', wallet_policy.descriptor_template) - # for each match, count it if the corresponding key is internal - for key_index in simple_key_placeholders: - if is_key_internal[int(key_index)]: - count += 1 + if not only_musig: + simple_key_placeholders = re.findall( + r'@(\d+)/', wallet_policy.descriptor_template) + # for each match, count it if the corresponding key is internal + for key_index in simple_key_placeholders: + if is_key_internal[int(key_index)]: + count += 1 if wallet_policy.version != WalletType.WALLET_POLICY_V1: # no musig in V1 policies musig_key_placeholders = re.findall( diff --git a/tests/test_e2e_musig2.py b/tests/test_e2e_musig2.py index 8e402e9c2..ef14479e4 100644 --- a/tests/test_e2e_musig2.py +++ b/tests/test_e2e_musig2.py @@ -6,6 +6,7 @@ from hashlib import sha256 from decimal import Decimal +from ledger_bitcoin._script import is_p2tr from ledger_bitcoin.exception.errors import IncorrectDataError, NotSupportedError from ledger_bitcoin.exception.device_exception import DeviceException from ledger_bitcoin.psbt import PSBT @@ -26,6 +27,20 @@ from .conftest import AuthServiceProxy, create_new_wallet, generate_blocks, get_unique_wallet_name, get_wallet_rpc, import_descriptors_with_privkeys, testnet_to_regtest_addr as T +# Removes all the BIP_IN_TAP_BIP32_DERIVATION entries that are not for the musig aggregate keys +# Returns a new PSBT without modifying the original +def strip_non_musig2_derivations(psbt: PSBT) -> PSBT: + psbt_clone = PSBT() + psbt_clone.deserialize(psbt.serialize()) + for input in psbt_clone.inputs: + if input.witness_utxo is not None and is_p2tr(input.witness_utxo.scriptPubKey): + for key, (_, deriv) in list(input.tap_bip32_paths.items()): + # a bit hacky, but musig key derivations in wallet policies are always 2 steps + if len(deriv.path) != 2: + del input.tap_bip32_paths[key] + return psbt_clone + + def run_test_e2e_musig2(navigator: Navigator, client: RaggerClient, wallet_policy: WalletPolicy, core_wallet_names: List[str], rpc: AuthServiceProxy, rpc_test_wallet: AuthServiceProxy, speculos_globals: SpeculosGlobals, instructions_register_wallet: Instructions, instructions_sign_psbt: Instructions, test_name: str): @@ -124,12 +139,13 @@ def printb(*args): # ==> get nonce from the hww n_internal_keys = count_internal_key_placeholders( - speculos_globals.seed, "test", wallet_policy) + speculos_globals.seed, "test", wallet_policy, only_musig=True) psbt = PSBT() psbt.deserialize(psbt_b64) - hww_yielded: List[Tuple[int, SignPsbtYieldedObject]] = client.sign_psbt(psbt, wallet_policy, wallet_hmac, navigator, + psbt_stripped = strip_non_musig2_derivations(psbt) + hww_yielded: List[Tuple[int, SignPsbtYieldedObject]] = client.sign_psbt(psbt_stripped, wallet_policy, wallet_hmac, navigator, instructions=instructions_sign_psbt, testname=f"{test_name}_sign") @@ -190,7 +206,8 @@ def printb(*args): psbt = PSBT() psbt.deserialize(combined_psbt) - hww_yielded: List[Tuple[int, SignPsbtYieldedObject]] = client.sign_psbt(psbt, wallet_policy, wallet_hmac, navigator, + psbt_stripped = strip_non_musig2_derivations(psbt) + hww_yielded: List[Tuple[int, SignPsbtYieldedObject]] = client.sign_psbt(psbt_stripped, wallet_policy, wallet_hmac, navigator, instructions=instructions_sign_psbt, testname=f"{test_name}_sign") @@ -273,6 +290,28 @@ def test_e2e_musig2_keypath(navigator: Navigator, firmware: Firmware, client: Ra e2e_register_wallet_instruction(firmware, wallet_policy.n_keys), e2e_sign_psbt_instruction(firmware), test_name) +def test_e2e_musig2_keypath2(navigator: Navigator, firmware: Firmware, client: RaggerClient, + test_name: str, rpc, rpc_test_wallet, speculos_globals: SpeculosGlobals): + # We spend with the musig2 in the keypath, but there is a taptree + + path = "48'/1'/0'/2'" + internal_xpub = get_internal_xpub(speculos_globals.seed, path) + + core_wallet_name, core_xpub_orig = create_new_wallet() + _, core_xpub_orig_2 = create_new_wallet() + wallet_policy = WalletPolicy( + name="Musig 2 my ears", + descriptor_template="tr(musig(@0,@1)/**,pk(@2/**))", + keys_info=[ + f"[{speculos_globals.master_key_fingerprint.hex()}/{path}]{internal_xpub}", + f"{core_xpub_orig}", + f"{core_xpub_orig_2}", + ]) + + run_test_e2e_musig2(navigator, client, wallet_policy, [core_wallet_name], rpc, rpc_test_wallet, speculos_globals, + e2e_register_wallet_instruction(firmware, wallet_policy.n_keys), e2e_sign_psbt_instruction(firmware), test_name) + + def test_e2e_musig2_scriptpath(navigator: Navigator, firmware: Firmware, client: RaggerClient, test_name: str, rpc, rpc_test_wallet, speculos_globals: SpeculosGlobals): path = "48'/1'/0'/2'" From 392b9422bd1aafd6ef717ca4cea16e33fbfdd784 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Thu, 21 Nov 2024 11:22:33 +0000 Subject: [PATCH 43/45] Only compute the aggregate key once for each key expression --- src/handler/sign_psbt.c | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/handler/sign_psbt.c b/src/handler/sign_psbt.c index d5b5f2b5e..88d96a5b5 100644 --- a/src/handler/sign_psbt.c +++ b/src/handler/sign_psbt.c @@ -2606,16 +2606,8 @@ static bool compute_musig_per_input_info(dispatcher_context_t *dc, } } - musig_keyagg_context_t musig_ctx; - musig_key_agg(out->keys, musig_info->n, &musig_ctx); - - // compute the aggregated extended pubkey - memset(&ext_pubkey, 0, sizeof(ext_pubkey)); - write_u32_be(ext_pubkey.version, 0, BIP32_PUBKEY_VERSION); - - ext_pubkey.compressed_pubkey[0] = (musig_ctx.Q.y[31] % 2 == 0) ? 2 : 3; - memcpy(&ext_pubkey.compressed_pubkey[1], musig_ctx.Q.x, sizeof(musig_ctx.Q.x)); - memcpy(&ext_pubkey.chain_code, BIP_328_CHAINCODE, sizeof(BIP_328_CHAINCODE)); + // we already computed the aggregate (pre-tweaks) xpub in the keyexpr_info + memcpy(&ext_pubkey, &keyexpr_info->pubkey, sizeof(serialized_extended_pubkey_t)); // 2) compute musig2 tweaks // We always have exactly 2 BIP32 tweaks in wallet policies; if the musig is in the keypath From 752aaa8aa6ef07228a02af760af62b400c1b4013 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Thu, 21 Nov 2024 13:37:56 +0000 Subject: [PATCH 44/45] Fix wrong documentation for get_extended_pubkey; renamed to get_extended_pubkey_from_client --- src/handler/lib/policy.c | 12 +++++++++--- src/handler/lib/policy.h | 11 +++-------- src/handler/sign_psbt.c | 2 +- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/handler/lib/policy.c b/src/handler/lib/policy.c index 7af8280f0..9ecf54281 100644 --- a/src/handler/lib/policy.c +++ b/src/handler/lib/policy.c @@ -420,7 +420,7 @@ execute_processor(policy_parser_state_t *state, policy_parser_processor_t proc, // convenience function, split from get_derived_pubkey only to improve stack usage // returns -1 on error, 0 if the returned key info has no wildcard (**), 1 if it has the wildcard -__attribute__((noinline, warn_unused_result)) int get_extended_pubkey( +__attribute__((noinline, warn_unused_result)) int get_extended_pubkey_from_client( dispatcher_context_t *dispatcher_context, const wallet_derivation_info_t *wdi, int key_index, @@ -464,7 +464,10 @@ __attribute__((warn_unused_result)) static int get_derived_pubkey( serialized_extended_pubkey_t ext_pubkey; if (key_expr->type == KEY_EXPRESSION_NORMAL) { - if (0 > get_extended_pubkey(dispatcher_context, wdi, key_expr->k.key_index, &ext_pubkey)) { + if (0 > get_extended_pubkey_from_client(dispatcher_context, + wdi, + key_expr->k.key_index, + &ext_pubkey)) { return -1; } } else if (key_expr->type == KEY_EXPRESSION_MUSIG) { @@ -473,7 +476,10 @@ __attribute__((warn_unused_result)) static int get_derived_pubkey( plain_pk_t keys[MAX_PUBKEYS_PER_MUSIG]; for (int i = 0; i < musig_info->n; i++) { // we use ext_pubkey as a temporary variable; will overwrite later - if (0 > get_extended_pubkey(dispatcher_context, wdi, key_indexes[i], &ext_pubkey)) { + if (0 > get_extended_pubkey_from_client(dispatcher_context, + wdi, + key_indexes[i], + &ext_pubkey)) { return -1; } memcpy(keys[i], ext_pubkey.compressed_pubkey, sizeof(ext_pubkey.compressed_pubkey)); diff --git a/src/handler/lib/policy.h b/src/handler/lib/policy.h index 25f0e564d..e5cd24189 100644 --- a/src/handler/lib/policy.h +++ b/src/handler/lib/policy.h @@ -54,23 +54,18 @@ typedef struct { } wallet_derivation_info_t; /** - * Computes the a derived compressed pubkey for one of the key of the wallet policy, - * for a given change/address_index combination. - * - * This function computes the extended public key (xpub) based on the provided - * BIP32 derivation path. It supports both standard BIP32 derivation and - * the derivation of Musig (multi-signature) keys. + * Requests and parses the serialized extended public key from the client. * * @param[in] dispatcher_context Pointer to the dispatcher content * @param[in] wdi Pointer to a `wallet_derivation_info_t` struct with the details of the - * necessary details of the wallet policy, and the desired change/address_index pair. + * necessary details of the wallet policy. The change/addr_index pairs are not * @param[in] key_index Index of the pubkey in the vector of keys of the wallet policy. * @param[out] out Pointer to a `serialized_extended_pubkey_t` that will contain the requested * extended pubkey. * * @return -1 on error, 0 if the returned key info has no wildcard (**), 1 if it has the wildcard. */ -__attribute__((warn_unused_result)) int get_extended_pubkey( +__attribute__((warn_unused_result)) int get_extended_pubkey_from_client( dispatcher_context_t *dispatcher_context, const wallet_derivation_info_t *wdi, int key_index, diff --git a/src/handler/sign_psbt.c b/src/handler/sign_psbt.c index 88d96a5b5..67f969bd1 100644 --- a/src/handler/sign_psbt.c +++ b/src/handler/sign_psbt.c @@ -2588,7 +2588,7 @@ static bool compute_musig_per_input_info(dispatcher_context_t *dc, LEDGER_ASSERT(musig_info->n <= MAX_PUBKEYS_PER_MUSIG, "Too many keys in musig key expression"); for (int i = 0; i < musig_info->n; i++) { // we use ext_pubkey as a temporary variable; will overwrite later - if (0 > get_extended_pubkey(dc, &wdi, key_indexes[i], &ext_pubkey)) { + if (0 > get_extended_pubkey_from_client(dc, &wdi, key_indexes[i], &ext_pubkey)) { return -1; } memcpy(out->keys[i], ext_pubkey.compressed_pubkey, sizeof(ext_pubkey.compressed_pubkey)); From e6b07cebd91efe0f8c58f59f3066b21f4bd5b943 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Thu, 21 Nov 2024 15:38:17 +0000 Subject: [PATCH 45/45] Refactor most of the MuSig2-related code out of sign_psbt.c --- src/handler/sign_psbt.c | 645 +------------------------- src/handler/sign_psbt.h | 152 ++++++ src/handler/sign_psbt/musig_signing.c | 490 +++++++++++++++++++ src/handler/sign_psbt/musig_signing.h | 60 +++ 4 files changed, 705 insertions(+), 642 deletions(-) create mode 100644 src/handler/sign_psbt.h create mode 100644 src/handler/sign_psbt/musig_signing.c create mode 100644 src/handler/sign_psbt/musig_signing.h diff --git a/src/handler/sign_psbt.c b/src/handler/sign_psbt.c index 67f969bd1..de8337786 100644 --- a/src/handler/sign_psbt.c +++ b/src/handler/sign_psbt.c @@ -48,10 +48,12 @@ #include "lib/psbt_parse_rawtx.h" #include "handlers.h" +#include "sign_psbt.h" -#include "sign_psbt/sign_psbt_cache.h" #include "sign_psbt/compare_wallet_script_at_path.h" #include "sign_psbt/extract_bip32_derivation.h" +#include "sign_psbt/musig_signing.h" +#include "sign_psbt/sign_psbt_cache.h" #include "sign_psbt/update_hashes_with_map_value.h" #include "../swap/swap_globals.h" @@ -59,153 +61,6 @@ #include "../musig/musig.h" #include "../musig/musig_sessions.h" -// common info that applies to either the current input or the current output -typedef struct { - merkleized_map_commitment_t map; - - bool unexpected_pubkey_error; // Set to true if the pubkey in the keydata of - // PSBT_{IN,OUT}_BIP32_DERIVATION or - // PSBT_{IN,OUT}_TAP_BIP32_DERIVATION is not the correct length. - - bool key_expression_found; // Set to true if the input/output info in the psbt was correctly - // matched with the current key expression in the signing flow - - bool is_change; - int address_index; - - // For an output, its scriptPubKey - // for an input, the prevout's scriptPubKey (either from the non-witness-utxo, or from the - // witness-utxo) - - uint8_t scriptPubKey[MAX_OUTPUT_SCRIPTPUBKEY_LEN]; - size_t scriptPubKey_len; -} in_out_info_t; - -typedef struct { - in_out_info_t in_out; - bool has_witnessUtxo; - bool has_nonWitnessUtxo; - bool has_redeemScript; - bool has_sighash_type; - - uint64_t prevout_amount; // the value of the prevout of the current input - - // we no longer need the script when we compute the taptree hash right before a taproot key-path - // spending; therefore, we reuse the same memory - union { - // the script used when signing, either from the witness utxo or the redeem script - uint8_t script[MAX_PREVOUT_SCRIPTPUBKEY_LEN]; - uint8_t taptree_hash[32]; - }; - - size_t script_len; - - uint32_t sighash_type; -} input_info_t; - -typedef struct { - in_out_info_t in_out; - uint64_t value; -} output_info_t; - -typedef struct { - policy_node_keyexpr_t *key_expression_ptr; - // index of this key expression in the descriptor template, in parsing order - int index; - uint32_t fingerprint; - - // we only sign for keys expressions for which we find a matching key derivation in the PSBT, - // at least for one of the inputs - bool to_sign; - - // info about the internal key of this key expression - // used at signing time to derive the correct key - uint32_t key_derivation[MAX_BIP32_PATH_STEPS]; - uint8_t key_derivation_length; - - // same as key_derivation_length for internal key - // expressions; 0 for musig, as the key derivation in - // the PSBT use the aggregate key as the root - // used to identify the correct change/address_index from the psbt - uint8_t psbt_root_key_derivation_length; - - // the root pubkey of this key expression - serialized_extended_pubkey_t pubkey; - // the pubkey of the internal key of this key expression. - // same as `pubkey` for simple key expressions, but it's the actual - // internal key for musig key expressions - serialized_extended_pubkey_t internal_pubkey; - - bool is_tapscript; // true if signing with a BIP342 tapleaf script path spend - // only used for tapscripts - const policy_node_t *tapleaf_ptr; - uint8_t tapleaf_hash[32]; -} keyexpr_info_t; - -// Cache for partial hashes during signing (avoid quadratic hashing for segwit transactions) -typedef struct tx_hashes_s { - uint8_t sha_prevouts[32]; - uint8_t sha_amounts[32]; - uint8_t sha_scriptpubkeys[32]; - uint8_t sha_sequences[32]; - uint8_t sha_outputs[32]; -} tx_hashes_t; - -// the signing state for the current transaction; it does not contain any per-input state -typedef struct signing_state_s { - tx_hashes_t tx_hashes; - musig_signing_state_t musig; -} signing_state_t; - -// We cache the first 2 external outputs; that's needed for the swap checks -// Moreover, this helps the code for the simplified UX for transactions that -// have a single external output. -#define N_CACHED_EXTERNAL_OUTPUTS 2 - -typedef struct { - uint32_t master_key_fingerprint; - uint32_t tx_version; - uint32_t locktime; - - unsigned int n_internal_key_expressions; - keyexpr_info_t internal_key_expressions[MAX_INTERNAL_KEY_EXPRESSIONS]; - - unsigned int n_inputs; - uint8_t inputs_root[32]; // merkle root of the vector of input maps commitments - unsigned int n_outputs; - uint8_t outputs_root[32]; // merkle root of the vector of output maps commitments - - uint64_t inputs_total_amount; - - policy_map_wallet_header_t wallet_header; - - unsigned int n_external_inputs; - unsigned int n_external_outputs; - - // set to true if at least a PSBT_IN_MUSIG2_PUB_NONCE field is present in the PSBT - bool has_musig2_pub_nonces; - - // aggregate info on outputs - struct { - uint64_t total_amount; // amount of all the outputs (external + change) - uint64_t change_total_amount; // total amount of all change outputs - int n_change; // count of outputs compatible with change outputs - size_t output_script_lengths[N_CACHED_EXTERNAL_OUTPUTS]; - uint8_t output_scripts[N_CACHED_EXTERNAL_OUTPUTS][MAX_OUTPUT_SCRIPTPUBKEY_LEN]; - uint64_t output_amounts[N_CACHED_EXTERNAL_OUTPUTS]; - } outputs; - - bool is_wallet_default; - - uint8_t protocol_version; - - __attribute__((aligned(4))) uint8_t wallet_policy_map_bytes[MAX_WALLET_POLICY_BYTES]; - policy_node_t *wallet_policy_map; - - tx_ux_warning_t warnings; - -} sign_psbt_state_t; - /* BIP0341 tags for computing the tagged hashes when computing he sighash */ static const uint8_t BIP0341_sighash_tag[] = {'T', 'a', 'p', 'S', 'i', 'g', 'h', 'a', 's', 'h'}; @@ -2483,437 +2338,6 @@ static bool __attribute__((noinline)) sign_sighash_schnorr_and_yield(dispatcher_ return true; } -static bool __attribute__((noinline)) yield_musig_data(dispatcher_context_t *dc, - sign_psbt_state_t *st, - unsigned int cur_input_index, - const uint8_t *data, - size_t data_len, - uint32_t tag, - const uint8_t participant_pk[static 33], - const uint8_t aggregate_pubkey[static 33], - const uint8_t *tapleaf_hash) { - LOG_PROCESSOR(__FILE__, __LINE__, __func__); - - if (st->protocol_version == 0) { - // Only support version 1 of the protocol - return false; - } - - // bytes: 1 5 varint data_len 33 33 0 or 32 - // CMD_YIELD - // - - // Yield signature - uint8_t cmd = CCMD_YIELD; - dc->add_to_response(&cmd, 1); - - uint8_t buf[9]; - - // Add tag - int tag_varint_len = varint_write(buf, 0, tag); - dc->add_to_response(buf, tag_varint_len); - - // Add input index - int input_index_varint_len = varint_write(buf, 0, cur_input_index); - dc->add_to_response(buf, input_index_varint_len); - - // Add data (pubnonce or partial signature) - dc->add_to_response(data, data_len); - - // Add participant public key - dc->add_to_response(participant_pk, 33); - - // Add aggregate public key - dc->add_to_response(aggregate_pubkey, 33); - - // Add tapleaf hash if provided - if (tapleaf_hash != NULL) { - dc->add_to_response(tapleaf_hash, 32); - } - - dc->finalize_response(SW_INTERRUPTED_EXECUTION); - - if (dc->process_interruption(dc) < 0) { - return false; - } - return true; -} - -// Struct to hold the info computed for a given input in either of the two rounds -typedef struct { - plain_pk_t keys[MAX_PUBKEYS_PER_MUSIG]; - serialized_extended_pubkey_t agg_key_tweaked; - uint8_t psbt_session_id[32]; - uint8_t tweaks[3][32]; // 2 or three tweaks - size_t n_tweaks; - bool is_xonly[3]; // 2 or 3 elements -} musig_per_input_info_t; - -static bool compute_musig_per_input_info(dispatcher_context_t *dc, - sign_psbt_state_t *st, - signing_state_t *signing_state, - const input_info_t *input, - const keyexpr_info_t *keyexpr_info, - musig_per_input_info_t *out) { - LOG_PROCESSOR(__FILE__, __LINE__, __func__); - - if (st->wallet_policy_map->type != TOKEN_TR) { - SEND_SW(dc, SW_BAD_STATE); // should never happen - return false; - } - - const policy_node_tr_t *tr_policy = (policy_node_tr_t *) st->wallet_policy_map; - - // plan: - // 1) compute aggregate pubkey - // 2) compute musig2 tweaks - // 3) compute taproot tweak (if keypath spend) - // 4) compute the psbt_session_id that identifies the psbt-level signing session - - wallet_derivation_info_t wdi = {.n_keys = st->wallet_header.n_keys, - .wallet_version = st->wallet_header.version, - .keys_merkle_root = st->wallet_header.keys_info_merkle_root, - .change = input->in_out.is_change, - .address_index = input->in_out.address_index, - .sign_psbt_cache = NULL}; - - // TODO: code duplication with policy.c::get_derived_pubkey; worth extracting a common method? - - serialized_extended_pubkey_t ext_pubkey; - - const policy_node_keyexpr_t *key_expr = keyexpr_info->key_expression_ptr; - const musig_aggr_key_info_t *musig_info = r_musig_aggr_key_info(&key_expr->m.musig_info); - const uint16_t *key_indexes = r_uint16(&musig_info->key_indexes); - - LEDGER_ASSERT(musig_info->n <= MAX_PUBKEYS_PER_MUSIG, "Too many keys in musig key expression"); - for (int i = 0; i < musig_info->n; i++) { - // we use ext_pubkey as a temporary variable; will overwrite later - if (0 > get_extended_pubkey_from_client(dc, &wdi, key_indexes[i], &ext_pubkey)) { - return -1; - } - memcpy(out->keys[i], ext_pubkey.compressed_pubkey, sizeof(ext_pubkey.compressed_pubkey)); - } - - // sort the keys in ascending order using bubble sort - for (int i = 0; i < musig_info->n; i++) { - for (int j = 0; j < musig_info->n - 1; j++) { - if (memcmp(out->keys[j], out->keys[j + 1], sizeof(plain_pk_t)) > 0) { - uint8_t tmp[sizeof(plain_pk_t)]; - memcpy(tmp, out->keys[j], sizeof(plain_pk_t)); - memcpy(out->keys[j], out->keys[j + 1], sizeof(plain_pk_t)); - memcpy(out->keys[j + 1], tmp, sizeof(plain_pk_t)); - } - } - } - - // we already computed the aggregate (pre-tweaks) xpub in the keyexpr_info - memcpy(&ext_pubkey, &keyexpr_info->pubkey, sizeof(serialized_extended_pubkey_t)); - - // 2) compute musig2 tweaks - // We always have exactly 2 BIP32 tweaks in wallet policies; if the musig is in the keypath - // spend, we also have an x-only taptweak with the taproot tree hash (or BIP-86/BIP-386 style if - // there is no taproot tree). - - uint32_t change_step = input->in_out.is_change ? keyexpr_info->key_expression_ptr->num_second - : keyexpr_info->key_expression_ptr->num_first; - uint32_t addr_index_step = input->in_out.address_index; - - // in wallet policies, we always have at least two bip32-tweaks, and we might have - // one x-only tweak per BIP-0341 (if spending from the keypath). - out->is_xonly[0] = false; - out->is_xonly[1] = false; - out->n_tweaks = 2; // might be changed to 3 below - - if (0 > bip32_CKDpub(&ext_pubkey, change_step, &out->agg_key_tweaked, out->tweaks[0])) { - SEND_SW(dc, SW_BAD_STATE); // should never happen - return false; - } - - if (0 > bip32_CKDpub(&out->agg_key_tweaked, - addr_index_step, - &out->agg_key_tweaked, - out->tweaks[1])) { - SEND_SW(dc, SW_BAD_STATE); // should never happen - return false; - } - - // 3) compute taproot tweak (if keypath spend) - memset(out->tweaks[2], 0, 32); - if (!keyexpr_info->is_tapscript) { - out->n_tweaks = 3; - out->is_xonly[2] = true; - - crypto_tr_tagged_hash( - BIP0341_taptweak_tag, - sizeof(BIP0341_taptweak_tag), - out->agg_key_tweaked.compressed_pubkey + 1, // xonly key, after BIP-32 tweaks - 32, - input->taptree_hash, - // BIP-86 compliant tweak if there's no taptree, otherwise use the taptree hash - isnull_policy_node_tree(&tr_policy->tree) ? 0 : 32, - out->tweaks[2]); - - // also apply the taptweak to agg_key_tweaked - - uint8_t parity = 0; - crypto_tr_tweak_pubkey(out->agg_key_tweaked.compressed_pubkey + 1, - input->taptree_hash, - isnull_policy_node_tree(&tr_policy->tree) ? 0 : 32, - &parity, - out->agg_key_tweaked.compressed_pubkey + 1); - out->agg_key_tweaked.compressed_pubkey[0] = 0x02 + parity; - } - - // we will no longer use the other fields of the extended pubkey, so we zero them for sanity - memset(out->agg_key_tweaked.chain_code, 0, sizeof(out->agg_key_tweaked.chain_code)); - memset(out->agg_key_tweaked.child_number, 0, sizeof(out->agg_key_tweaked.child_number)); - out->agg_key_tweaked.depth = 0; - memset(out->agg_key_tweaked.parent_fingerprint, - 0, - sizeof(out->agg_key_tweaked.parent_fingerprint)); - memset(out->agg_key_tweaked.version, 0, sizeof(out->agg_key_tweaked.version)); - - // The psbt_session_id identifies the musig signing session for the entire (psbt, wallet_policy) - // pair, in both rounds 1 and 2 of the protocol; it is the same for all the musig placeholders - // in the policy (if more than one), and it is the same for all the inputs in the psbt. By - // making the hash depend on both the wallet policy and the transaction hashes, we make sure - // that an accidental collision is impossible, allowing for independent, parallel MuSig2 signing - // sessions for different transactions or wallet policies. - // Malicious collisions are not a concern, as they would only result in a signing failure (since - // the nonces would be incorrectly regenerated during round 2 of MuSig2). - crypto_tr_tagged_hash( - (uint8_t[]){'P', 's', 'b', 't', 'S', 'e', 's', 's', 'i', 'o', 'n', 'I', 'd'}, - 13, - st->wallet_header.keys_info_merkle_root, // TODO: wallet policy id would be more precise - 32, - (uint8_t *) &signing_state->tx_hashes, - sizeof(tx_hashes_t), - out->psbt_session_id); - - return true; -} - -static bool yield_musig_pubnonce(dispatcher_context_t *dc, - sign_psbt_state_t *st, - unsigned int cur_input_index, - const musig_pubnonce_t *pubnonce, - const uint8_t participant_pk[static 33], - const uint8_t aggregate_pubkey[static 33], - const uint8_t *tapleaf_hash) { - return yield_musig_data(dc, - st, - cur_input_index, - (const uint8_t *) pubnonce, - sizeof(musig_pubnonce_t), - CCMD_YIELD_MUSIG_PUBNONCE_TAG, - participant_pk, - aggregate_pubkey, - tapleaf_hash); -} - -static bool yield_musig_partial_signature(dispatcher_context_t *dc, - sign_psbt_state_t *st, - unsigned int cur_input_index, - const uint8_t psig[static 32], - const uint8_t participant_pk[static 33], - const uint8_t aggregate_pubkey[static 33], - const uint8_t *tapleaf_hash) { - return yield_musig_data(dc, - st, - cur_input_index, - psig, - 32, - CCMD_YIELD_MUSIG_PARTIALSIGNATURE_TAG, - participant_pk, - aggregate_pubkey, - tapleaf_hash); -} - -static bool __attribute__((noinline)) -sign_sighash_musig_and_yield(dispatcher_context_t *dc, - sign_psbt_state_t *st, - signing_state_t *signing_state, - const keyexpr_info_t *keyexpr_info, - const input_info_t *input, - unsigned int cur_input_index, - uint8_t sighash[static 32]) { - LOG_PROCESSOR(__FILE__, __LINE__, __func__); - - musig_per_input_info_t musig_per_input_info; - if (!compute_musig_per_input_info(dc, - st, - signing_state, - input, - keyexpr_info, - &musig_per_input_info)) { - return false; - } - - // 4) check if my pubnonce is in the psbt - // Compute musig_my_psbt_id. It is the psbt key that this signer uses to find pubnonces and - // partial signatures (PSBT_IN_MUSIG2_PUB_NONCE and PSBT_IN_MUSIG2_PARTIAL_SIG fields). The - // length is either 33+33 (keypath spend), or 33+33+32 bytes (tapscript spend). It's the - // concatenation of: - // - the 33-byte compressed pubkey of this participant - // - the 33-byte aggregate compressed pubkey (after all the tweaks) - // - (tapscript only) the 32-byte tapleaf hash - uint8_t musig_my_psbt_id_key[1 + 33 + 33 + 32]; - musig_my_psbt_id_key[0] = PSBT_IN_MUSIG2_PUB_NONCE; - - uint8_t *musig_my_psbt_id = musig_my_psbt_id_key + 1; - size_t psbt_id_len = keyexpr_info->is_tapscript ? 33 + 33 + 32 : 33 + 33; - memcpy(musig_my_psbt_id, keyexpr_info->internal_pubkey.compressed_pubkey, 33); - memcpy(musig_my_psbt_id + 33, musig_per_input_info.agg_key_tweaked.compressed_pubkey, 33); - if (keyexpr_info->is_tapscript) { - memcpy(musig_my_psbt_id + 33 + 33, keyexpr_info->tapleaf_hash, 32); - } - musig_pubnonce_t my_pubnonce; - if (sizeof(musig_pubnonce_t) != call_get_merkleized_map_value(dc, - &input->in_out.map, - musig_my_psbt_id_key, - 1 + psbt_id_len, - my_pubnonce.raw, - sizeof(musig_pubnonce_t))) { - PRINTF("Missing pubnonce in PSBT\n"); - SEND_SW(dc, SW_BAD_STATE); - return false; - } - /** - * Round 2 of the MuSig2 protocol - **/ - - const musig_psbt_session_t *psbt_session = - musigsession_round2_initialize(musig_per_input_info.psbt_session_id, &signing_state->musig); - - if (psbt_session == NULL) { - // The PSBT contains a partial nonce, but we do not have the corresponding psbt - // session in storage. Either it was deleted, or the pubnonces were not real. Either - // way, we cannot continue. - PRINTF("Missing MuSig2 session\n"); - SEND_SW(dc, SW_BAD_STATE); - return false; - } - - // 6) generate and yield partial signature - - const policy_node_keyexpr_t *key_expr = keyexpr_info->key_expression_ptr; - const musig_aggr_key_info_t *musig_info = r_musig_aggr_key_info(&key_expr->m.musig_info); - - musig_pubnonce_t nonces[MAX_PUBKEYS_PER_MUSIG]; - - for (int i = 0; i < musig_info->n; i++) { - uint8_t musig_ith_psbt_id_key[1 + 33 + 33 + 32]; - uint8_t *musig_ith_psbt_id = musig_ith_psbt_id_key + 1; - // copy from musig_my_psbt_id_key, but replace the corresponding pubkey - memcpy(musig_ith_psbt_id_key, musig_my_psbt_id_key, sizeof(musig_my_psbt_id_key)); - memcpy(musig_ith_psbt_id, musig_per_input_info.keys[i], sizeof(plain_pk_t)); - - if (sizeof(musig_pubnonce_t) != call_get_merkleized_map_value(dc, - &input->in_out.map, - musig_ith_psbt_id_key, - 1 + psbt_id_len, - nonces[i].raw, - sizeof(musig_pubnonce_t))) { - PRINTF("Missing or incorrect pubnonce for a MuSig2 cosigner\n"); - SEND_SW(dc, SW_INCORRECT_DATA); - return false; - } - } - - // compute aggregate nonce - musig_pubnonce_t aggnonce; - int res = musig_nonce_agg(nonces, musig_info->n, &aggnonce); - if (res < 0) { - PRINTF("Musig aggregation failed; disruptive signer has index %d\n", -res); - SEND_SW(dc, SW_INCORRECT_DATA); - } - - // recompute secnonce from psbt_session randomness - uint8_t rand_i_j[32]; - compute_rand_i_j(psbt_session, cur_input_index, keyexpr_info->index, rand_i_j); - - musig_secnonce_t secnonce; - musig_pubnonce_t pubnonce; - - if (0 > musig_nonce_gen(rand_i_j, - keyexpr_info->internal_pubkey.compressed_pubkey, - musig_per_input_info.agg_key_tweaked.compressed_pubkey + 1, - &secnonce, - &pubnonce)) { - PRINTF("MuSig2 nonce generation failed\n"); - SEND_SW(dc, SW_BAD_STATE); // should never happen - return false; - } - - // derive secret key - - cx_ecfp_private_key_t private_key = {0}; - uint8_t psig[32]; - bool err = false; - do { // block executed once, only to allow safely breaking out on error - - // derive secret key - uint32_t sign_path[MAX_BIP32_PATH_STEPS]; - - for (int i = 0; i < keyexpr_info->key_derivation_length; i++) { - sign_path[i] = keyexpr_info->key_derivation[i]; - } - int sign_path_len = keyexpr_info->key_derivation_length; - - if (bip32_derive_init_privkey_256(CX_CURVE_256K1, - sign_path, - sign_path_len, - &private_key, - NULL) != CX_OK) { - err = true; - break; - } - - // Create partial signature - uint8_t *tweaks_ptrs[3] = { - musig_per_input_info.tweaks[0], - musig_per_input_info.tweaks[1], - musig_per_input_info.tweaks[2] // the last element is ignored if n_tweaks == 2 - }; - musig_session_context_t musig_session_context = {.aggnonce = &aggnonce, - .n_keys = musig_info->n, - .pubkeys = musig_per_input_info.keys, - .n_tweaks = musig_per_input_info.n_tweaks, - .tweaks = tweaks_ptrs, - .is_xonly = musig_per_input_info.is_xonly, - .msg = sighash, - .msg_len = 32}; - - if (0 > musig_sign(&secnonce, private_key.d, &musig_session_context, psig)) { - PRINTF("Musig2 signature failed\n"); - err = true; - break; - } - } while (false); - - explicit_bzero(&private_key, sizeof(private_key)); - - if (err) { - PRINTF("Partial signature generation failed\n"); - return false; - } - - if (!yield_musig_partial_signature( - dc, - st, - cur_input_index, - psig, - keyexpr_info->internal_pubkey.compressed_pubkey, - musig_per_input_info.agg_key_tweaked.compressed_pubkey, - keyexpr_info->is_tapscript ? keyexpr_info->tapleaf_hash : NULL)) { - PRINTF("Failed yielding MuSig2 partial signature\n"); - SEND_SW(dc, SW_BAD_STATE); // should never happen - return false; - } - - return true; -} - static bool __attribute__((noinline)) compute_tx_hashes(dispatcher_context_t *dc, sign_psbt_state_t *st, tx_hashes_t *hashes) { { @@ -3279,69 +2703,6 @@ fill_taproot_keyexpr_info(dispatcher_context_t *dc, return true; } -static bool produce_and_yield_pubnonce(dispatcher_context_t *dc, - sign_psbt_state_t *st, - signing_state_t *signing_state, - const keyexpr_info_t *keyexpr_info, - const input_info_t *input, - unsigned int cur_input_index) { - LOG_PROCESSOR(__FILE__, __LINE__, __func__); - - musig_per_input_info_t musig_per_input_info; - if (!compute_musig_per_input_info(dc, - st, - signing_state, - input, - keyexpr_info, - &musig_per_input_info)) { - return false; - } - - /** - * Round 1 of the MuSig2 protocol - **/ - - const musig_psbt_session_t *psbt_session = - musigsession_round1_initialize(musig_per_input_info.psbt_session_id, &signing_state->musig); - if (psbt_session == NULL) { - // This should never happen - PRINTF("Unexpected: failed to initialize MuSig2 round 1\n"); - SEND_SW(dc, SW_BAD_STATE); - return false; - } - - // 5) generate and yield pubnonce - - uint8_t rand_i_j[32]; - compute_rand_i_j(psbt_session, cur_input_index, keyexpr_info->index, rand_i_j); - - musig_secnonce_t secnonce; - musig_pubnonce_t pubnonce; - if (0 > musig_nonce_gen(rand_i_j, - keyexpr_info->internal_pubkey.compressed_pubkey, - musig_per_input_info.agg_key_tweaked.compressed_pubkey + 1, - &secnonce, - &pubnonce)) { - PRINTF("MuSig2 nonce generation failed\n"); - SEND_SW(dc, SW_BAD_STATE); // should never happen - return false; - } - - if (!yield_musig_pubnonce(dc, - st, - cur_input_index, - &pubnonce, - keyexpr_info->internal_pubkey.compressed_pubkey, - musig_per_input_info.agg_key_tweaked.compressed_pubkey, - keyexpr_info->is_tapscript ? keyexpr_info->tapleaf_hash : NULL)) { - PRINTF("Failed yielding MuSig2 pubnonce\n"); - SEND_SW(dc, SW_BAD_STATE); // should never happen - return false; - } - - return true; -} - static bool __attribute__((noinline)) produce_musig2_pubnonces( dispatcher_context_t *dc, sign_psbt_state_t *st, diff --git a/src/handler/sign_psbt.h b/src/handler/sign_psbt.h new file mode 100644 index 000000000..c132b17f2 --- /dev/null +++ b/src/handler/sign_psbt.h @@ -0,0 +1,152 @@ +#pragma once + +#include "../musig/musig_sessions.h" +#include "../common/merkle.h" +#include "../ui/display.h" + +// common info that applies to either the current input or the current output +typedef struct { + merkleized_map_commitment_t map; + + bool unexpected_pubkey_error; // Set to true if the pubkey in the keydata of + // PSBT_{IN,OUT}_BIP32_DERIVATION or + // PSBT_{IN,OUT}_TAP_BIP32_DERIVATION is not the correct length. + + bool key_expression_found; // Set to true if the input/output info in the psbt was correctly + // matched with the current key expression in the signing flow + + bool is_change; + int address_index; + + // For an output, its scriptPubKey + // for an input, the prevout's scriptPubKey (either from the non-witness-utxo, or from the + // witness-utxo) + + uint8_t scriptPubKey[MAX_OUTPUT_SCRIPTPUBKEY_LEN]; + size_t scriptPubKey_len; +} in_out_info_t; + +typedef struct { + in_out_info_t in_out; + bool has_witnessUtxo; + bool has_nonWitnessUtxo; + bool has_redeemScript; + bool has_sighash_type; + + uint64_t prevout_amount; // the value of the prevout of the current input + + // we no longer need the script when we compute the taptree hash right before a taproot key-path + // spending; therefore, we reuse the same memory + union { + // the script used when signing, either from the witness utxo or the redeem script + uint8_t script[MAX_PREVOUT_SCRIPTPUBKEY_LEN]; + uint8_t taptree_hash[32]; + }; + + size_t script_len; + + uint32_t sighash_type; +} input_info_t; + +typedef struct { + in_out_info_t in_out; + uint64_t value; +} output_info_t; + +typedef struct { + policy_node_keyexpr_t *key_expression_ptr; + // index of this key expression in the descriptor template, in parsing order + int index; + uint32_t fingerprint; + + // we only sign for keys expressions for which we find a matching key derivation in the PSBT, + // at least for one of the inputs + bool to_sign; + + // info about the internal key of this key expression + // used at signing time to derive the correct key + uint32_t key_derivation[MAX_BIP32_PATH_STEPS]; + uint8_t key_derivation_length; + + // same as key_derivation_length for internal key + // expressions; 0 for musig, as the key derivation in + // the PSBT use the aggregate key as the root + // used to identify the correct change/address_index from the psbt + uint8_t psbt_root_key_derivation_length; + + // the root pubkey of this key expression + serialized_extended_pubkey_t pubkey; + // the pubkey of the internal key of this key expression. + // same as `pubkey` for simple key expressions, but it's the actual + // internal key for musig key expressions + serialized_extended_pubkey_t internal_pubkey; + + bool is_tapscript; // true if signing with a BIP342 tapleaf script path spend + // only used for tapscripts + const policy_node_t *tapleaf_ptr; + uint8_t tapleaf_hash[32]; +} keyexpr_info_t; + +// Cache for partial hashes during signing (avoid quadratic hashing for segwit transactions) +typedef struct tx_hashes_s { + uint8_t sha_prevouts[32]; + uint8_t sha_amounts[32]; + uint8_t sha_scriptpubkeys[32]; + uint8_t sha_sequences[32]; + uint8_t sha_outputs[32]; +} tx_hashes_t; + +// the signing state for the current transaction; it does not contain any per-input state +typedef struct signing_state_s { + tx_hashes_t tx_hashes; + musig_signing_state_t musig; +} signing_state_t; + +// We cache the first 2 external outputs; that's needed for the swap checks +// Moreover, this helps the code for the simplified UX for transactions that +// have a single external output. +#define N_CACHED_EXTERNAL_OUTPUTS 2 + +typedef struct { + uint32_t master_key_fingerprint; + uint32_t tx_version; + uint32_t locktime; + + unsigned int n_internal_key_expressions; + keyexpr_info_t internal_key_expressions[MAX_INTERNAL_KEY_EXPRESSIONS]; + + unsigned int n_inputs; + uint8_t inputs_root[32]; // merkle root of the vector of input maps commitments + unsigned int n_outputs; + uint8_t outputs_root[32]; // merkle root of the vector of output maps commitments + + uint64_t inputs_total_amount; + + policy_map_wallet_header_t wallet_header; + + unsigned int n_external_inputs; + unsigned int n_external_outputs; + + // set to true if at least a PSBT_IN_MUSIG2_PUB_NONCE field is present in the PSBT + bool has_musig2_pub_nonces; + + // aggregate info on outputs + struct { + uint64_t total_amount; // amount of all the outputs (external + change) + uint64_t change_total_amount; // total amount of all change outputs + int n_change; // count of outputs compatible with change outputs + size_t output_script_lengths[N_CACHED_EXTERNAL_OUTPUTS]; + uint8_t output_scripts[N_CACHED_EXTERNAL_OUTPUTS][MAX_OUTPUT_SCRIPTPUBKEY_LEN]; + uint64_t output_amounts[N_CACHED_EXTERNAL_OUTPUTS]; + } outputs; + + bool is_wallet_default; + + uint8_t protocol_version; + + __attribute__((aligned(4))) uint8_t wallet_policy_map_bytes[MAX_WALLET_POLICY_BYTES]; + policy_node_t *wallet_policy_map; + + tx_ux_warning_t warnings; + +} sign_psbt_state_t; diff --git a/src/handler/sign_psbt/musig_signing.c b/src/handler/sign_psbt/musig_signing.c new file mode 100644 index 000000000..068b320b7 --- /dev/null +++ b/src/handler/sign_psbt/musig_signing.c @@ -0,0 +1,490 @@ +#include "musig_signing.h" +#include "lib_standard_app/crypto_helpers.h" +#include "../boilerplate/sw.h" +#include "../common/psbt.h" +#include "../client_commands.h" +#include "../lib/get_merkleized_map_value.h" +#include "../lib/policy.h" + +bool compute_musig_per_input_info(dispatcher_context_t *dc, + sign_psbt_state_t *st, + signing_state_t *signing_state, + const input_info_t *input, + const keyexpr_info_t *keyexpr_info, + musig_per_input_info_t *out) { + LOG_PROCESSOR(__FILE__, __LINE__, __func__); + + if (st->wallet_policy_map->type != TOKEN_TR) { + SEND_SW(dc, SW_BAD_STATE); // should never happen + return false; + } + + const policy_node_tr_t *tr_policy = (policy_node_tr_t *) st->wallet_policy_map; + + // plan: + // 1) compute aggregate pubkey + // 2) compute musig2 tweaks + // 3) compute taproot tweak (if keypath spend) + // 4) compute the psbt_session_id that identifies the psbt-level signing session + + wallet_derivation_info_t wdi = {.n_keys = st->wallet_header.n_keys, + .wallet_version = st->wallet_header.version, + .keys_merkle_root = st->wallet_header.keys_info_merkle_root, + .change = input->in_out.is_change, + .address_index = input->in_out.address_index, + .sign_psbt_cache = NULL}; + + // TODO: code duplication with policy.c::get_derived_pubkey; worth extracting a common method? + + serialized_extended_pubkey_t ext_pubkey; + + const policy_node_keyexpr_t *key_expr = keyexpr_info->key_expression_ptr; + const musig_aggr_key_info_t *musig_info = r_musig_aggr_key_info(&key_expr->m.musig_info); + const uint16_t *key_indexes = r_uint16(&musig_info->key_indexes); + + LEDGER_ASSERT(musig_info->n <= MAX_PUBKEYS_PER_MUSIG, "Too many keys in musig key expression"); + for (int i = 0; i < musig_info->n; i++) { + // we use ext_pubkey as a temporary variable; will overwrite later + if (0 > get_extended_pubkey_from_client(dc, &wdi, key_indexes[i], &ext_pubkey)) { + return -1; + } + memcpy(out->keys[i], ext_pubkey.compressed_pubkey, sizeof(ext_pubkey.compressed_pubkey)); + } + + // sort the keys in ascending order using bubble sort + for (int i = 0; i < musig_info->n; i++) { + for (int j = 0; j < musig_info->n - 1; j++) { + if (memcmp(out->keys[j], out->keys[j + 1], sizeof(plain_pk_t)) > 0) { + uint8_t tmp[sizeof(plain_pk_t)]; + memcpy(tmp, out->keys[j], sizeof(plain_pk_t)); + memcpy(out->keys[j], out->keys[j + 1], sizeof(plain_pk_t)); + memcpy(out->keys[j + 1], tmp, sizeof(plain_pk_t)); + } + } + } + + // we already computed the aggregate (pre-tweaks) xpub in the keyexpr_info + memcpy(&ext_pubkey, &keyexpr_info->pubkey, sizeof(serialized_extended_pubkey_t)); + + // 2) compute musig2 tweaks + // We always have exactly 2 BIP32 tweaks in wallet policies; if the musig is in the keypath + // spend, we also have an x-only taptweak with the taproot tree hash (or BIP-86/BIP-386 style if + // there is no taproot tree). + + uint32_t change_step = input->in_out.is_change ? keyexpr_info->key_expression_ptr->num_second + : keyexpr_info->key_expression_ptr->num_first; + uint32_t addr_index_step = input->in_out.address_index; + + // in wallet policies, we always have at least two bip32-tweaks, and we might have + // one x-only tweak per BIP-0341 (if spending from the keypath). + out->is_xonly[0] = false; + out->is_xonly[1] = false; + out->n_tweaks = 2; // might be changed to 3 below + + if (0 > bip32_CKDpub(&ext_pubkey, change_step, &out->agg_key_tweaked, out->tweaks[0])) { + SEND_SW(dc, SW_BAD_STATE); // should never happen + return false; + } + + if (0 > bip32_CKDpub(&out->agg_key_tweaked, + addr_index_step, + &out->agg_key_tweaked, + out->tweaks[1])) { + SEND_SW(dc, SW_BAD_STATE); // should never happen + return false; + } + + // 3) compute taproot tweak (if keypath spend) + memset(out->tweaks[2], 0, 32); + if (!keyexpr_info->is_tapscript) { + out->n_tweaks = 3; + out->is_xonly[2] = true; + + crypto_tr_tagged_hash( + BIP0341_taptweak_tag, + sizeof(BIP0341_taptweak_tag), + out->agg_key_tweaked.compressed_pubkey + 1, // xonly key, after BIP-32 tweaks + 32, + input->taptree_hash, + // BIP-86 compliant tweak if there's no taptree, otherwise use the taptree hash + isnull_policy_node_tree(&tr_policy->tree) ? 0 : 32, + out->tweaks[2]); + + // also apply the taptweak to agg_key_tweaked + + uint8_t parity = 0; + crypto_tr_tweak_pubkey(out->agg_key_tweaked.compressed_pubkey + 1, + input->taptree_hash, + isnull_policy_node_tree(&tr_policy->tree) ? 0 : 32, + &parity, + out->agg_key_tweaked.compressed_pubkey + 1); + out->agg_key_tweaked.compressed_pubkey[0] = 0x02 + parity; + } + + // we will no longer use the other fields of the extended pubkey, so we zero them for sanity + memset(out->agg_key_tweaked.chain_code, 0, sizeof(out->agg_key_tweaked.chain_code)); + memset(out->agg_key_tweaked.child_number, 0, sizeof(out->agg_key_tweaked.child_number)); + out->agg_key_tweaked.depth = 0; + memset(out->agg_key_tweaked.parent_fingerprint, + 0, + sizeof(out->agg_key_tweaked.parent_fingerprint)); + memset(out->agg_key_tweaked.version, 0, sizeof(out->agg_key_tweaked.version)); + + // The psbt_session_id identifies the musig signing session for the entire (psbt, wallet_policy) + // pair, in both rounds 1 and 2 of the protocol; it is the same for all the musig placeholders + // in the policy (if more than one), and it is the same for all the inputs in the psbt. By + // making the hash depend on both the wallet policy and the transaction hashes, we make sure + // that an accidental collision is impossible, allowing for independent, parallel MuSig2 signing + // sessions for different transactions or wallet policies. + // Malicious collisions are not a concern, as they would only result in a signing failure (since + // the nonces would be incorrectly regenerated during round 2 of MuSig2). + crypto_tr_tagged_hash( + (uint8_t[]){'P', 's', 'b', 't', 'S', 'e', 's', 's', 'i', 'o', 'n', 'I', 'd'}, + 13, + st->wallet_header.keys_info_merkle_root, // TODO: wallet policy id would be more precise + 32, + (uint8_t *) &signing_state->tx_hashes, + sizeof(tx_hashes_t), + out->psbt_session_id); + + return true; +} + +static bool __attribute__((noinline)) yield_musig_data(dispatcher_context_t *dc, + sign_psbt_state_t *st, + unsigned int cur_input_index, + const uint8_t *data, + size_t data_len, + uint32_t tag, + const uint8_t participant_pk[static 33], + const uint8_t aggregate_pubkey[static 33], + const uint8_t *tapleaf_hash) { + LOG_PROCESSOR(__FILE__, __LINE__, __func__); + + if (st->protocol_version == 0) { + // Only support version 1 of the protocol + return false; + } + + // bytes: 1 5 varint data_len 33 33 0 or 32 + // CMD_YIELD + // + + // Yield signature + uint8_t cmd = CCMD_YIELD; + dc->add_to_response(&cmd, 1); + + uint8_t buf[9]; + + // Add tag + int tag_varint_len = varint_write(buf, 0, tag); + dc->add_to_response(buf, tag_varint_len); + + // Add input index + int input_index_varint_len = varint_write(buf, 0, cur_input_index); + dc->add_to_response(buf, input_index_varint_len); + + // Add data (pubnonce or partial signature) + dc->add_to_response(data, data_len); + + // Add participant public key + dc->add_to_response(participant_pk, 33); + + // Add aggregate public key + dc->add_to_response(aggregate_pubkey, 33); + + // Add tapleaf hash if provided + if (tapleaf_hash != NULL) { + dc->add_to_response(tapleaf_hash, 32); + } + + dc->finalize_response(SW_INTERRUPTED_EXECUTION); + + if (dc->process_interruption(dc) < 0) { + return false; + } + return true; +} + +static bool yield_musig_pubnonce(dispatcher_context_t *dc, + sign_psbt_state_t *st, + unsigned int cur_input_index, + const musig_pubnonce_t *pubnonce, + const uint8_t participant_pk[static 33], + const uint8_t aggregate_pubkey[static 33], + const uint8_t *tapleaf_hash) { + return yield_musig_data(dc, + st, + cur_input_index, + (const uint8_t *) pubnonce, + sizeof(musig_pubnonce_t), + CCMD_YIELD_MUSIG_PUBNONCE_TAG, + participant_pk, + aggregate_pubkey, + tapleaf_hash); +} + +static bool yield_musig_partial_signature(dispatcher_context_t *dc, + sign_psbt_state_t *st, + unsigned int cur_input_index, + const uint8_t psig[static 32], + const uint8_t participant_pk[static 33], + const uint8_t aggregate_pubkey[static 33], + const uint8_t *tapleaf_hash) { + return yield_musig_data(dc, + st, + cur_input_index, + psig, + 32, + CCMD_YIELD_MUSIG_PARTIALSIGNATURE_TAG, + participant_pk, + aggregate_pubkey, + tapleaf_hash); +} + +bool produce_and_yield_pubnonce(dispatcher_context_t *dc, + sign_psbt_state_t *st, + signing_state_t *signing_state, + const keyexpr_info_t *keyexpr_info, + const input_info_t *input, + unsigned int cur_input_index) { + LOG_PROCESSOR(__FILE__, __LINE__, __func__); + + musig_per_input_info_t musig_per_input_info; + if (!compute_musig_per_input_info(dc, + st, + signing_state, + input, + keyexpr_info, + &musig_per_input_info)) { + return false; + } + + /** + * Round 1 of the MuSig2 protocol + **/ + + const musig_psbt_session_t *psbt_session = + musigsession_round1_initialize(musig_per_input_info.psbt_session_id, &signing_state->musig); + if (psbt_session == NULL) { + // This should never happen + PRINTF("Unexpected: failed to initialize MuSig2 round 1\n"); + SEND_SW(dc, SW_BAD_STATE); + return false; + } + + // 5) generate and yield pubnonce + + uint8_t rand_i_j[32]; + compute_rand_i_j(psbt_session, cur_input_index, keyexpr_info->index, rand_i_j); + + musig_secnonce_t secnonce; + musig_pubnonce_t pubnonce; + if (0 > musig_nonce_gen(rand_i_j, + keyexpr_info->internal_pubkey.compressed_pubkey, + musig_per_input_info.agg_key_tweaked.compressed_pubkey + 1, + &secnonce, + &pubnonce)) { + PRINTF("MuSig2 nonce generation failed\n"); + SEND_SW(dc, SW_BAD_STATE); // should never happen + return false; + } + + if (!yield_musig_pubnonce(dc, + st, + cur_input_index, + &pubnonce, + keyexpr_info->internal_pubkey.compressed_pubkey, + musig_per_input_info.agg_key_tweaked.compressed_pubkey, + keyexpr_info->is_tapscript ? keyexpr_info->tapleaf_hash : NULL)) { + PRINTF("Failed yielding MuSig2 pubnonce\n"); + SEND_SW(dc, SW_BAD_STATE); // should never happen + return false; + } + + return true; +} + +bool __attribute__((noinline)) sign_sighash_musig_and_yield(dispatcher_context_t *dc, + sign_psbt_state_t *st, + signing_state_t *signing_state, + const keyexpr_info_t *keyexpr_info, + const input_info_t *input, + unsigned int cur_input_index, + uint8_t sighash[static 32]) { + LOG_PROCESSOR(__FILE__, __LINE__, __func__); + + musig_per_input_info_t musig_per_input_info; + if (!compute_musig_per_input_info(dc, + st, + signing_state, + input, + keyexpr_info, + &musig_per_input_info)) { + return false; + } + + // 4) check if my pubnonce is in the psbt + // Compute musig_my_psbt_id. It is the psbt key that this signer uses to find pubnonces and + // partial signatures (PSBT_IN_MUSIG2_PUB_NONCE and PSBT_IN_MUSIG2_PARTIAL_SIG fields). The + // length is either 33+33 (keypath spend), or 33+33+32 bytes (tapscript spend). It's the + // concatenation of: + // - the 33-byte compressed pubkey of this participant + // - the 33-byte aggregate compressed pubkey (after all the tweaks) + // - (tapscript only) the 32-byte tapleaf hash + uint8_t musig_my_psbt_id_key[1 + 33 + 33 + 32]; + musig_my_psbt_id_key[0] = PSBT_IN_MUSIG2_PUB_NONCE; + + uint8_t *musig_my_psbt_id = musig_my_psbt_id_key + 1; + size_t psbt_id_len = keyexpr_info->is_tapscript ? 33 + 33 + 32 : 33 + 33; + memcpy(musig_my_psbt_id, keyexpr_info->internal_pubkey.compressed_pubkey, 33); + memcpy(musig_my_psbt_id + 33, musig_per_input_info.agg_key_tweaked.compressed_pubkey, 33); + if (keyexpr_info->is_tapscript) { + memcpy(musig_my_psbt_id + 33 + 33, keyexpr_info->tapleaf_hash, 32); + } + musig_pubnonce_t my_pubnonce; + if (sizeof(musig_pubnonce_t) != call_get_merkleized_map_value(dc, + &input->in_out.map, + musig_my_psbt_id_key, + 1 + psbt_id_len, + my_pubnonce.raw, + sizeof(musig_pubnonce_t))) { + PRINTF("Missing pubnonce in PSBT\n"); + SEND_SW(dc, SW_BAD_STATE); + return false; + } + /** + * Round 2 of the MuSig2 protocol + **/ + + const musig_psbt_session_t *psbt_session = + musigsession_round2_initialize(musig_per_input_info.psbt_session_id, &signing_state->musig); + + if (psbt_session == NULL) { + // The PSBT contains a partial nonce, but we do not have the corresponding psbt + // session in storage. Either it was deleted, or the pubnonces were not real. Either + // way, we cannot continue. + PRINTF("Missing MuSig2 session\n"); + SEND_SW(dc, SW_BAD_STATE); + return false; + } + + // 6) generate and yield partial signature + + const policy_node_keyexpr_t *key_expr = keyexpr_info->key_expression_ptr; + const musig_aggr_key_info_t *musig_info = r_musig_aggr_key_info(&key_expr->m.musig_info); + + musig_pubnonce_t nonces[MAX_PUBKEYS_PER_MUSIG]; + + for (int i = 0; i < musig_info->n; i++) { + uint8_t musig_ith_psbt_id_key[1 + 33 + 33 + 32]; + uint8_t *musig_ith_psbt_id = musig_ith_psbt_id_key + 1; + // copy from musig_my_psbt_id_key, but replace the corresponding pubkey + memcpy(musig_ith_psbt_id_key, musig_my_psbt_id_key, sizeof(musig_my_psbt_id_key)); + memcpy(musig_ith_psbt_id, musig_per_input_info.keys[i], sizeof(plain_pk_t)); + + if (sizeof(musig_pubnonce_t) != call_get_merkleized_map_value(dc, + &input->in_out.map, + musig_ith_psbt_id_key, + 1 + psbt_id_len, + nonces[i].raw, + sizeof(musig_pubnonce_t))) { + PRINTF("Missing or incorrect pubnonce for a MuSig2 cosigner\n"); + SEND_SW(dc, SW_INCORRECT_DATA); + return false; + } + } + + // compute aggregate nonce + musig_pubnonce_t aggnonce; + int res = musig_nonce_agg(nonces, musig_info->n, &aggnonce); + if (res < 0) { + PRINTF("Musig aggregation failed; disruptive signer has index %d\n", -res); + SEND_SW(dc, SW_INCORRECT_DATA); + } + + // recompute secnonce from psbt_session randomness + uint8_t rand_i_j[32]; + compute_rand_i_j(psbt_session, cur_input_index, keyexpr_info->index, rand_i_j); + + musig_secnonce_t secnonce; + musig_pubnonce_t pubnonce; + + if (0 > musig_nonce_gen(rand_i_j, + keyexpr_info->internal_pubkey.compressed_pubkey, + musig_per_input_info.agg_key_tweaked.compressed_pubkey + 1, + &secnonce, + &pubnonce)) { + PRINTF("MuSig2 nonce generation failed\n"); + SEND_SW(dc, SW_BAD_STATE); // should never happen + return false; + } + + // derive secret key + + cx_ecfp_private_key_t private_key = {0}; + uint8_t psig[32]; + bool err = false; + do { // block executed once, only to allow safely breaking out on error + + // derive secret key + uint32_t sign_path[MAX_BIP32_PATH_STEPS]; + + for (int i = 0; i < keyexpr_info->key_derivation_length; i++) { + sign_path[i] = keyexpr_info->key_derivation[i]; + } + int sign_path_len = keyexpr_info->key_derivation_length; + + if (bip32_derive_init_privkey_256(CX_CURVE_256K1, + sign_path, + sign_path_len, + &private_key, + NULL) != CX_OK) { + err = true; + break; + } + + // Create partial signature + uint8_t *tweaks_ptrs[3] = { + musig_per_input_info.tweaks[0], + musig_per_input_info.tweaks[1], + musig_per_input_info.tweaks[2] // the last element is ignored if n_tweaks == 2 + }; + musig_session_context_t musig_session_context = {.aggnonce = &aggnonce, + .n_keys = musig_info->n, + .pubkeys = musig_per_input_info.keys, + .n_tweaks = musig_per_input_info.n_tweaks, + .tweaks = tweaks_ptrs, + .is_xonly = musig_per_input_info.is_xonly, + .msg = sighash, + .msg_len = 32}; + + if (0 > musig_sign(&secnonce, private_key.d, &musig_session_context, psig)) { + PRINTF("Musig2 signature failed\n"); + err = true; + break; + } + } while (false); + + explicit_bzero(&private_key, sizeof(private_key)); + + if (err) { + PRINTF("Partial signature generation failed\n"); + return false; + } + + if (!yield_musig_partial_signature( + dc, + st, + cur_input_index, + psig, + keyexpr_info->internal_pubkey.compressed_pubkey, + musig_per_input_info.agg_key_tweaked.compressed_pubkey, + keyexpr_info->is_tapscript ? keyexpr_info->tapleaf_hash : NULL)) { + PRINTF("Failed yielding MuSig2 partial signature\n"); + SEND_SW(dc, SW_BAD_STATE); // should never happen + return false; + } + + return true; +} diff --git a/src/handler/sign_psbt/musig_signing.h b/src/handler/sign_psbt/musig_signing.h new file mode 100644 index 000000000..ae20c484b --- /dev/null +++ b/src/handler/sign_psbt/musig_signing.h @@ -0,0 +1,60 @@ +#include +#include "../common/wallet.h" +#include "../musig/musig.h" +#include "../boilerplate/dispatcher.h" +#include "../sign_psbt.h" + +// Struct to hold the info computed for a given input in either of the two rounds +typedef struct { + plain_pk_t keys[MAX_PUBKEYS_PER_MUSIG]; + serialized_extended_pubkey_t agg_key_tweaked; + uint8_t psbt_session_id[32]; + uint8_t tweaks[3][32]; // 2 or three tweaks + size_t n_tweaks; // always 2 or 3 for supported BIP-388 wallet policies + bool is_xonly[3]; // 2 or 3 elements +} musig_per_input_info_t; + +/** + * Computes the MuSig2 per-input, per-key-expression information. + * + * This function calculates the necessary information for each input in the MuSig protocol. + * It is the shared logic that is common between both rounds of the MuSig2 protocol. + * + * Returns true if the computation is successful, false otherwise. In case of failure, it already + * sends an error status word. + */ +bool compute_musig_per_input_info(dispatcher_context_t *dc, + sign_psbt_state_t *st, + signing_state_t *signing_state, + const input_info_t *input, + const keyexpr_info_t *keyexpr_info, + musig_per_input_info_t *out); + +/** + * Computes and yields the pubnonce for the current input and placeholder, during Round 1 of the + * MuSig2 protocol. + * + * Returns true if the computation is successful, false otherwise. In case of failure, it already + * sends an error status word. + */ +bool produce_and_yield_pubnonce(dispatcher_context_t *dc, + sign_psbt_state_t *st, + signing_state_t *signing_state, + const keyexpr_info_t *keyexpr_info, + const input_info_t *input, + unsigned int cur_input_index); + +/** + * Computes and yields the partial signature for a certain sighash, during Round 2 of the MuSig2 + * protocol. + * + * Returns true if the computation is successful, false otherwise. In case of failure, it already + * sends an error status word. + */ +bool sign_sighash_musig_and_yield(dispatcher_context_t *dc, + sign_psbt_state_t *st, + signing_state_t *signing_state, + const keyexpr_info_t *keyexpr_info, + const input_info_t *input, + unsigned int cur_input_index, + uint8_t sighash[static 32]);