Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multisig: signature fixes #8149

Merged
merged 1 commit into from
Jul 13, 2022
Merged

Conversation

UkoeHB
Copy link
Contributor

@UkoeHB UkoeHB commented Jan 18, 2022

Follow up to PR #8114. I lost contact with the original author two weeks ago.

This PR has the same commits as #8114, with [EDIT: additional commits to address reviewers, fix rebases, and general code quality].

Summary (added post-merge)

  • Fix the Drijvers attack that allows a malicious multisig participant to forge signatures (e.g. construct arbitrary transactions spending the multisig wallet's funds) given the right conditions. Sources: Drijvers attack, ROS solvers, MuSig2, multisig proofs, FROST. Note that MRL-0009 prevented the Drijvers attack with a commit-and-reveal pattern that Monero's existing multisig implementation does not use. The binonce signing approach from MuSig2/etc. is more efficient than commit-and-reveal, hence it was implemented in this PR.
  • Fix a nonce-reuse bug that could allow a malicious multisig participant to obtain the private keys of other participants. Source: hackerone report.
  • Fix an unvalidated-signing issue, where a malicious multisig participant could obtain a partial signature from other participants on arbitrary messages (e.g. get them to sign transactions whose contents they are unaware of). Source: hackerone report.
    • An entire new tx builder was implemented to resolve this (src/multisig/signing_protocol.h/.cpp, largly by copying and slightly modifying code from construct_tx_with_tx_key()), because it is necessary for all multisig participants to fully reconstruct the message they are asked to sign.
    • NOTE: It is the responsibility of multisig users to fully validate all tx details before signing a multisig transaction (e.g. before calling the RPC method on_sign_multisig()). This PR only ensures that when sign_multisig_tx() is called, the tx signed actually corresponds to the tx details passed in.

Possible Future Work

Here are items of work/research related to multisig that are out-of-scope for this PR.

  • Multisig core tests should use set_events(), but it's blocked by this PR.
  • Multisig should use aggregation-style signing instead of round-robin-style signing (this does not have an impact on the UX of 2-of-3, but is meaningful when M < N - 1).
  • It should be possible to make a multisig tx proposal without knowing other signers' nonces in advance (i.e. propose the set of inputs (sources) and outputs (destinations) even with stale/inadequate export_multisig() information). The lack of this ability isn't a big UX problem for escrowed 2-of-3 markets, but is sub-optimal for generic use of multisig.
  • Currently, if a multisig signing attempt fails due to bad data, it is not practical (or possible, depending on the failure), to identify which participant caused the problem. It would be nice if the multisig protocol could identify and flag bad participants, so they can be excluded from future signing attempts (etc.).
  • If a multisig tx proposal is created by an honest participant using weak RNG (a different threat vector than a malicious participant, who can trivially compromise a multisig group's privacy by publicizing everything he knows), this may reduce the privacy of the multisig group (e.g. if outputs aren't randomized well, the change output might be identifiable). It may be worth investigating ways to mitigate that, such as allowing all multisig participants to contribute entropy to tx proposals.

Copy link
Contributor

@vtnerd vtnerd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eh sorry, found one last thing to comment about with the commitment portion of musig. I don't think it matters, but I was curious about your thoughts (and I will probably think about it some more in the interim).

src/multisig/signing_protocol.cpp Outdated Show resolved Hide resolved
src/multisig/signing_protocol.cpp Outdated Show resolved Hide resolved
src/multisig/signing_protocol.cpp Outdated Show resolved Hide resolved
src/multisig/signing_protocol.cpp Outdated Show resolved Hide resolved
src/multisig/signing_protocol.cpp Outdated Show resolved Hide resolved
Copy link
Contributor

@vtnerd vtnerd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The remaining comments are trivial.

@domol
Copy link

domol commented Feb 1, 2022

Just wanted to note here that I tested these changes on ubuntu:20.04:

  • Generating new 2/3 multisig wallet,
  • Receiving funds,
  • Creating/signing transaction,
  • Restoring from seed,
  • Creating transaction with restored bit.

Probably not all possible cases, but still.

Thanks for fixing!

@arnuschky
Copy link
Contributor

I care about this getting merged. Is there anything I can do to help?

@UkoeHB
Copy link
Contributor Author

UkoeHB commented Feb 1, 2022

Thank you @domol!

@arnuschky I'm not sure... there are some non-public review processes going on that I need to respect. It's mostly a waiting game I guess.

Note that this PR is not super useful without #7877.

@tmoravec
Copy link
Contributor

tmoravec commented Feb 3, 2022

Note that this PR is not super useful without #7877.

Would you mind elaborating a bit, please? #7877 is a major change that updates the flows, changes the APIs, and so on. It requires heavy integration work. This one is closer to a bugfix that can more or less be merged.

Is it possible to merge this one to get the security improvement, and leave #7877 for later, possibly several months later for example?

Thank you!

@UkoeHB
Copy link
Contributor Author

UkoeHB commented Feb 3, 2022

@tmoravec Without #7877, you cannot be confident that an M-of-N multisig wallet is actually an M-of-N multisig wallet. With key cancellation or an address hostage attack during the setup ceremony, it could be 1-of-N or (1+(M-1))-of-N, where the attacker always must participate in signing.

I'm pretty sure the only API/flow change with #7877 is changing finalize_multisig() to a no-op. All the other changes are opaque (unless you have some custom code to deserialize/examine multisig kex messages).

Is it possible to merge this one to get the security improvement, and leave #7877 for later, possibly several months later for example?

Unfortunately it's pretty much out of my hands. Review doesn't happen instantaneously.

@tmoravec
Copy link
Contributor

tmoravec commented Feb 3, 2022

@UkoeHB Fantastic, thanks for the explanation!

Unfortunately it's pretty much out of my hands. Review doesn't happen instantaneously.

I meant merge to downstream libraries and products, not to monero core master. I would never dare to push anyone :) .

@UkoeHB
Copy link
Contributor Author

UkoeHB commented Feb 3, 2022

@tmoravec Yes, you are free to use any PR as you wish (assuming it isn't marked WIP or has an active discussion), with the caveat that an unmerged PR is probably a not-fully-reviewed PR (anything you do with it is 'experimental').

@tmoravec
Copy link
Contributor

tmoravec commented Feb 9, 2022

If it helps, I've backported the changes to v0.17.3.0 and performed a basic smoke test:

  • Create a wallet.
  • Turn three wallets into 2/3 multisig.
  • Receive and send transactions.
  • Try to send a transaction not signed by enough participants (expected to fail)

It works :) . Thanks for the fixes!

@UkoeHB
Copy link
Contributor Author

UkoeHB commented Feb 14, 2022

Note (todo): It will take a bit of work to rebase this PR onto #8061.

@jonathancross
Copy link
Contributor

Seems this needs a rebase?

@UkoeHB UkoeHB force-pushed the multisig_signfixes branch from 6bf408b to 4b2c4fc Compare March 4, 2022 18:52
@UkoeHB
Copy link
Contributor Author

UkoeHB commented Mar 4, 2022

rebased (to clean up merge conflicts)

@UkoeHB
Copy link
Contributor Author

UkoeHB commented Apr 9, 2022

rebased (onto BP+)

@UkoeHB UkoeHB force-pushed the multisig_signfixes branch 2 times, most recently from 1f0cee2 to d7875ac Compare April 10, 2022 23:04
Copy link

@kayabaNerve kayabaNerve left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Read through the code, except the tests due to the other people saying this does work according to external expectations. The cryptography seems solid and well implemented to me, yet Ukoe definitely is the more informed here. I had one comment arguably more about ringct and then I think one comment is miswritten with what would be a critical security flaw, by my reading of it (yet not one that seems to be present in the code). Once that's clarified, I'd be happy to put my approval behind this :)

I can read/write C++, but I haven't contributed to Monero before so it's not my strongest suit. My only other concern would be if the TX handling code had some level of duplication which could be reduced (input sorting, yet it's a pretty trivial few lines...).

src/multisig/signing_protocol.cpp Outdated Show resolved Hide resolved
src/wallet/wallet2.cpp Outdated Show resolved Hide resolved
@UkoeHB UkoeHB force-pushed the multisig_signfixes branch from cf1956d to 810b1ed Compare April 20, 2022 20:48
@UkoeHB
Copy link
Contributor Author

UkoeHB commented Apr 20, 2022

rebased (for view tags)

@UkoeHB UkoeHB force-pushed the multisig_signfixes branch from cdfef46 to b9a3625 Compare April 20, 2022 21:53
@UkoeHB UkoeHB force-pushed the multisig_signfixes branch from 792a9b6 to b1f649a Compare May 11, 2022 14:30
src/wallet/wallet2.h Outdated Show resolved Hide resolved
src/wallet/wallet2.cpp Outdated Show resolved Hide resolved
@UkoeHB UkoeHB force-pushed the multisig_signfixes branch from 4e9885a to 9ce4b4a Compare May 16, 2022 07:42
@UkoeHB
Copy link
Contributor Author

UkoeHB commented May 16, 2022

Squashed

@UkoeHB UkoeHB force-pushed the multisig_signfixes branch from 9ce4b4a to 4e9885a Compare May 16, 2022 17:10
@UkoeHB
Copy link
Contributor Author

UkoeHB commented May 16, 2022

Undid squash (some miscommunication sorry).

@UkoeHB UkoeHB force-pushed the multisig_signfixes branch from 4e9885a to a717af3 Compare May 17, 2022 21:37
@UkoeHB
Copy link
Contributor Author

UkoeHB commented May 17, 2022

Another rebase

@UkoeHB UkoeHB force-pushed the multisig_signfixes branch from a717af3 to 97904eb Compare May 23, 2022 17:23
@UkoeHB
Copy link
Contributor Author

UkoeHB commented May 23, 2022

I rolled back the deterministic sender-receiver secrets, which were a bit of scope creep on this PR (they will be proposed in a follow-up PR after this one).

EDIT: here is a branch with a commit for deterministic sender-receiver secrets on top of this PR's branch.

@tmoravec
Copy link
Contributor

tmoravec commented Jun 1, 2022

@UkoeHB FYI I've just tested this branch and I'm getting an error when creating a transaction (2/3 multisig wallet, stagenet):

transfer 78NSXsfhd5U9YvkothaB7iPcgehvWUAfmT8w6JH3yd69QCDi6MLeLrcNbGp3Ajf8QMLdj3Y6hHPvYVhPzi8Vc61AFEL1JQk 0.01
Wallet password:
Error: internal error: error: multisig::signing::tx_builder_ringct_t::init
Error: There was an error, which could mean the node may be trying to get you to retry creating a transaction, and zero in on which outputs you own. Or it could be a bona fide error. It may be prudent to disconnect from this node, and not try to send a transaction immediately. Alternatively, connect to another node so the original node cannot correlate information.

@UkoeHB
Copy link
Contributor Author

UkoeHB commented Jun 1, 2022

@tmoravec is BP+ active on your branch (i.e. is your wallet2 trying to make BP+ txs?)? This PR only works with BP+. Edit: the current testnet is running BP+, so that might be a better place to test it right now.

@rbrunner7
Copy link
Contributor

FWIW - multisig is a moving target for quite a while already:

Over the last few days I tested this as current master plus all commits in this PR, on testnet. I built 2/2, 2/3 and 3/5 wallets, funded, synced and then built out-transactions to spend some funds.

Everything worked flawlessly.

@rbrunner7
Copy link
Contributor

I tested the latest update today, building transactions for stagenet in 2/2 and 4/4 wallets.

Again no problems detected.

I also did a 2/2 testnet transaction with this code, making sure those did not break. Worked as well.

@tmoravec
Copy link
Contributor

tmoravec commented Jun 7, 2022

Thanks @UkoeHB for the pre-BP+ patch! I've tested it and it works with my 2/3 wallet tests. Creating and funding wallets, sending transactions, etc. on stagenet.

Copy link

@kayabaNerve kayabaNerve left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

None of these are required, yet there's a few questions/nice to haves.

I'm not the most familiar with wallet2 as a whole, leading to C++ omissions in my review such as not checking for proper usage of memwipe everywhere, yet the cryptography in this change set should be secure and implement a secure multisig signing process for Monero as a whole. User tests confirm it meets the expected UX, and nonces are explicitly removed from memory on first usage (as distinct from a previously noted work with Monero multisig).

EDIT: I have to clarify multisig transaction creation is insecure without the burning bug fix, which was moved to another PR, which I missed when doing my review here. Accordingly, this review is just on the "signing" process.

const rct::key& D,
const unsigned int l,
const rct::keyV& s,
const std::size_t num_alpha_components

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see why num_alpha_components is practically variable. While Musig2 does leave it ambiguous, consensus seems to have been practically reached around 2. Leaving this variable potentially poses issues as it's half static, half variable.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree it's a little overkill, although from a design standpoint it's nice to have a centralized config parameter instead of a hard-coded value.

c_params_L_offset = c_params.size();
b_params_L_offset = b_params.size();
c_params.resize(c_params.size() + 1); //this is where L will be inserted later
b_params.resize(b_params.size() + num_alpha_components); //multisig aggregate public nonces for L will be inserted here later

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here, the variable num_alpha_components enables a transcript conflict. A ring of 11 with 3 alpha components has the exact same length as a ring of 12 with 2 alpha components. While, practically, this isn't an issue due to the message hash being based off ring size and therefore shifting if this happens, there's a lack of a clear distinction here with this naive transcript (which I don't mean to criticize on its own as this is consistent with the rest of Monero). Adding

b_params.emplace_back();
encode_int_to_key_le(num_alpha_components, b_params.back());

would be appreciated to make num_alpha_components properly sized in this transcript though.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, since this binds to the aggregate nonces, mutability is trivial. While this follows MuSig2, FROST not only defined a binomial nonce scheme, yet also transcripted them as participant, D, E, removing any chance at mutability. Not an issue here, yet I do believe that'd be an improvement.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Musig2 is simpler and there is no concrete reason to change the design at this point.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has been resolved with the most recent edits :)

alpha_combined = rct::zero();
for (std::size_t i = 0; i < num_alpha_components; ++i) {
rct::addKeys(L_l, L_l, rct::scalarmultKey(total_alpha_G[i], b_i));
rct::addKeys(R_l, R_l, rct::scalarmultKey(total_alpha_H[i], b_i));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using sc_add and doing the scalarmultKey at the end outside of the loop would be more efficient.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically more efficient yes, but negligible in practice. If this were verification code or if it were egregious then I'd change it. I generally prefer legibility over marginal efficiency gains in cases like this.

FIELD(sigs)
FIELD(ignore)
FIELD(used_L)
FIELD(signing_keys)
FIELD(msout)
if (version < 1)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't believe we ever check a version 1 struct is used. I assume these fields will be left uninitialized and therefore naturally error without issue, yet may be good to enforce this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the standard pattern for updating serialization versions in this codebase (when the old version is assumed to be deprecated).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although it does make me wonder if this should return false instead, since old stuff won't work anyway.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved

const rct::keyV& total_alpha_H,
const rct::keyV& alpha,
rct::key& alpha_combined,
rct::key& c_0,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CLSAG code calls this c1. While I honestly kind of prefer c0, and it turns out that's the term from the paper, the discrepancy isn't preferred. We also can't change it within the CLSAG code anymore, which is unfortunate.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree this isn't ideal... however, c_0 is set here:

for (std::size_t i = (l + 1) % n; i != l; i = (i + 1) % n) {
    if (i == 0)
      c_0 = c;
   ...
}

So I think c_0 makes more sense than doubling down on c1.

I added a comment about the notation discrepancy.

bool view_tag_required(const int bp_version)
{
// view tags were introduced at the same time as BP+, so they are needed after BP+ (v4 and later)
if (bp_version <= 3)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd hope we could utilize a proper enum here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, however the version is defined as an integer upstream so we are kind of stuck in this case.

s.resize(ring_size);
for (std::size_t j = 0; j < ring_size; ++j) {
if (j != l)
s[j] = rct::skGen(); //make fake responses

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be converted into a RNG in a manner similar to tx_secret_key to reduce bandwidth and consolidate code between creator/reconstructors.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree you can do this, and it does reduce bandwidth. I'm not sure it is useful in practice, because the payload size would only shrink by a few hundred bytes at most (in the typical case). The goal here is to get something that works correctly, rather than engineer it into the ideal state.

bool tx_builder_ringct_t::init(
const cryptonote::account_keys& account_keys,
const std::vector<std::uint8_t>& extra,
const std::uint64_t unlock_time,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While I understand why this is technically valid, the unlock time has minimal benefit and is largely considered extremely detrimental. Including it here in multisig does not seem beneficial.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The calling code (wallet2 tx builder) expects timelocks to work as intended by the user (who has to manually specify the unlock time). Deprecating timelocks is a bigger project that's out of scope here.

auto ignore_set = ignore_sets.empty() ? std::unordered_set<crypto::public_key>() : ignore_sets.front();
src.multisig_kLRki = get_multisig_composite_kLRki(idx, ignore_set, used_L, used_L);
}
//TODO: multisig_kLRki as used in tx_source_entry is just a key image shuttle into the multisig tx builder, need to simplify

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left over TODO?

sig.ignore = ignore_sets[i];
sig.signing_keys = signing_keys; //the local signer signed with ALL of their multisig key shares, record their pubkeys for reference by other signers
}
if (m_multisig_threshold <= 1) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do I dare ask what 0 is?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't exist in the current implementation. IIRC I added the <= as defense in depth against bugs.

@UkoeHB
Copy link
Contributor Author

UkoeHB commented Jun 27, 2022

Inference AG recently completed an audit of this PR funded by RINO. Overall, the actionable points in the report are minor.

Here are my comments on the points raised (bolded points were addressed in the latest commit):

  • modulo bias: no change for this PR, in seraphis will use hash to 64 bytes then mod group order for hash to scalar
  • domain separating lists: include num_alphas, ring size in b_params (adding these is fairly redundant, but nothing wrong with redundancy)
  • exception safety: it is generally assumed that most of the monero library is not exception safe
  • unchecked D: cannot reconstruct this because the random pseudo-output commitment blinding factors are discarded
    • the amount commitments to zero are signed by the tx initiator in the 'initial partial response' (this is why it's possible to discard the pseudo output commitment masks when initializing)
  • unchecked s: do nothing (this class of problems might be solved by all participants contributing to a common rng, but it isn't clear what practical benefit that would have; it would, however, make the protocol more complicated e.g. either by requiring a different messaging protocol [not round-robin] or by increasing the amount of data that needs to be tracked alongside pre-prepated nonces [i.e. pre-prepared entropy shares])
  • fee overflow: use boost::multiprecision::uint128_t to remove all doubts (also increases robustness for unit tests, if needed)
  • integer overflow in signer combinations: not a practical issue, because signer groups of that size are unusable
  • hash to curve validity: the asserts are for debugging, as the function is supposed to be no-fail
  • redundant precomps: unimportant microoptimization for tx construction (optimizations like these only matter for validation code)
  • threshold of 1 is allowed: there is no practical reason to prevent 1-of-N

Copy link

@kayabaNerve kayabaNerve left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While I have nitpicks, my noted flaws (except the burning bug which was moved out of the PR) appear fixed, and Inference AG's appears competently responded to as well.

@UkoeHB UkoeHB force-pushed the multisig_signfixes branch from 040b093 to c7b2944 Compare June 30, 2022 20:25
@UkoeHB
Copy link
Contributor Author

UkoeHB commented Jun 30, 2022

Commits squashed

@luigi1111 luigi1111 merged commit 6fed8c2 into monero-project:master Jul 13, 2022
@UkoeHB UkoeHB deleted the multisig_signfixes branch July 13, 2022 18:00
@UkoeHB UkoeHB mentioned this pull request Jul 13, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

10 participants