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: fixes #8114

Open
wants to merge 6 commits into
base: master
Choose a base branch
from

Conversation

perfect-daemon
Copy link

No description provided.

Copy link
Contributor

@UkoeHB UkoeHB left a comment

Choose a reason for hiding this comment

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

This is a solid patch for the known vulnerabilities (to be disclosed publicly after this patch is merged). Thank you for all the hard work.

p.s. yes, I checked the diff against my local branch

static_assert(std::is_integral<T>::value, "");
static_assert(std::is_unsigned<T>::value, "");
static_assert((sizeof(T) * 8 + 6) / 7 <= N, ""); //output array must be large enough to store any varint encoding of 't'
for (std::size_t i = 0; i < N && t; ++i) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

This will write nothing if the value passed is 0. I don't think this is the intent. A unit test checking it writes the same as the existing varint writer (or that the reader gets back the same values) would find this.

Copy link
Contributor

Choose a reason for hiding this comment

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

And why the new function at all? Static bounds checking? Couldn't it just do the static asserts, and then forward to the other existing function (so there isn't unnecessary code duplication)? This does have an extra runtime check for the array bounds, but if that triggers a truncation occurs that isn't handled.

Also, trivial bikeshedding, but the parameters are flipped from the other function, which bothers my OCD.

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.

My review isn't quite finished, but I have a question that I felt needed to go out sooner rather than later. It may require someone else from the MRL to respond (either here or directly me to IRC). The adaption of the relevant paper doesn't seem accurate, but the adaption is necessarily a bit more complex due to CLSAG (when compared to schnorr), so its possible that I misunderstood some key detail.

static_assert(std::is_integral<T>::value, "");
static_assert(std::is_unsigned<T>::value, "");
static_assert((sizeof(T) * 8 + 6) / 7 <= N, ""); //output array must be large enough to store any varint encoding of 't'
for (std::size_t i = 0; i < N && t; ++i) {
Copy link
Contributor

Choose a reason for hiding this comment

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

And why the new function at all? Static bounds checking? Couldn't it just do the static asserts, and then forward to the other existing function (so there isn't unnecessary code duplication)? This does have an extra runtime check for the array bounds, but if that triggers a truncation occurs that isn't handled.

Also, trivial bikeshedding, but the parameters are flipped from the other function, which bothers my OCD.

bool initialized;
bool reconstruction;
rct::keyV cached_w;
std::vector<cached_CLSAG_Gen_t> cached_CLSAG;
Copy link
Contributor

Choose a reason for hiding this comment

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

If this is the only place cached_CLSAG_Gen_t is referenced outside of the cpp, then the entire class declaration can be moved into the cpp and only a forward declare is needed in this header.

I don't think the headers can be reduced further, so its primarily just useful for keeping things out of the header and C++ parser.

sc_muladd(cached_w[i].bytes, mu_C.bytes, z.bytes, cached_w[i].bytes);
}
}
unsigned_tx.rct_signatures = rv;
Copy link
Contributor

Choose a reason for hiding this comment

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

Might as well std::move this.

for (const auto& e: sources[i].outputs)
offsets.emplace_back(e.first);
unsigned_tx.vin[i] = cryptonote::txin_to_key{
.amount = 0,
Copy link
Contributor

@vtnerd vtnerd Dec 15, 2021

Choose a reason for hiding this comment

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

Is there a check somewhere to ensure the inputs are ringct, as opposed to version 0? Its probably in wallet2 somewhere?

EDIT: I meant to delete this, because I noticed later there was a check for this further up the call stack.

tx_aux_secret_keys,
tx_aux_public_keys,
output_amount_secret_keys,
reinterpret_cast<crypto::public_key &>(output_public_keys[i])
Copy link
Contributor

Choose a reason for hiding this comment

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

This is an aliasing violation (but probably won't blow up). Still safer to run the conversion function.

}

// musig2-style combination factor 'b'
rct::key b = rct::hash_to_scalar(b_params);
Copy link
Contributor

Choose a reason for hiding this comment

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

const rct::key -> don't want this one to accidentally change.

b_params.insert(b_params.end(), s.begin(), s.begin() + l); //fake responses before 'l'
b_params.insert(b_params.end(), s.begin() + l + 1, s.end()); //fake responses after 'l'
b_params.emplace_back();
tools::encode_varint(l, b_params.back().bytes); //real signing index 'l'
Copy link
Contributor

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 this needs to be encoded as a varint at all - copying to a uint32_t with SWAP32LE (or uint64_t if concerned about portability), and two static_asserts would do the same thing without the specialized function for varint.

Copy link
Contributor

Choose a reason for hiding this comment

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

b_params is hashed by key cn_fast_hash(const keyV &keys), so all elements need to be rct::keys (not ints).

Copy link
Contributor

@vtnerd vtnerd Dec 18, 2021

Choose a reason for hiding this comment

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

It doesn't matter - the bit encoding just needs to be same. Do an endianess swap and memcpy into a rct::key.

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

Choose a reason for hiding this comment

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

At a glance, this doesn't seem correct. In the source paper (Musig2), the randomly point (R) is chosen cooperatively so that secret shares never leak to the other participants. It seems like something similar would have to be done here, but I haven't gone through the math details to verify any potential issue.

Has the adaption of musig2 been reviewed by anyone else (besides me)?

Copy link
Contributor

@UkoeHB UkoeHB Dec 17, 2021

Choose a reason for hiding this comment

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

This conditional is generating the fake CLSAG responses for all decoy ring members. Yes, I did a thorough review already (hence why I approved this PR...).

Copy link
Contributor

Choose a reason for hiding this comment

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

I will probably finish up my review tomorrow, the hardest part is done (going through the relevant papers to track how it mapped to code, and changes necessary for the custom CLSAG scheme). So far it appears to be conforming to the two relevant papers; my review is "downhill" from here.

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't have anything else to add, but there are some comments that the OP should comment on or update.

Copy link
Contributor

Choose a reason for hiding this comment

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

but there are some comments that the OP should comment on or update.

Care to elaborate?

Copy link
Contributor

Choose a reason for hiding this comment

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

  • An aliasing issue that can be trivial removed
  • The new varint function isn't needed (chop it for maintenance purposes)
  • A scalarmult can be easily removed
  • A std::move can be added easily
  • A simple const can be added

They are all simple changes, so it seemed easiest to make them now. Only the first two are useful before merge. The varint thing in particular would need to be chopped before a release, otherwise it becomes part of the protocol and must be maintained. The simple swap+memcpy is preferable to the maintenance of that specific function.

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh, I thought you meant code comments.

if (i == 0)
c_0 = c;
rct::addKeys3(c_params[c_params_L_offset], s[i], G_precomp.k, c, W_precomp[i].k);
rct::addKeys3(c_params[c_params_R_offset], s[i], H_precomp[i].k, c, wH_l_precomp.k);
Copy link
Contributor

@vtnerd vtnerd Dec 17, 2021

Choose a reason for hiding this comment

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

This is a follow-up to my other comment about the random s values being selected by a single participant. The participants providing randomized public keys affect the outcome of these iterative c hash values, but that output value was never controllable (that's the point of this hash function usage from the verifiers perspective). That should also mean that the randomized public keys are trivial in terms of the purpose with respect to the musig algorithm - it doesn't seem like they serve any useful purpose unless they affect the selection of the s values.

EDIT: Ignore this comment, and focus on the other one about the s value. There's another randomly generated value in here, and the current implementation uses each participants random for that.

Copy link
Contributor

@UkoeHB UkoeHB Dec 17, 2021

Choose a reason for hiding this comment

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

This code block is a bit hard to read due to the indexing. It's only using the decoy ring members' fake responses (not the responses from the multisig participants).

You can think of a ring signature as a Schnorr signature, where the real signature challenge is just a complicated hashing sequence of decoy members and fake responses. This code block is just computing the real signature challenge for multisig participants to respond to.

The line c = rct::hash_to_scalar(c_params); above this block is where the multisig participants' nonces are hashed (the real responses s[l] aren't defined in this function, they are defined way down in finalize_tx()).

Copy link
Contributor

@vtnerd vtnerd Dec 18, 2021

Choose a reason for hiding this comment

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

I had a reasonably good idea of what this was doing (as in, you just explained bits I already knew).

My question (below, this was mostly noise here as I was thinking outloud) was whether s was a set of nonce values, and since you did a review already, presumably your answer is that they are not nonce values.

The CLSAG paper doesn't list any values as nonce, only that alpha and a series of s values are selected randomly. At a glance this musig algorithm appears to be violating the paper, because randomness isn't provided/enforced for the s values (the initiator gets to select all of the values).

The "fake"/random s values are definitely just for symmetry (logical "OR") to conceal the real P in use - more obvious after some fresh eyes (was staring at this too long earlier). You can't recover anything from a re-use in those values because they are used in equations isolated from any secret keys

Copy link
Contributor

Choose a reason for hiding this comment

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

'Nonce' is the same as alpha. IIRC the newer papers all use nonce, which imo is a better semantic than alpha.

Copy link
Contributor

Choose a reason for hiding this comment

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

The "fake"/random s values are definitely just for symmetry (logical "OR") to conceal the real P in use - more obvious after some fresh eyes (was staring at this too long earlier). You can't recover anything from a re-use in those values because they are used in equations isolated from any secret keys

Right, the real s is the real response, which is computed from a sum of multisig partial responses. The fake s are just random numbers/decoy responses. Do you still think these fake responses need to be computed from material provided by all multisig participants? Afaict, the only advantage this offers is if the tx proposer has weak RNG, then adding RNG from other participants will improve it (i.e. prevent observers from figuring out the real spend's index). If the tx proposer is malicious (or any of the signers), then adding more entropy from other participants is meaningless (the malicious individual can just publish the real index).

Copy link
Contributor

Choose a reason for hiding this comment

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

Do you still think these fake responses need to be computed from material provided by all multisig participants?

I do not think the s values need to be included in the shared random computation.

Afaict, the only advantage this offers is if the tx proposer has weak RNG, then adding RNG from other participants will improve it (i.e. prevent observers from figuring out the real spend's index). If the tx proposer is malicious (or any of the signers), then adding more entropy from other participants is meaningless (the malicious individual can just publish the real index).

The "penalty" for making the scheme easier appears to be a standard mechanism for publicly leaking the real spend. Since any of the signers can do this anyway, as you stated, this doesn't appear to be an issue worth fixing.

);
}
for (std::size_t i = 0; i < num_sources - 1; ++i) {
rct::skGen(a[i]);
Copy link
Contributor

Choose a reason for hiding this comment

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

FWIW, I don't think the randomized values from all participants needs to influence this value, just the s values.

Copy link
Contributor

@UkoeHB UkoeHB Dec 17, 2021

Choose a reason for hiding this comment

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

This code block is only executed if (not reconstruction), which is only true for the initial tx proposer. All other participants reconstruct his tx.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, I know.

Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry - this comment was (somewhat) pointless, I was stating that I think all participants need to alter s somehow, but I didn't think these randomly generated a values needed that.

@rbrunner7
Copy link
Contributor

I did some functional tests on this, i.e. I took the code, compiled it and made multisig transactions in the normal way, to check whether something might be broken. I did not look at the code, and I did not try to check whether the security problems this code tries to solve are in fact solved. Still, I thought just checking whether anything still runs normally should be worthwhile.

Running the CLI wallet plus MMS on the branch I built new testnet multisig wallets for 2/2, 2/3 and 3/5 multisig, funded those and then made outgoing transactions. Especially the 3/5 case gave me the opportunity to do a row of transactions with varying signers.

I connected to a daemon running current master code.

Everything worked without any problem.

I could only test a single error condition: Does the wallet detect that a signer tries to sign a transaction a second time? Result: It does.

@jonathancross
Copy link
Contributor

test-ubuntu:

Testing monero: URI
Traceback (most recent call last):
  File "/home/runner/work/monero/monero/tests/functional_tests/uri.py", line 234, in <module>
    URITest().run_test()
  File "/home/runner/work/monero/monero/tests/functional_tests/uri.py", line 46, in run_test
    self.test_monero_uri()
  File "/home/runner/work/monero/monero/tests/functional_tests/uri.py", line 145, in test_monero_uri
    res = wallet.make_uri(address = address, recipient_name = utf8string[0], tx_description = utf8string[1], amount = 1000000000000, payment_id = '1' * 64)
  File "/home/runner/work/monero/monero/utils/python-rpc/framework/wallet.py", line 982, in make_uri
    return self.rpc.send_json_rpc_request(make_uri)
  File "/home/runner/work/monero/monero/utils/python-rpc/framework/rpc.py", line 80, in send_json_rpc_request
    return self.send_request("/json_rpc", inputs, 'result')
  File "/home/runner/work/monero/monero/utils/python-rpc/framework/rpc.py", line 72, in send_request
    assert 'error' not in res, res
AssertionError: {'error': {'code': -11, 'message': 'Cannot make URI from supplied parameters: Standalone payment id deprecated, use integrated address instead'}, 'id': '0', 'jsonrpc': '2.0'}
[TEST FAILED] uri

@selsta
Copy link
Collaborator

selsta commented Mar 8, 2022

@jonathancross this is fixed in master

@UkoeHB UkoeHB mentioned this pull request Apr 4, 2022
@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
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants