From 9089b94918b54e921e12a485167fb8e13ff43bb8 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Fri, 17 May 2024 17:58:22 +0200 Subject: [PATCH] [WIP] Round 1 --- src/commands.h | 4 + src/handler/lib/policy.c | 2 +- src/handler/lib/policy.h | 23 ++ src/handler/sign_psbt.c | 433 ++++++++++++++++++++++++++++------ src/musig/musig.c | 62 +++++ src/musig/musig.h | 33 +++ src/musig/musig_sessions.c | 64 +++++ src/musig/musig_sessions.h | 20 ++ tests/test_sign_psbt_musig.py | 153 ++++++++++++ 9 files changed, 727 insertions(+), 67 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 b0a9fb9fa..8abe4b986 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 d62f3e4d0..8b0e91df3 100644 --- a/src/handler/lib/policy.h +++ b/src/handler/lib/policy.h @@ -50,6 +50,29 @@ typedef struct { bool change; // whether a change address or a receive address is derived } 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 3bbe894f6..310124e25 100644 --- a/src/handler/sign_psbt.c +++ b/src/handler/sign_psbt.c @@ -54,6 +54,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 { @@ -381,7 +383,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, @@ -439,16 +440,18 @@ static int read_change_and_index_from_psbt_bip32_derivation( uint32_t change = fpt_der[1 + der_len - 2]; uint32_t addr_index = fpt_der[1 + der_len - 1]; - - // check that we can indeed derive the same key from the current key expression - serialized_extended_pubkey_t pubkey; - if (0 > bip32_CKDpub(&keyexpr_info->pubkey, change, &pubkey)) return -1; - if (0 > bip32_CKDpub(&pubkey, addr_index, &pubkey)) 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 > bip32_CKDpub(&keyexpr_info->pubkey, change, &pubkey, NULL)) 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; + // } // check if the 'change' derivation step is indeed coherent with the key expression if (change == keyexpr_info->key_expression_ptr->num_first) { @@ -704,37 +707,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->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); @@ -742,33 +736,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, @@ -2044,6 +2067,267 @@ static bool __attribute__((noinline)) sign_sighash_schnorr_and_yield(dispatcher_ return true; } +static bool __attribute__((noinline)) 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 agg_xonlykey[static 32], + const uint8_t *tapleaf_hash) { + LOG_PROCESSOR(__FILE__, __LINE__, __func__); + + if (st->protocol_version == 0) { + // we only support yielding pubnonces in version 1 ofr the protocol + SEND_SW(dc, SW_NOT_SUPPORTED); + return false; + } + + // bytes: 1 5 varint 66 33 32 0 or 32 + // CMD_YIELD + + // yield signature + uint8_t cmd = CCMD_YIELD; + dc->add_to_response(&cmd, 1); + + uint8_t buf[9]; + + // pubnonce tag + int tag_varint_len = varint_write(buf, 0, CCMD_YIELD_MUSIG_PUBNONCE_TAG); + dc->add_to_response(&buf, tag_varint_len); + + // input index + int input_index_varint_len = varint_write(buf, 0, cur_input_index); + dc->add_to_response(&buf, input_index_varint_len); + + // pubnonce + dc->add_to_response(pubnonce->R_s1, sizeof(pubnonce->R_s1)); + dc->add_to_response(pubnonce->R_s2, sizeof(pubnonce->R_s2)); + + // participant pubkey + dc->add_to_response(participant_pk, 33); + + // aggregate xonly key + dc->add_to_response(agg_xonlykey, 32); + + // tapleaf_hash, if given + if (tapleaf_hash != NULL) { + dc->add_to_response(tapleaf_hash, 32); + } + + dc->finalize_response(SW_INTERRUPTED_EXECUTION); + + if (dc->process_interruption(dc) < 0) { + SEND_SW(dc, SW_BAD_STATE); + return false; + } + return true; +} + +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; + + uint8_t bip32_tweak_1[32]; + uint8_t bip32_tweak_2[32]; + serialized_extended_pubkey_t agg_key_tweaked; + if (0 > bip32_CKDpub(&ext_pubkey, change_step, &agg_key_tweaked, bip32_tweak_1)) { + SEND_SW(dc, SW_BAD_STATE); // should never happen + return false; + } + if (0 > bip32_CKDpub(&agg_key_tweaked, addr_index_step, &agg_key_tweaked, bip32_tweak_1)) { + SEND_SW(dc, SW_BAD_STATE); // should never happen + return false; + } + + // 3) compute taproot tweak (if keypath spend) + uint8_t taptweak[32]; + memset(taptweak, 0, sizeof(taptweak)); + if (!keyexpr_info->is_tapscript) { + 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, + taptweak); + + // also apply the taptweak to agg_key_tweaked + + uint8_t parity = 0; + crypto_tr_tweak_pubkey(agg_key_tweaked.compressed_pubkey, + 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 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 musig_my_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, input->taptree_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 + uint8_t my_pubnonce[66]; + if (66 != call_get_merkleized_map_value(dc, + &input->in_out.map, + musig_my_psbt_id_key, + musig_my_psbt_id_len + 1, + my_pubnonce, + sizeof(my_pubnonce))) { + // 5) + // TODO: figure out what to do with the session id + + // 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"); + } + + 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"); + 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 + 1, + keyexpr_info->is_tapscript ? keyexpr_info->tapleaf_hash : NULL)) { + SEND_SW(dc, SW_BAD_STATE); // should never happen + return false; + } + } else { + // 6) + LEDGER_ASSERT(false, "Musig partial signatures not implemented"); + } + + return true; +} + static bool __attribute__((noinline)) compute_segwit_hashes(dispatcher_context_t *dc, sign_psbt_state_t *st, segwit_hashes_t *hashes) { { @@ -2197,6 +2481,9 @@ 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) @@ -2277,6 +2564,9 @@ 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; @@ -2327,14 +2617,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 return false; diff --git a/src/musig/musig.c b/src/musig/musig.c index b4ffb4432..4adf1859d 100644 --- a/src/musig/musig.c +++ b/src/musig/musig.c @@ -9,6 +9,7 @@ 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 inline bool is_point_infinite(const point_t *P) { return P->prefix == 0; @@ -144,3 +145,64 @@ int musig_key_agg(const plain_pk_t pubkeys[], size_t n_keys, musig_keyagg_contex ctx->gacc[31] = 1; return 0; } + +static void 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)); + + crypto_hash_update(&hash_context.header, rand, 32); + + crypto_hash_update_u8(&hash_context.header, sizeof(plain_pk_t)); + crypto_hash_update(&hash_context.header, pk, sizeof(plain_pk_t)); + + crypto_hash_update_u8(&hash_context.header, sizeof(xonly_pk_t)); + crypto_hash_update(&hash_context.header, aggpk, sizeof(xonly_pk_t)); + + crypto_hash_update(&hash_context.header, msg_prefixed, msg_prefixed_len); + + 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 +// TODO: handle errors +int musig_nonce_gen(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}; + + 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; + 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 R1, R2; + + memcpy(R1.raw, secp256k1_generator, sizeof(secp256k1_generator)); + if (CX_OK != cx_ecfp_scalar_mult_no_throw(CX_CURVE_SECP256K1, R1.raw, secnonce->k_1, 32)) + return -1; + memcpy(R2.raw, secp256k1_generator, sizeof(secp256k1_generator)); + if (CX_OK != cx_ecfp_scalar_mult_no_throw(CX_CURVE_SECP256K1, R2.raw, secnonce->k_2, 32)) + return -1; + + crypto_get_compressed_pubkey(R1.raw, pubnonce->R_s1); + crypto_get_compressed_pubkey(R2.raw, pubnonce->R_s2); + + return 0; +} diff --git a/src/musig/musig.h b/src/musig/musig.h index d17f89b67..3d0b947bc 100644 --- a/src/musig/musig.h +++ b/src/musig/musig.h @@ -30,6 +30,17 @@ 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 { + uint8_t R_s1[33]; + uint8_t R_s2[33]; +} musig_pubnonce_t; + /** * Computes the KeyAgg Context per BIP-0327. * @@ -43,3 +54,25 @@ 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(uint8_t rand[32], + const plain_pk_t pk, + const xonly_pk_t aggpk, + musig_secnonce_t *secnonce, + musig_pubnonce_t *pubnonce); \ No newline at end of file diff --git a/src/musig/musig_sessions.c b/src/musig/musig_sessions.c new file mode 100644 index 000000000..08c28d3b3 --- /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); +} \ No newline at end of file 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..b407cef41 --- /dev/null +++ b/tests/test_sign_psbt_musig.py @@ -0,0 +1,153 @@ + +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 +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.agg_xonlykey, + yielded.tapleaf_hash + ) + print("Adding pubnonce to psbt for input", input_index) + print("Key:", psbt_key) + print("Value:", yielded.pubnonce) + + 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) + for (input_index, yielded) in res: + if isinstance(yielded, MusigPartialSignature): + # yielded. + psbt_key = ( + yielded.participant_pubkey, + yielded.agg_xonlykey, + yielded.tapleaf_hash + ) + psbt.inputs[input_index].musig2_partial_sigs[psbt_key] = yielded.partial_signature + + +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)