diff options
41 files changed, 2355 insertions, 962 deletions
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b97be58b9..bd2e26484 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,6 +1,11 @@ name: ci/gh-actions/cli -on: [push, pull_request] +on: + push: + pull_request: + paths-ignore: + - 'docs/**' + - '**/README.md' # The below variables reduce repetitions across similar targets env: diff --git a/.github/workflows/depends.yml b/.github/workflows/depends.yml index 74d0ceabc..9385338de 100644 --- a/.github/workflows/depends.yml +++ b/.github/workflows/depends.yml @@ -1,6 +1,11 @@ name: ci/gh-actions/depends -on: [push, pull_request] +on: + push: + pull_request: + paths-ignore: + - 'docs/**' + - '**/README.md' env: APT_SET_CONF: | diff --git a/docs/LEVIN_PROTOCOL.md b/docs/LEVIN_PROTOCOL.md index 81f6f52ee..2a5dacb84 100644 --- a/docs/LEVIN_PROTOCOL.md +++ b/docs/LEVIN_PROTOCOL.md @@ -1,5 +1,5 @@ # Levin Protocol -This is a document explaining the current design of the levin protocol, as +This is a document explaining the current design of the Levin protocol, as used by Monero. The protocol is largely inherited from cryptonote, but has undergone some changes. @@ -9,7 +9,7 @@ extensibility. One of the goals of this document is to clearly indicate what is being sent "on the wire" to identify metadata that could de-anonymize users over I2P/Tor. -These issues will be addressed as they are found. See `ANONMITY_NETWORKS.md` in +These issues will be addressed as they are found. See `ANONYMITY_NETWORKS.md` in the `docs` folder for any outstanding issues. > This document does not currently list all data being sent by the monero diff --git a/src/crypto/crypto-ops.c b/src/crypto/crypto-ops.c index 508709280..54ca39775 100644 --- a/src/crypto/crypto-ops.c +++ b/src/crypto/crypto-ops.c @@ -3830,15 +3830,51 @@ int sc_isnonzero(const unsigned char *s) { s[27] | s[28] | s[29] | s[30] | s[31]) - 1) >> 8) + 1; } -int ge_p3_is_point_at_infinity(const ge_p3 *p) { - // X = 0 and Y == Z - int n; - for (n = 0; n < 10; ++n) +int ge_p3_is_point_at_infinity_vartime(const ge_p3 *p) { + // https://eprint.iacr.org/2008/522 + // X == T == 0 and Y/Z == 1 + // note: convert all pieces to canonical bytes in case rounding is required (i.e. an element is > q) + // note2: even though T = XY/Z is true for valid point representations (implying it isn't necessary to + // test T == 0), the input to this function might NOT be valid, so we must test T == 0 + char result_X_bytes[32]; + fe_tobytes((unsigned char*)&result_X_bytes, p->X); + + // X != 0 + for (int i = 0; i < 32; ++i) { - if (p->X[n] | p->T[n]) + if (result_X_bytes[i]) return 0; - if (p->Y[n] != p->Z[n]) + } + + char result_T_bytes[32]; + fe_tobytes((unsigned char*)&result_T_bytes, p->T); + + // T != 0 + for (int i = 0; i < 32; ++i) + { + if (result_T_bytes[i]) + return 0; + } + + char result_Y_bytes[32]; + char result_Z_bytes[32]; + fe_tobytes((unsigned char*)&result_Y_bytes, p->Y); + fe_tobytes((unsigned char*)&result_Z_bytes, p->Z); + + // Y != Z + for (int i = 0; i < 32; ++i) + { + if (result_Y_bytes[i] != result_Z_bytes[i]) return 0; } - return 1; + + // is Y nonzero? then Y/Z == 1 + for (int i = 0; i < 32; ++i) + { + if (result_Y_bytes[i] != 0) + return 1; + } + + // Y/Z = 0/0 + return 0; } diff --git a/src/crypto/crypto-ops.h b/src/crypto/crypto-ops.h index 22f76974b..96da16cbd 100644 --- a/src/crypto/crypto-ops.h +++ b/src/crypto/crypto-ops.h @@ -162,4 +162,4 @@ void fe_add(fe h, const fe f, const fe g); void fe_tobytes(unsigned char *, const fe); void fe_invert(fe out, const fe z); -int ge_p3_is_point_at_infinity(const ge_p3 *p); +int ge_p3_is_point_at_infinity_vartime(const ge_p3 *p); diff --git a/src/crypto/crypto.h b/src/crypto/crypto.h index 7ddc0150f..599ae4f91 100644 --- a/src/crypto/crypto.h +++ b/src/crypto/crypto.h @@ -64,6 +64,11 @@ namespace crypto { friend class crypto_ops; }; + POD_CLASS public_key_memsafe : epee::mlocked<tools::scrubbed<public_key>> { + public_key_memsafe() = default; + public_key_memsafe(const public_key &original) { memcpy(this->data, original.data, 32); } + }; + using secret_key = epee::mlocked<tools::scrubbed<ec_scalar>>; POD_CLASS public_keyV { @@ -100,7 +105,7 @@ namespace crypto { void random32_unbiased(unsigned char *bytes); static_assert(sizeof(ec_point) == 32 && sizeof(ec_scalar) == 32 && - sizeof(public_key) == 32 && sizeof(secret_key) == 32 && + sizeof(public_key) == 32 && sizeof(public_key_memsafe) == 32 && sizeof(secret_key) == 32 && sizeof(key_derivation) == 32 && sizeof(key_image) == 32 && sizeof(signature) == 64, "Invalid structure size"); @@ -310,9 +315,13 @@ namespace crypto { const extern crypto::public_key null_pkey; const extern crypto::secret_key null_skey; + + inline bool operator<(const public_key &p1, const public_key &p2) { return memcmp(&p1, &p2, sizeof(public_key)) < 0; } + inline bool operator>(const public_key &p1, const public_key &p2) { return p2 < p1; } } CRYPTO_MAKE_HASHABLE(public_key) CRYPTO_MAKE_HASHABLE_CONSTANT_TIME(secret_key) +CRYPTO_MAKE_HASHABLE_CONSTANT_TIME(public_key_memsafe) CRYPTO_MAKE_HASHABLE(key_image) CRYPTO_MAKE_COMPARABLE(signature) diff --git a/src/crypto/rx-slow-hash.c b/src/crypto/rx-slow-hash.c index 801987e37..247c9032f 100644 --- a/src/crypto/rx-slow-hash.c +++ b/src/crypto/rx-slow-hash.c @@ -63,6 +63,7 @@ static rx_state rx_s[2] = {{CTHR_MUTEX_INIT,{0},0,0},{CTHR_MUTEX_INIT,{0},0,0}}; static randomx_dataset *rx_dataset; static int rx_dataset_nomem; +static int rx_dataset_nolp; static uint64_t rx_dataset_height; static THREADV randomx_vm *rx_vm = NULL; @@ -316,10 +317,11 @@ void rx_slow_hash(const uint64_t mainheight, const uint64_t seedheight, const ch } CTHR_MUTEX_UNLOCK(rx_dataset_mutex); } - if (!(disabled_flags() & RANDOMX_FLAG_LARGE_PAGES)) { + if (!(disabled_flags() & RANDOMX_FLAG_LARGE_PAGES) && !rx_dataset_nolp) { rx_vm = randomx_create_vm(flags | RANDOMX_FLAG_LARGE_PAGES, rx_sp->rs_cache, rx_dataset); if(rx_vm == NULL) { //large pages failed mdebug(RX_LOGCAT, "Couldn't use largePages for RandomX VM"); + rx_dataset_nolp = 1; } } if (rx_vm == NULL) @@ -370,5 +372,6 @@ void rx_stop_mining(void) { randomx_release_dataset(rd); } rx_dataset_nomem = 0; + rx_dataset_nolp = 0; CTHR_MUTEX_UNLOCK(rx_dataset_mutex); } diff --git a/src/cryptonote_basic/account.cpp b/src/cryptonote_basic/account.cpp index 36ff41684..9927351a9 100644 --- a/src/cryptonote_basic/account.cpp +++ b/src/cryptonote_basic/account.cpp @@ -253,11 +253,6 @@ DISABLE_VS_WARNINGS(4244 4345) return crypto::secret_key_to_public_key(view_secret_key, m_keys.m_account_address.m_view_public_key); } //----------------------------------------------------------------- - void account_base::finalize_multisig(const crypto::public_key &spend_public_key) - { - m_keys.m_account_address.m_spend_public_key = spend_public_key; - } - //----------------------------------------------------------------- const account_keys& account_base::get_keys() const { return m_keys; diff --git a/src/cryptonote_basic/account.h b/src/cryptonote_basic/account.h index 5288b9b04..96b024c3c 100644 --- a/src/cryptonote_basic/account.h +++ b/src/cryptonote_basic/account.h @@ -82,7 +82,6 @@ namespace cryptonote void create_from_keys(const cryptonote::account_public_address& address, const crypto::secret_key& spendkey, const crypto::secret_key& viewkey); void create_from_viewkey(const cryptonote::account_public_address& address, const crypto::secret_key& viewkey); bool make_multisig(const crypto::secret_key &view_secret_key, const crypto::secret_key &spend_secret_key, const crypto::public_key &spend_public_key, const std::vector<crypto::secret_key> &multisig_keys); - void finalize_multisig(const crypto::public_key &spend_public_key); const account_keys& get_keys() const; std::string get_public_address_str(network_type nettype) const; std::string get_public_integrated_address_str(const crypto::hash8 &payment_id, network_type nettype) const; diff --git a/src/cryptonote_basic/cryptonote_format_utils.cpp b/src/cryptonote_basic/cryptonote_format_utils.cpp index 17adcdc35..835f59d69 100644 --- a/src/cryptonote_basic/cryptonote_format_utils.cpp +++ b/src/cryptonote_basic/cryptonote_format_utils.cpp @@ -306,7 +306,26 @@ namespace cryptonote { // derive secret key with subaddress - step 1: original CN derivation crypto::secret_key scalar_step1; - hwdev.derive_secret_key(recv_derivation, real_output_index, ack.m_spend_secret_key, scalar_step1); // computes Hs(a*R || idx) + b + crypto::secret_key spend_skey = crypto::null_skey; + + if (ack.m_multisig_keys.empty()) + { + // if not multisig, use normal spend skey + spend_skey = ack.m_spend_secret_key; + } + else + { + // if multisig, use sum of multisig privkeys (local account's share of aggregate spend key) + for (const auto &multisig_key : ack.m_multisig_keys) + { + sc_add((unsigned char*)spend_skey.data, + (const unsigned char*)multisig_key.data, + (const unsigned char*)spend_skey.data); + } + } + + // computes Hs(a*R || idx) + b + hwdev.derive_secret_key(recv_derivation, real_output_index, spend_skey, scalar_step1); // step 2: add Hs(a || index_major || index_minor) crypto::secret_key subaddr_sk; diff --git a/src/cryptonote_config.h b/src/cryptonote_config.h index 915835d1b..b738960a3 100644 --- a/src/cryptonote_config.h +++ b/src/cryptonote_config.h @@ -229,6 +229,7 @@ namespace config const unsigned char HASH_KEY_RPC_PAYMENT_NONCE = 0x58; const unsigned char HASH_KEY_MEMORY = 'k'; const unsigned char HASH_KEY_MULTISIG[] = {'M', 'u', 'l', 't' , 'i', 's', 'i', 'g', 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; + const unsigned char HASH_KEY_MULTISIG_KEY_AGGREGATION[] = "Multisig_key_agg"; const unsigned char HASH_KEY_TXPROOF_V2[] = "TXPROOF_V2"; const unsigned char HASH_KEY_CLSAG_ROUND[] = "CLSAG_round"; const unsigned char HASH_KEY_CLSAG_AGG_0[] = "CLSAG_agg_0"; @@ -236,6 +237,9 @@ namespace config const char HASH_KEY_MESSAGE_SIGNING[] = "MoneroMessageSignature"; const unsigned char HASH_KEY_MM_SLOT = 'm'; + // Multisig + const uint32_t MULTISIG_MAX_SIGNERS{16}; + namespace testnet { uint64_t const CRYPTONOTE_PUBLIC_ADDRESS_BASE58_PREFIX = 53; diff --git a/src/cryptonote_core/cryptonote_tx_utils.cpp b/src/cryptonote_core/cryptonote_tx_utils.cpp index f6061b803..a50ebb550 100644 --- a/src/cryptonote_core/cryptonote_tx_utils.cpp +++ b/src/cryptonote_core/cryptonote_tx_utils.cpp @@ -43,7 +43,6 @@ using namespace epee; #include "crypto/crypto.h" #include "crypto/hash.h" #include "ringct/rctSigs.h" -#include "multisig/multisig.h" using namespace crypto; diff --git a/src/gen_multisig/gen_multisig.cpp b/src/gen_multisig/gen_multisig.cpp index 4aa21b149..87de3c351 100644 --- a/src/gen_multisig/gen_multisig.cpp +++ b/src/gen_multisig/gen_multisig.cpp @@ -95,55 +95,35 @@ static bool generate_multisig(uint32_t threshold, uint32_t total, const std::str } // gather the keys - std::vector<crypto::secret_key> sk(total); - std::vector<crypto::public_key> pk(total); + std::vector<std::string> first_round_msgs; + first_round_msgs.reserve(total); for (size_t n = 0; n < total; ++n) { wallets[n]->decrypt_keys(pwd_container->password()); - if (!tools::wallet2::verify_multisig_info(wallets[n]->get_multisig_info(), sk[n], pk[n])) - { - tools::fail_msg_writer() << genms::tr("Failed to verify multisig info"); - return false; - } + + first_round_msgs.emplace_back(wallets[n]->get_multisig_first_kex_msg()); + wallets[n]->encrypt_keys(pwd_container->password()); } // make the wallets multisig - std::vector<std::string> extra_info(total); + std::vector<std::string> kex_msgs_intermediate(total); std::stringstream ss; for (size_t n = 0; n < total; ++n) { std::string name = basename + "-" + std::to_string(n + 1); - std::vector<crypto::secret_key> skn; - std::vector<crypto::public_key> pkn; - for (size_t k = 0; k < total; ++k) - { - if (k != n) - { - skn.push_back(sk[k]); - pkn.push_back(pk[k]); - } - } - extra_info[n] = wallets[n]->make_multisig(pwd_container->password(), skn, pkn, threshold); + + kex_msgs_intermediate[n] = wallets[n]->make_multisig(pwd_container->password(), first_round_msgs, threshold); + ss << " " << name << std::endl; } //exchange keys unless exchange_multisig_keys returns no extra info - while (!extra_info[0].empty()) + while (!kex_msgs_intermediate[0].empty()) { - std::unordered_set<crypto::public_key> pkeys; - std::vector<crypto::public_key> signers(total); - for (size_t n = 0; n < total; ++n) - { - if (!tools::wallet2::verify_extra_multisig_info(extra_info[n], pkeys, signers[n])) - { - tools::fail_msg_writer() << genms::tr("Error verifying multisig extra info"); - return false; - } - } for (size_t n = 0; n < total; ++n) { - extra_info[n] = wallets[n]->exchange_multisig_keys(pwd_container->password(), pkeys, signers); + kex_msgs_intermediate[n] = wallets[n]->exchange_multisig_keys(pwd_container->password(), kex_msgs_intermediate); } } diff --git a/src/multisig/CMakeLists.txt b/src/multisig/CMakeLists.txt index eaa2c6f71..14099e64a 100644 --- a/src/multisig/CMakeLists.txt +++ b/src/multisig/CMakeLists.txt @@ -27,12 +27,17 @@ # THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. set(multisig_sources - multisig.cpp) + multisig.cpp + multisig_account.cpp + multisig_account_kex_impl.cpp + multisig_kex_msg.cpp) set(multisig_headers) set(multisig_private_headers - multisig.h) + multisig.h + multisig_account.h + multisig_kex_msg.h) monero_private_headers(multisig ${multisig_private_headers}) diff --git a/src/multisig/multisig.cpp b/src/multisig/multisig.cpp index 272de73b2..85c45bc31 100644 --- a/src/multisig/multisig.cpp +++ b/src/multisig/multisig.cpp @@ -1,4 +1,4 @@ -// Copyright (c) 2017-2020, The Monero Project +// Copyright (c) 2017-2021, The Monero Project // // All rights reserved. // @@ -26,29 +26,34 @@ // STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF // THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -#include <unordered_set> -#include "include_base_utils.h" #include "crypto/crypto.h" -#include "ringct/rctOps.h" #include "cryptonote_basic/account.h" #include "cryptonote_basic/cryptonote_format_utils.h" -#include "multisig.h" #include "cryptonote_config.h" +#include "include_base_utils.h" +#include "multisig.h" +#include "ringct/rctOps.h" + +#include <algorithm> +#include <unordered_map> +#include <unordered_set> +#include <vector> #undef MONERO_DEFAULT_LOG_CATEGORY #define MONERO_DEFAULT_LOG_CATEGORY "multisig" -using namespace std; - -namespace cryptonote +namespace multisig { - //----------------------------------------------------------------- + //---------------------------------------------------------------------------------------------------------------------- crypto::secret_key get_multisig_blinded_secret_key(const crypto::secret_key &key) { + CHECK_AND_ASSERT_THROW_MES(key != crypto::null_skey, "Unexpected null secret key (danger!)."); + rct::key multisig_salt; static_assert(sizeof(rct::key) == sizeof(config::HASH_KEY_MULTISIG), "Hash domain separator is an unexpected size"); memcpy(multisig_salt.bytes, config::HASH_KEY_MULTISIG, sizeof(rct::key)); + // private key = H(key, domain-sep) rct::keyV data; data.reserve(2); data.push_back(rct::sk2rct(key)); @@ -57,134 +62,79 @@ namespace cryptonote memwipe(&data[0], sizeof(rct::key)); return result; } - //----------------------------------------------------------------- - void generate_multisig_N_N(const account_keys &keys, const std::vector<crypto::public_key> &spend_keys, std::vector<crypto::secret_key> &multisig_keys, rct::key &spend_skey, rct::key &spend_pkey) - { - // the multisig spend public key is the sum of all spend public keys - multisig_keys.clear(); - const crypto::secret_key spend_secret_key = get_multisig_blinded_secret_key(keys.m_spend_secret_key); - CHECK_AND_ASSERT_THROW_MES(crypto::secret_key_to_public_key(spend_secret_key, (crypto::public_key&)spend_pkey), "Failed to derive public key"); - for (const auto &k: spend_keys) - rct::addKeys(spend_pkey, spend_pkey, rct::pk2rct(k)); - multisig_keys.push_back(spend_secret_key); - spend_skey = rct::sk2rct(spend_secret_key); - } - //----------------------------------------------------------------- - void generate_multisig_N1_N(const account_keys &keys, const std::vector<crypto::public_key> &spend_keys, std::vector<crypto::secret_key> &multisig_keys, rct::key &spend_skey, rct::key &spend_pkey) - { - multisig_keys.clear(); - spend_pkey = rct::identity(); - spend_skey = rct::zero(); - - // create all our composite private keys - crypto::secret_key blinded_skey = get_multisig_blinded_secret_key(keys.m_spend_secret_key); - for (const auto &k: spend_keys) - { - rct::key sk = rct::scalarmultKey(rct::pk2rct(k), rct::sk2rct(blinded_skey)); - crypto::secret_key msk = get_multisig_blinded_secret_key(rct::rct2sk(sk)); - memwipe(&sk, sizeof(sk)); - multisig_keys.push_back(msk); - sc_add(spend_skey.bytes, spend_skey.bytes, (const unsigned char*)msk.data); - } - } - //----------------------------------------------------------------- - std::vector<crypto::public_key> generate_multisig_derivations(const account_keys &keys, const std::vector<crypto::public_key> &derivations) - { - std::vector<crypto::public_key> multisig_keys; - crypto::secret_key blinded_skey = get_multisig_blinded_secret_key(keys.m_spend_secret_key); - for (const auto &k: derivations) - { - rct::key d = rct::scalarmultKey(rct::pk2rct(k), rct::sk2rct(blinded_skey)); - multisig_keys.push_back(rct::rct2pk(d)); - } - - return multisig_keys; - } - //----------------------------------------------------------------- - crypto::secret_key calculate_multisig_signer_key(const std::vector<crypto::secret_key>& multisig_keys) - { - rct::key secret_key = rct::zero(); - for (const auto &k: multisig_keys) - { - sc_add(secret_key.bytes, secret_key.bytes, (const unsigned char*)k.data); - } - - return rct::rct2sk(secret_key); - } - //----------------------------------------------------------------- - std::vector<crypto::secret_key> calculate_multisig_keys(const std::vector<crypto::public_key>& derivations) - { - std::vector<crypto::secret_key> multisig_keys; - multisig_keys.reserve(derivations.size()); - - for (const auto &k: derivations) - { - multisig_keys.emplace_back(get_multisig_blinded_secret_key(rct::rct2sk(rct::pk2rct(k)))); - } - - return multisig_keys; - } - //----------------------------------------------------------------- - crypto::secret_key generate_multisig_view_secret_key(const crypto::secret_key &skey, const std::vector<crypto::secret_key> &skeys) - { - crypto::secret_key view_skey = get_multisig_blinded_secret_key(skey); - for (const auto &k: skeys) - sc_add((unsigned char*)&view_skey, rct::sk2rct(view_skey).bytes, rct::sk2rct(k).bytes); - return view_skey; - } - //----------------------------------------------------------------- - crypto::public_key generate_multisig_M_N_spend_public_key(const std::vector<crypto::public_key> &pkeys) - { - rct::key spend_public_key = rct::identity(); - for (const auto &pk: pkeys) - { - rct::addKeys(spend_public_key, spend_public_key, rct::pk2rct(pk)); - } - return rct::rct2pk(spend_public_key); - } - //----------------------------------------------------------------- - bool generate_multisig_key_image(const account_keys &keys, size_t multisig_key_index, const crypto::public_key& out_key, crypto::key_image& ki) + //---------------------------------------------------------------------------------------------------------------------- + bool generate_multisig_key_image(const cryptonote::account_keys &keys, + std::size_t multisig_key_index, + const crypto::public_key& out_key, + crypto::key_image& ki) { if (multisig_key_index >= keys.m_multisig_keys.size()) return false; crypto::generate_key_image(out_key, keys.m_multisig_keys[multisig_key_index], ki); return true; } - //----------------------------------------------------------------- - void generate_multisig_LR(const crypto::public_key pkey, const crypto::secret_key &k, crypto::public_key &L, crypto::public_key &R) + //---------------------------------------------------------------------------------------------------------------------- + void generate_multisig_LR(const crypto::public_key pkey, + const crypto::secret_key &k, + crypto::public_key &L, + crypto::public_key &R) { rct::scalarmultBase((rct::key&)L, rct::sk2rct(k)); crypto::generate_key_image(pkey, k, (crypto::key_image&)R); } - //----------------------------------------------------------------- - bool generate_multisig_composite_key_image(const account_keys &keys, const std::unordered_map<crypto::public_key, subaddress_index>& subaddresses, const crypto::public_key& out_key, const crypto::public_key &tx_public_key, const std::vector<crypto::public_key>& additional_tx_public_keys, size_t real_output_index, const std::vector<crypto::key_image> &pkis, crypto::key_image &ki) + //---------------------------------------------------------------------------------------------------------------------- + bool generate_multisig_composite_key_image(const cryptonote::account_keys &keys, + const std::unordered_map<crypto::public_key, cryptonote::subaddress_index> &subaddresses, + const crypto::public_key &out_key, + const crypto::public_key &tx_public_key, + const std::vector<crypto::public_key> &additional_tx_public_keys, + std::size_t real_output_index, + const std::vector<crypto::key_image> &pkis, + crypto::key_image &ki) { + // create a multisig partial key image + // KI_partial = ([view key component] + [subaddress component] + [multisig privkeys]) * Hp(output one-time address) + // - the 'multisig priv keys' here are those held by the local account + // - later, we add in the components held by other participants cryptonote::keypair in_ephemeral; if (!cryptonote::generate_key_image_helper(keys, subaddresses, out_key, tx_public_key, additional_tx_public_keys, real_output_index, in_ephemeral, ki, keys.get_device())) return false; std::unordered_set<crypto::key_image> used; - for (size_t m = 0; m < keys.m_multisig_keys.size(); ++m) + + // create a key image component for each of the local account's multisig private keys + for (std::size_t m = 0; m < keys.m_multisig_keys.size(); ++m) { crypto::key_image pki; - bool r = cryptonote::generate_multisig_key_image(keys, m, out_key, pki); + // pki = keys.m_multisig_keys[m] * Hp(out_key) + // pki = key image component + // out_key = one-time address of an output owned by the multisig group + bool r = generate_multisig_key_image(keys, m, out_key, pki); if (!r) return false; + + // this KI component is 'used' because it was included in the partial key image 'ki' above used.insert(pki); } + + // add the KI components from other participants to the partial KI + // if they not included yet for (const auto &pki: pkis) { if (used.find(pki) == used.end()) { + // ignore components that have already been 'used' used.insert(pki); + + // KI_partial = KI_partial + KI_component[...] rct::addKeys((rct::key&)ki, rct::ki2rct(ki), rct::ki2rct(pki)); } } + + // at the end, 'ki' will hold the true key image for our output if inputs were sufficient + // - if 'pkis' (the other participants' KI components) is missing some components + // then 'ki' will not be complete + return true; } - //----------------------------------------------------------------- - uint32_t multisig_rounds_required(uint32_t participants, uint32_t threshold) - { - CHECK_AND_ASSERT_THROW_MES(participants >= threshold, "participants must be greater or equal than threshold"); - return participants - threshold + 1; - } -} + //---------------------------------------------------------------------------------------------------------------------- +} //namespace multisig diff --git a/src/multisig/multisig.h b/src/multisig/multisig.h index eab32187c..e041ea670 100644 --- a/src/multisig/multisig.h +++ b/src/multisig/multisig.h @@ -1,4 +1,4 @@ -// Copyright (c) 2017-2020, The Monero Project +// Copyright (c) 2017-2021, The Monero Project // // All rights reserved. // @@ -28,44 +28,42 @@ #pragma once -#include <vector> -#include <unordered_map> #include "crypto/crypto.h" #include "cryptonote_basic/cryptonote_format_utils.h" #include "ringct/rctTypes.h" -namespace cryptonote -{ - struct account_keys; +#include <unordered_map> +#include <unordered_set> +#include <vector> - crypto::secret_key get_multisig_blinded_secret_key(const crypto::secret_key &key); - void generate_multisig_N_N(const account_keys &keys, const std::vector<crypto::public_key> &spend_keys, std::vector<crypto::secret_key> &multisig_keys, rct::key &spend_skey, rct::key &spend_pkey); - void generate_multisig_N1_N(const account_keys &keys, const std::vector<crypto::public_key> &spend_keys, std::vector<crypto::secret_key> &multisig_keys, rct::key &spend_skey, rct::key &spend_pkey); - /** - * @brief generate_multisig_derivations performs common DH key derivation. - * Each middle round in M/N scheme is DH exchange of public multisig keys of other participants multiplied by secret spend key of current participant. - * this functions does the following: new multisig key = secret spend * public multisig key - * @param keys - current wallet's keys - * @param derivations - public multisig keys of other participants - * @return new public multisig keys derived from previous round. This data needs to be exchange with other participants - */ - std::vector<crypto::public_key> generate_multisig_derivations(const account_keys &keys, const std::vector<crypto::public_key> &derivations); - crypto::secret_key calculate_multisig_signer_key(const std::vector<crypto::secret_key>& derivations); - /** - * @brief calculate_multisig_keys. Calculates secret multisig keys from others' participants ones as follows: mi = H(Mi) - * @param derivations - others' participants public multisig keys. - * @return vector of current wallet's multisig secret keys - */ - std::vector<crypto::secret_key> calculate_multisig_keys(const std::vector<crypto::public_key>& derivations); - crypto::secret_key generate_multisig_view_secret_key(const crypto::secret_key &skey, const std::vector<crypto::secret_key> &skeys); +namespace cryptonote { struct account_keys; } + +namespace multisig +{ /** - * @brief generate_multisig_M_N_spend_public_key calculates multisig wallet's spend public key by summing all of public multisig keys - * @param pkeys unique public multisig keys - * @return multisig wallet's spend public key - */ - crypto::public_key generate_multisig_M_N_spend_public_key(const std::vector<crypto::public_key> &pkeys); - bool generate_multisig_key_image(const account_keys &keys, size_t multisig_key_index, const crypto::public_key& out_key, crypto::key_image& ki); - void generate_multisig_LR(const crypto::public_key pkey, const crypto::secret_key &k, crypto::public_key &L, crypto::public_key &R); - bool generate_multisig_composite_key_image(const account_keys &keys, const std::unordered_map<crypto::public_key, cryptonote::subaddress_index>& subaddresses, const crypto::public_key& out_key, const crypto::public_key &tx_public_key, const std::vector<crypto::public_key>& additional_tx_public_keys, size_t real_output_index, const std::vector<crypto::key_image> &pkis, crypto::key_image &ki); - uint32_t multisig_rounds_required(uint32_t participants, uint32_t threshold); -} + * @brief get_multisig_blinded_secret_key - converts an input private key into a blinded multisig private key + * Use 1a: converts account private spend key into multisig private key, which is used for key exchange and message signing + * Use 1b: converts account private view key into ancillary private key share, for the composite multisig private view key + * Use 2: converts DH shared secrets (curve points) into private keys, which are intermediate private keys in multisig key exchange + * @param key - private key to transform + * @return transformed private key + */ + crypto::secret_key get_multisig_blinded_secret_key(const crypto::secret_key &key); + + bool generate_multisig_key_image(const cryptonote::account_keys &keys, + std::size_t multisig_key_index, + const crypto::public_key& out_key, + crypto::key_image& ki); + void generate_multisig_LR(const crypto::public_key pkey, + const crypto::secret_key &k, + crypto::public_key &L, + crypto::public_key &R); + bool generate_multisig_composite_key_image(const cryptonote::account_keys &keys, + const std::unordered_map<crypto::public_key, cryptonote::subaddress_index> &subaddresses, + const crypto::public_key &out_key, + const crypto::public_key &tx_public_key, + const std::vector<crypto::public_key> &additional_tx_public_keys, + std::size_t real_output_index, + const std::vector<crypto::key_image> &pkis, + crypto::key_image &ki); +} //namespace multisig diff --git a/src/multisig/multisig_account.cpp b/src/multisig/multisig_account.cpp new file mode 100644 index 000000000..b7298c4b6 --- /dev/null +++ b/src/multisig/multisig_account.cpp @@ -0,0 +1,184 @@ +// Copyright (c) 2021, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "multisig_account.h" + +#include "crypto/crypto.h" +#include "cryptonote_config.h" +#include "include_base_utils.h" +#include "multisig.h" +#include "multisig_kex_msg.h" +#include "ringct/rctOps.h" +#include "ringct/rctTypes.h" + +#include <cstdint> +#include <utility> +#include <vector> + + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "multisig" + +namespace multisig +{ + //---------------------------------------------------------------------------------------------------------------------- + // multisig_account: EXTERNAL + //---------------------------------------------------------------------------------------------------------------------- + multisig_account::multisig_account(const crypto::secret_key &base_privkey, + const crypto::secret_key &base_common_privkey) : + m_base_privkey{base_privkey}, + m_base_common_privkey{base_common_privkey}, + m_multisig_pubkey{rct::rct2pk(rct::identity())}, + m_common_pubkey{rct::rct2pk(rct::identity())}, + m_kex_rounds_complete{0}, + m_next_round_kex_message{multisig_kex_msg{1, base_privkey, std::vector<crypto::public_key>{}, base_common_privkey}.get_msg()} + { + CHECK_AND_ASSERT_THROW_MES(crypto::secret_key_to_public_key(m_base_privkey, m_base_pubkey), + "Failed to derive public key"); + } + //---------------------------------------------------------------------------------------------------------------------- + // multisig_account: EXTERNAL + //---------------------------------------------------------------------------------------------------------------------- + multisig_account::multisig_account(const std::uint32_t threshold, + std::vector<crypto::public_key> signers, + const crypto::secret_key &base_privkey, + const crypto::secret_key &base_common_privkey, + std::vector<crypto::secret_key> multisig_privkeys, + const crypto::secret_key &common_privkey, + const crypto::public_key &multisig_pubkey, + const crypto::public_key &common_pubkey, + const std::uint32_t kex_rounds_complete, + kex_origins_map_t kex_origins_map, + std::string next_round_kex_message) : + m_base_privkey{base_privkey}, + m_base_common_privkey{base_common_privkey}, + m_multisig_privkeys{std::move(multisig_privkeys)}, + m_common_privkey{common_privkey}, + m_multisig_pubkey{multisig_pubkey}, + m_common_pubkey{common_pubkey}, + m_kex_rounds_complete{kex_rounds_complete}, + m_kex_keys_to_origins_map{std::move(kex_origins_map)}, + m_next_round_kex_message{std::move(next_round_kex_message)} + { + CHECK_AND_ASSERT_THROW_MES(kex_rounds_complete > 0, "multisig account: can't reconstruct account if its kex wasn't initialized"); + CHECK_AND_ASSERT_THROW_MES(crypto::secret_key_to_public_key(m_base_privkey, m_base_pubkey), + "Failed to derive public key"); + set_multisig_config(threshold, std::move(signers)); + } + //---------------------------------------------------------------------------------------------------------------------- + // multisig_account: EXTERNAL + //---------------------------------------------------------------------------------------------------------------------- + bool multisig_account::account_is_active() const + { + return m_kex_rounds_complete > 0; + } + //---------------------------------------------------------------------------------------------------------------------- + // multisig_account: EXTERNAL + //---------------------------------------------------------------------------------------------------------------------- + bool multisig_account::multisig_is_ready() const + { + if (account_is_active()) + return multisig_kex_rounds_required(m_signers.size(), m_threshold) == m_kex_rounds_complete; + else + return false; + } + //---------------------------------------------------------------------------------------------------------------------- + // multisig_account: INTERNAL + //---------------------------------------------------------------------------------------------------------------------- + void multisig_account::set_multisig_config(const std::size_t threshold, std::vector<crypto::public_key> signers) + { + // validate + CHECK_AND_ASSERT_THROW_MES(threshold > 0 && threshold <= signers.size(), "multisig account: tried to set invalid threshold."); + CHECK_AND_ASSERT_THROW_MES(signers.size() >= 2 && signers.size() <= config::MULTISIG_MAX_SIGNERS, + "multisig account: tried to set invalid number of signers."); + + for (auto signer_it = signers.begin(); signer_it != signers.end(); ++signer_it) + { + // signers should all be unique + CHECK_AND_ASSERT_THROW_MES(std::find(signers.begin(), signer_it, *signer_it) == signer_it, + "multisig account: tried to set signers, but found a duplicate signer unexpectedly."); + + // signer pubkeys must be in main subgroup, and not identity + CHECK_AND_ASSERT_THROW_MES(rct::isInMainSubgroup(rct::pk2rct(*signer_it)) && !(*signer_it == rct::rct2pk(rct::identity())), + "multisig account: tried to set signers, but a signer pubkey is invalid."); + } + + // own pubkey should be in signers list + CHECK_AND_ASSERT_THROW_MES(std::find(signers.begin(), signers.end(), m_base_pubkey) != signers.end(), + "multisig account: tried to set signers, but did not find the account's base pubkey in signer list."); + + // sort signers + std::sort(signers.begin(), signers.end(), + [](const crypto::public_key &key1, const crypto::public_key &key2) -> bool + { + return memcmp(&key1, &key2, sizeof(crypto::public_key)) < 0; + } + ); + + // set + m_threshold = threshold; + m_signers = std::move(signers); + } + //---------------------------------------------------------------------------------------------------------------------- + // multisig_account: EXTERNAL + //---------------------------------------------------------------------------------------------------------------------- + void multisig_account::initialize_kex(const std::uint32_t threshold, + std::vector<crypto::public_key> signers, + const std::vector<multisig_kex_msg> &expanded_msgs_rnd1) + { + CHECK_AND_ASSERT_THROW_MES(!account_is_active(), "multisig account: tried to initialize kex, but already initialized"); + + // only mutate account if update succeeds + multisig_account temp_account{*this}; + temp_account.set_multisig_config(threshold, std::move(signers)); + temp_account.kex_update_impl(expanded_msgs_rnd1); + *this = std::move(temp_account); + } + //---------------------------------------------------------------------------------------------------------------------- + // multisig_account: EXTERNAL + //---------------------------------------------------------------------------------------------------------------------- + void multisig_account::kex_update(const std::vector<multisig_kex_msg> &expanded_msgs) + { + CHECK_AND_ASSERT_THROW_MES(account_is_active(), "multisig account: tried to update kex, but kex isn't initialized yet."); + CHECK_AND_ASSERT_THROW_MES(!multisig_is_ready(), "multisig account: tried to update kex, but kex is already complete."); + + multisig_account temp_account{*this}; + temp_account.kex_update_impl(expanded_msgs); + *this = std::move(temp_account); + } + //---------------------------------------------------------------------------------------------------------------------- + // EXTERNAL + //---------------------------------------------------------------------------------------------------------------------- + std::uint32_t multisig_kex_rounds_required(const std::uint32_t num_signers, const std::uint32_t threshold) + { + CHECK_AND_ASSERT_THROW_MES(num_signers >= threshold, "num_signers must be >= threshold"); + CHECK_AND_ASSERT_THROW_MES(threshold >= 1, "threshold must be >= 1"); + return num_signers - threshold + 1; + } + //---------------------------------------------------------------------------------------------------------------------- +} //namespace multisig diff --git a/src/multisig/multisig_account.h b/src/multisig/multisig_account.h new file mode 100644 index 000000000..b01ae6c88 --- /dev/null +++ b/src/multisig/multisig_account.h @@ -0,0 +1,246 @@ +// Copyright (c) 2021, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include "crypto/crypto.h" +#include "multisig_kex_msg.h" + +#include <cstdint> +#include <string> +#include <unordered_map> +#include <unordered_set> +#include <vector> + + +namespace multisig +{ + /** + * multisig account: + * + * - handles account keys for an M-of-N multisig participant (M <= N; M >= 1; N >= 2) + * - encapsulates multisig account construction process (via key exchange [kex]) + * - TODO: encapsulates key preparation for aggregation-style signing + * + * :: multisig pubkey: the private key is split, M group participants are required to reassemble (e.g. to sign something) + * - in cryptonote, this is the multisig spend key + * :: multisig common pubkey: the private key is known to all participants (e.g. for authenticating as a group member) + * - in cryptonote, this is the multisig view key + * + * + * multisig key exchange: + * + * An 'M-of-N' (M <= N; M >= 1; N >= 2) multisignature key is a public key where at least 'M' out of 'N' + * possible co-signers must collaborate in order to create a signature. + * + * Constructing a multisig key involves a series of Diffie-Hellman exchanges between participants. + * At the end of key exchange (kex), each participant will hold a number of private keys. Each private + * key is shared by a group of (N - M + 1) participants. This way if (N - M) co-signers are missing, every + * private key will be held by at least one of the remaining M people. + * + * Note on MULTISIG_MAX_SIGNERS: During key exchange, participants will have up to '(N - 1) choose (N - M)' + * key shares. If N is large, then the max number of key shares (when M = (N-1)/2) can be huge. A limit of N <= 16 was + * arbitrarily chosen as a power of 2 that can accomodate the vast majority of practical use-cases. To increase the + * limit, FROST-style key aggregation should be used instead (it is more efficient than DH-based key generation + * when N - M > 1). + * + * - Further reading + * - MRL-0009: https://www.getmonero.org/resources/research-lab/pubs/MRL-0009.pdf + * - MuSig2: https://eprint.iacr.org/2020/1261 + * - ZtM2: https://web.getmonero.org/library/Zero-to-Monero-2-0-0.pdf Ch. 9, especially Section 9.6.3 + * - FROST: https://eprint.iacr.org/2018/417 + */ + class multisig_account final + { + public: + //member types + using kex_origins_map_t = std::unordered_map<crypto::public_key_memsafe, std::unordered_set<crypto::public_key>>; + + //constructors + // default constructor + multisig_account() = default; + + /** + * construct from base privkeys + * + * - prepares a kex msg for the first round of multisig key construction. + * - the local account's kex msgs are signed with the base_privkey + * - the first kex msg transmits the local base_common_privkey to other participants, for creating the group's common_privkey + */ + multisig_account(const crypto::secret_key &base_privkey, + const crypto::secret_key &base_common_privkey); + + // reconstruct from full account details (not recommended) + multisig_account(const std::uint32_t threshold, + std::vector<crypto::public_key> signers, + const crypto::secret_key &base_privkey, + const crypto::secret_key &base_common_privkey, + std::vector<crypto::secret_key> multisig_privkeys, + const crypto::secret_key &common_privkey, + const crypto::public_key &multisig_pubkey, + const crypto::public_key &common_pubkey, + const std::uint32_t kex_rounds_complete, + kex_origins_map_t kex_origins_map, + std::string next_round_kex_message); + + // copy constructor: default + + //destructor: default + ~multisig_account() = default; + + //overloaded operators: none + + //getters + // get threshold + std::uint32_t get_threshold() const { return m_threshold; } + // get signers + const std::vector<crypto::public_key>& get_signers() const { return m_signers; } + // get base privkey + const crypto::secret_key& get_base_privkey() const { return m_base_privkey; } + // get base pubkey + const crypto::public_key& get_base_pubkey() const { return m_base_pubkey; } + // get base common privkey + const crypto::secret_key& get_base_common_privkey() const { return m_base_common_privkey; } + // get multisig privkeys + const std::vector<crypto::secret_key>& get_multisig_privkeys() const { return m_multisig_privkeys; } + // get common privkey + const crypto::secret_key& get_common_privkey() const { return m_common_privkey; } + // get multisig pubkey + const crypto::public_key& get_multisig_pubkey() const { return m_multisig_pubkey; } + // get common pubkey + const crypto::public_key& get_common_pubkey() const { return m_common_pubkey; } + // get kex rounds complete + std::uint32_t get_kex_rounds_complete() const { return m_kex_rounds_complete; } + // get kex keys to origins map + const kex_origins_map_t& get_kex_keys_to_origins_map() const { return m_kex_keys_to_origins_map; } + // get the kex msg for the next round + const std::string& get_next_kex_round_msg() const { return m_next_round_kex_message; } + + //account status functions + // account has been intialized, and the account holder can use the 'common' key + bool account_is_active() const; + // account is ready to make multisig signatures + bool multisig_is_ready() const; + + //account helpers + private: + // set the threshold (M) and signers (N) + void set_multisig_config(const std::size_t threshold, std::vector<crypto::public_key> signers); + + //account mutators: key exchange to set up account + public: + /** + * brief: initialize_kex - initialize key exchange + * - Updates the account with a 'transactional' model. This account will only be mutated if the update succeeds. + */ + void initialize_kex(const std::uint32_t threshold, + std::vector<crypto::public_key> signers, + const std::vector<multisig_kex_msg> &expanded_msgs_rnd1); + /** + * brief: kex_update - Complete the 'in progress' kex round and set the kex message for the next round. + * - Updates the account with a 'transactional' model. This account will only be mutated if the update succeeds. + * - The main interface for multisig key exchange, this handles all the work of processing input messages, + * creating new messages for new rounds, and finalizing the multisig shared public key when kex is complete. + * param: expanded_msgs - kex messages corresponding to the account's 'in progress' round + */ + void kex_update(const std::vector<multisig_kex_msg> &expanded_msgs); + + private: + // implementation of kex_update() (non-transactional) + void kex_update_impl(const std::vector<multisig_kex_msg> &expanded_msgs); + /** + * brief: initialize_kex_update - Helper for kex_update_impl() + * - Collect the local signer's shared keys to ignore in incoming messages, build the aggregate ancillary key + * if appropriate. + * param: expanded_msgs - set of multisig kex messages to process + * param: rounds_required - number of rounds required for kex + * outparam: exclude_pubkeys_out - keys held by the local account corresponding to round 'current_round' + * - If 'current_round' is the final round, these are the local account's shares of the final aggregate key. + */ + void initialize_kex_update(const std::vector<multisig_kex_msg> &expanded_msgs, + const std::uint32_t rounds_required, + std::vector<crypto::public_key> &exclude_pubkeys_out); + /** + * brief: finalize_kex_update - Helper for kex_update_impl() + * param: rounds_required - number of rounds required for kex + * param: result_keys_to_origins_map - map between keys for the next round and the other participants they correspond to + * inoutparam: temp_account_inout - account to perform last update steps on + */ + void finalize_kex_update(const std::uint32_t rounds_required, + kex_origins_map_t result_keys_to_origins_map); + + //member variables + private: + /// misc. account details + // [M] minimum number of co-signers to sign a message with the aggregate pubkey + std::uint32_t m_threshold{0}; + // [N] base keys of all participants in the multisig (used to initiate key exchange, and as participant ids for msg signing) + std::vector<crypto::public_key> m_signers; + + /// local participant's personal keys + // base keypair of the participant + // - used for signing messages, as the initial base key for key exchange, and to make DH derivations for key exchange + crypto::secret_key m_base_privkey; + crypto::public_key m_base_pubkey; + // common base privkey, used to produce the aggregate common privkey + crypto::secret_key m_base_common_privkey; + + /// core multisig account keys + // the account's private key shares of the multisig address + // TODO: also record which other signers have these privkeys, to enable aggregation signing (instead of round-robin) + std::vector<crypto::secret_key> m_multisig_privkeys; + // a privkey owned by all multisig participants (e.g. a cryptonote view key) + crypto::secret_key m_common_privkey; + // the multisig public key (e.g. a cryptonote spend key) + crypto::public_key m_multisig_pubkey; + // the common public key (e.g. a view spend key) + crypto::public_key m_common_pubkey; + + /// kex variables + // number of key exchange rounds that have been completed (all messages for the round collected and processed) + std::uint32_t m_kex_rounds_complete{0}; + // this account's pubkeys for the in-progress key exchange round + // - either DH derivations (intermediate rounds), H(derivation)*G (final round), empty (when kex is done) + kex_origins_map_t m_kex_keys_to_origins_map; + // the account's message for the in-progress key exchange round + std::string m_next_round_kex_message; + }; + + /** + * brief: multisig_kex_rounds_required - The number of key exchange rounds required to produce an M-of-N shared key. + * - Key exchange (kex) is a synchronous series of 'rounds'. In an 'active round', participants send messages + * to each other. + * - A participant considers a round 'complete' when they have collected sufficient messages + * from other participants, processed those messages, and updated their multisig account state. + * - Typically (as implemented in this module), completing a round coincides with making a message for the next round. + * param: num_signers - number of participants in multisig (N) + * param: threshold - threshold of multisig (M) + * return: number of kex rounds required + */ + std::uint32_t multisig_kex_rounds_required(const std::uint32_t num_signers, const std::uint32_t threshold); +} //namespace multisig diff --git a/src/multisig/multisig_account_kex_impl.cpp b/src/multisig/multisig_account_kex_impl.cpp new file mode 100644 index 000000000..0a0ca7bdc --- /dev/null +++ b/src/multisig/multisig_account_kex_impl.cpp @@ -0,0 +1,726 @@ +// Copyright (c) 2021, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "multisig_account.h" + +#include "crypto/crypto.h" +#include "cryptonote_config.h" +#include "include_base_utils.h" +#include "multisig.h" +#include "multisig_kex_msg.h" +#include "ringct/rctOps.h" + +#include <boost/math/special_functions/binomial.hpp> + +#include <algorithm> +#include <cmath> +#include <cstdint> +#include <limits> +#include <memory> +#include <unordered_map> +#include <unordered_set> +#include <utility> +#include <vector> + + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "multisig" + +namespace multisig +{ + //---------------------------------------------------------------------------------------------------------------------- + /** + * INTERNAL + * + * brief: calculate_multisig_keypair_from_derivation - wrapper on calculate_multisig_keypair() for an input public key + * Converts an input public key into a crypto private key (type cast, does not change serialization), + * then passes it to get_multisig_blinded_secret_key(). + * + * Result: + * - privkey = H(derivation) + * - pubkey = privkey * G + * param: derivation - a curve point + * outparam: derived_pubkey_out - public key of the resulting privkey + * return: multisig private key + */ + //---------------------------------------------------------------------------------------------------------------------- + static crypto::secret_key calculate_multisig_keypair_from_derivation(const crypto::public_key_memsafe &derivation, + crypto::public_key &derived_pubkey_out) + { + crypto::secret_key blinded_skey = get_multisig_blinded_secret_key(rct::rct2sk(rct::pk2rct(derivation))); + CHECK_AND_ASSERT_THROW_MES(crypto::secret_key_to_public_key(blinded_skey, derived_pubkey_out), "Failed to derive public key"); + + return blinded_skey; + } + //---------------------------------------------------------------------------------------------------------------------- + /** + * INTERNAL + * + * brief: make_multisig_common_privkey - Create the 'common' multisig privkey, owned by all multisig participants. + * - common privkey = H(sorted base common privkeys) + * param: participant_base_common_privkeys - Base common privkeys contributed by multisig participants. + * outparam: common_privkey_out - result + */ + //---------------------------------------------------------------------------------------------------------------------- + static void make_multisig_common_privkey(std::vector<crypto::secret_key> participant_base_common_privkeys, + crypto::secret_key &common_privkey_out) + { + // sort the privkeys for consistency + //TODO: need a constant-time operator< for sorting secret keys + std::sort(participant_base_common_privkeys.begin(), participant_base_common_privkeys.end(), + [](const crypto::secret_key &key1, const crypto::secret_key &key2) -> bool + { + return memcmp(&key1, &key2, sizeof(crypto::secret_key)) < 0; + } + ); + + // privkey = H(sorted ancillary base privkeys) + crypto::hash_to_scalar(participant_base_common_privkeys.data(), + participant_base_common_privkeys.size()*sizeof(crypto::secret_key), + common_privkey_out); + + CHECK_AND_ASSERT_THROW_MES(common_privkey_out != crypto::null_skey, "Unexpected null secret key (danger!)."); + } + //---------------------------------------------------------------------------------------------------------------------- + /** + * INTERNAL + * + * brief: compute_multisig_aggregation_coefficient - creates aggregation coefficient for a specific public key in a set + * of public keys + * + * WARNING: The coefficient will only be deterministic if... + * 1) input keys are pre-sorted + * - tested here + * 2) input keys are in canonical form (compressed points in the prime-order subgroup of Ed25519) + * - untested here for performance + * param: sorted_keys - set of component public keys that will be merged into a multisig public spend key + * param: aggregation_key - one of the component public keys + * return: aggregation coefficient + */ + //---------------------------------------------------------------------------------------------------------------------- + static rct::key compute_multisig_aggregation_coefficient(const std::vector<crypto::public_key> &sorted_keys, + const crypto::public_key &aggregation_key) + { + CHECK_AND_ASSERT_THROW_MES(std::is_sorted(sorted_keys.begin(), sorted_keys.end()), + "Keys for aggregation coefficient aren't sorted."); + + // aggregation key must be in sorted_keys + CHECK_AND_ASSERT_THROW_MES(std::find(sorted_keys.begin(), sorted_keys.end(), aggregation_key) != sorted_keys.end(), + "Aggregation key expected to be in input keyset."); + + // aggregation coefficient salt + rct::key salt = rct::zero(); + static_assert(sizeof(rct::key) >= sizeof(config::HASH_KEY_MULTISIG_KEY_AGGREGATION), "Hash domain separator is too big."); + memcpy(salt.bytes, config::HASH_KEY_MULTISIG_KEY_AGGREGATION, sizeof(config::HASH_KEY_MULTISIG_KEY_AGGREGATION)); + + // coeff = H(aggregation_key, sorted_keys, domain-sep) + rct::keyV data; + data.reserve(sorted_keys.size() + 2); + data.push_back(rct::pk2rct(aggregation_key)); + for (const auto &key : sorted_keys) + data.push_back(rct::pk2rct(key)); + data.push_back(salt); + + // note: coefficient is considered public knowledge, no need to memwipe data + return rct::hash_to_scalar(data); + } + //---------------------------------------------------------------------------------------------------------------------- + /** + * INTERNAL + * + * brief: generate_multisig_aggregate_key - generates a multisig public spend key via key aggregation + * Key aggregation via aggregation coefficients prevents key cancellation attacks. + * See: https://www.getmonero.org/resources/research-lab/pubs/MRL-0009.pdf + * param: final_keys - address components (public keys) obtained from other participants (not shared with local) + * param: privkeys_inout - private keys of address components known by local; each key will be multiplied by an aggregation coefficient (return by reference) + * return: final multisig public spend key for the account + */ + //---------------------------------------------------------------------------------------------------------------------- + static crypto::public_key generate_multisig_aggregate_key(std::vector<crypto::public_key> final_keys, + std::vector<crypto::secret_key> &privkeys_inout) + { + // collect all public keys that will go into the spend key (these don't need to be memsafe) + final_keys.reserve(final_keys.size() + privkeys_inout.size()); + + // 1. convert local multisig private keys to pub keys + // 2. insert to final keyset if not there yet + // 3. save the corresponding index of input priv key set for later reference + std::unordered_map<crypto::public_key, std::size_t> own_keys_mapping; + + for (std::size_t multisig_keys_index{0}; multisig_keys_index < privkeys_inout.size(); ++multisig_keys_index) + { + crypto::public_key pubkey; + CHECK_AND_ASSERT_THROW_MES(crypto::secret_key_to_public_key(privkeys_inout[multisig_keys_index], pubkey), "Failed to derive public key"); + + own_keys_mapping[pubkey] = multisig_keys_index; + + final_keys.push_back(pubkey); + } + + // sort input final keys for computing aggregation coefficients (lowest to highest) + // note: input should be sanitized (no duplicates) + std::sort(final_keys.begin(), final_keys.end()); + CHECK_AND_ASSERT_THROW_MES(std::adjacent_find(final_keys.begin(), final_keys.end()) == final_keys.end(), + "Unexpected duplicate found in input list."); + + // key aggregation + rct::key aggregate_key = rct::identity(); + + for (const crypto::public_key &key : final_keys) + { + // get aggregation coefficient + rct::key coeff = compute_multisig_aggregation_coefficient(final_keys, key); + + // convert private key if possible + // note: retain original priv key index in input list, in case order matters upstream + auto found_key = own_keys_mapping.find(key); + if (found_key != own_keys_mapping.end()) + { + // k_agg = coeff*k_base + sc_mul((unsigned char*)&(privkeys_inout[found_key->second]), + coeff.bytes, + (const unsigned char*)&(privkeys_inout[found_key->second])); + + CHECK_AND_ASSERT_THROW_MES(privkeys_inout[found_key->second] != crypto::null_skey, + "Multisig privkey with aggregation coefficient unexpectedly null."); + } + + // convert public key (pre-merge operation) + // K_agg = coeff*K_base + rct::key converted_pubkey = rct::scalarmultKey(rct::pk2rct(key), coeff); + + // build aggregate key (merge operation) + rct::addKeys(aggregate_key, aggregate_key, converted_pubkey); + } + + return rct::rct2pk(aggregate_key); + } + //---------------------------------------------------------------------------------------------------------------------- + /** + * INTERNAL + * + * brief: multisig_kex_make_next_msg - Construct a kex msg for any round > 1 of multisig key construction. + * - Involves DH exchanges with pubkeys provided by other participants. + * - Conserves mapping [pubkey -> DH derivation] : [origin keys of participants that share this secret with you]. + * param: base_privkey - account's base private key, for performing DH exchanges and signing messages + * param: round - the round of the message that should be produced + * param: threshold - threshold for multisig (M in M-of-N) + * param: num_signers - number of participants in multisig (N) + * param: pubkey_origins_map - map between pubkeys to produce DH derivations with and identity keys of + * participants who will share each derivation with you + * outparam: derivation_origins_map_out - map between DH derivations (shared secrets) and identity keys + * - If msg is not for the last round, then these derivations are also stored in the output message + * so they can be sent to other participants, who will make more DH derivations for the next kex round. + * - If msg is for the last round, then these derivations won't be sent to other participants. + * Instead, they are converted to share secrets (i.e. s = H(derivation)) and multiplied by G. + * The keys s*G are sent to other participants in the message, so they can be used to produce the final + * multisig key via generate_multisig_spend_public_key(). + * - The values s are the local account's shares of the final multisig key's private key. The caller can + * compute those values with calculate_multisig_keypair_from_derivation() (or compute them directly). + * return: multisig kex message for the specified round + */ + //---------------------------------------------------------------------------------------------------------------------- + static multisig_kex_msg multisig_kex_make_next_msg(const crypto::secret_key &base_privkey, + const std::uint32_t round, + const std::uint32_t threshold, + const std::uint32_t num_signers, + const std::unordered_map<crypto::public_key_memsafe, std::unordered_set<crypto::public_key>> &pubkey_origins_map, + std::unordered_map<crypto::public_key_memsafe, std::unordered_set<crypto::public_key>> &derivation_origins_map_out) + { + CHECK_AND_ASSERT_THROW_MES(num_signers > 1, "Must be at least one other multisig signer."); + CHECK_AND_ASSERT_THROW_MES(num_signers <= config::MULTISIG_MAX_SIGNERS, + "Too many multisig signers specified (limit = 16 to prevent dangerous combinatorial explosion during key exchange)."); + CHECK_AND_ASSERT_THROW_MES(num_signers >= threshold, + "Multisig threshold may not be larger than number of signers."); + CHECK_AND_ASSERT_THROW_MES(threshold > 0, "Multisig threshold must be > 0."); + CHECK_AND_ASSERT_THROW_MES(round > 1, "Round for next msg must be > 1."); + CHECK_AND_ASSERT_THROW_MES(round <= multisig_kex_rounds_required(num_signers, threshold), + "Trying to make key exchange message for an invalid round."); + + // make shared secrets with input pubkeys + std::vector<crypto::public_key> msg_pubkeys; + msg_pubkeys.reserve(pubkey_origins_map.size()); + derivation_origins_map_out.clear(); + + for (const auto &pubkey_and_origins : pubkey_origins_map) + { + // D = 8 * k_base * K_pubkey + // note: must be mul8 (cofactor), otherwise it is possible to leak to a malicious participant if the local + // base_privkey is a multiple of 8 or not + // note2: avoid making temporaries that won't be memwiped + rct::key derivation_rct; + auto a_wiper = epee::misc_utils::create_scope_leave_handler([&]{ + memwipe(&derivation_rct, sizeof(rct::key)); + }); + + rct::scalarmultKey(derivation_rct, rct::pk2rct(pubkey_and_origins.first), rct::sk2rct(base_privkey)); + rct::scalarmultKey(derivation_rct, derivation_rct, rct::EIGHT); + + crypto::public_key_memsafe derivation{rct::rct2pk(derivation_rct)}; + + // retain mapping between pubkey's origins and the DH derivation + // note: if msg for last round, then caller must know how to handle these derivations properly + derivation_origins_map_out[derivation] = pubkey_and_origins.second; + + // if the last round, convert derivations to public keys for the output message + if (round == multisig_kex_rounds_required(num_signers, threshold)) + { + // derived_pubkey = H(derivation)*G + crypto::public_key derived_pubkey; + calculate_multisig_keypair_from_derivation(derivation, derived_pubkey); + msg_pubkeys.push_back(derived_pubkey); + } + // otherwise, put derivations in message directly, so other signers can in turn create derivations (shared secrets) + // with them for the next round + else + msg_pubkeys.push_back(derivation); + } + + return multisig_kex_msg{round, base_privkey, std::move(msg_pubkeys)}; + } + //---------------------------------------------------------------------------------------------------------------------- + /** + * INTERNAL + * + * brief: multisig_kex_msgs_sanitize_pubkeys - Sanitize multisig kex messages. + * - Removes duplicates from msg pubkeys, ignores pubkeys equal to the local account's signing key, + * ignores messages signed by the local account, ignores keys found in input 'exclusion set', + * constructs map of pubkey:origins. + * - Requires that all input msgs have the same round number. + * + * origins = all the signing pubkeys that recommended a given pubkey found in input msgs + * + * - If the messages' round numbers are all '1', then only the message signing pubkey is considered + * 'recommended'. Furthermore, the 'exclusion set' is ignored. + * param: own_pubkey - local account's signing key (key used to sign multisig messages) + * param: expanded_msgs - set of multisig kex messages to process + * param: exclude_pubkeys - pubkeys to exclude from output set + * outparam: sanitized_pubkeys_out - processed pubkeys obtained from msgs, mapped to their origins + * return: round number shared by all input msgs + */ + //---------------------------------------------------------------------------------------------------------------------- + static std::uint32_t multisig_kex_msgs_sanitize_pubkeys(const crypto::public_key &own_pubkey, + const std::vector<multisig_kex_msg> &expanded_msgs, + const std::vector<crypto::public_key> &exclude_pubkeys, + std::unordered_map<crypto::public_key_memsafe, std::unordered_set<crypto::public_key>> &sanitized_pubkeys_out) + { + CHECK_AND_ASSERT_THROW_MES(expanded_msgs.size() > 0, "At least one input message expected."); + + std::uint32_t round = expanded_msgs[0].get_round(); + sanitized_pubkeys_out.clear(); + + // get all pubkeys from input messages, add them to pubkey:origins map + // - origins = all the signing pubkeys that recommended a given msg pubkey + for (const auto &expanded_msg : expanded_msgs) + { + CHECK_AND_ASSERT_THROW_MES(expanded_msg.get_round() == round, "All messages must have the same kex round number."); + + // ignore messages from self + if (expanded_msg.get_signing_pubkey() == own_pubkey) + continue; + + // in round 1, only the signing pubkey is treated as a msg pubkey + if (round == 1) + { + // note: ignores duplicates + sanitized_pubkeys_out[expanded_msg.get_signing_pubkey()].insert(expanded_msg.get_signing_pubkey()); + } + // in other rounds, only the msg pubkeys are treated as msg pubkeys + else + { + // copy all pubkeys from message into list + for (const auto &pubkey : expanded_msg.get_msg_pubkeys()) + { + // ignore own pubkey + if (pubkey == own_pubkey) + continue; + + // ignore pubkeys in 'ignore' set + if (std::find(exclude_pubkeys.begin(), exclude_pubkeys.end(), pubkey) != exclude_pubkeys.end()) + continue; + + // note: ignores duplicates + sanitized_pubkeys_out[pubkey].insert(expanded_msg.get_signing_pubkey()); + } + } + } + + return round; + } + //---------------------------------------------------------------------------------------------------------------------- + /** + * INTERNAL + * + * brief: evaluate_multisig_kex_round_msgs - Evaluate pubkeys from a kex round in order to prepare for the next round. + * - Sanitizes input msgs. + * - Require uniqueness in: 'signers', 'exclude_pubkeys'. + * - Requires each input pubkey be recommended by 'num_recommendations = expected_round' msg signers. + * - For a final multisig key to be truly 'M-of-N', each of the the private key's components must be + * shared by (N - M + 1) signers. + * - Requires that msgs are signed by only keys in 'signers'. + * - Requires that each key in 'signers' recommends [num_signers - 2 CHOOSE (expected_round - 1)] pubkeys. + * - These should be derivations each signer recommends for round 'expected_round', excluding derivations shared + * with the local account. + * - Requires that 'exclude_pubkeys' has [num_signers - 1 CHOOSE (expected_round - 1)] pubkeys. + * - These should be derivations the local account has corresponding to round 'expected_round'. + * param: base_privkey - multisig account's base private key + * param: expected_round - expected kex round of input messages + * param: threshold - threshold for multisig (M in M-of-N) + * param: signers - expected participants in multisig kex + * param: expanded_msgs - set of multisig kex messages to process + * param: exclude_pubkeys - derivations held by the local account corresponding to round 'expected_round' + * return: fully sanitized and validated pubkey:origins map for building the account's next kex round message + */ + //---------------------------------------------------------------------------------------------------------------------- + static std::unordered_map<crypto::public_key_memsafe, std::unordered_set<crypto::public_key>> evaluate_multisig_kex_round_msgs( + const crypto::public_key &base_pubkey, + const std::uint32_t expected_round, + const std::uint32_t threshold, + const std::vector<crypto::public_key> &signers, + const std::vector<multisig_kex_msg> &expanded_msgs, + const std::vector<crypto::public_key> &exclude_pubkeys) + { + CHECK_AND_ASSERT_THROW_MES(signers.size() > 1, "Must be at least one other multisig signer."); + CHECK_AND_ASSERT_THROW_MES(signers.size() <= config::MULTISIG_MAX_SIGNERS, + "Too many multisig signers specified (limit = 16 to prevent dangerous combinatorial explosion during key exchange)."); + CHECK_AND_ASSERT_THROW_MES(signers.size() >= threshold, "Multisig threshold may not be larger than number of signers."); + CHECK_AND_ASSERT_THROW_MES(threshold > 0, "Multisig threshold must be > 0."); + CHECK_AND_ASSERT_THROW_MES(expected_round > 0, "Expected round must be > 0."); + CHECK_AND_ASSERT_THROW_MES(expected_round <= multisig_kex_rounds_required(signers.size(), threshold), + "Expecting key exchange messages for an invalid round."); + + std::unordered_map<crypto::public_key_memsafe, std::unordered_set<crypto::public_key>> pubkey_origins_map; + + // leave early in the last round of 1-of-N, where all signers share a key so the local signer doesn't care about + // recommendations from other signers + if (threshold == 1 && expected_round == multisig_kex_rounds_required(signers.size(), threshold)) + return pubkey_origins_map; + + // exclude_pubkeys should all be unique + for (auto it = exclude_pubkeys.begin(); it != exclude_pubkeys.end(); ++it) + { + CHECK_AND_ASSERT_THROW_MES(std::find(exclude_pubkeys.begin(), it, *it) == it, + "Found duplicate pubkeys for exclusion unexpectedly."); + } + + // sanitize input messages + std::uint32_t round = multisig_kex_msgs_sanitize_pubkeys(base_pubkey, expanded_msgs, exclude_pubkeys, pubkey_origins_map); + CHECK_AND_ASSERT_THROW_MES(round == expected_round, + "Kex messages were for round [" << round << "], but expected round is [" << expected_round << "]"); + + // evaluate pubkeys collected + std::unordered_map<crypto::public_key, std::unordered_set<crypto::public_key>> origin_pubkeys_map; + + // 1. each pubkey should be recommended by a precise number of signers + for (const auto &pubkey_and_origins : pubkey_origins_map) + { + // expected amount = round_num + // With each successive round, pubkeys are shared by incrementally larger groups, + // starting at 1 in round 1 (i.e. the local multisig key to start kex with). + CHECK_AND_ASSERT_THROW_MES(pubkey_and_origins.second.size() == round, + "A pubkey recommended by multisig kex messages had an unexpected number of recommendations."); + + // map (sanitized) pubkeys back to origins + for (const auto &origin : pubkey_and_origins.second) + origin_pubkeys_map[origin].insert(pubkey_and_origins.first); + } + + // 2. the number of unique signers recommending pubkeys should equal the number of signers passed in (minus the local signer) + CHECK_AND_ASSERT_THROW_MES(origin_pubkeys_map.size() == signers.size() - 1, + "Number of unique other signers does not equal number of other signers that recommended pubkeys."); + + // 3. each origin should recommend a precise number of pubkeys + + // TODO: move to a 'math' library, with unit tests + auto n_choose_k_f = + [](const std::uint32_t n, const std::uint32_t k) -> std::uint32_t + { + static_assert(std::numeric_limits<std::int32_t>::digits <= std::numeric_limits<double>::digits, + "n_choose_k requires no rounding issues when converting between int32 <-> double."); + + if (n < k) + return 0; + + double fp_result = boost::math::binomial_coefficient<double>(n, k); + + if (fp_result < 0) + return 0; + + if (fp_result > std::numeric_limits<std::int32_t>::max()) // note: std::round() returns std::int32_t + return 0; + + return static_cast<std::uint32_t>(std::round(fp_result)); + }; + + // other signers: (N - 2) choose (msg_round_num - 1) + // - Each signer recommends keys they share with other signers. + // - In each round, a signer shares a key with 'round num - 1' other signers. + // - Since 'origins pubkey map' excludes keys shared with the local account, + // only keys shared with participants 'other than local and self' will be in the map (e.g. N - 2 signers). + // - So other signers will recommend (N - 2) choose (msg_round_num - 1) pubkeys (after removing keys shared with local). + // - Each origin should have a shared key with each group of size 'round - 1'. + // Note: Keys shared with local are ignored to facilitate kex round boosting, where one or more signers may + // have boosted the local signer (implying they didn't have access to the local signer's previous round msg). + std::uint32_t expected_recommendations_others = n_choose_k_f(signers.size() - 2, round - 1); + + // local: (N - 1) choose (msg_round_num - 1) + std::uint32_t expected_recommendations_self = n_choose_k_f(signers.size() - 1, round - 1); + + // note: expected_recommendations_others would be 0 in the last round of 1-of-N, but we return early for that case + CHECK_AND_ASSERT_THROW_MES(expected_recommendations_self > 0 && expected_recommendations_others > 0, + "Bad num signers or round num (possibly numerical limits exceeded)."); + + // check that local account recommends expected number of keys + CHECK_AND_ASSERT_THROW_MES(exclude_pubkeys.size() == expected_recommendations_self, + "Local account did not recommend expected number of multisig keys."); + + // check that other signers recommend expected number of keys + for (const auto &origin_and_pubkeys : origin_pubkeys_map) + { + CHECK_AND_ASSERT_THROW_MES(origin_and_pubkeys.second.size() == expected_recommendations_others, + "A pubkey recommended by multisig kex messages had an unexpected number of recommendations."); + + // 2 (continued). only expected signers should be recommending keys + CHECK_AND_ASSERT_THROW_MES(std::find(signers.begin(), signers.end(), origin_and_pubkeys.first) != signers.end(), + "Multisig kex message with unexpected signer encountered."); + } + + // note: above tests implicitly detect if the total number of recommended keys is correct or not + return pubkey_origins_map; + } + //---------------------------------------------------------------------------------------------------------------------- + /** + * INTERNAL + * + * brief: multisig_kex_process_round - Process kex messages for the active kex round. + * - A wrapper around evaluate_multisig_kex_round_msgs() -> multisig_kex_make_next_msg(). + * - In other words, evaluate the input messages and try to make a message for the next round. + * - Note: Must be called on the final round's msgs to evaluate the final key components + * recommended by other participants. + * param: base_privkey - multisig account's base private key + * param: current_round - round of kex the input messages should be designed for + * param: threshold - threshold for multisig (M in M-of-N) + * param: signers - expected participants in multisig kex + * param: expanded_msgs - set of multisig kex messages to process + * param: exclude_pubkeys - keys held by the local account corresponding to round 'current_round' + * - If 'current_round' is the final round, these are the local account's shares of the final aggregate key. + * outparam: keys_to_origins_map_out - map between round keys and identity keys + * - If in the final round, these are key shares recommended by other signers for the final aggregate key. + * - Otherwise, these are the local account's DH derivations for the next round. + * - See multisig_kex_make_next_msg() for an explanation. + * return: multisig kex message for next round, or empty message if 'current_round' is the final round + */ + //---------------------------------------------------------------------------------------------------------------------- + static multisig_kex_msg multisig_kex_process_round(const crypto::secret_key &base_privkey, + const crypto::public_key &base_pubkey, + const std::uint32_t current_round, + const std::uint32_t threshold, + const std::vector<crypto::public_key> &signers, + const std::vector<multisig_kex_msg> &expanded_msgs, + const std::vector<crypto::public_key> &exclude_pubkeys, + std::unordered_map<crypto::public_key_memsafe, std::unordered_set<crypto::public_key>> &keys_to_origins_map_out) + { + // evaluate messages + std::unordered_map<crypto::public_key_memsafe, std::unordered_set<crypto::public_key>> evaluated_pubkeys = + evaluate_multisig_kex_round_msgs(base_pubkey, current_round, threshold, signers, expanded_msgs, exclude_pubkeys); + + // produce message for next round (if there is one) + if (current_round < multisig_kex_rounds_required(signers.size(), threshold)) + { + return multisig_kex_make_next_msg(base_privkey, + current_round + 1, + threshold, + signers.size(), + evaluated_pubkeys, + keys_to_origins_map_out); + } + else + { + // no more rounds, so collect the key shares recommended by other signers for the final aggregate key + keys_to_origins_map_out.clear(); + keys_to_origins_map_out = std::move(evaluated_pubkeys); + + return multisig_kex_msg{}; + } + } + //---------------------------------------------------------------------------------------------------------------------- + // multisig_account: INTERNAL + //---------------------------------------------------------------------------------------------------------------------- + void multisig_account::initialize_kex_update(const std::vector<multisig_kex_msg> &expanded_msgs, + const std::uint32_t rounds_required, + std::vector<crypto::public_key> &exclude_pubkeys_out) + { + if (m_kex_rounds_complete == 0) + { + // the first round of kex msgs will contain each participant's base pubkeys and ancillary privkeys + + // collect participants' base common privkey shares + // note: duplicate privkeys are acceptable, and duplicates due to duplicate signers + // will be blocked by duplicate-signer errors after this function is called + std::vector<crypto::secret_key> participant_base_common_privkeys; + participant_base_common_privkeys.reserve(expanded_msgs.size() + 1); + + // add local ancillary base privkey + participant_base_common_privkeys.emplace_back(m_base_common_privkey); + + // add other signers' base common privkeys + for (const auto &expanded_msg : expanded_msgs) + { + if (expanded_msg.get_signing_pubkey() != m_base_pubkey) + { + participant_base_common_privkeys.emplace_back(expanded_msg.get_msg_privkey()); + } + } + + // make common privkey + make_multisig_common_privkey(std::move(participant_base_common_privkeys), m_common_privkey); + + // set common pubkey + CHECK_AND_ASSERT_THROW_MES(crypto::secret_key_to_public_key(m_common_privkey, m_common_pubkey), + "Failed to derive public key"); + + // if N-of-N, then the base privkey will be used directly to make the account's share of the final key + if (rounds_required == 1) + { + m_multisig_privkeys.clear(); + m_multisig_privkeys.emplace_back(m_base_privkey); + } + + // exclude all keys the local account recommends + // - in the first round, only the local pubkey is recommended by the local signer + exclude_pubkeys_out.emplace_back(m_base_pubkey); + } + else + { + // in other rounds, kex msgs will contain participants' shared keys + + // ignore shared keys the account helped create for this round + for (const auto &shared_key_with_origins : m_kex_keys_to_origins_map) + { + exclude_pubkeys_out.emplace_back(shared_key_with_origins.first); + } + } + } + //---------------------------------------------------------------------------------------------------------------------- + // multisig_account: INTERNAL + //---------------------------------------------------------------------------------------------------------------------- + void multisig_account::finalize_kex_update(const std::uint32_t rounds_required, + std::unordered_map<crypto::public_key_memsafe, std::unordered_set<crypto::public_key>> result_keys_to_origins_map) + { + // prepare for next round (or complete the multisig account fully) + if (rounds_required == m_kex_rounds_complete + 1) + { + // finished (have set of msgs to complete address) + + // when 'completing the final round', result keys are other signers' shares of the final key + std::vector<crypto::public_key> result_keys; + result_keys.reserve(result_keys_to_origins_map.size()); + + for (const auto &result_key_and_origins : result_keys_to_origins_map) + { + result_keys.emplace_back(result_key_and_origins.first); + } + + // compute final aggregate key, update local multisig privkeys with aggregation coefficients applied + m_multisig_pubkey = generate_multisig_aggregate_key(std::move(result_keys), m_multisig_privkeys); + + // no longer need the account's pubkeys saved for this round (they were only used to build exclude_pubkeys) + // TODO: record [pre-aggregation pubkeys : origins] map for aggregation-style signing + m_kex_keys_to_origins_map.clear(); + } + else if (rounds_required == m_kex_rounds_complete + 2) + { + // one more round (must send/receive one more set of kex msgs) + // - at this point, have local signer's pre-aggregation private key shares of the final address + + // result keys are the local signer's DH derivations for the next round + + // derivations are shared secrets between each group of N - M + 1 signers of which the local account is a member + // - convert them to private keys: multisig_key = H(derivation) + // - note: shared key = multisig_key[i]*G is recorded in the kex msg for sending to other participants + // instead of the original 'derivation' value (which MUST be kept secret!) + m_multisig_privkeys.clear(); + m_multisig_privkeys.reserve(result_keys_to_origins_map.size()); + + m_kex_keys_to_origins_map.clear(); + + for (const auto &derivation_and_origins : result_keys_to_origins_map) + { + // multisig_privkey = H(derivation) + // derived pubkey = multisig_key * G + crypto::public_key_memsafe derived_pubkey; + m_multisig_privkeys.push_back( + calculate_multisig_keypair_from_derivation(derivation_and_origins.first, derived_pubkey)); + + // save the account's kex key mappings for this round [derived pubkey : other signers who will have the same key] + m_kex_keys_to_origins_map[derived_pubkey] = std::move(derivation_and_origins.second); + } + } + else + { + // next round is an 'intermediate' key exchange round, so there is nothing special to do here + + // save the account's kex keys for this round [DH derivation : other signers who will have the same derivation] + m_kex_keys_to_origins_map = std::move(result_keys_to_origins_map); + } + + // a full set of msgs has been collected and processed, so the 'round is complete' + ++m_kex_rounds_complete; + } + //---------------------------------------------------------------------------------------------------------------------- + // multisig_account: INTERNAL + //---------------------------------------------------------------------------------------------------------------------- + void multisig_account::kex_update_impl(const std::vector<multisig_kex_msg> &expanded_msgs) + { + CHECK_AND_ASSERT_THROW_MES(expanded_msgs.size() > 0, "No key exchange messages passed in."); + + const std::uint32_t rounds_required = multisig_kex_rounds_required(m_signers.size(), m_threshold); + CHECK_AND_ASSERT_THROW_MES(rounds_required > 0, "Multisig kex rounds required unexpectedly 0."); + + // initialize account update + std::vector<crypto::public_key> exclude_pubkeys; + initialize_kex_update(expanded_msgs, rounds_required, exclude_pubkeys); + + // evaluate messages and get this account's kex msg for the next round + std::unordered_map<crypto::public_key_memsafe, std::unordered_set<crypto::public_key>> result_keys_to_origins_map; + + m_next_round_kex_message = multisig_kex_process_round( + m_base_privkey, + m_base_pubkey, + m_kex_rounds_complete + 1, + m_threshold, + m_signers, + expanded_msgs, + exclude_pubkeys, + result_keys_to_origins_map).get_msg(); + + // finish account update + finalize_kex_update(rounds_required, std::move(result_keys_to_origins_map)); + } + //---------------------------------------------------------------------------------------------------------------------- +} //namespace multisig diff --git a/src/multisig/multisig_kex_msg.cpp b/src/multisig/multisig_kex_msg.cpp new file mode 100644 index 000000000..2bbceb19d --- /dev/null +++ b/src/multisig/multisig_kex_msg.cpp @@ -0,0 +1,290 @@ +// Copyright (c) 2021, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "multisig_kex_msg.h" +#include "multisig_kex_msg_serialization.h" + +#include "common/base58.h" +#include "crypto/crypto.h" +extern "C" +{ +#include "crypto/crypto-ops.h" +} +#include "cryptonote_basic/cryptonote_format_utils.h" +#include "include_base_utils.h" +#include "ringct/rctOps.h" +#include "serialization/binary_archive.h" +#include "serialization/serialization.h" + +#include <boost/utility/string_ref.hpp> + +#include <sstream> +#include <utility> +#include <vector> + + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "multisig" + +const boost::string_ref MULTISIG_KEX_V1_MAGIC{"MultisigV1"}; +const boost::string_ref MULTISIG_KEX_MSG_V1_MAGIC{"MultisigxV1"}; +const boost::string_ref MULTISIG_KEX_MSG_V2_MAGIC_1{"MultisigxV2R1"}; //round 1 +const boost::string_ref MULTISIG_KEX_MSG_V2_MAGIC_N{"MultisigxV2Rn"}; //round n > 1 + +namespace multisig +{ + //---------------------------------------------------------------------------------------------------------------------- + // multisig_kex_msg: EXTERNAL + //---------------------------------------------------------------------------------------------------------------------- + multisig_kex_msg::multisig_kex_msg(const std::uint32_t round, + const crypto::secret_key &signing_privkey, + std::vector<crypto::public_key> msg_pubkeys, + const crypto::secret_key &msg_privkey) : + m_kex_round{round} + { + CHECK_AND_ASSERT_THROW_MES(round > 0, "Kex round must be > 0."); + CHECK_AND_ASSERT_THROW_MES(sc_check((const unsigned char*)&signing_privkey) == 0 && + signing_privkey != crypto::null_skey, "Invalid msg signing key."); + + if (round == 1) + { + CHECK_AND_ASSERT_THROW_MES(sc_check((const unsigned char*)&msg_privkey) == 0 && + msg_privkey != crypto::null_skey, "Invalid msg privkey."); + + m_msg_privkey = msg_privkey; + } + else + { + for (const auto &pubkey : msg_pubkeys) + { + CHECK_AND_ASSERT_THROW_MES(pubkey != crypto::null_pkey && pubkey != rct::rct2pk(rct::identity()), + "Pubkey for message was invalid."); + CHECK_AND_ASSERT_THROW_MES((rct::scalarmultKey(rct::pk2rct(pubkey), rct::curveOrder()) == rct::identity()), + "Pubkey for message was not in prime subgroup."); + } + + m_msg_pubkeys = std::move(msg_pubkeys); + } + CHECK_AND_ASSERT_THROW_MES(crypto::secret_key_to_public_key(signing_privkey, m_signing_pubkey), + "Failed to derive public key"); + + // sets message and signing pub key + construct_msg(signing_privkey); + } + //---------------------------------------------------------------------------------------------------------------------- + // multisig_kex_msg: EXTERNAL + //---------------------------------------------------------------------------------------------------------------------- + multisig_kex_msg::multisig_kex_msg(std::string msg) : m_msg{std::move(msg)} + { + parse_and_validate_msg(); + } + //---------------------------------------------------------------------------------------------------------------------- + // multisig_kex_msg: INTERNAL + //---------------------------------------------------------------------------------------------------------------------- + crypto::hash multisig_kex_msg::get_msg_to_sign() const + { + //// + // msg_content = kex_round | signing_pubkey | expand(msg_pubkeys) | OPTIONAL msg_privkey + // sign_msg = versioning-domain-sep | msg_content + /// + + std::string data; + CHECK_AND_ASSERT_THROW_MES(MULTISIG_KEX_MSG_V2_MAGIC_1.size() == MULTISIG_KEX_MSG_V2_MAGIC_N.size(), + "Multisig kex msg magic inconsistency."); + data.reserve(MULTISIG_KEX_MSG_V2_MAGIC_1.size() + 4 + 32*(1 + (m_kex_round == 1 ? 1 : 0) + m_msg_pubkeys.size())); + + // versioning domain-sep + if (m_kex_round == 1) + data.append(MULTISIG_KEX_MSG_V2_MAGIC_1.data(), MULTISIG_KEX_MSG_V2_MAGIC_1.size()); + else + data.append(MULTISIG_KEX_MSG_V2_MAGIC_N.data(), MULTISIG_KEX_MSG_V2_MAGIC_N.size()); + + // kex_round as little-endian bytes + for (std::size_t i{0}; i < 4; ++i) + { + data += static_cast<char>(m_kex_round >> i*8); + } + + // signing pubkey + data.append((const char *)&m_signing_pubkey, sizeof(crypto::public_key)); + + // add msg privkey if kex_round == 1 + if (m_kex_round == 1) + data.append((const char *)&m_msg_privkey, sizeof(crypto::secret_key)); + else + { + // only add pubkeys if not round 1 + + // msg pubkeys + for (const auto &key : m_msg_pubkeys) + data.append((const char *)&key, sizeof(crypto::public_key)); + } + + // message to sign + crypto::hash hash; + crypto::cn_fast_hash(data.data(), data.size(), hash); + + return hash; + } + //---------------------------------------------------------------------------------------------------------------------- + // multisig_kex_msg: INTERNAL + //---------------------------------------------------------------------------------------------------------------------- + void multisig_kex_msg::construct_msg(const crypto::secret_key &signing_privkey) + { + //// + // msg_content = kex_round | signing_pubkey | expand(msg_pubkeys) | OPTIONAL msg_privkey + // sign_msg = versioning-domain-sep | msg_content + // msg = versioning-domain-sep | serialize(msg_content | crypto_sig[signing_privkey](sign_msg)) + /// + + // sign the message + crypto::signature msg_signature; + crypto::hash msg_to_sign{get_msg_to_sign()}; + crypto::generate_signature(msg_to_sign, m_signing_pubkey, signing_privkey, msg_signature); + + // assemble the message + m_msg.clear(); + + std::stringstream serialized_msg_ss; + binary_archive<true> b_archive(serialized_msg_ss); + + if (m_kex_round == 1) + { + m_msg.append(MULTISIG_KEX_MSG_V2_MAGIC_1.data(), MULTISIG_KEX_MSG_V2_MAGIC_1.size()); + + multisig_kex_msg_serializable_round1 msg_serializable; + msg_serializable.msg_privkey = m_msg_privkey; + msg_serializable.signing_pubkey = m_signing_pubkey; + msg_serializable.signature = msg_signature; + + CHECK_AND_ASSERT_THROW_MES(::serialization::serialize(b_archive, msg_serializable), + "Failed to serialize multisig kex msg"); + } + else + { + m_msg.append(MULTISIG_KEX_MSG_V2_MAGIC_N.data(), MULTISIG_KEX_MSG_V2_MAGIC_N.size()); + + multisig_kex_msg_serializable_general msg_serializable; + msg_serializable.kex_round = m_kex_round; + msg_serializable.msg_pubkeys = m_msg_pubkeys; + msg_serializable.signing_pubkey = m_signing_pubkey; + msg_serializable.signature = msg_signature; + + CHECK_AND_ASSERT_THROW_MES(::serialization::serialize(b_archive, msg_serializable), + "Failed to serialize multisig kex msg"); + } + + m_msg.append(tools::base58::encode(serialized_msg_ss.str())); + } + //---------------------------------------------------------------------------------------------------------------------- + // multisig_kex_msg: INTERNAL + //---------------------------------------------------------------------------------------------------------------------- + void multisig_kex_msg::parse_and_validate_msg() + { + // check message type + CHECK_AND_ASSERT_THROW_MES(m_msg.size() > 0, "Kex message unexpectedly empty."); + CHECK_AND_ASSERT_THROW_MES(m_msg.substr(0, MULTISIG_KEX_V1_MAGIC.size()) != MULTISIG_KEX_V1_MAGIC, + "V1 multisig kex messages are deprecated (unsafe)."); + CHECK_AND_ASSERT_THROW_MES(m_msg.substr(0, MULTISIG_KEX_MSG_V1_MAGIC.size()) != MULTISIG_KEX_MSG_V1_MAGIC, + "V1 multisig kex messages are deprecated (unsafe)."); + + // deserialize the message + std::string msg_no_magic; + CHECK_AND_ASSERT_THROW_MES(MULTISIG_KEX_MSG_V2_MAGIC_1.size() == MULTISIG_KEX_MSG_V2_MAGIC_N.size(), + "Multisig kex msg magic inconsistency."); + CHECK_AND_ASSERT_THROW_MES(tools::base58::decode(m_msg.substr(MULTISIG_KEX_MSG_V2_MAGIC_1.size()), msg_no_magic), + "Multisig kex msg decoding error."); + binary_archive<false> b_archive{epee::strspan<std::uint8_t>(msg_no_magic)}; + crypto::signature msg_signature; + + if (m_msg.substr(0, MULTISIG_KEX_MSG_V2_MAGIC_1.size()) == MULTISIG_KEX_MSG_V2_MAGIC_1) + { + // try round 1 message + multisig_kex_msg_serializable_round1 kex_msg_rnd1; + + if (::serialization::serialize(b_archive, kex_msg_rnd1)) + { + // in round 1 the message stores a private ancillary key component for the multisig account + // that will be shared by all participants (e.g. a shared private view key) + m_kex_round = 1; + m_msg_privkey = kex_msg_rnd1.msg_privkey; + m_signing_pubkey = kex_msg_rnd1.signing_pubkey; + msg_signature = kex_msg_rnd1.signature; + } + else + { + CHECK_AND_ASSERT_THROW_MES(false, "Deserializing kex msg failed."); + } + } + else if (m_msg.substr(0, MULTISIG_KEX_MSG_V2_MAGIC_N.size()) == MULTISIG_KEX_MSG_V2_MAGIC_N) + { + // try general message + multisig_kex_msg_serializable_general kex_msg_general; + + if (::serialization::serialize(b_archive, kex_msg_general)) + { + m_kex_round = kex_msg_general.kex_round; + m_msg_privkey = crypto::null_skey; + m_msg_pubkeys = std::move(kex_msg_general.msg_pubkeys); + m_signing_pubkey = kex_msg_general.signing_pubkey; + msg_signature = kex_msg_general.signature; + + CHECK_AND_ASSERT_THROW_MES(m_kex_round > 1, "Invalid kex message round (must be > 1 for the general msg type)."); + } + else + { + CHECK_AND_ASSERT_THROW_MES(false, "Deserializing kex msg failed."); + } + } + else + { + // unknown message type + CHECK_AND_ASSERT_THROW_MES(false, "Only v2 multisig kex messages are supported."); + } + + // checks + for (const auto &pubkey: m_msg_pubkeys) + { + CHECK_AND_ASSERT_THROW_MES(pubkey != crypto::null_pkey && pubkey != rct::rct2pk(rct::identity()), + "Pubkey from message was invalid."); + CHECK_AND_ASSERT_THROW_MES(rct::isInMainSubgroup(rct::pk2rct(pubkey)), + "Pubkey from message was not in prime subgroup."); + } + + CHECK_AND_ASSERT_THROW_MES(m_signing_pubkey != crypto::null_pkey && m_signing_pubkey != rct::rct2pk(rct::identity()), + "Message signing key was invalid."); + CHECK_AND_ASSERT_THROW_MES(rct::isInMainSubgroup(rct::pk2rct(m_signing_pubkey)), + "Message signing key was not in prime subgroup."); + + // validate signature + crypto::hash signed_msg{get_msg_to_sign()}; + CHECK_AND_ASSERT_THROW_MES(crypto::check_signature(signed_msg, m_signing_pubkey, msg_signature), + "Multisig kex msg signature invalid."); + } + //---------------------------------------------------------------------------------------------------------------------- +} //namespace multisig diff --git a/src/multisig/multisig_kex_msg.h b/src/multisig/multisig_kex_msg.h new file mode 100644 index 000000000..23e3042f2 --- /dev/null +++ b/src/multisig/multisig_kex_msg.h @@ -0,0 +1,109 @@ +// Copyright (c) 2021, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include "crypto/crypto.h" + +#include <cstdint> +#include <vector> + + +namespace multisig +{ + //// + // multisig key exchange message + // - can parse and validate an input message + // - can construct and sign a new message + // + // msg_content = kex_round | signing_pubkey | expand(msg_pubkeys) | OPTIONAL msg_privkey + // msg_to_sign = versioning-domain-sep | msg_content + // msg = versioning-domain-sep | b58(msg_content | crypto_sig[signing_privkey](msg_to_sign)) + // + // note: round 1 messages will contain a private key (e.g. for the aggregate multisig private view key) + /// + class multisig_kex_msg final + { + //member types: none + + //constructors + public: + // default constructor + multisig_kex_msg() = default; + + // construct from info + multisig_kex_msg(const std::uint32_t round, + const crypto::secret_key &signing_privkey, + std::vector<crypto::public_key> msg_pubkeys, + const crypto::secret_key &msg_privkey = crypto::null_skey); + + // construct from string + multisig_kex_msg(std::string msg); + + // copy constructor: default + + //destructor: default + ~multisig_kex_msg() = default; + + //overloaded operators: none + + //member functions + // get msg string + const std::string& get_msg() const { return m_msg; } + // get kex round + std::uint32_t get_round() const { return m_kex_round; } + // get msg pubkeys + const std::vector<crypto::public_key>& get_msg_pubkeys() const { return m_msg_pubkeys; } + // get msg privkey + const crypto::secret_key& get_msg_privkey() const { return m_msg_privkey; } + // get msg signing pubkey + const crypto::public_key& get_signing_pubkey() const { return m_signing_pubkey; } + + private: + // msg_to_sign = versioning-domain-sep | kex_round | signing_pubkey | expand(msg_pubkeys) | OPTIONAL msg_privkey + crypto::hash get_msg_to_sign() const; + // set: msg string based on msg contents, signing pubkey based on input privkey + void construct_msg(const crypto::secret_key &signing_privkey); + // parse msg string into parts, validate contents and signature + void parse_and_validate_msg(); + + //member variables + private: + // message as string + std::string m_msg; + + // key exchange round this msg was produced for + std::uint32_t m_kex_round; + // pubkeys stored in msg + std::vector<crypto::public_key> m_msg_pubkeys; + // privkey stored in msg (if kex round 1) + crypto::secret_key m_msg_privkey; + // pubkey used to sign this msg + crypto::public_key m_signing_pubkey; + }; +} //namespace multisig diff --git a/src/multisig/multisig_kex_msg_serialization.h b/src/multisig/multisig_kex_msg_serialization.h new file mode 100644 index 000000000..9c7b993a7 --- /dev/null +++ b/src/multisig/multisig_kex_msg_serialization.h @@ -0,0 +1,78 @@ +// Copyright (c) 2021, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include "crypto/crypto.h" +#include "serialization/containers.h" +#include "serialization/crypto.h" +#include "serialization/serialization.h" + +#include <cstdint> +#include <vector> + + +namespace multisig +{ + /// round 1 kex message + struct multisig_kex_msg_serializable_round1 + { + // privkey stored in msg + crypto::secret_key msg_privkey; + // pubkey used to sign this msg + crypto::public_key signing_pubkey; + // message signature + crypto::signature signature; + + BEGIN_SERIALIZE() + FIELD(msg_privkey) + FIELD(signing_pubkey) + FIELD(signature) + END_SERIALIZE() + }; + + /// general kex message (if round > 1) + struct multisig_kex_msg_serializable_general + { + // key exchange round this msg was produced for + std::uint32_t kex_round; + // pubkeys stored in msg + std::vector<crypto::public_key> msg_pubkeys; + // pubkey used to sign this msg + crypto::public_key signing_pubkey; + // message signature + crypto::signature signature; + + BEGIN_SERIALIZE() + VARINT_FIELD(kex_round) + FIELD(msg_pubkeys) + FIELD(signing_pubkey) + FIELD(signature) + END_SERIALIZE() + }; +} //namespace multisig diff --git a/src/ringct/multiexp.cc b/src/ringct/multiexp.cc index 784c90a4e..f256325a1 100644 --- a/src/ringct/multiexp.cc +++ b/src/ringct/multiexp.cc @@ -235,7 +235,7 @@ rct::key bos_coster_heap_conv_robust(std::vector<MultiexpData> data) heap.reserve(points); for (size_t n = 0; n < points; ++n) { - if (!(data[n].scalar == rct::zero()) && !ge_p3_is_point_at_infinity(&data[n].point)) + if (!(data[n].scalar == rct::zero()) && !ge_p3_is_point_at_infinity_vartime(&data[n].point)) heap.push_back(n); } points = heap.size(); @@ -457,7 +457,7 @@ rct::key straus(const std::vector<MultiexpData> &data, const std::shared_ptr<str MULTIEXP_PERF(PERF_TIMER_START_UNIT(skip, 1000000)); std::vector<uint8_t> skip(data.size()); for (size_t i = 0; i < data.size(); ++i) - skip[i] = data[i].scalar == rct::zero() || ge_p3_is_point_at_infinity(&data[i].point); + skip[i] = data[i].scalar == rct::zero() || ge_p3_is_point_at_infinity_vartime(&data[i].point); MULTIEXP_PERF(PERF_TIMER_STOP(skip)); #endif diff --git a/src/simplewallet/simplewallet.cpp b/src/simplewallet/simplewallet.cpp index fd784c5ae..891253830 100644 --- a/src/simplewallet/simplewallet.cpp +++ b/src/simplewallet/simplewallet.cpp @@ -235,7 +235,6 @@ namespace const char* USAGE_IMPORT_OUTPUTS("import_outputs <filename>"); const char* USAGE_SHOW_TRANSFER("show_transfer <txid>"); const char* USAGE_MAKE_MULTISIG("make_multisig <threshold> <string1> [<string>...]"); - const char* USAGE_FINALIZE_MULTISIG("finalize_multisig <string> [<string>...]"); const char* USAGE_EXCHANGE_MULTISIG_KEYS("exchange_multisig_keys <string> [<string>...]"); const char* USAGE_EXPORT_MULTISIG_INFO("export_multisig_info <filename>"); const char* USAGE_IMPORT_MULTISIG_INFO("import_multisig_info <filename> [<filename>...]"); @@ -1021,7 +1020,7 @@ bool simple_wallet::prepare_multisig_main(const std::vector<std::string> &args, SCOPED_WALLET_UNLOCK_ON_BAD_PASSWORD(return false;); - std::string multisig_info = m_wallet->get_multisig_info(); + std::string multisig_info = m_wallet->get_multisig_first_kex_msg(); success_msg_writer() << multisig_info; success_msg_writer() << tr("Send this multisig info to all other participants, then use make_multisig <threshold> <info1> [<info2>...] with others' multisig info"); success_msg_writer() << tr("This includes the PRIVATE view key, so needs to be disclosed only to that multisig wallet's participants "); @@ -1122,58 +1121,6 @@ bool simple_wallet::make_multisig_main(const std::vector<std::string> &args, boo return true; } -bool simple_wallet::finalize_multisig(const std::vector<std::string> &args) -{ - bool ready; - if (m_wallet->key_on_device()) - { - fail_msg_writer() << tr("command not supported by HW wallet"); - return true; - } - - const auto pwd_container = get_and_verify_password(); - if(pwd_container == boost::none) - { - fail_msg_writer() << tr("Your original password was incorrect."); - return true; - } - - if (!m_wallet->multisig(&ready)) - { - fail_msg_writer() << tr("This wallet is not multisig"); - return true; - } - if (ready) - { - fail_msg_writer() << tr("This wallet is already finalized"); - return true; - } - - LOCK_IDLE_SCOPE(); - - if (args.size() < 2) - { - PRINT_USAGE(USAGE_FINALIZE_MULTISIG); - return true; - } - - try - { - if (!m_wallet->finalize_multisig(pwd_container->password(), args)) - { - fail_msg_writer() << tr("Failed to finalize multisig"); - return true; - } - } - catch (const std::exception &e) - { - fail_msg_writer() << tr("Failed to finalize multisig: ") << e.what(); - return true; - } - - return true; -} - bool simple_wallet::exchange_multisig_keys(const std::vector<std::string> &args) { exchange_multisig_keys_main(args, false); @@ -3627,10 +3574,6 @@ simple_wallet::simple_wallet() m_cmd_binder.set_handler("make_multisig", boost::bind(&simple_wallet::on_command, this, &simple_wallet::make_multisig, _1), tr(USAGE_MAKE_MULTISIG), tr("Turn this wallet into a multisig wallet")); - m_cmd_binder.set_handler("finalize_multisig", - boost::bind(&simple_wallet::on_command, this, &simple_wallet::finalize_multisig, _1), - tr(USAGE_FINALIZE_MULTISIG), - tr("Turn this wallet into a multisig wallet, extra step for N-1/N wallets")); m_cmd_binder.set_handler("exchange_multisig_keys", boost::bind(&simple_wallet::on_command, this, &simple_wallet::exchange_multisig_keys, _1), tr(USAGE_EXCHANGE_MULTISIG_KEYS), @@ -10971,8 +10914,8 @@ void simple_wallet::mms_init(const std::vector<std::string> &args) std::vector<std::string> numbers; boost::split(numbers, mn, boost::is_any_of("/")); bool mn_ok = (numbers.size() == 2) - && get_number_from_arg(numbers[1], num_authorized_signers, 2, 100) - && get_number_from_arg(numbers[0], num_required_signers, 2, num_authorized_signers); + && get_number_from_arg(numbers[1], num_authorized_signers, 2, config::MULTISIG_MAX_SIGNERS) + && get_number_from_arg(numbers[0], num_required_signers, 1, num_authorized_signers); if (!mn_ok) { fail_msg_writer() << tr("Error in the number of required signers and/or authorized signers"); diff --git a/src/simplewallet/simplewallet.h b/src/simplewallet/simplewallet.h index 8780bee1d..4bb2c6a71 100644 --- a/src/simplewallet/simplewallet.h +++ b/src/simplewallet/simplewallet.h @@ -233,7 +233,6 @@ namespace cryptonote bool prepare_multisig_main(const std::vector<std::string>& args, bool called_by_mms); bool make_multisig(const std::vector<std::string>& args); bool make_multisig_main(const std::vector<std::string>& args, bool called_by_mms); - bool finalize_multisig(const std::vector<std::string> &args); bool exchange_multisig_keys(const std::vector<std::string> &args); bool exchange_multisig_keys_main(const std::vector<std::string> &args, bool called_by_mms); bool export_multisig(const std::vector<std::string>& args); diff --git a/src/wallet/api/wallet.cpp b/src/wallet/api/wallet.cpp index 989061250..b058619a3 100644 --- a/src/wallet/api/wallet.cpp +++ b/src/wallet/api/wallet.cpp @@ -1332,7 +1332,7 @@ MultisigState WalletImpl::multisig() const { string WalletImpl::getMultisigInfo() const { try { clearStatus(); - return m_wallet->get_multisig_info(); + return m_wallet->get_multisig_first_kex_msg(); } catch (const exception& e) { LOG_ERROR("Error on generating multisig info: " << e.what()); setStatusError(string(tr("Failed to get multisig info: ")) + e.what()); @@ -1341,7 +1341,7 @@ string WalletImpl::getMultisigInfo() const { return string(); } -string WalletImpl::makeMultisig(const vector<string>& info, uint32_t threshold) { +string WalletImpl::makeMultisig(const vector<string>& info, const uint32_t threshold) { try { clearStatus(); @@ -1366,30 +1366,12 @@ std::string WalletImpl::exchangeMultisigKeys(const std::vector<std::string> &inf return m_wallet->exchange_multisig_keys(epee::wipeable_string(m_password), info); } catch (const exception& e) { LOG_ERROR("Error on exchanging multisig keys: " << e.what()); - setStatusError(string(tr("Failed to make multisig: ")) + e.what()); + setStatusError(string(tr("Failed to exchange multisig keys: ")) + e.what()); } return string(); } -bool WalletImpl::finalizeMultisig(const vector<string>& extraMultisigInfo) { - try { - clearStatus(); - checkMultisigWalletNotReady(m_wallet); - - if (m_wallet->finalize_multisig(epee::wipeable_string(m_password), extraMultisigInfo)) { - return true; - } - - setStatusError(tr("Failed to finalize multisig wallet creation")); - } catch (const exception& e) { - LOG_ERROR("Error on finalizing multisig wallet creation: " << e.what()); - setStatusError(string(tr("Failed to finalize multisig wallet creation: ")) + e.what()); - } - - return false; -} - bool WalletImpl::exportMultisigImages(string& images) { try { clearStatus(); diff --git a/src/wallet/api/wallet.h b/src/wallet/api/wallet.h index 67fc2c08a..7e1e62081 100644 --- a/src/wallet/api/wallet.h +++ b/src/wallet/api/wallet.h @@ -147,7 +147,6 @@ public: std::string getMultisigInfo() const override; std::string makeMultisig(const std::vector<std::string>& info, uint32_t threshold) override; std::string exchangeMultisigKeys(const std::vector<std::string> &info) override; - bool finalizeMultisig(const std::vector<std::string>& extraMultisigInfo) override; bool exportMultisigImages(std::string& images) override; size_t importMultisigImages(const std::vector<std::string>& images) override; bool hasMultisigPartialKeyImages() const override; diff --git a/src/wallet/api/wallet2_api.h b/src/wallet/api/wallet2_api.h index f9c421a93..6da547054 100644 --- a/src/wallet/api/wallet2_api.h +++ b/src/wallet/api/wallet2_api.h @@ -790,7 +790,7 @@ struct Wallet /** * @brief makeMultisig - switches wallet in multisig state. The one and only creation phase for N / N wallets * @param info - vector of multisig infos from other participants obtained with getMulitisInfo call - * @param threshold - number of required signers to make valid transaction. Must be equal to number of participants (N) or N - 1 + * @param threshold - number of required signers to make valid transaction. Must be <= number of participants * @return in case of N / N wallets returns empty string since no more key exchanges needed. For N - 1 / N wallets returns base58 encoded extra multisig info */ virtual std::string makeMultisig(const std::vector<std::string>& info, uint32_t threshold) = 0; @@ -801,12 +801,6 @@ struct Wallet */ virtual std::string exchangeMultisigKeys(const std::vector<std::string> &info) = 0; /** - * @brief finalizeMultisig - finalizes N - 1 / N multisig wallets creation - * @param extraMultisigInfo - wallet participants' extra multisig info obtained with makeMultisig call - * @return true if success - */ - virtual bool finalizeMultisig(const std::vector<std::string>& extraMultisigInfo) = 0; - /** * @brief exportMultisigImages - exports transfers' key images * @param images - output paramter for hex encoded array of images * @return true if success diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp index 006aef33e..9e95a26c1 100644 --- a/src/wallet/wallet2.cpp +++ b/src/wallet/wallet2.cpp @@ -28,6 +28,7 @@ // // Parts of this file are originally copyright (c) 2012-2013 The Cryptonote developers +#include <algorithm> #include <numeric> #include <tuple> #include <queue> @@ -59,6 +60,8 @@ using namespace epee; #include "misc_language.h" #include "cryptonote_basic/cryptonote_basic_impl.h" #include "multisig/multisig.h" +#include "multisig/multisig_account.h" +#include "multisig/multisig_kex_msg.h" #include "common/boost_serialization_helper.h" #include "common/command_line.h" #include "common/threadpool.h" @@ -149,7 +152,6 @@ using namespace cryptonote; #define RECENT_SPEND_WINDOW (15 * DIFFICULTY_TARGET_V2) static const std::string MULTISIG_SIGNATURE_MAGIC = "SigMultisigPkV1"; -static const std::string MULTISIG_EXTRA_INFO_MAGIC = "MultisigxV1"; static const std::string ASCII_OUTPUT_MAGIC = "MoneroAsciiDataV1"; @@ -167,42 +169,6 @@ namespace return dir.string(); } - std::string pack_multisignature_keys(const std::string& prefix, const std::vector<crypto::public_key>& keys, const crypto::secret_key& signer_secret_key) - { - std::string data; - crypto::public_key signer; - CHECK_AND_ASSERT_THROW_MES(crypto::secret_key_to_public_key(signer_secret_key, signer), "Failed to derive public spend key"); - data += std::string((const char *)&signer, sizeof(crypto::public_key)); - - for (const auto &key: keys) - { - data += std::string((const char *)&key, sizeof(crypto::public_key)); - } - - data.resize(data.size() + sizeof(crypto::signature)); - - crypto::hash hash; - crypto::cn_fast_hash(data.data(), data.size() - sizeof(crypto::signature), hash); - crypto::signature &signature = *(crypto::signature*)&data[data.size() - sizeof(crypto::signature)]; - crypto::generate_signature(hash, signer, signer_secret_key, signature); - - return MULTISIG_EXTRA_INFO_MAGIC + tools::base58::encode(data); - } - - std::vector<crypto::public_key> secret_keys_to_public_keys(const std::vector<crypto::secret_key>& keys) - { - std::vector<crypto::public_key> public_keys; - public_keys.reserve(keys.size()); - - std::transform(keys.begin(), keys.end(), std::back_inserter(public_keys), [] (const crypto::secret_key& k) -> crypto::public_key { - crypto::public_key p; - CHECK_AND_ASSERT_THROW_MES(crypto::secret_key_to_public_key(k, p), "Failed to derive public spend key"); - return p; - }); - - return public_keys; - } - bool keys_intersect(const std::unordered_set<crypto::public_key>& s1, const std::unordered_set<crypto::public_key>& s2) { if (s1.empty() || s2.empty()) @@ -4763,7 +4729,6 @@ void wallet2::generate(const std::string& wallet_, const epee::wipeable_string& memwipe(&skey, sizeof(rct::key)); m_account.make_multisig(view_secret_key, spend_secret_key, spend_public_key, multisig_keys); - m_account.finalize_multisig(spend_public_key); // Not possible to restore a multisig wallet that is able to activate the MMS // (because the original keys are not (yet) part of the restore info), so @@ -4978,24 +4943,12 @@ void wallet2::restore(const std::string& wallet_, const epee::wipeable_string& p store(); } } - +//---------------------------------------------------------------------------------------------------- std::string wallet2::make_multisig(const epee::wipeable_string &password, - const std::vector<crypto::secret_key> &view_keys, - const std::vector<crypto::public_key> &spend_keys, - uint32_t threshold) + const std::vector<std::string> &initial_kex_msgs, + const std::uint32_t threshold) { - CHECK_AND_ASSERT_THROW_MES(!view_keys.empty(), "empty view keys"); - CHECK_AND_ASSERT_THROW_MES(view_keys.size() == spend_keys.size(), "Mismatched view/spend key sizes"); - CHECK_AND_ASSERT_THROW_MES(threshold > 1 && threshold <= spend_keys.size() + 1, "Invalid threshold"); - - std::string extra_multisig_info; - std::vector<crypto::secret_key> multisig_keys; - rct::key spend_pkey = rct::identity(); - rct::key spend_skey; - auto wiper = epee::misc_utils::create_scope_leave_handler([&](){memwipe(&spend_skey, sizeof(spend_skey));}); - std::vector<crypto::public_key> multisig_signers; - - // decrypt keys + // decrypt account keys epee::misc_utils::auto_scope_leave_caller keys_reencryptor; if (m_ask_password == AskPasswordToDecrypt && !m_unattended && !m_watch_only) { @@ -5003,104 +4956,89 @@ std::string wallet2::make_multisig(const epee::wipeable_string &password, crypto::generate_chacha_key(password.data(), password.size(), chacha_key, m_kdf_rounds); m_account.encrypt_viewkey(chacha_key); m_account.decrypt_keys(chacha_key); - keys_reencryptor = epee::misc_utils::create_scope_leave_handler([&, this, chacha_key]() { m_account.encrypt_keys(chacha_key); m_account.decrypt_viewkey(chacha_key); }); + keys_reencryptor = epee::misc_utils::create_scope_leave_handler( + [&, this, chacha_key]() + { + m_account.encrypt_keys(chacha_key); + m_account.decrypt_viewkey(chacha_key); + } + ); } - // In common multisig scheme there are 4 types of key exchange rounds: - // 1. First round is exchange of view secret keys and public spend keys. - // 2. Middle round is exchange of derivations: Ki = b * Mj, where b - spend secret key, - // M - public multisig key (in first round it equals to public spend key), K - new public multisig key. - // 3. Secret spend establishment round sets your secret multisig keys as follows: kl = H(Ml), where M - is *your* public multisig key, - // k - secret multisig key used to sign transactions. k and M are sets of keys, of course. - // And secret spend key as the sum of all participant's secret multisig keys - // 4. Last round establishes multisig wallet's public spend key. Participants exchange their public multisig keys - // and calculate common spend public key as sum of all unique participants' public multisig keys. - // Note that N/N scheme has only first round. N-1/N has 2 rounds: first and last. Common M/N has all 4 rounds. - - // IMPORTANT: wallet's public spend key is not equal to secret_spend_key * G! - // Wallet's public spend key is the sum of unique public multisig keys of all participants. - // secret_spend_key * G = public signer key - - if (threshold == spend_keys.size() + 1) - { - // In N / N case we only need to do one round and calculate secret multisig keys and new secret spend key - MINFO("Creating spend key..."); + // create multisig account + multisig::multisig_account multisig_account{ + multisig::get_multisig_blinded_secret_key(get_account().get_keys().m_spend_secret_key), + multisig::get_multisig_blinded_secret_key(get_account().get_keys().m_view_secret_key) + }; - // Calculates all multisig keys and spend key - cryptonote::generate_multisig_N_N(get_account().get_keys(), spend_keys, multisig_keys, spend_skey, spend_pkey); + // open initial kex messages, validate them, extract signers + std::vector<multisig::multisig_kex_msg> expanded_msgs; + std::vector<crypto::public_key> signers; + expanded_msgs.reserve(initial_kex_msgs.size()); + signers.reserve(initial_kex_msgs.size() + 1); - // Our signer key is b * G, where b is secret spend key. - multisig_signers = spend_keys; - multisig_signers.push_back(get_multisig_signer_public_key(get_account().get_keys().m_spend_secret_key)); - } - else + for (const auto &msg : initial_kex_msgs) { - // We just got public spend keys of all participants and deriving multisig keys (set of Mi = b * Bi). - // note that derivations are public keys as DH exchange suppose it to be - auto derivations = cryptonote::generate_multisig_derivations(get_account().get_keys(), spend_keys); - - spend_pkey = rct::identity(); - multisig_signers = std::vector<crypto::public_key>(spend_keys.size() + 1, crypto::null_pkey); - - if (threshold == spend_keys.size()) - { - // N - 1 / N case + expanded_msgs.emplace_back(msg); - // We need an extra step, so we package all the composite public keys - // we know about, and make a signed string out of them - MINFO("Creating spend key..."); + // validate each message + // 1. must be 'round 1' + CHECK_AND_ASSERT_THROW_MES(expanded_msgs.back().get_round() == 1, + "Trying to make multisig with message that has invalid multisig kex round (should be '1')."); - // Calculating set of our secret multisig keys as follows: mi = H(Mi), - // where mi - secret multisig key, Mi - others' participants public multisig key - multisig_keys = cryptonote::calculate_multisig_keys(derivations); + // 2. duplicate signers not allowed + CHECK_AND_ASSERT_THROW_MES(std::find(signers.begin(), signers.end(), expanded_msgs.back().get_signing_pubkey()) == signers.end(), + "Duplicate signers not allowed when converting a wallet to multisig."); - // calculating current participant's spend secret key as sum of all secret multisig keys for current participant. - // IMPORTANT: participant's secret spend key is not an entire wallet's secret spend! - // Entire wallet's secret spend is sum of all unique secret multisig keys - // among all of participants and is not held by anyone! - spend_skey = rct::sk2rct(cryptonote::calculate_multisig_signer_key(multisig_keys)); + // add signer (skip self for now) + if (expanded_msgs.back().get_signing_pubkey() != multisig_account.get_base_pubkey()) + signers.push_back(expanded_msgs.back().get_signing_pubkey()); + } - // Preparing data for the last round to calculate common public spend key. The data contains public multisig keys. - extra_multisig_info = pack_multisignature_keys(MULTISIG_EXTRA_INFO_MAGIC, secret_keys_to_public_keys(multisig_keys), rct::rct2sk(spend_skey)); - } - else - { - // M / N case - MINFO("Preparing keys for next exchange round..."); + // add self to signers + signers.push_back(multisig_account.get_base_pubkey()); - // Preparing data for middle round - packing new public multisig keys to exchage with others. - extra_multisig_info = pack_multisignature_keys(MULTISIG_EXTRA_INFO_MAGIC, derivations, m_account.get_keys().m_spend_secret_key); - spend_skey = rct::sk2rct(m_account.get_keys().m_spend_secret_key); + // intialize key exchange + multisig_account.initialize_kex(threshold, signers, expanded_msgs); + CHECK_AND_ASSERT_THROW_MES(multisig_account.account_is_active(), "Failed to activate multisig account."); - // Need to store middle keys to be able to proceed in case of wallet shutdown. - m_multisig_derivations = derivations; - } - } - + // update wallet state if (!m_original_keys_available) { // Save the original i.e. non-multisig keys so the MMS can continue to use them to encrypt and decrypt messages // (making a wallet multisig overwrites those keys, see account_base::make_multisig) - m_original_address = m_account.get_keys().m_account_address; - m_original_view_secret_key = m_account.get_keys().m_view_secret_key; + m_original_address = get_account().get_keys().m_account_address; + m_original_view_secret_key = get_account().get_keys().m_view_secret_key; m_original_keys_available = true; } clear(); - MINFO("Creating view key..."); - crypto::secret_key view_skey = cryptonote::generate_multisig_view_secret_key(get_account().get_keys().m_view_secret_key, view_keys); + // account base MINFO("Creating multisig address..."); - CHECK_AND_ASSERT_THROW_MES(m_account.make_multisig(view_skey, rct::rct2sk(spend_skey), rct::rct2pk(spend_pkey), multisig_keys), - "Failed to create multisig wallet due to bad keys"); - memwipe(&spend_skey, sizeof(rct::key)); + CHECK_AND_ASSERT_THROW_MES(m_account.make_multisig(multisig_account.get_common_privkey(), + multisig_account.get_base_privkey(), + multisig_account.get_multisig_pubkey(), + multisig_account.get_multisig_privkeys()), + "Failed to create multisig wallet account due to bad keys"); init_type(hw::device::device_type::SOFTWARE); m_original_keys_available = true; m_multisig = true; m_multisig_threshold = threshold; - m_multisig_signers = multisig_signers; - ++m_multisig_rounds_passed; + m_multisig_signers = signers; + m_multisig_rounds_passed = 1; + + // derivations stored (should be empty in last round) + // TODO: make use of the origins map for aggregation-style signing (instead of round-robin) + m_multisig_derivations.clear(); + m_multisig_derivations.reserve(multisig_account.get_kex_keys_to_origins_map().size()); + + for (const auto &key_to_origins : multisig_account.get_kex_keys_to_origins_map()) + m_multisig_derivations.push_back(key_to_origins.first); + + // address + m_account_public_address.m_spend_public_key = multisig_account.get_multisig_pubkey(); // re-encrypt keys keys_reencryptor = epee::misc_utils::auto_scope_leave_caller(); @@ -5113,42 +5051,18 @@ std::string wallet2::make_multisig(const epee::wipeable_string &password, if (!m_wallet_file.empty()) store(); - return extra_multisig_info; -} - -std::string wallet2::exchange_multisig_keys(const epee::wipeable_string &password, - const std::vector<std::string> &info) -{ - THROW_WALLET_EXCEPTION_IF(info.empty(), - error::wallet_internal_error, "Empty multisig info"); - - if (info[0].substr(0, MULTISIG_EXTRA_INFO_MAGIC.size()) != MULTISIG_EXTRA_INFO_MAGIC) - { - THROW_WALLET_EXCEPTION_IF(false, - error::wallet_internal_error, "Unsupported info string"); - } - - std::vector<crypto::public_key> signers; - std::unordered_set<crypto::public_key> pkeys; - - THROW_WALLET_EXCEPTION_IF(!unpack_extra_multisig_info(info, signers, pkeys), - error::wallet_internal_error, "Bad extra multisig info"); - - return exchange_multisig_keys(password, pkeys, signers); + return multisig_account.get_next_kex_round_msg(); } - +//---------------------------------------------------------------------------------------------------- std::string wallet2::exchange_multisig_keys(const epee::wipeable_string &password, - std::unordered_set<crypto::public_key> derivations, - std::vector<crypto::public_key> signers) + const std::vector<std::string> &kex_messages) { - CHECK_AND_ASSERT_THROW_MES(!derivations.empty(), "empty pkeys"); - CHECK_AND_ASSERT_THROW_MES(!signers.empty(), "empty signers"); - - bool ready = false; + bool ready{false}; CHECK_AND_ASSERT_THROW_MES(multisig(&ready), "The wallet is not multisig"); CHECK_AND_ASSERT_THROW_MES(!ready, "Multisig wallet creation process has already been finished"); + CHECK_AND_ASSERT_THROW_MES(kex_messages.size() > 0, "No key exchange messages passed in."); - // keys are decrypted + // decrypt account keys epee::misc_utils::auto_scope_leave_caller keys_reencryptor; if (m_ask_password == AskPasswordToDecrypt && !m_unattended && !m_watch_only) { @@ -5156,37 +5070,72 @@ std::string wallet2::exchange_multisig_keys(const epee::wipeable_string &passwor crypto::generate_chacha_key(password.data(), password.size(), chacha_key, m_kdf_rounds); m_account.encrypt_viewkey(chacha_key); m_account.decrypt_keys(chacha_key); - keys_reencryptor = epee::misc_utils::create_scope_leave_handler([&, this, chacha_key]() { m_account.encrypt_keys(chacha_key); m_account.decrypt_viewkey(chacha_key); }); - } - - if (m_multisig_rounds_passed == multisig_rounds_required(m_multisig_signers.size(), m_multisig_threshold) - 1) - { - // the last round is passed and we have to calculate spend public key - // add ours if not included - crypto::public_key local_signer = get_multisig_signer_public_key(); - - if (std::find(signers.begin(), signers.end(), local_signer) == signers.end()) - { - signers.push_back(local_signer); - for (const auto &msk: get_account().get_multisig_keys()) + keys_reencryptor = epee::misc_utils::create_scope_leave_handler( + [&, this, chacha_key]() { - derivations.insert(rct::rct2pk(rct::scalarmultBase(rct::sk2rct(msk)))); + m_account.encrypt_keys(chacha_key); + m_account.decrypt_viewkey(chacha_key); } - } + ); + } + + // open kex messages + std::vector<multisig::multisig_kex_msg> expanded_msgs; + expanded_msgs.reserve(kex_messages.size()); + + for (const auto &msg : kex_messages) + expanded_msgs.emplace_back(msg); + + // reconstruct multisig account + crypto::public_key dummy; + multisig::multisig_account::kex_origins_map_t kex_origins_map; + + for (const auto &derivation : m_multisig_derivations) + kex_origins_map[derivation]; + + multisig::multisig_account multisig_account{ + m_multisig_threshold, + m_multisig_signers, + get_account().get_keys().m_spend_secret_key, + crypto::null_skey, //base common privkey: not used + get_account().get_keys().m_multisig_keys, + get_account().get_keys().m_view_secret_key, + m_account_public_address.m_spend_public_key, + dummy, //common pubkey: not used + m_multisig_rounds_passed, + std::move(kex_origins_map), + "" + }; - CHECK_AND_ASSERT_THROW_MES(signers.size() == m_multisig_signers.size(), "Bad signers size"); + // update multisig kex + multisig_account.kex_update(expanded_msgs); - // Summing all of unique public multisig keys to calculate common public spend key - crypto::public_key spend_public_key = cryptonote::generate_multisig_M_N_spend_public_key(std::vector<crypto::public_key>(derivations.begin(), derivations.end())); - m_account_public_address.m_spend_public_key = spend_public_key; - m_account.finalize_multisig(spend_public_key); + // update wallet state - m_multisig_signers = signers; - std::sort(m_multisig_signers.begin(), m_multisig_signers.end(), [](const crypto::public_key &e0, const crypto::public_key &e1){ return memcmp(&e0, &e1, sizeof(e0)) < 0; }); + // address + m_account_public_address.m_spend_public_key = multisig_account.get_multisig_pubkey(); - ++m_multisig_rounds_passed; - m_multisig_derivations.clear(); + // account base + CHECK_AND_ASSERT_THROW_MES(m_account.make_multisig(multisig_account.get_common_privkey(), + multisig_account.get_base_privkey(), + multisig_account.get_multisig_pubkey(), + multisig_account.get_multisig_privkeys()), + "Failed to update multisig wallet account due to bad keys"); + + // derivations stored (should be empty in last round) + // TODO: make use of the origins map for aggregation-style signing (instead of round-robin) + m_multisig_derivations.clear(); + m_multisig_derivations.reserve(multisig_account.get_kex_keys_to_origins_map().size()); + + for (const auto &key_to_origins : multisig_account.get_kex_keys_to_origins_map()) + m_multisig_derivations.push_back(key_to_origins.first); + + // rounds passed + m_multisig_rounds_passed = multisig_account.get_kex_rounds_complete(); + // why is this necessary? who knows... + if (multisig_account.multisig_is_ready()) + { // keys are encrypted again keys_reencryptor = epee::misc_utils::auto_scope_leave_caller(); @@ -5208,270 +5157,28 @@ std::string wallet2::exchange_multisig_keys(const epee::wipeable_string &passwor if (!m_wallet_file.empty()) store(); - - return {}; - } - - // Below are either middle or secret spend key establishment rounds - - for (const auto& key: m_multisig_derivations) - derivations.erase(key); - - // Deriving multisig keys (set of Mi = b * Bi) according to DH from other participants' multisig keys. - auto new_derivations = cryptonote::generate_multisig_derivations(get_account().get_keys(), std::vector<crypto::public_key>(derivations.begin(), derivations.end())); - - std::string extra_multisig_info; - if (m_multisig_rounds_passed == multisig_rounds_required(m_multisig_signers.size(), m_multisig_threshold) - 2) // next round is last - { - // Next round is last therefore we are performing secret spend establishment round as described above. - MINFO("Creating spend key..."); - - // Calculating our secret multisig keys by hashing our public multisig keys. - auto multisig_keys = cryptonote::calculate_multisig_keys(std::vector<crypto::public_key>(new_derivations.begin(), new_derivations.end())); - // And summing it to get personal secret spend key - crypto::secret_key spend_skey = cryptonote::calculate_multisig_signer_key(multisig_keys); - - m_account.make_multisig(m_account.get_keys().m_view_secret_key, spend_skey, rct::rct2pk(rct::identity()), multisig_keys); - - // Packing public multisig keys to exchange with others and calculate common public spend key in the last round - extra_multisig_info = pack_multisignature_keys(MULTISIG_EXTRA_INFO_MAGIC, secret_keys_to_public_keys(multisig_keys), spend_skey); } - else - { - // This is just middle round - MINFO("Preparing keys for next exchange round..."); - extra_multisig_info = pack_multisignature_keys(MULTISIG_EXTRA_INFO_MAGIC, new_derivations, m_account.get_keys().m_spend_secret_key); - m_multisig_derivations = new_derivations; - } - - ++m_multisig_rounds_passed; + // wallet/file relationship if (!m_wallet_file.empty()) create_keys_file(m_wallet_file, false, password, boost::filesystem::exists(m_wallet_file + ".address.txt")); - return extra_multisig_info; -} - -void wallet2::unpack_multisig_info(const std::vector<std::string>& info, - std::vector<crypto::public_key> &public_keys, - std::vector<crypto::secret_key> &secret_keys) const -{ - // parse all multisig info - public_keys.resize(info.size()); - secret_keys.resize(info.size()); - for (size_t i = 0; i < info.size(); ++i) - { - THROW_WALLET_EXCEPTION_IF(!verify_multisig_info(info[i], secret_keys[i], public_keys[i]), - error::wallet_internal_error, "Bad multisig info: " + info[i]); - } - - // remove duplicates - for (size_t i = 0; i < secret_keys.size(); ++i) - { - for (size_t j = i + 1; j < secret_keys.size(); ++j) - { - if (rct::sk2rct(secret_keys[i]) == rct::sk2rct(secret_keys[j])) - { - MDEBUG("Duplicate key found, ignoring"); - secret_keys[j] = secret_keys.back(); - public_keys[j] = public_keys.back(); - secret_keys.pop_back(); - public_keys.pop_back(); - --j; - } - } - } - - // people may include their own, weed it out - const crypto::secret_key local_skey = cryptonote::get_multisig_blinded_secret_key(get_account().get_keys().m_view_secret_key); - const crypto::public_key local_pkey = get_multisig_signer_public_key(get_account().get_keys().m_spend_secret_key); - for (size_t i = 0; i < secret_keys.size(); ++i) - { - if (secret_keys[i] == local_skey) - { - MDEBUG("Local key is present, ignoring"); - secret_keys[i] = secret_keys.back(); - public_keys[i] = public_keys.back(); - secret_keys.pop_back(); - public_keys.pop_back(); - --i; - } - else - { - THROW_WALLET_EXCEPTION_IF(public_keys[i] == local_pkey, error::wallet_internal_error, - "Found local spend public key, but not local view secret key - something very weird"); - } - } + return multisig_account.get_next_kex_round_msg(); } - -std::string wallet2::make_multisig(const epee::wipeable_string &password, - const std::vector<std::string> &info, - uint32_t threshold) -{ - std::vector<crypto::secret_key> secret_keys(info.size()); - std::vector<crypto::public_key> public_keys(info.size()); - unpack_multisig_info(info, public_keys, secret_keys); - return make_multisig(password, secret_keys, public_keys, threshold); -} - -bool wallet2::finalize_multisig(const epee::wipeable_string &password, const std::unordered_set<crypto::public_key> &pkeys, std::vector<crypto::public_key> signers) -{ - bool ready; - uint32_t threshold, total; - if (!multisig(&ready, &threshold, &total)) - { - MERROR("This is not a multisig wallet"); - return false; - } - if (ready) - { - MERROR("This multisig wallet is already finalized"); - return false; - } - if (threshold + 1 != total) - { - MERROR("finalize_multisig should only be used for N-1/N wallets, use exchange_multisig_keys instead"); - return false; - } - exchange_multisig_keys(password, pkeys, signers); - return true; -} - -bool wallet2::unpack_extra_multisig_info(const std::vector<std::string>& info, - std::vector<crypto::public_key> &signers, - std::unordered_set<crypto::public_key> &pkeys) const -{ - // parse all multisig info - signers.resize(info.size(), crypto::null_pkey); - for (size_t i = 0; i < info.size(); ++i) - { - if (!verify_extra_multisig_info(info[i], pkeys, signers[i])) - { - return false; - } - } - - return true; -} - -bool wallet2::finalize_multisig(const epee::wipeable_string &password, const std::vector<std::string> &info) -{ - std::unordered_set<crypto::public_key> public_keys; - std::vector<crypto::public_key> signers; - if (!unpack_extra_multisig_info(info, signers, public_keys)) - { - MERROR("Bad multisig info"); - return false; - } - - return finalize_multisig(password, public_keys, signers); -} - -std::string wallet2::get_multisig_info() const -{ - // It's a signed package of private view key and public spend key - const crypto::secret_key skey = cryptonote::get_multisig_blinded_secret_key(get_account().get_keys().m_view_secret_key); - const crypto::public_key pkey = get_multisig_signer_public_key(get_account().get_keys().m_spend_secret_key); - crypto::hash hash; - - std::string data; - data += std::string((const char *)&skey, sizeof(crypto::secret_key)); - data += std::string((const char *)&pkey, sizeof(crypto::public_key)); - - data.resize(data.size() + sizeof(crypto::signature)); - crypto::cn_fast_hash(data.data(), data.size() - sizeof(signature), hash); - crypto::signature &signature = *(crypto::signature*)&data[data.size() - sizeof(crypto::signature)]; - crypto::generate_signature(hash, pkey, get_multisig_blinded_secret_key(get_account().get_keys().m_spend_secret_key), signature); - - return std::string("MultisigV1") + tools::base58::encode(data); -} - -bool wallet2::verify_multisig_info(const std::string &data, crypto::secret_key &skey, crypto::public_key &pkey) -{ - const size_t header_len = strlen("MultisigV1"); - if (data.size() < header_len || data.substr(0, header_len) != "MultisigV1") - { - MERROR("Multisig info header check error"); - return false; - } - std::string decoded; - if (!tools::base58::decode(data.substr(header_len), decoded)) - { - MERROR("Multisig info decoding error"); - return false; - } - if (decoded.size() != sizeof(crypto::secret_key) + sizeof(crypto::public_key) + sizeof(crypto::signature)) - { - MERROR("Multisig info is corrupt"); - return false; - } - - size_t offset = 0; - skey = *(const crypto::secret_key*)(decoded.data() + offset); - offset += sizeof(skey); - pkey = *(const crypto::public_key*)(decoded.data() + offset); - offset += sizeof(pkey); - const crypto::signature &signature = *(const crypto::signature*)(decoded.data() + offset); - - crypto::hash hash; - crypto::cn_fast_hash(decoded.data(), decoded.size() - sizeof(signature), hash); - if (!crypto::check_signature(hash, pkey, signature)) - { - MERROR("Multisig info signature is invalid"); - return false; - } - - return true; -} - -bool wallet2::verify_extra_multisig_info(const std::string &data, std::unordered_set<crypto::public_key> &pkeys, crypto::public_key &signer) +//---------------------------------------------------------------------------------------------------- +std::string wallet2::get_multisig_first_kex_msg() const { - if (data.size() < MULTISIG_EXTRA_INFO_MAGIC.size() || data.substr(0, MULTISIG_EXTRA_INFO_MAGIC.size()) != MULTISIG_EXTRA_INFO_MAGIC) - { - MERROR("Multisig info header check error"); - return false; - } - std::string decoded; - if (!tools::base58::decode(data.substr(MULTISIG_EXTRA_INFO_MAGIC.size()), decoded)) - { - MERROR("Multisig info decoding error"); - return false; - } - if (decoded.size() < sizeof(crypto::public_key) + sizeof(crypto::signature)) - { - MERROR("Multisig info is corrupt"); - return false; - } - if ((decoded.size() - (sizeof(crypto::public_key) + sizeof(crypto::signature))) % sizeof(crypto::public_key)) - { - MERROR("Multisig info is corrupt"); - return false; - } - - const size_t n_keys = (decoded.size() - (sizeof(crypto::public_key) + sizeof(crypto::signature))) / sizeof(crypto::public_key); - size_t offset = 0; - signer = *(const crypto::public_key*)(decoded.data() + offset); - offset += sizeof(signer); - const crypto::signature &signature = *(const crypto::signature*)(decoded.data() + offset + n_keys * sizeof(crypto::public_key)); - - crypto::hash hash; - crypto::cn_fast_hash(decoded.data(), decoded.size() - sizeof(signature), hash); - if (!crypto::check_signature(hash, signer, signature)) - { - MERROR("Multisig info signature is invalid"); - return false; - } - - for (size_t n = 0; n < n_keys; ++n) - { - crypto::public_key mspk = *(const crypto::public_key*)(decoded.data() + offset); - pkeys.insert(mspk); - offset += sizeof(mspk); - } + // create multisig account + multisig::multisig_account multisig_account{ + // k_base = H(normal private spend key) + multisig::get_multisig_blinded_secret_key(get_account().get_keys().m_spend_secret_key), + // k_view = H(normal private view key) + multisig::get_multisig_blinded_secret_key(get_account().get_keys().m_view_secret_key) + }; - return true; + return multisig_account.get_next_kex_round_msg(); } - +//---------------------------------------------------------------------------------------------------- bool wallet2::multisig(bool *ready, uint32_t *threshold, uint32_t *total) const { if (!m_multisig) @@ -5484,7 +5191,7 @@ bool wallet2::multisig(bool *ready, uint32_t *threshold, uint32_t *total) const *ready = !(get_account().get_keys().m_account_address.m_spend_public_key == rct::rct2pk(rct::identity())); return true; } - +//---------------------------------------------------------------------------------------------------- bool wallet2::has_multisig_partial_key_images() const { if (!m_multisig) @@ -5494,7 +5201,7 @@ bool wallet2::has_multisig_partial_key_images() const return true; return false; } - +//---------------------------------------------------------------------------------------------------- bool wallet2::has_unknown_key_images() const { for (const auto &td: m_transfers) @@ -13331,13 +13038,6 @@ size_t wallet2::import_outputs_from_str(const std::string &outputs_st) return imported_outputs; } //---------------------------------------------------------------------------------------------------- -crypto::public_key wallet2::get_multisig_signer_public_key(const crypto::secret_key &spend_skey) const -{ - crypto::public_key pkey; - crypto::secret_key_to_public_key(get_multisig_blinded_secret_key(spend_skey), pkey); - return pkey; -} -//---------------------------------------------------------------------------------------------------- crypto::public_key wallet2::get_multisig_signer_public_key() const { CHECK_AND_ASSERT_THROW_MES(m_multisig, "Wallet is not multisig"); @@ -13381,7 +13081,7 @@ rct::multisig_kLRki wallet2::get_multisig_kLRki(size_t n, const rct::key &k) con CHECK_AND_ASSERT_THROW_MES(n < m_transfers.size(), "Bad m_transfers index"); rct::multisig_kLRki kLRki; kLRki.k = k; - cryptonote::generate_multisig_LR(m_transfers[n].get_public_key(), rct::rct2sk(kLRki.k), (crypto::public_key&)kLRki.L, (crypto::public_key&)kLRki.R); + multisig::generate_multisig_LR(m_transfers[n].get_public_key(), rct::rct2sk(kLRki.k), (crypto::public_key&)kLRki.L, (crypto::public_key&)kLRki.R); kLRki.ki = rct::ki2rct(m_transfers[n].m_key_image); return kLRki; } @@ -13428,7 +13128,7 @@ crypto::key_image wallet2::get_multisig_composite_key_image(size_t n) const for (const auto &info: td.m_multisig_info) for (const auto &pki: info.m_partial_key_images) pkis.push_back(pki); - bool r = cryptonote::generate_multisig_composite_key_image(get_account().get_keys(), m_subaddresses, td.get_public_key(), tx_key, additional_tx_keys, td.m_internal_output_index, pkis, ki); + bool r = multisig::generate_multisig_composite_key_image(get_account().get_keys(), m_subaddresses, td.get_public_key(), tx_key, additional_tx_keys, td.m_internal_output_index, pkis, ki); THROW_WALLET_EXCEPTION_IF(!r, error::wallet_internal_error, "Failed to generate key image"); return ki; } @@ -13451,7 +13151,7 @@ cryptonote::blobdata wallet2::export_multisig() for (size_t m = 0; m < get_account().get_multisig_keys().size(); ++m) { // we want to export the partial key image, not the full one, so we can't use td.m_key_image - bool r = generate_multisig_key_image(get_account().get_keys(), m, td.get_public_key(), ki); + bool r = multisig::generate_multisig_key_image(get_account().get_keys(), m, td.get_public_key(), ki); CHECK_AND_ASSERT_THROW_MES(r, "Failed to generate key image"); info[n].m_partial_key_images.push_back(ki); } diff --git a/src/wallet/wallet2.h b/src/wallet/wallet2.h index 7648becc8..d64832b13 100644 --- a/src/wallet/wallet2.h +++ b/src/wallet/wallet2.h @@ -757,45 +757,20 @@ private: * to other participants */ std::string make_multisig(const epee::wipeable_string &password, - const std::vector<std::string> &info, - uint32_t threshold); + const std::vector<std::string> &kex_messages, + const std::uint32_t threshold); /*! - * \brief Creates a multisig wallet + * \brief Increment the multisig key exchange round * \return empty if done, non empty if we need to send another string * to other participants */ - std::string make_multisig(const epee::wipeable_string &password, - const std::vector<crypto::secret_key> &view_keys, - const std::vector<crypto::public_key> &spend_keys, - uint32_t threshold); std::string exchange_multisig_keys(const epee::wipeable_string &password, - const std::vector<std::string> &info); - /*! - * \brief Any but first round of keys exchange - */ - std::string exchange_multisig_keys(const epee::wipeable_string &password, - std::unordered_set<crypto::public_key> pkeys, - std::vector<crypto::public_key> signers); - /*! - * \brief Finalizes creation of a multisig wallet - */ - bool finalize_multisig(const epee::wipeable_string &password, const std::vector<std::string> &info); - /*! - * \brief Finalizes creation of a multisig wallet - */ - bool finalize_multisig(const epee::wipeable_string &password, const std::unordered_set<crypto::public_key> &pkeys, std::vector<crypto::public_key> signers); - /*! - * Get a packaged multisig information string - */ - std::string get_multisig_info() const; - /*! - * Verifies and extracts keys from a packaged multisig information string - */ - static bool verify_multisig_info(const std::string &data, crypto::secret_key &skey, crypto::public_key &pkey); + const std::vector<std::string> &kex_messages); /*! - * Verifies and extracts keys from a packaged multisig information string + * \brief Get initial message to start multisig key exchange (before 'make_multisig()' is called) + * \return string to send to other participants */ - static bool verify_extra_multisig_info(const std::string &data, std::unordered_set<crypto::public_key> &pkeys, crypto::public_key &signer); + std::string get_multisig_first_kex_msg() const; /*! * Export multisig info * This will generate and remember new k values @@ -1477,7 +1452,6 @@ private: void set_attribute(const std::string &key, const std::string &value); bool get_attribute(const std::string &key, std::string &value) const; - crypto::public_key get_multisig_signer_public_key(const crypto::secret_key &spend_skey) const; crypto::public_key get_multisig_signer_public_key() const; crypto::public_key get_multisig_signing_public_key(size_t idx) const; crypto::public_key get_multisig_signing_public_key(const crypto::secret_key &skey) const; @@ -1641,12 +1615,6 @@ private: bool get_rct_distribution(uint64_t &start_height, std::vector<uint64_t> &distribution); uint64_t get_segregation_fork_height() const; - void unpack_multisig_info(const std::vector<std::string>& info, - std::vector<crypto::public_key> &public_keys, - std::vector<crypto::secret_key> &secret_keys) const; - bool unpack_extra_multisig_info(const std::vector<std::string>& info, - std::vector<crypto::public_key> &signers, - std::unordered_set<crypto::public_key> &pkeys) const; void cache_tx_data(const cryptonote::transaction& tx, const crypto::hash &txid, tx_cache_data &tx_cache_data) const; std::shared_ptr<std::map<std::pair<uint64_t, uint64_t>, size_t>> create_output_tracker_cache() const; diff --git a/src/wallet/wallet_rpc_server.cpp b/src/wallet/wallet_rpc_server.cpp index 4655e24cd..a173b5a50 100644 --- a/src/wallet/wallet_rpc_server.cpp +++ b/src/wallet/wallet_rpc_server.cpp @@ -3938,7 +3938,7 @@ namespace tools return false; } - res.multisig_info = m_wallet->get_multisig_info(); + res.multisig_info = m_wallet->get_multisig_first_kex_msg(); return true; } //------------------------------------------------------------------------------------------------------------------------------ @@ -4069,7 +4069,7 @@ namespace tools catch (const std::exception &e) { er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR; - er.message = "Error calling import_multisig"; + er.message = std::string{"Error calling import_multisig: "} + e.what(); return false; } @@ -4094,53 +4094,7 @@ namespace tools //------------------------------------------------------------------------------------------------------------------------------ bool wallet_rpc_server::on_finalize_multisig(const wallet_rpc::COMMAND_RPC_FINALIZE_MULTISIG::request& req, wallet_rpc::COMMAND_RPC_FINALIZE_MULTISIG::response& res, epee::json_rpc::error& er, const connection_context *ctx) { - if (!m_wallet) return not_open(er); - if (m_restricted) - { - er.code = WALLET_RPC_ERROR_CODE_DENIED; - er.message = "Command unavailable in restricted mode."; - return false; - } - bool ready; - uint32_t threshold, total; - if (!m_wallet->multisig(&ready, &threshold, &total)) - { - er.code = WALLET_RPC_ERROR_CODE_NOT_MULTISIG; - er.message = "This wallet is not multisig"; - return false; - } - if (ready) - { - er.code = WALLET_RPC_ERROR_CODE_ALREADY_MULTISIG; - er.message = "This wallet is multisig, and already finalized"; - return false; - } - - if (req.multisig_info.size() < 1 || req.multisig_info.size() > total) - { - er.code = WALLET_RPC_ERROR_CODE_THRESHOLD_NOT_REACHED; - er.message = "Needs multisig info from more participants"; - return false; - } - - try - { - if (!m_wallet->finalize_multisig(req.password, req.multisig_info)) - { - er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR; - er.message = "Error calling finalize_multisig"; - return false; - } - } - catch (const std::exception &e) - { - er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR; - er.message = std::string("Error calling finalize_multisig: ") + e.what(); - return false; - } - res.address = m_wallet->get_account().get_public_address_str(m_wallet->nettype()); - - return true; + return false; } //------------------------------------------------------------------------------------------------------------------------------ bool wallet_rpc_server::on_exchange_multisig_keys(const wallet_rpc::COMMAND_RPC_EXCHANGE_MULTISIG_KEYS::request& req, wallet_rpc::COMMAND_RPC_EXCHANGE_MULTISIG_KEYS::response& res, epee::json_rpc::error& er, const connection_context *ctx) @@ -4168,7 +4122,7 @@ namespace tools return false; } - if (req.multisig_info.size() < 1 || req.multisig_info.size() > total) + if (req.multisig_info.size() + 1 < total) { er.code = WALLET_RPC_ERROR_CODE_THRESHOLD_NOT_REACHED; er.message = "Needs multisig info from more participants"; @@ -4426,7 +4380,11 @@ namespace tools return false; } - if (!m_wallet->set_daemon(req.address, boost::none, req.trusted, std::move(ssl_options))) + boost::optional<epee::net_utils::http::login> daemon_login{}; + if (!req.username.empty() || !req.password.empty()) + daemon_login.emplace(req.username, req.password); + + if (!m_wallet->set_daemon(req.address, daemon_login, req.trusted, std::move(ssl_options))) { er.code = WALLET_RPC_ERROR_CODE_NO_DAEMON_CONNECTION; er.message = std::string("Unable to set daemon"); diff --git a/src/wallet/wallet_rpc_server_commands_defs.h b/src/wallet/wallet_rpc_server_commands_defs.h index 248d31aa4..867ea54bd 100644 --- a/src/wallet/wallet_rpc_server_commands_defs.h +++ b/src/wallet/wallet_rpc_server_commands_defs.h @@ -47,7 +47,7 @@ // advance which version they will stop working with // Don't go over 32767 for any of these #define WALLET_RPC_VERSION_MAJOR 1 -#define WALLET_RPC_VERSION_MINOR 23 +#define WALLET_RPC_VERSION_MINOR 24 #define MAKE_WALLET_RPC_VERSION(major,minor) (((major)<<16)|(minor)) #define WALLET_RPC_VERSION MAKE_WALLET_RPC_VERSION(WALLET_RPC_VERSION_MAJOR, WALLET_RPC_VERSION_MINOR) namespace tools @@ -2504,24 +2504,17 @@ namespace wallet_rpc struct COMMAND_RPC_FINALIZE_MULTISIG { + // NOP struct request_t { - std::string password; - std::vector<std::string> multisig_info; - BEGIN_KV_SERIALIZE_MAP() - KV_SERIALIZE(password) - KV_SERIALIZE(multisig_info) END_KV_SERIALIZE_MAP() }; typedef epee::misc_utils::struct_init<request_t> request; struct response_t { - std::string address; - BEGIN_KV_SERIALIZE_MAP() - KV_SERIALIZE(address) END_KV_SERIALIZE_MAP() }; typedef epee::misc_utils::struct_init<response_t> response; @@ -2664,6 +2657,8 @@ namespace wallet_rpc struct request_t { std::string address; + std::string username; + std::string password; bool trusted; std::string ssl_support; // disabled, enabled, autodetect std::string ssl_private_key_path; @@ -2674,6 +2669,8 @@ namespace wallet_rpc BEGIN_KV_SERIALIZE_MAP() KV_SERIALIZE(address) + KV_SERIALIZE(username) + KV_SERIALIZE(password) KV_SERIALIZE_OPT(trusted, false) KV_SERIALIZE_OPT(ssl_support, (std::string)"autodetect") KV_SERIALIZE(ssl_private_key_path) diff --git a/tests/core_tests/chaingen.h b/tests/core_tests/chaingen.h index 3f9f01a11..a1101a3b1 100644 --- a/tests/core_tests/chaingen.h +++ b/tests/core_tests/chaingen.h @@ -836,7 +836,7 @@ inline bool do_replay_file(const std::string& filename) { \ for (size_t msidx = 0; msidx < total; ++msidx) \ account[msidx].generate(); \ - make_multisig_accounts(account, threshold); \ + CHECK_AND_ASSERT_MES(make_multisig_accounts(account, threshold), false, "Failed to make multisig accounts."); \ } while(0) #define MAKE_ACCOUNT(VEC_EVENTS, account) \ diff --git a/tests/core_tests/multisig.cpp b/tests/core_tests/multisig.cpp index f098e1bce..73bc1f104 100644 --- a/tests/core_tests/multisig.cpp +++ b/tests/core_tests/multisig.cpp @@ -28,98 +28,88 @@ // // Parts of this file are originally copyright (c) 2012-2013 The Cryptonote developers -#include "ringct/rctSigs.h" -#include "cryptonote_basic/cryptonote_basic.h" -#include "multisig/multisig.h" -#include "common/apply_permutation.h" #include "chaingen.h" #include "multisig.h" + +#include "common/apply_permutation.h" +#include "crypto/crypto.h" +#include "cryptonote_basic/cryptonote_basic.h" #include "device/device.hpp" +#include "multisig/multisig.h" +#include "multisig/multisig_account.h" +#include "multisig/multisig_kex_msg.h" +#include "ringct/rctOps.h" +#include "ringct/rctSigs.h" + using namespace epee; using namespace crypto; using namespace cryptonote; +using namespace multisig; //#define NO_MULTISIG -void make_multisig_accounts(std::vector<cryptonote::account_base>& account, uint32_t threshold) +static bool make_multisig_accounts(std::vector<cryptonote::account_base> &accounts, const uint32_t threshold) { - std::vector<crypto::secret_key> all_view_keys; - std::vector<std::vector<crypto::public_key>> derivations(account.size()); - //storage for all set of multisig derivations and spend public key (in first round) - std::unordered_set<crypto::public_key> exchanging_keys; + CHECK_AND_ASSERT_MES(accounts.size() > 0, false, "Invalid multisig scheme"); - for (size_t msidx = 0; msidx < account.size(); ++msidx) + std::vector<multisig_account> multisig_accounts; + std::vector<crypto::public_key> signers; + std::vector<multisig_kex_msg> round_msgs; + multisig_accounts.reserve(accounts.size()); + signers.reserve(accounts.size()); + round_msgs.reserve(accounts.size()); + + // create multisig accounts + for (std::size_t account_index{0}; account_index < accounts.size(); ++account_index) { - crypto::secret_key vkh = cryptonote::get_multisig_blinded_secret_key(account[msidx].get_keys().m_view_secret_key); - all_view_keys.push_back(vkh); + // create account and collect signer + multisig_accounts.emplace_back( + multisig_account{ + get_multisig_blinded_secret_key(accounts[account_index].get_keys().m_spend_secret_key), + get_multisig_blinded_secret_key(accounts[account_index].get_keys().m_view_secret_key) + } + ); - crypto::secret_key skh = cryptonote::get_multisig_blinded_secret_key(account[msidx].get_keys().m_spend_secret_key); - crypto::public_key pskh; - crypto::secret_key_to_public_key(skh, pskh); + signers.emplace_back(multisig_accounts.back().get_base_pubkey()); - derivations[msidx].push_back(pskh); - exchanging_keys.insert(pskh); + // collect account's first kex msg + round_msgs.emplace_back(multisig_accounts.back().get_next_kex_round_msg()); } - uint32_t roundsTotal = 1; - if (threshold < account.size()) - roundsTotal = account.size() - threshold; - - //secret multisig keys of every account - std::vector<std::vector<crypto::secret_key>> multisig_keys(account.size()); - std::vector<crypto::secret_key> spend_skey(account.size()); - std::vector<crypto::public_key> spend_pkey(account.size()); - for (uint32_t round = 0; round < roundsTotal; ++round) + // initialize accounts and collect kex messages for the next round + std::vector<multisig_kex_msg> temp_round_msgs(multisig_accounts.size()); + for (std::size_t account_index{0}; account_index < accounts.size(); ++account_index) { - std::unordered_set<crypto::public_key> roundKeys; - for (size_t msidx = 0; msidx < account.size(); ++msidx) - { - // subtracting one's keys from set of all unique keys is the same as key exchange - auto myKeys = exchanging_keys; - for (const auto& d: derivations[msidx]) - myKeys.erase(d); - - if (threshold == account.size()) - { - cryptonote::generate_multisig_N_N(account[msidx].get_keys(), std::vector<crypto::public_key>(myKeys.begin(), myKeys.end()), multisig_keys[msidx], (rct::key&)spend_skey[msidx], (rct::key&)spend_pkey[msidx]); - } - else - { - derivations[msidx] = cryptonote::generate_multisig_derivations(account[msidx].get_keys(), std::vector<crypto::public_key>(myKeys.begin(), myKeys.end())); - roundKeys.insert(derivations[msidx].begin(), derivations[msidx].end()); - } - } + multisig_accounts[account_index].initialize_kex(threshold, signers, round_msgs); - exchanging_keys = roundKeys; - roundKeys.clear(); + if (!multisig_accounts[account_index].multisig_is_ready()) + temp_round_msgs[account_index] = multisig_accounts[account_index].get_next_kex_round_msg(); } - std::unordered_set<crypto::public_key> all_multisig_keys; - for (size_t msidx = 0; msidx < account.size(); ++msidx) + // perform key exchange rounds + while (!multisig_accounts[0].multisig_is_ready()) { - std::unordered_set<crypto::secret_key> view_keys(all_view_keys.begin(), all_view_keys.end()); - view_keys.erase(all_view_keys[msidx]); + round_msgs = temp_round_msgs; - crypto::secret_key view_skey = cryptonote::generate_multisig_view_secret_key(account[msidx].get_keys().m_view_secret_key, std::vector<secret_key>(view_keys.begin(), view_keys.end())); - if (threshold < account.size()) + for (std::size_t account_index{0}; account_index < multisig_accounts.size(); ++account_index) { - multisig_keys[msidx] = cryptonote::calculate_multisig_keys(derivations[msidx]); - spend_skey[msidx] = cryptonote::calculate_multisig_signer_key(multisig_keys[msidx]); - } - account[msidx].make_multisig(view_skey, spend_skey[msidx], spend_pkey[msidx], multisig_keys[msidx]); - for (const auto &k: multisig_keys[msidx]) { - all_multisig_keys.insert(rct::rct2pk(rct::scalarmultBase(rct::sk2rct(k)))); + multisig_accounts[account_index].kex_update(round_msgs); + + if (!multisig_accounts[account_index].multisig_is_ready()) + temp_round_msgs[account_index] = multisig_accounts[account_index].get_next_kex_round_msg(); } } - if (threshold < account.size()) + // update accounts post key exchange + for (std::size_t account_index{0}; account_index < accounts.size(); ++account_index) { - std::vector<crypto::public_key> public_keys(std::vector<crypto::public_key>(all_multisig_keys.begin(), all_multisig_keys.end())); - crypto::public_key spend_pkey = cryptonote::generate_multisig_M_N_spend_public_key(public_keys); - - for (size_t msidx = 0; msidx < account.size(); ++msidx) - account[msidx].finalize_multisig(spend_pkey); + accounts[account_index].make_multisig(multisig_accounts[account_index].get_common_privkey(), + multisig_accounts[account_index].get_base_privkey(), + multisig_accounts[account_index].get_multisig_pubkey(), + multisig_accounts[account_index].get_multisig_privkeys()); } + + return true; } //---------------------------------------------------------------------------------------------------------------------- @@ -238,13 +228,13 @@ bool gen_multisig_tx_validation_base::generate_with(std::vector<test_event_entry for (size_t n = 0; n < nlr; ++n) { account_k[msidx][tdidx].push_back(rct::rct2sk(rct::skGen())); - cryptonote::generate_multisig_LR(output_pub_key[tdidx], account_k[msidx][tdidx][n], account_L[msidx][tdidx][n], account_R[msidx][tdidx][n]); + multisig::generate_multisig_LR(output_pub_key[tdidx], account_k[msidx][tdidx][n], account_L[msidx][tdidx][n], account_R[msidx][tdidx][n]); } size_t numki = miner_account[msidx].get_multisig_keys().size(); account_ki[msidx][tdidx].resize(numki); for (size_t kiidx = 0; kiidx < numki; ++kiidx) { - r = cryptonote::generate_multisig_key_image(miner_account[msidx].get_keys(), kiidx, output_pub_key[tdidx], account_ki[msidx][tdidx][kiidx]); + r = multisig::generate_multisig_key_image(miner_account[msidx].get_keys(), kiidx, output_pub_key[tdidx], account_ki[msidx][tdidx][kiidx]); CHECK_AND_ASSERT_MES(r, false, "Failed to generate multisig export key image"); } MDEBUG("Party " << msidx << ":"); @@ -303,7 +293,7 @@ bool gen_multisig_tx_validation_base::generate_with(std::vector<test_event_entry for (size_t msidx = 0; msidx < total; ++msidx) for (size_t n = 0; n < account_ki[msidx][tdidx].size(); ++n) pkis.push_back(account_ki[msidx][tdidx][n]); - r = cryptonote::generate_multisig_composite_key_image(miner_account[0].get_keys(), subaddresses, output_pub_key[tdidx], tx_pub_key[tdidx], additional_tx_keys, 0, pkis, (crypto::key_image&)kLRki.ki); + r = multisig::generate_multisig_composite_key_image(miner_account[0].get_keys(), subaddresses, output_pub_key[tdidx], tx_pub_key[tdidx], additional_tx_keys, 0, pkis, (crypto::key_image&)kLRki.ki); CHECK_AND_ASSERT_MES(r, false, "Failed to generate composite key image"); MDEBUG("composite ki: " << kLRki.ki); MDEBUG("L: " << kLRki.L); @@ -311,7 +301,7 @@ bool gen_multisig_tx_validation_base::generate_with(std::vector<test_event_entry for (size_t n = 1; n < total; ++n) { rct::key ki; - r = cryptonote::generate_multisig_composite_key_image(miner_account[n].get_keys(), subaddresses, output_pub_key[tdidx], tx_pub_key[tdidx], additional_tx_keys, 0, pkis, (crypto::key_image&)ki); + r = multisig::generate_multisig_composite_key_image(miner_account[n].get_keys(), subaddresses, output_pub_key[tdidx], tx_pub_key[tdidx], additional_tx_keys, 0, pkis, (crypto::key_image&)ki); CHECK_AND_ASSERT_MES(r, false, "Failed to generate composite key image"); CHECK_AND_ASSERT_MES(kLRki.ki == ki, false, "Composite key images do not match"); } diff --git a/tests/crypto/crypto-tests.h b/tests/crypto/crypto-tests.h index e833c0444..29a50e0fb 100644 --- a/tests/crypto/crypto-tests.h +++ b/tests/crypto/crypto-tests.h @@ -46,4 +46,6 @@ void random_scalar(crypto::ec_scalar &res); void hash_to_scalar(const void *data, std::size_t length, crypto::ec_scalar &res); void hash_to_point(const crypto::hash &h, crypto::ec_point &res); void hash_to_ec(const crypto::public_key &key, crypto::ec_point &res); +bool check_ge_p3_identity_failure(const crypto::public_key &point); +bool check_ge_p3_identity_success(const crypto::public_key &point); #endif diff --git a/tests/crypto/crypto.cpp b/tests/crypto/crypto.cpp index 145ec1d86..e1be38054 100644 --- a/tests/crypto/crypto.cpp +++ b/tests/crypto/crypto.cpp @@ -32,6 +32,36 @@ #include "crypto-tests.h" +static void get_ge_p3_for_identity_test(const crypto::public_key &point, crypto::ge_p3 &result_out_p3) +{ + // compute (K + K) - K - K to get a specific ge_p3 point representation of identity + crypto::ge_cached temp_cache; + crypto::ge_p1p1 temp_p1p1; + + crypto::ge_frombytes_vartime(&result_out_p3, &point); // K + crypto::ge_p3_to_cached(&temp_cache, &result_out_p3); + crypto::ge_add(&temp_p1p1, &result_out_p3, &temp_cache); // K + K + crypto::ge_p1p1_to_p3(&result_out_p3, &temp_p1p1); + crypto::ge_sub(&temp_p1p1, &result_out_p3, &temp_cache); // (K + K) - K + crypto::ge_p1p1_to_p3(&result_out_p3, &temp_p1p1); + crypto::ge_sub(&temp_p1p1, &result_out_p3, &temp_cache); // ((K + K) - K) - K + crypto::ge_p1p1_to_p3(&result_out_p3, &temp_p1p1); +} + +static int ge_p3_is_point_at_infinity_vartime_bad(const crypto::ge_p3 *p) { + // X = 0 and Y == Z + // bad: components of 'p' are not reduced mod q + int n; + for (n = 0; n < 10; ++n) + { + if (p->X[n] | p->T[n]) + return 0; + if (p->Y[n] != p->Z[n]) + return 0; + } + return 1; +} + bool check_scalar(const crypto::ec_scalar &scalar) { return crypto::sc_check(crypto::operator &(scalar)) == 0; } @@ -55,3 +85,19 @@ void hash_to_ec(const crypto::public_key &key, crypto::ec_point &res) { crypto::hash_to_ec(key, tmp); crypto::ge_p3_tobytes(crypto::operator &(res), &tmp); } + +bool check_ge_p3_identity_failure(const crypto::public_key &point) +{ + crypto::ge_p3 ident_p3; + get_ge_p3_for_identity_test(point, ident_p3); + + return ge_p3_is_point_at_infinity_vartime_bad(&ident_p3) == 1; +} + +bool check_ge_p3_identity_success(const crypto::public_key &point) +{ + crypto::ge_p3 ident_p3; + get_ge_p3_for_identity_test(point, ident_p3); + + return crypto::ge_p3_is_point_at_infinity_vartime(&ident_p3) == 1; +} diff --git a/tests/crypto/main.cpp b/tests/crypto/main.cpp index f804c45dc..5486937c2 100644 --- a/tests/crypto/main.cpp +++ b/tests/crypto/main.cpp @@ -259,6 +259,16 @@ int main(int argc, char *argv[]) { if (expected != actual) { goto error; } + } else if (cmd == "check_ge_p3_identity") { + cerr << "Testing: " << cmd << endl; + public_key point; + bool expected_bad, expected_good, result_badfunc, result_goodfunc; + get(input, point, expected_bad, expected_good); + result_badfunc = check_ge_p3_identity_failure(point); + result_goodfunc = check_ge_p3_identity_success(point); + if (expected_bad != result_badfunc || expected_good != result_goodfunc) { + goto error; + } } else { throw ios_base::failure("Unknown function: " + cmd); } diff --git a/tests/crypto/tests.txt b/tests/crypto/tests.txt index 8e6534a87..3a7fe5453 100644 --- a/tests/crypto/tests.txt +++ b/tests/crypto/tests.txt @@ -5467,3 +5467,9 @@ check_ring_signature 8427e7179050bc38d5c25e68f70c2c98990042388e326d6bebd5674ca8d check_ring_signature f3d2b5b25d663325acca133163bbf3219f1b22fea6bd6d6e3194db8bc30dc6fa 448071ee63780f0fcfe35245353e4fd28f5c5362d9a5f3d74e5bd6685986729f 6 fb706555f8358ac3db60d9a52eb4981f91d28cc4d518c1a5c988ce94c7051379 e07dbe16cee565a221af2353c4761cdb7c7fab5880372b0e46d49ab4842d3b4c 05db3f4b53f17fe0525e2b5002664d9b0d5680c10146640cbbf23a118d6d88fb a446a6e907f653e0db5888927971d0dfd5c854d2b04f02367e18b02378271b11 72ce0ff9a80faaf2ea826cfe8244cc2d345a7c887143e481ffe826b8630b6f93 d17c0b9ba4aeb04ddb8288222a0d5d22abe327981786f790cf8b9ee8831090a7 8a11ee60dd8470e1bd447f472b60723d8a30ac8a84fb4bd98580b2f3ddfc32027193f368c87e02deb9219eb50e0a9b2d9d43bab27c14ac4be23640a1fca01a0dd12337332fdfe9f3aef0235281614f3e94cf7902486b8a5b76444e9a56e21a09beab12ff902fdc05b16a2ad417d92711107bcf7a554cf82fa069e9432874680dce00e8a862e417b2efaa66cb9e4693e00b7d6cd1c452fc71630473799fdc7603080d006b562aafe0e75e456e6e09f1a655dcc40e296f0f3083c5f58c75e9dd03c61115f05d75c65e04f2d02c6232c72c99a32e9f8c851b23219e2dbcdbd6190de6c9ae374530c0f268758611fffac7dbb8f16d8c71a0da950ae12603d942b808146f9131049a75364f45cc606ab4d6882a137999c163312c87fb3d8e78ef3406b97dbd90bbdd20a8025d4ba438491ff0923da5055b7c3ee446b7ddac341f350056efeaa3f706fa2ac8a6d02d5f2a4cd5644af7f9a48a699801469d33601667081fa0971b1b6dc3c86d539197a531a76ebe611fc967d26067f2c2f0d879ef0309 false check_ring_signature 07a23b78f73ca487ec5ab0f4d7725d7ffb547543ae4f96e30df871c2241489ca 2073d5a5ccb03402ded68c31d3de658f7c5be2265bec656a12212c83e2499a75 9 ce3fe390c5309c0aa6c0a1e4dc29ef63fdf55ad2fef737b775bae9857c666966 08088bfe1f076131d82458ef08e0a6d8003d26d824360033895e62409fceadba 4faffacfacc069e09f80a0249daf97a40b53a64ab870c62dfd08998d382dba52 386e435138fba8c063966d8308927f8c8788782a3a263500133325c9c82d38a3 efd955c96135d72fee34c765998cf714f37365af8f77cae145ba5d126f1fe914 60dbea373e81c0276c4fbb83ca1fcbb647e2fa11a8bbc62c8e56d4d147b39bd5 1495004110d8e2fe1774ba6eb9492b1bbc54f674ed2082401cd6eab71ad9dc40 7a2af9f6dad76479c5f3345ffd250f55b234682b4fd51d3ee9ae8da2e5a35cb2 02548abda0688fc63cc485b956fd4303bf0dfc43a581e01c59c62243461ea348 e784e4e19099667803c6f08fd0877e85937bec50ab02f75b6f2e3dd61876b40510bf63faa2346e3b35b0ade96bfef145dd17b92ebdb1cf96899e10e3442aaf0d782eafc8be306390ffcda93025f6832a2d4c4e4d1c2b56add53550b3b512b71af959bc1b5902a7b628eae1d16d242c099fb4ce33a4bee12af49a41aab958940910b0d0ef596b753688f228a7184e38e7df8644cbecfa658d721736ed2882e20f9a2fb61818c0060b3fd4b2ced7984bb17b10381efde2330bbb1de208655e080cd33d9e5d5d853a8875623195ed30e2c2e475ebe4ac97e3c5216520b0a201a608b6c6013266ab0e63a461dae171af4c3a14a5fa8c4c33ca25b84540e32efcf302b18e610545ff35d45dae4116ba6d51ac9125cdb7f681739744237827e768bb0487b6b0a1d9e8d5ce809b9e17a6c32df9ef77a26af987b2348c748cba73e917034e21609b0d92a5b7106d39c8de2f057013bd347be67e553e608cbb70683f2e04ca7ed84ed7f4c671efac5deb3db17498f23170cc8deb4596d1ea958caa7e4e06204833104aed36ee0b7808dba1194cc374c193e4e926832ac171d03f3abe6607803d6b33a1350ec428afc88977eb411505bcb54113da91f63fdd2bb85a08140cbc040fadb23ba1c92fd3bad8c4930f36d37403f8e9d4b87bf3636ccffc53c30cc4eeb9945214f9d8e288edbfa1d7546baae6860f9f56af7d79f9349c104579064d404a3d7a893d64fbcca32c5d4e6158d92cd87c8a78bb3a61eb55ec92a7ee0aaabe59d33af5534ae258f48bca9d5459e0708ebf9289d9bb22acb540b0f39003 false check_ring_signature 0e6194f7b3ec1594e6b727b990c6bf65a2b1eb1a9b73ea00d18a709ffdee1276 f931b871addf92f407f087ae176804734ba35fd65086f6c3e07d9d7b001be265 51 8daf0c2e434171ef0e31f1fd17307a30690639fc7fef1a85a9a7858868924c1d 93edf23dcd46477698f2e4795ab9e4e75ef04c8ca561670c22d0a379f7cc9d12 5fb23ddfd7bc6db6798d0dfadc6accc8c7fd75adb090ecb6bad2021bc3bac5da d093463f76e271597f6cddc74b5685b0c4d4ab6b6f6bae4b0217c7097ced9bb6 1504b6cab3b2454a066fe9462be135eda844480da85ad3baf67fbf39679cdbf0 e812fa7c7f0b6e11dd0d87fd88767a768f9e89f2c54396a9a53d443d0edafdcf 6570828a5e056a4e035b1042b80007a872c71545ea225feb982d24eea14373f0 a3adddfa513dca877eca62e12255c981b148a605c998458c6786aba6aaf1207a 43f3fb57f9a0c90200a940217893685296537f34041fd9586586937386f8d33d 9ff696ea156a4468e1d0d32590fcc865f491d821254594535c1694d7eb0102c2 3f0245304a5047ce42fb0b36544220d7df36c919aa2321bf39e83fa71c4de21c 01c527e626c1b5c928058d7ea772cda93833ba111792da212a5eae0041ccae51 61a79b26403f88acf0d4a7bf86efff4bd9f32e05e9a900e7d890ce36ab8abbef be302d5d4b83cd447536c08dfe66a32fab021889d9eb7a8621e59a6e3756b14a 693a9d20a1d12828a94d01a2bdc856f3499745e4c5830cda407962d2b6953784 62438ad0ca1dc718def66c97d8a4b2a1c4e61322c7cb8c3b904177cc8d8fbcaf 88dbaf2eab9896bac05f93c953c548de45c032d40b7c7e452c17ac73606b942b 894905861e3c952ab0e350b12900b454b3870ce3e9b590f94203ce2bbc41f944 cbf933f67076f66793bf06f2a5008bd01b36b6aa094483269da52bbba7465580 557046df81dbcfe18c65e1364448c8d94c710cd1fbf8a3c10550254d97afcdf7 7b24c0ee3a7836c1bef02956b81358d8f698da40f0ccd706daf3038cb0b28793 28ddf7553a328fe5ef8c8a6abd52ba6d725240f1511665884cb68c96d46d92d5 ffd267319c8ed9e424371ef74e5301102997c26265a2044bbbe05ee291f5bcc9 d467502be7c3753d9e4d2a147d3a40d1d817a865184030870bf97ef6f1610da7 b1ec5f6565cc19a6d015696594ada55b5fbe82108fcec0d54fb11b05bfd37408 19e7e65cdd957ab0d25dfc9e4e1449b1d4283d2561754fc147adb51e057dc7ba c5d4274eef9f5d92e7a8dda59de38f5728d60350d8e98871968765925d18ed09 a44814c8f2912d13c09242b0b75dad90a6357b844e021dc96668c45e311b64b3 cd88baa50640c7409a98921e0ad1e7f2496b39a3b07544eeef2455fc5018b17e 9ab6f34f081bccc8d8b29513851a402759c96b8d95d836db977e24e595deb2fa 56123960591257a70a334628d9868505176533debe06cdfc24f906ec8a997efd a37b03d5446abb9a3fb7aee94d6740d85f984fb363a9e78a9d363d5882cd2823 bd9607c5e6c167f5eb6a289be620592c7e850590393787d01acd958fd717b0c2 d0d69b420c93d9b1b8eee6796db2c7cc2110dc78b624396e144c135fe69c812d b6a2ee98807a00bcff0fa5403102c9a628948d3478234cdde85e5effbefcf204 e183a5d8daabdea01f7cd7f0960b75566aec78874cc933f5acc3dce0512ab335 ea7f86e49af2a4883e11b3362127dfdb14f0f517170ddb4338a5a38d2c92566a 4ea9d24ebb852962e745d4ec30679bed40f581d1ccf6825e80ddb38759025ac7 d0c887ff472e65d7fcb502988dfb0c919c8d23c2897d49b8c8de90d725604ea8 0177735a5271e254705979ef38a0737897f608aa144f4c4ed97e220dcb32f1af fc49aac2eaf95de87156c0348381d78d9b468a1da3eae6cc25bb398563b0bdad 84a98d5f20b608d31af41925052dfd378f0252e1bf7c13cf9b773e0cd4b95a52 8fd6df156a95abf7ca37fa1d65e820b56f2498c9ada0a4027f0dbc285b9a9b86 f7bcadf3751cdc85db850619a857288d1291c2f403732d79daec6eb774fd26cb c87e4373dadc7a65f53df413d22b3370f7a8c47a929ded626f3f71f3b249eb4d 02398630b92a7f782dd0153d39a56123ec7ea8d5a0fde77aebd302e641de2a54 f655dea86af87ed4d93ea10c2ffd5e52aff83a6fb5fab65a1ab67a02cfae6523 990747d3687fcc37ba0fe4845f9b247ebddd0d0a5a61f2637bb498f0b05e9fb3 38050abbc28c5847708f0142d8e89d44dad6d0ee8bd60b5771bd03d590a7ff85 867b6cdc08dbecaf1b6f9ee2add2d2c9dfe18474a805a028533b619f52abf6b3 0bda8ca264a6747f18195bbf56fafe96c9a0c8e6557f1b17c50eec71ae52fb50 cc925498a1aaccee819efd183df25795472fda1c7abbad6eaf42b00192c1e7005cacf5a965e831702169f97d374b7b30a7cb3343170c89c325090815e663a805f1d64336bfcdc25794467e68c093403faa50071b992863da69b6399f8bd33308abac8a69fe14a4a3de62ccb4bb5b49f760d1567fb1c966881476f7c5cdd20d0738b0452fa686ec759a6f19f50f1cee1ef06d69162d6d9c6d97055ce7b746a7038ad3913cf9c92a4a5faebecc5417ee900e8cb9e58c35d06e7b554a38815efa05a2f578665a2f96fff87bc9c025b0a23775a8e079f98805a350cac06456ed230f3bf10e3a133abd448f811ed82663f4bfd3fd6492d3ff6e49625b1efc8b9a090bbd70ee46aba76648c7e899cc19433a230f59314dc72d10d7c5dff9c9e2b79907161b00e307b10a9f53332a325c305744d2b96229831fefd891fa1f0776278005fc792d7d1882368def90bb0c75c927464eb4a95ad133e9cca7c57708c620520cb4a4a25880d80cbd1101d0a78e5cc7cf9aae6fe52449911be59a405f35356709977a9532074a05627c13787192cdad3b40f9434d469f189e311c0d917d788d011693c8319cc0b0c4c0ef4fdf08f945be0c6e65fd1c548bba7608a128c3a327020818f93aad968cd094149dbe064e3873b4d704bd0a5ee97fe040944de667a3063ecf7be8aedfd54bd73d13a56b944c546c4c6fb7cc5a9c74a6508c8af26410095f34c6787f38abd729bbb728a4213e9e079b98b81d5f6562f29ebf21ac85010fd4d83ffb1851ed601ae7bc93ae5b36da273101f7485a2d38ed29f68636dd5700b2fcdc9709461a027e02850560fc3e3f1c6fd8dbf4cf1084e059bdddcb8d450f8f3bc1834060ec6375cb94cab89cfce4aabcfc3ef1bf13018535607234e63800d9f6d20030fab8384f287271183563e0730708203261ebbcc0531f4637e0540c7354ffb862e9c435afcf4a11313a2c60f8fccc6483cb70b3dd7b1e7abdbc8e0779dae0268351518fe07b03fe764675ba91f7e612c6d07231e6deab9a909bdd0f419adc092fe62fb5378be479f7cde0e80eb642aa89441feb6b0119595bd7770446d253edeaf3c0dee883f046f0f7e1d2a26701eff4d7b3e9c5f76e27bed51002ee4a57c4e51ee3d2bece02c959ddf90e422f739f1f3b628b5be04f8845f06b00fc891ff96e22af1869f22c1b79afa8ce7eaa4afa70158cde42310f312f92d8013e3aeca05cceb2ee5bcf0a50a5c267f4e548bf32e7cb935ee28c644511beaa071923a1d8b15b2e72b864cf5a4e3f4c290467c9a70dcf924345cd82c2d1f68c0e031f05602b53686d50a7a7bc429e1b54287fbedd16c6cc4aedfc9d66bdf31f0959689980fba888d99ffc6d40aa6f7914bbf593fe736e9edad1a4fc7811853e03f927c357cf3d5dbf58327d4b43ce3f8797063492961a04c30995bbddfe281b0b153043fe51178ff43933a0699ab3a2d995457e7cfba7149ca8d3da1e99d2280bf505e8bed2cdb4f037c2d5c1efd00cda5db5cede0d37a3ebdd94b0f12c457b05f546748f69c5212c58f028bc6a29e11ddb4647e58414ee25060e47506fdb7a0232a7ac6df4481bde4ca5f3ee88a97dd96396ad4727b076743e64e8ee1eaeff0b2532804e4cc7a28306c80b9498eeaaf5be9087caa51807bca9377f76ef6a500cadc72f6f95aea184d559be04f14bb0b05f0ce303fb68cb7091ac96007264ec05c442a68d867351823252d8951d7f6b9671b857244d477e75466c3017f8621808fd511a14c5dab254e5d685d08a922317dadc6fa90b6b9df5b2d23dd392ac910bc8fec0ac56c9ee5d491d9685709fb0d3683905fa390472ecaa01090bd4969c0df17f699e6c78942625672228e7474c37db2336b1136b4dea290940f4a35285013264411791a89ae770a63257478339572c85e7b838078a56db7c97a029a04e0ff040a4d5f08fa4e4a0933b7566d8dce9970eb471b270b70970b5ea19124e0d09e643c89b98f199fae32c088512e6839c33ebd646920e360294d869ad6bffa30de976920a838bc8b88025514d0312f6f28066aec9b4d2d9253ce94e85358b54078ce1b56aed1de3f9071419e2d3590df4d3072ea1d93f11d1c9bb011097f2380ab7341e0b1c7a2d4de64e82f4203a87c9805e1298d69f14b3911622f1119a900d49de89000147275fc18aa830228bafb687e8cc9eb8ca4c2ec4c90bb33fe25201417fd7408111e0efdb16d59412852deac263800de4fcb96b19a2ebbbe4d719051ae02b8b3c4292f097380c9587e96bdfd82037c54649154034d95057e883e00cfe4250f079e2a5460a06aeb4103e6c4b7b64a38a851e1650d758e9a5bc12d3093fc4e9f701612625c969d7dbfc1369349c255f883dc436abaef5c16e453d7a0571cf9797a89446077045d3eb3169261025698dbbff5f1354426e7375bf04ed07b930b24bf02049e577c98bd7abe8c5903f48af2eb03686166364ce54aaf9c20a9774b7b04b922ad510182f41a8b84e0b5e8e8cb404e775f9cf146dcae0fa5f0460da3dfb2ecabe850b5dda46d54c8375003ffaa9f7efd508ffc799523c6c1b0ec1406363554dce15ecd47d91936292b448022b5f162ec09937f50f86d1e50908762bc5330f51c639e035dd07b227e6f499884b51829d920ebb517c3732044500150323d68280d23c6711473abe8d5d835c5f071779af65647334d656caded90a6e66585fa0cc6806375853481eb0b409211473afdd46d6686c8ab95c27aee304a048fadac986152ba89037f084dee9368e5768434610f7904a3552d0d1bcdd06ee33c31485d28838266987b1584ee02a1ff0383a134d11114bbd11b5f8e0b10cd19a175be6520ec55c0380478f723bfc1f0ad1f14ee839547657979ee73e9907939245c535483d3fd79ccc65d776903ebd014303ee2c508d87f053666b42200e8731cb3e79f0a543673cbe1acd23740c442a00db5c6c23c06d008029184dea0970641273d935bc76bdef35f483052e57cfd00b1188c54948f7856857dcc58f05c0735df9a9896da2fc8b82b5c3ceaf98db68508a1004230e0f8f78595da2b702508216f0ba4d65d5dfa14f0cf2d54c67ab4bb54eb4e820c5af761978d0531b0a25a136f6bca068eeafb0828bb76bf4b8bae4cbfce93e8cf9a4c5ce2a1d00c20c2a713c981bd9e46e48dc3e90d2a09e40722212dc3d46c7ec98c830af1545dd014cddd1f0492fc19c597f24acdb0cb9836bf16507be54d9eb229d6b778c20370149e7ee1d371105f331f829e9b611340e1120ee6ce4bffe9c3a7f96f1adf2b802a62f4331c3052309a74465d7f3ac8059bb7b1e0093977c1ce177c04a4be8060447c3a9a49e13f4a2f8579668d929b6782c76b41ef127b21cf91c766de44c930005089f2fad932b12ec662c1bfd7d17c8384b98db2a64ff3b952034131cfa370b5ed0b3bc48d3fd66cf1200984c8929298bf7102932b658432ba2e93d0c01400a75775b1dbea79c9acb1b3348ba24a454f104b72c39ffff01a56d60fe2084af02078204e1c51773f8a91aac91564e8b5e069ce2e88319e47e65f01965743ace06f6540d22fc0e0e2038173960189818af53095c82b0726b6ddeeef14edb01950e7c381d852224666623294305816e713561e1146a37a1212b41df75079a26e601ae81025abd07cd3bfe475f3c44db05a2c715ed902bb43cdcf4e505925d3e18006518b18a46dd21adb117bfb342c68b96f974f1be18c47e7bd5f00a90c225500aa0d5c11207714e4bc729f3662664534064a6fdae4c03456e9e7a75821eefbc0b9cfa05ec403734d1ac35e3a03ca5bf085d0473195dd9490375d241c73ef5d50bf79ed1fe14e93d0710ebf915f8f52d0062b7986129ae8903f78a4b449b7ce7013f5b2aebbd5fcc76b71d0c36dbbb24cb6ed6cb50de9b06a01ba11de6025f2d0c4af71c28d9eb53d9ab885981e27554255cd0aed13438469d4713c0e37fb8ad0c751d50fd4db099dac66358fbada177b5f69f3a6518216f0eb7c92878f94da6d6c18a4c5606f55afb9e66610ef4a41953cff6f1ac7181b6f76ee54aa728466f0c73129d70c6affca90b8931deef27bb1185e7097a93b12b3d389388e1ad1adc07dfd96e6dc926eefdcf373c64566f690b970717afd1df3e4326d73c6d1cc5330c8fdee071684fe4e042b8aec497e2e4fc1098149d0707ce95b550b63fa74cd808ae6a0059002b016d814e0a6e716dde400e5b3716ae33fc8c92915488ce7fe50d94c19f927482e0d6b3cc2dcd9e5fb9d741aeb81c07a9dbad27b926257895af087791a6b9c4048f17832148be5c58f89e4877e22c5087ddbf0a7bf273ae45a50b71d9305c8813c886ea454d6c24174f143856abadc9319d6eabd8d48ec3c66a031394f78c9c9cdb2d0de8f44a932d30a6b913378999ebf0560c1c5f7c4098ce06f117b53db72a4436c885eb9c748deb2587ca0904743138de7e98bc3b0494e40a5a56d71f4204e8b8cad941bfd3abd384b976baa2f6b88c987e61f049f80c890df6470aad55283b8b891b0707aa22089351441fac575587b2ea4ac2f9f9c38b071fdbcc7f5efab14d86b22181e741ed824447a2facb46a63e91538af78c5fdf02 false +check_ge_p3_identity 078c63ceebcfc9d8af51e232b497fb3cd5f491bb9bed5aa3c556a47e62cb186b false true +check_ge_p3_identity 62218e5f0710af551bee941adb3981650862d2a3c4c18c794f450b5bb538975b false true +check_ge_p3_identity 046e1450f147f3ade34d149973913cc75d4e7b9669eb1ed61da0f1d4a0bd7f13 false true +check_ge_p3_identity ca8a2f621cfc7aa3efcd7ddf55dce5352e757b38aca0869b050c0a27824e5c5e true true +check_ge_p3_identity 64a247eef6087d86e1e9fa048a3c181fdb1728431f29ba738634bdc38f02a859 true true +check_ge_p3_identity cff0c7170a41395b0658ee42b76545c45360736b973ab2f31f6f227b9415df67 true true diff --git a/tests/functional_tests/multisig.py b/tests/functional_tests/multisig.py index 0a58f469a..bb7ccbe56 100755 --- a/tests/functional_tests/multisig.py +++ b/tests/functional_tests/multisig.py @@ -39,40 +39,40 @@ from framework.wallet import Wallet class MultisigTest(): def run_test(self): self.reset() - self.mine('493DsrfJPqiN3Suv9RcRDoZEbQtKZX1sNcGPA3GhkKYEEmivk8kjQrTdRdVc4ZbmzWJuE157z9NNUKmF2VDfdYDR3CziGMk', 5) - self.mine('42jSRGmmKN96V2j3B8X2DbiNThBXW1tSi1rW1uwkqbyURenq3eC3yosNm8HEMdHuWwKMFGzMUB3RCTvcTaW9kHpdRPP7p5y', 5) - self.mine('47fF32AdrmXG84FcPY697uZdd42pMMGiH5UpiTRTt3YX2pZC7t7wkzEMStEicxbQGRfrYvAAYxH6Fe8rnD56EaNwUgxRd53', 5) - self.mine('44SKxxLQw929wRF6BA9paQ1EWFshNnKhXM3qz6Mo3JGDE2YG3xyzVutMStEicxbQGRfrYvAAYxH6Fe8rnD56EaNwUiqhcwR', 5) - self.mine('4ADHswEU3XBUee8pudBkZQd9beJainqNo1BQKkHJujAEPJyQrLj9U4dNm8HEMdHuWwKMFGzMUB3RCTvcTaW9kHpdRUDxgjW', 5) + self.mine('45J58b7PmKJFSiNPFFrTdtfMcFGnruP7V4CMuRpX7NsH4j3jGHKAjo3YJP2RePX6HMaSkbvTbrWUFhDNcNcHgtNmQ3gr7sG', 5) + self.mine('44G2TQNfsiURKkvxp7gbgaJY8WynZvANnhmyMAwv6WeEbAvyAWMfKXRhh3uBXT2UAKhAsUJ7Fg5zjjF2U1iGciFk5duN94i', 5) + self.mine('41mro238grj56GnrWkakAKTkBy2yDcXYsUZ2iXCM9pe5Ueajd2RRc6Fhh3uBXT2UAKhAsUJ7Fg5zjjF2U1iGciFk5ief4ZP', 5) + self.mine('44vZSprQKJQRFe6t1VHgU4ESvq2dv7TjBLVGE7QscKxMdFSiyyPCEV64NnKUQssFPyWxc2meyt7j63F2S2qtCTRL6dakeff', 5) + self.mine('47puypSwsV1gvUDratmX4y58fSwikXVehEiBhVLxJA1gRCxHyrRgTDr4NnKUQssFPyWxc2meyt7j63F2S2qtCTRL6aRPj5U', 5) self.mine('42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm', 60) self.test_states() - self.create_multisig_wallets(2, 2, '493DsrfJPqiN3Suv9RcRDoZEbQtKZX1sNcGPA3GhkKYEEmivk8kjQrTdRdVc4ZbmzWJuE157z9NNUKmF2VDfdYDR3CziGMk') + self.create_multisig_wallets(2, 2, '45J58b7PmKJFSiNPFFrTdtfMcFGnruP7V4CMuRpX7NsH4j3jGHKAjo3YJP2RePX6HMaSkbvTbrWUFhDNcNcHgtNmQ3gr7sG') self.import_multisig_info([1, 0], 5) txid = self.transfer([1, 0]) self.import_multisig_info([0, 1], 6) self.check_transaction(txid) - self.create_multisig_wallets(2, 3, '42jSRGmmKN96V2j3B8X2DbiNThBXW1tSi1rW1uwkqbyURenq3eC3yosNm8HEMdHuWwKMFGzMUB3RCTvcTaW9kHpdRPP7p5y') + self.create_multisig_wallets(2, 3, '44G2TQNfsiURKkvxp7gbgaJY8WynZvANnhmyMAwv6WeEbAvyAWMfKXRhh3uBXT2UAKhAsUJ7Fg5zjjF2U1iGciFk5duN94i') self.import_multisig_info([0, 2], 5) txid = self.transfer([0, 2]) self.import_multisig_info([0, 1, 2], 6) self.check_transaction(txid) - self.create_multisig_wallets(3, 3, '4ADHswEU3XBUee8pudBkZQd9beJainqNo1BQKkHJujAEPJyQrLj9U4dNm8HEMdHuWwKMFGzMUB3RCTvcTaW9kHpdRUDxgjW') + self.create_multisig_wallets(3, 3, '41mro238grj56GnrWkakAKTkBy2yDcXYsUZ2iXCM9pe5Ueajd2RRc6Fhh3uBXT2UAKhAsUJ7Fg5zjjF2U1iGciFk5ief4ZP') self.import_multisig_info([2, 0, 1], 5) txid = self.transfer([2, 1, 0]) self.import_multisig_info([0, 2, 1], 6) self.check_transaction(txid) - self.create_multisig_wallets(3, 4, '47fF32AdrmXG84FcPY697uZdd42pMMGiH5UpiTRTt3YX2pZC7t7wkzEMStEicxbQGRfrYvAAYxH6Fe8rnD56EaNwUgxRd53') + self.create_multisig_wallets(3, 4, '44vZSprQKJQRFe6t1VHgU4ESvq2dv7TjBLVGE7QscKxMdFSiyyPCEV64NnKUQssFPyWxc2meyt7j63F2S2qtCTRL6dakeff') self.import_multisig_info([0, 2, 3], 5) txid = self.transfer([0, 2, 3]) self.import_multisig_info([0, 1, 2, 3], 6) self.check_transaction(txid) - self.create_multisig_wallets(2, 4, '44SKxxLQw929wRF6BA9paQ1EWFshNnKhXM3qz6Mo3JGDE2YG3xyzVutMStEicxbQGRfrYvAAYxH6Fe8rnD56EaNwUiqhcwR') + self.create_multisig_wallets(2, 4, '47puypSwsV1gvUDratmX4y58fSwikXVehEiBhVLxJA1gRCxHyrRgTDr4NnKUQssFPyWxc2meyt7j63F2S2qtCTRL6aRPj5U') self.import_multisig_info([1, 2], 5) txid = self.transfer([1, 2]) self.import_multisig_info([0, 1, 2, 3], 6) @@ -177,10 +177,6 @@ class MultisigTest(): for i in range(3): ok = False - try: res = wallet[i].finalize_multisig(info) - except: ok = True - assert ok - ok = False try: res = wallet[i].exchange_multisig_keys(info) except: ok = True assert ok @@ -193,11 +189,6 @@ class MultisigTest(): assert res.ready ok = False - try: res = wallet[0].finalize_multisig(info) - except: ok = True - assert ok - - ok = False try: res = wallet[0].prepare_multisig() except: ok = True assert ok diff --git a/tests/unit_tests/multisig.cpp b/tests/unit_tests/multisig.cpp index 79775960d..362a658de 100644 --- a/tests/unit_tests/multisig.cpp +++ b/tests/unit_tests/multisig.cpp @@ -26,12 +26,16 @@ // STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF // THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +#include "crypto/crypto.h" +#include "multisig/multisig_account.h" +#include "multisig/multisig_kex_msg.h" +#include "ringct/rctOps.h" +#include "wallet/wallet2.h" + #include "gtest/gtest.h" #include <cstdint> -#include "wallet/wallet2.h" - static const struct { const char *address; @@ -86,59 +90,145 @@ static void make_wallet(unsigned int idx, tools::wallet2 &wallet) } } -static std::vector<std::string> exchange_round(std::vector<tools::wallet2>& wallets, const std::vector<std::string>& mis) +static std::vector<std::string> exchange_round(std::vector<tools::wallet2>& wallets, const std::vector<std::string>& infos) { std::vector<std::string> new_infos; - for (size_t i = 0; i < wallets.size(); ++i) { - new_infos.push_back(wallets[i].exchange_multisig_keys("", mis)); + new_infos.reserve(infos.size()); + + for (size_t i = 0; i < wallets.size(); ++i) + { + new_infos.push_back(wallets[i].exchange_multisig_keys("", infos)); } return new_infos; } +static void check_results(const std::vector<std::string> &intermediate_infos, + std::vector<tools::wallet2>& wallets, + std::uint32_t M) +{ + // check results + std::unordered_set<crypto::secret_key> unique_privkeys; + rct::key composite_pubkey = rct::identity(); + + wallets[0].decrypt_keys(""); + crypto::public_key spend_pubkey = wallets[0].get_account().get_keys().m_account_address.m_spend_public_key; + crypto::secret_key view_privkey = wallets[0].get_account().get_keys().m_view_secret_key; + crypto::public_key view_pubkey; + EXPECT_TRUE(crypto::secret_key_to_public_key(view_privkey, view_pubkey)); + wallets[0].encrypt_keys(""); + + for (size_t i = 0; i < wallets.size(); ++i) + { + EXPECT_TRUE(intermediate_infos[i].empty()); + bool ready; + uint32_t threshold, total; + EXPECT_TRUE(wallets[i].multisig(&ready, &threshold, &total)); + EXPECT_TRUE(ready); + EXPECT_TRUE(threshold == M); + EXPECT_TRUE(total == wallets.size()); + + wallets[i].decrypt_keys(""); + + if (i != 0) + { + // "equals" is transitive relation so we need only to compare first wallet's address to each others' addresses. + // no need to compare 0's address with itself. + EXPECT_TRUE(wallets[0].get_account().get_public_address_str(cryptonote::TESTNET) == + wallets[i].get_account().get_public_address_str(cryptonote::TESTNET)); + + EXPECT_EQ(spend_pubkey, wallets[i].get_account().get_keys().m_account_address.m_spend_public_key); + EXPECT_EQ(view_privkey, wallets[i].get_account().get_keys().m_view_secret_key); + EXPECT_EQ(view_pubkey, wallets[i].get_account().get_keys().m_account_address.m_view_public_key); + } + + // sum together unique multisig keys + for (const auto &privkey : wallets[i].get_account().get_keys().m_multisig_keys) + { + EXPECT_NE(privkey, crypto::null_skey); + + if (unique_privkeys.find(privkey) == unique_privkeys.end()) + { + unique_privkeys.insert(privkey); + crypto::public_key pubkey; + crypto::secret_key_to_public_key(privkey, pubkey); + EXPECT_NE(privkey, crypto::null_skey); + EXPECT_NE(pubkey, crypto::null_pkey); + EXPECT_NE(pubkey, rct::rct2pk(rct::identity())); + rct::addKeys(composite_pubkey, composite_pubkey, rct::pk2rct(pubkey)); + } + } + wallets[i].encrypt_keys(""); + } + + // final key via sums should equal the wallets' public spend key + wallets[0].decrypt_keys(""); + EXPECT_EQ(wallets[0].get_account().get_keys().m_account_address.m_spend_public_key, rct::rct2pk(composite_pubkey)); + wallets[0].encrypt_keys(""); +} + static void make_wallets(std::vector<tools::wallet2>& wallets, unsigned int M) { ASSERT_TRUE(wallets.size() > 1 && wallets.size() <= KEYS_COUNT); ASSERT_TRUE(M <= wallets.size()); + std::uint32_t rounds_required = multisig::multisig_kex_rounds_required(wallets.size(), M); + std::uint32_t rounds_complete{0}; - std::vector<std::string> mis(wallets.size()); + // initialize wallets, get first round multisig kex msgs + std::vector<std::string> initial_infos(wallets.size()); - for (size_t i = 0; i < wallets.size(); ++i) { + for (size_t i = 0; i < wallets.size(); ++i) + { make_wallet(i, wallets[i]); wallets[i].decrypt_keys(""); - mis[i] = wallets[i].get_multisig_info(); + initial_infos[i] = wallets[i].get_multisig_first_kex_msg(); wallets[i].encrypt_keys(""); } - for (auto& wallet: wallets) { + // wallets should not be multisig yet + for (const auto &wallet: wallets) + { ASSERT_FALSE(wallet.multisig()); } - std::vector<std::string> mxis; - for (size_t i = 0; i < wallets.size(); ++i) { - // it's ok to put all of multisig keys in this function. it throws in case of error - mxis.push_back(wallets[i].make_multisig("", mis, M)); - } + // make wallets multisig, get second round kex messages (if appropriate) + std::vector<std::string> intermediate_infos(wallets.size()); - while (!mxis[0].empty()) { - mxis = exchange_round(wallets, mxis); + for (size_t i = 0; i < wallets.size(); ++i) + { + intermediate_infos[i] = wallets[i].make_multisig("", initial_infos, M); } - for (size_t i = 0; i < wallets.size(); ++i) { - ASSERT_TRUE(mxis[i].empty()); - bool ready; - uint32_t threshold, total; - ASSERT_TRUE(wallets[i].multisig(&ready, &threshold, &total)); - ASSERT_TRUE(ready); - ASSERT_TRUE(threshold == M); - ASSERT_TRUE(total == wallets.size()); - - if (i != 0) { - // "equals" is transitive relation so we need only to compare first wallet's address to each others' addresses. no need to compare 0's address with itself. - ASSERT_TRUE(wallets[0].get_account().get_public_address_str(cryptonote::TESTNET) == wallets[i].get_account().get_public_address_str(cryptonote::TESTNET)); - } + ++rounds_complete; + + // perform kex rounds until kex is complete + while (!intermediate_infos[0].empty()) + { + bool ready{false}; + wallets[0].multisig(&ready); + EXPECT_FALSE(ready); + + intermediate_infos = exchange_round(wallets, intermediate_infos); + + ++rounds_complete; } + + EXPECT_EQ(rounds_required, rounds_complete); + + check_results(intermediate_infos, wallets, M); +} + +TEST(multisig, make_1_2) +{ + std::vector<tools::wallet2> wallets(2); + make_wallets(wallets, 1); +} + +TEST(multisig, make_1_3) +{ + std::vector<tools::wallet2> wallets(3); + make_wallets(wallets, 1); } TEST(multisig, make_2_2) @@ -165,8 +255,88 @@ TEST(multisig, make_2_4) make_wallets(wallets, 2); } -TEST(multisig, make_2_5) +TEST(multisig, multisig_kex_msg) { - std::vector<tools::wallet2> wallets(5); - make_wallets(wallets, 2); + using namespace multisig; + + crypto::public_key pubkey1; + crypto::public_key pubkey2; + crypto::public_key pubkey3; + crypto::secret_key_to_public_key(rct::rct2sk(rct::skGen()), pubkey1); + crypto::secret_key_to_public_key(rct::rct2sk(rct::skGen()), pubkey2); + crypto::secret_key_to_public_key(rct::rct2sk(rct::skGen()), pubkey3); + + crypto::secret_key signing_skey = rct::rct2sk(rct::skGen()); + crypto::public_key signing_pubkey; + while(!crypto::secret_key_to_public_key(signing_skey, signing_pubkey)) + { + signing_skey = rct::rct2sk(rct::skGen()); + } + + crypto::secret_key ancillary_skey = rct::rct2sk(rct::skGen()); + while (ancillary_skey == crypto::null_skey) + ancillary_skey = rct::rct2sk(rct::skGen()); + + // misc. edge cases + EXPECT_NO_THROW((multisig_kex_msg{})); + EXPECT_ANY_THROW((multisig_kex_msg{multisig_kex_msg{}.get_msg()})); + EXPECT_ANY_THROW((multisig_kex_msg{"abc"})); + EXPECT_ANY_THROW((multisig_kex_msg{0, crypto::null_skey, std::vector<crypto::public_key>{}, crypto::null_skey})); + EXPECT_ANY_THROW((multisig_kex_msg{1, crypto::null_skey, std::vector<crypto::public_key>{}, crypto::null_skey})); + EXPECT_ANY_THROW((multisig_kex_msg{1, signing_skey, std::vector<crypto::public_key>{}, crypto::null_skey})); + EXPECT_ANY_THROW((multisig_kex_msg{1, crypto::null_skey, std::vector<crypto::public_key>{}, ancillary_skey})); + + // test that messages are both constructible and reversible + + // round 1 + EXPECT_NO_THROW((multisig_kex_msg{ + multisig_kex_msg{1, signing_skey, std::vector<crypto::public_key>{}, ancillary_skey}.get_msg() + })); + EXPECT_NO_THROW((multisig_kex_msg{ + multisig_kex_msg{1, signing_skey, std::vector<crypto::public_key>{pubkey1}, ancillary_skey}.get_msg() + })); + + // round 2 + EXPECT_NO_THROW((multisig_kex_msg{ + multisig_kex_msg{2, signing_skey, std::vector<crypto::public_key>{pubkey1}, ancillary_skey}.get_msg() + })); + EXPECT_NO_THROW((multisig_kex_msg{ + multisig_kex_msg{2, signing_skey, std::vector<crypto::public_key>{pubkey1}, crypto::null_skey}.get_msg() + })); + EXPECT_NO_THROW((multisig_kex_msg{ + multisig_kex_msg{2, signing_skey, std::vector<crypto::public_key>{pubkey1, pubkey2}, ancillary_skey}.get_msg() + })); + EXPECT_NO_THROW((multisig_kex_msg{ + multisig_kex_msg{2, signing_skey, std::vector<crypto::public_key>{pubkey1, pubkey2, pubkey3}, crypto::null_skey}.get_msg() + })); + + // test that keys can be recovered if stored in a message and the message's reverse + + // round 1 + multisig_kex_msg msg_rnd1{1, signing_skey, std::vector<crypto::public_key>{pubkey1}, ancillary_skey}; + multisig_kex_msg msg_rnd1_reverse{msg_rnd1.get_msg()}; + EXPECT_EQ(msg_rnd1.get_round(), 1); + EXPECT_EQ(msg_rnd1.get_round(), msg_rnd1_reverse.get_round()); + EXPECT_EQ(msg_rnd1.get_signing_pubkey(), signing_pubkey); + EXPECT_EQ(msg_rnd1.get_signing_pubkey(), msg_rnd1_reverse.get_signing_pubkey()); + EXPECT_EQ(msg_rnd1.get_msg_pubkeys().size(), 0); + EXPECT_EQ(msg_rnd1.get_msg_pubkeys().size(), msg_rnd1_reverse.get_msg_pubkeys().size()); + EXPECT_EQ(msg_rnd1.get_msg_privkey(), ancillary_skey); + EXPECT_EQ(msg_rnd1.get_msg_privkey(), msg_rnd1_reverse.get_msg_privkey()); + + // round 2 + multisig_kex_msg msg_rnd2{2, signing_skey, std::vector<crypto::public_key>{pubkey1, pubkey2}, ancillary_skey}; + multisig_kex_msg msg_rnd2_reverse{msg_rnd2.get_msg()}; + EXPECT_EQ(msg_rnd2.get_round(), 2); + EXPECT_EQ(msg_rnd2.get_round(), msg_rnd2_reverse.get_round()); + EXPECT_EQ(msg_rnd2.get_signing_pubkey(), signing_pubkey); + EXPECT_EQ(msg_rnd2.get_signing_pubkey(), msg_rnd2_reverse.get_signing_pubkey()); + ASSERT_EQ(msg_rnd2.get_msg_pubkeys().size(), 2); + ASSERT_EQ(msg_rnd2.get_msg_pubkeys().size(), msg_rnd2_reverse.get_msg_pubkeys().size()); + EXPECT_EQ(msg_rnd2.get_msg_pubkeys()[0], pubkey1); + EXPECT_EQ(msg_rnd2.get_msg_pubkeys()[1], pubkey2); + EXPECT_EQ(msg_rnd2.get_msg_pubkeys()[0], msg_rnd2_reverse.get_msg_pubkeys()[0]); + EXPECT_EQ(msg_rnd2.get_msg_pubkeys()[1], msg_rnd2_reverse.get_msg_pubkeys()[1]); + EXPECT_EQ(msg_rnd2.get_msg_privkey(), crypto::null_skey); + EXPECT_EQ(msg_rnd2.get_msg_privkey(), msg_rnd2_reverse.get_msg_privkey()); } diff --git a/utils/python-rpc/framework/wallet.py b/utils/python-rpc/framework/wallet.py index 02084620c..e531bf13d 100644 --- a/utils/python-rpc/framework/wallet.py +++ b/utils/python-rpc/framework/wallet.py @@ -512,14 +512,12 @@ class Wallet(object): } return self.rpc.send_json_rpc_request(make_multisig) - def finalize_multisig(self, multisig_info, password = ''): + def finalize_multisig(self): finalize_multisig = { 'method': 'finalize_multisig', 'params' : { - 'multisig_info': multisig_info, - 'password': password, }, - 'jsonrpc': '2.0', + 'jsonrpc': '2.0', 'id': '0' } return self.rpc.send_json_rpc_request(finalize_multisig) |