What changed, and why it matters
This Monero wallet update adds safety checks when a wallet imports 'multisig_info' data shared between co-signers in a multi-signature wallet. It now rejects files where the declared signer doesn't match the file header, or where the number of partial key images or signing nonces is wrong. These checks prevent malformed or mismatched multisig data from being processed further. The change includes new unit tests that confirm the rejections work.
Treat as a defensive hardening patch for the multisig import path. Reviewers should confirm the expected-count formulas match all supported M-of-N configurations and that the new exceptions do not break legitimate exports. Consider whether additional malformed cases (e.g., duplicate signers, empty blobs, invalid magic/version) need similar checks.
Security signals we found
Input validation added to deserialization path
Mismatch between header signer and body signer now rejected
Unexpected counts of partial key images and signing nonces now rejected
Existing main-subgroup checks for L/R values remain in place
New unit tests exercise malformed multisig_info rejection
Evidence from the diff
The patch hardens wallet2::import_multisig() by validating the shape and signer consistency of deserialized multisig_info entries before they are used. It computes expected counts from the wallet’s multisig configuration (expected_n_partial_key_images from get_multisig_keys().size(), expected_n_lr from combinations_count(…)*kAlphaComponents) and enforces: (1) each entry’s m_signer equals the header signer, (2) m_partial_key_images.size() matches the expected count, (3) m_LR.size() matches the expected count. Existing subgroup checks for L/R are retained. A unit test constructs a valid 2-of-2 import blob and verifies acceptance, plus three malformed variants are rejected.
Changed components
src/wallet/wallet2.cpptests/unit_tests/multisig.cppwallet2::import_multisigInspect captured patch +120 / −0
### src/wallet/wallet2.cpp
@@ -14937,6 +14937,11 @@ size_t wallet2::import_multisig(std::vector<cryptonote::blobdata> blobs, bool re
// parse and validate locally so failures preserve any pending rescan state
std::vector<std::vector<tools::wallet2::multisig_info>> info;
std::unordered_set<crypto::public_key> seen;
+
+ const size_t expected_n_partial_key_images = get_account().get_multisig_keys().size();
+ const size_t expected_n_lr = tools::combinations_count(m_multisig_signers.size() - m_multisig_threshold, m_multisig_signers.size() - 1)
+ * multisig::signing::kAlphaComponents;
+
for (cryptonote::blobdata &data: blobs)
{
const size_t magiclen = strlen(MULTISIG_EXPORT_FILE_MAGIC);
@@ -14983,6 +14988,13 @@ size_t wallet2::import_multisig(std::vector<cryptonote::blobdata> blobs, bool re
for (const auto &e: i)
{
+ CHECK_AND_ASSERT_THROW_MES(e.m_signer == signer, "Multisig info body signer does not match header signer");
+
+ CHECK_AND_ASSERT_THROW_MES(e.m_partial_key_images.size() == expected_n_partial_key_images,
+ "Multisig info has an unexpected number of partial key images");
+ CHECK_AND_ASSERT_THROW_MES(e.m_LR.size() == expected_n_lr,
+ "Multisig info has an unexpected number of signing nonces");
+
for (const auto &lr: e.m_LR)
{
CHECK_AND_ASSERT_THROW_MES(rct::isInMainSubgroup(lr.m_L), "Multisig value is not in the main subgroup");
### tests/unit_tests/multisig.cpp
@@ -27,14 +27,19 @@
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include "crypto/crypto.h"
+#include "multisig/multisig.h"
#include "multisig/multisig_account.h"
#include "multisig/multisig_kex_msg.h"
#include "ringct/rctOps.h"
+#include "serialization/binary_utils.h"
#include "wallet/wallet2.h"
+#include "wallet/wallet2_basic/wallet2_boost_serialization.h"
+#include "wallet/wallet2_basic/wallet2_serialization.h"
#include "gtest/gtest.h"
#include <cstdint>
+#include <sstream>
static const struct
{
@@ -497,3 +502,106 @@ TEST(multisig, multisig_kex_msg)
EXPECT_EQ(msg_rnd2.get_msg_privkey(), crypto::null_skey);
EXPECT_EQ(msg_rnd2.get_msg_privkey(), msg_rnd2_reverse.get_msg_privkey());
}
+
+TEST(multisig, import_multisig_validation)
+{
+ using namespace multisig;
+
+ // a 2-of-2 wallet pair is enough: no funded outputs are needed for this test, since the checks under test
+ // run on the deserialized contents of an import blob before anything touches m_transfers
+ const std::uint32_t M = 2, N = 2;
+ std::vector<tools::wallet2> wallets(N);
+
+ std::vector<std::string> initial_infos(wallets.size());
+ for (size_t i = 0; i < wallets.size(); ++i)
+ {
+ make_wallet(i, wallets[i]);
+ wallets[i].decrypt_keys("");
+ initial_infos[i] = wallets[i].get_multisig_first_kex_msg();
+ wallets[i].encrypt_keys("");
+ }
+
+ std::vector<std::string> intermediate_infos(wallets.size());
+ for (size_t i = 0; i < wallets.size(); ++i)
+ intermediate_infos[i] = wallets[i].make_multisig("", initial_infos, M);
+
+ multisig_account_status ms_status{wallets[0].get_multisig_status()};
+ while (!ms_status.is_ready)
+ {
+ intermediate_infos = exchange_round(wallets, intermediate_infos);
+ ms_status = wallets[0].get_multisig_status();
+ }
+
+ wallets[0].decrypt_keys("");
+ wallets[1].decrypt_keys("");
+
+ // 2-of-2 => each signer holds exactly 1 multisig private key, and each output needs exactly
+ // combinations_count(N-M, N-1) * kAlphaComponents = combinations_count(0, 1) * 2 = 2 LR nonce pairs
+ ASSERT_EQ(1u, wallets[1].get_account().get_multisig_keys().size());
+ const crypto::public_key other_signer = wallets[1].get_multisig_signer_public_key();
+
+ // a fake output pubkey: any valid curve point works here, since it's only used to derive LR pairs/key
+ // images with the right shape (main-subgroup curve points); it doesn't need to correspond to a real output
+ crypto::public_key fake_out_key;
+ ASSERT_TRUE(crypto::secret_key_to_public_key(rct::rct2sk(rct::skGen()), fake_out_key));
+
+ crypto::key_image valid_pki;
+ ASSERT_TRUE(generate_multisig_key_image(wallets[1].get_account().get_keys(), 0, fake_out_key, valid_pki));
+
+ tools::wallet2::multisig_info::LR valid_lr;
+ {
+ const crypto::secret_key k = rct::rct2sk(rct::skGen());
+ crypto::public_key L, R;
+ generate_multisig_LR(fake_out_key, k, L, R);
+ valid_lr.m_L = rct::pk2rct(L);
+ valid_lr.m_R = rct::pk2rct(R);
+ }
+
+ tools::wallet2::multisig_info valid_entry;
+ valid_entry.m_signer = other_signer;
+ valid_entry.m_LR = {valid_lr, valid_lr};
+ valid_entry.m_partial_key_images = {valid_pki};
+
+ // builds a raw multisig-info import blob (same wire format as wallet2::export_multisig()) for a single
+ // fake output, so wallet2::import_multisig()'s per-output validation can be exercised directly
+ const auto build_blob =
+ [&](const crypto::public_key &header_signer, const tools::wallet2::multisig_info &entry) -> cryptonote::blobdata
+ {
+ std::vector<tools::wallet2::multisig_info> info{entry};
+ std::stringstream oss;
+ binary_archive<true> ar(oss);
+ EXPECT_TRUE(::serialization::serialize(ar, info));
+
+ const cryptonote::account_public_address &addr = wallets[0].get_account().get_keys().m_account_address;
+ std::string header;
+ header.append((const char *)&addr.m_spend_public_key, sizeof(crypto::public_key));
+ header.append((const char *)&addr.m_view_public_key, sizeof(crypto::public_key));
+ header.append((const char *)&header_signer, sizeof(crypto::public_key));
+
+ return std::string("Monero multisig export\001") + wallets[0].encrypt_with_view_secret_key(header + oss.str());
+ };
+
+ // well-formed info is accepted
+ EXPECT_NO_THROW(wallets[0].import_multisig({build_blob(other_signer, valid_entry)}, false));
+
+ // wrong number of LR nonce pairs is rejected
+ {
+ tools::wallet2::multisig_info bad_entry = valid_entry;
+ bad_entry.m_LR = {valid_lr}; // should be 2
+ EXPECT_ANY_THROW(wallets[0].import_multisig({build_blob(other_signer, bad_entry)}, false));
+ }
+
+ // wrong number of partial key images is rejected
+ {
+ tools::wallet2::multisig_info bad_entry = valid_entry;
+ bad_entry.m_partial_key_images.clear(); // should be 1
+ EXPECT_ANY_THROW(wallets[0].import_multisig({build_blob(other_signer, bad_entry)}, false));
+ }
+
+ // a body-declared signer that doesn't match the header signer it was filed under is rejected
+ {
+ tools::wallet2::multisig_info bad_entry = valid_entry;
+ bad_entry.m_signer = wallets[0].get_multisig_signer_public_key();
+ EXPECT_ANY_THROW(wallets[0].import_multisig({build_blob(other_signer, bad_entry)}, false));
+ }
+}Why this scored 59/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.