-
-
Notifications
You must be signed in to change notification settings - Fork 3.1k
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
base: master
Are you sure you want to change the base?
multisig: fixes #8114
Conversation
9246396
to
e98c6be
Compare
There was a problem hiding this 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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this 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) { |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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]) |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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' |
There was a problem hiding this comment.
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_assert
s would do the same thing without the specialized function for varint.
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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)?
There was a problem hiding this comment.
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...).
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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()
).
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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]); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I know.
There was a problem hiding this comment.
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.
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 this is fixed in master |
No description provided.