-
-
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
[DONOT MERGE] Ledger HW code review 1 #3095
Conversation
Code totally untested but sync with news#5 Maybe some people can consider it as a vaporcommit ;)
Reactivate debug code Make init from device works fix somme compilation trouble fix subaddr precomp rang to 1.5 fix compilation opt to -00 -g in release mode
Fix code to make balance retrieving ok Add check/debug code
TX receive and send works Subaddress works But, the condition to detect the real last call to transfer_selected_rct in create_transactions_2 is broken
When modifying a function to add a device parameter which changes the whole function, please do something like this at the top of the function:
This will avoid the existing code being all changed by pointless indents and make reviewing easier. |
Kudos for this admirable work! However, are we sure that having an extra argument on every function just for Ledger is the best way to go? I'll use From what I see, I definitely appreciate that the API is backwards compatible. However, if/when we add support for more HW devices, should we expect to augment these arguments once more? Should our methods be a huge if statement switching between devices? What happens if we want to remove support for Ledger at some point in the future? Plus, if many devices (in the future) are passed to, say, In any case the It looks to me like the HW wallet and local wallet code should be clearly separated. Disclaimer: I didn't have the chance to fully examine this PR and I am in no way familiar with the Monero codebase. |
Could be something like this, assuming this sample function: void secret_key_to_public_key(crypto::secret_key&, crypto::public_key&); You rename that to: void secret_key_to_public_key_default(crypto::secret_key&, crypto::public_key&); Which gives you few diffs in the crypto code, and none in the callers, then: void secret_key_to_public_key(device_t *device, crypto::secret_key &skey, crypto::public_key &pkey) Then add a device for default: typedef struct device_t *default_device = { &secret_key_to_public_key_default }; Though, on second thought, you get diffs in the callers anyway to insert the device parameter, so might as well keep the original functions name the right way, and make the trampoline functiuon secret_key_to_public_key_for or something. |
src/wallet/wallet2.cpp
Outdated
char prekey[200]; | ||
device.get_chacha8_prekey(prekey); | ||
crypto::generate_chacha_key(&prekey[0], sizeof(prekey), key, true); | ||
memset(prekey, 0, sizeof(prekey)); |
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 compiler will optimize out this dead memset, leaving that key data on the stack.
Use scrubbed_arr<char, 200> prekey
instead.
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.
OK, noted.
src/ledger/device.cpp
Outdated
this->buffer_send[offset] = 0x00; | ||
offset += 1; | ||
|
||
this->buffer_send[4] = offset-5; |
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 writes into offset 4 again, instead of 6, over-writing what was stored on line 523. Is that intentional?
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 It is. Code is today not optimized to help debug and update.
Command in buffer_send is
00(u8) INS(u8) P1(u8) P1(u8) LEN(8) PAYLOAD(LEN bytes)
src/ledger/device.cpp
Outdated
this->buffer_send[offset] = 0x00; | ||
offset += 1; | ||
|
||
this->buffer_send[4] = offset-5; |
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 writes into offset 4 again, instead of 6, over-writing what was stored on line 487. Is that intentional?
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.
See above. All command use the same template. It will be factorized.
Thanks for all your comments
gtklocker, I aggree with you, but the separation between HW wallet and local wallet is a little bit hard today. Indeed today the Monero code is a mix of objects, global variables and stateless namspace. I try to find the best way to delegate computation to device with modifying the fewest API. (And had to change some part of my first approach for subaddress) moneromooo-monero, I like your approach but with slightly modification. Instead of defining struct with function pointers, I think it would be better to use inheritance. class Device {
virtual void secret_key_to_public_key(crypto::secret_key &skey, crypto::public_key &pkey) = 0;
/* other delegated function */
}
class DefaultDevice : Device {
void secret_key_to_public_key(crypto::secret_key &skey, crypto::public_key &pkey);
/* other concrete delegated function */
}
class LedgerHWDevice : Device {
void secret_key_to_public_key(crypto::secret_key &skey, crypto::public_key &pkey);
/* other concrete delegated function */
} Now you still have a problem to this kind of code: if (device) {
hashes.push_back(key());
} else {
hashes.push_back(hash2rct(h));
} Which is not a crypto delegation but local variablas handling. Then you also have to deal with the following problem with (for example) the sc_add function. Finally there is some tricky case such as in MLSAG_GEN: if (device) {
Hi = hashToPoint(pk[index][i]);
device.mlsag_prepare(Hi, xx[i], alpha[i] , aG[i] , aHP[i] , rv.II[i]);
toHash[3 * i + 1] = pk[index][i];
toHash[3 * i + 2] = aG[i];
toHash[3 * i + 3] = aHP[i];
precomp(Ip[i].k, rv.II[i]);
/*debug code removes for words clarity*/
} else {
toHash[3 * i + 1] = pk[index][i];
/*multisig code removed for words clarity */
{
Hi = hashToPoint(pk[index][i]);
skpkGen(alpha[i], aG[i]); //need to save alphas for later..
aHP[i] = scalarmultKey(Hi, alpha[i]);
toHash[3 * i + 2] = aG[i];
toHash[3 * i + 3] = aHP[i];
rv.II[i] = scalarmultKey(Hi, xx[i]);
}
precomp(Ip[i].k, rv.II[i]);
} A final remark, I do not says my mods are the best way but as the HW app code is open, (almost ;-) ) right specified, try to protect all part (key, secret TX key, derivation, key amount...) it can be implemented by all other future HW wallet. |
The advantage is using a struct is that it stays callable from C. In that example code above, prepare_mlsag is "skpkGen + a couplescalarmultKey", right ? All the other code seems the same. Then I think it'd be fine having a function which does this (maybe get it a more descriptive function, luigi might have a good name for it). |
But do we need to be callable at C level? for the tricky case, do you suggest to apply the same patten to all such cases? I mean replace code: void foo(..., Device: &device) {
COMMON_CODE;
if(device) {
code1;
} else {
code2
}
COMMON_CODE;
} by void sub_foo(..., Device: &device) {
if(device) {
code1;
} else {
code2
}
}
void foo(..., Device: &device) {
COMMON_CODE;
sub_foo(....,device);
COMMON_CODE;
} |
The current crypto functions are already called from C. As for your example, I guess it depends on the details, but my preliminary opinion is that if we end up adding more devices later, the "if" version will grow a lot, while the indirection one will not, so the indirection one seems best. I suppose if it's in a small performance sensitive path, that calculation might change. |
Ok for C calling. Is there any plan to clean code code and made it full C++ ? So does it mean that I should perform the following in tricky part: void foo(..., Device: &device) {
COMMON_CODE;
device->sub_foo(....);
COMMON_CODE;
} |
@moneromooo-monero any example with C calling? @cslashm Your inheritance proposal is what I had in mind. In this sense, just like you said, crypto_ops should become an interface and there should be crypto ops implemented for a local wallet and for Ledger. Then you select the correct crypto_ops once in your code, and you're good to go. This helps keep the code separate and less error prone (and makes extensibility a piece of cake). I realise that this is a big structural change that should probably not be the focus on this PR, and while I'm pretty excited to get this code merged ASAP I don't think we should just be hasty and not do this the way we think is right. Again, the tradeoff is obvious. Just my 2 cents. I'm not sure I understand your final remark, could you be a please restate it? |
About cleaning up: depends what there is to cleanup, you'll have to give examples: |
@gtklocker My last remark was about such code if (device) {
Hi = hashToPoint(pk[index][i]);
device.mlsag_prepare(Hi, xx[i], alpha[i] , aG[i] , aHP[i] , rv.II[i]);
toHash[3 * i + 1] = pk[index][i];
toHash[3 * i + 2] = aG[i];
toHash[3 * i + 3] = aHP[i];
precomp(Ip[i].k, rv.II[i]);
/*debug code removes for words clarity*/
} else {
toHash[3 * i + 1] = pk[index][i];
/*multisig code removed for words clarity */
{
Hi = hashToPoint(pk[index][i]);
skpkGen(alpha[i], aG[i]); //need to save alphas for later..
aHP[i] = scalarmultKey(Hi, alpha[i]);
toHash[3 * i + 2] = aG[i];
toHash[3 * i + 3] = aHP[i];
rv.II[i] = scalarmultKey(Hi, xx[i]);
}
precomp(Ip[i].k, rv.II[i]);
} that, if I correctly understood the @moneromooo-monero words, should be refactor as: device->well-named_func(hashToPoint,xx[i], alpha[i] , aG[i] , aHP[i] , rv.II[i], p[i].k) or at least as well-named_func(device, hashToPoint,xx[i], alpha[i] , aG[i] , aHP[i] , rv.II[i], p[i].k) |
@moneromooo-monero when I speak all in C++ I think about (for example) transforming the stateless crypto-ops in a non stateless object that could delegate itself some computation. Cleanup is just other things like that, wrong word maybe. But we are speaking about change the arch of the code which is a complicated subject and this PR is maybe not the right place. And I don't know if is the right timing :) The real question today is what about this PR and where to put the cursor between "as soon as possible" and "what about future HW" . I'm afraid the answer is not on my side :) I'm definitively open to any suggestion/request to modify my code to make this happen, and happen with compliance with future and other HW ;) |
You could have a stateful object, with the state being the device. But its member functions would need to call the C functions for the "no device" case. and those functions will have to keep in C since the're called by other C stuff. And frankly I enjoy the fact they compile in a split second rather than a split minute :) |
I'm already at around 20 min to compile, one more or less does not make the diff 🤣 More seriously, I will have a look (next week) to see what could be the device interface to avoid the |
thanks for doing this @cslashm -- was just checking to see if you've had any time to work on this lately |
BriefUpon discussion on IRC#monero-dev with moneromoo and some here remarks, The two main ideas are to minimize original code modification, especially Important NoteThe previous PR worked. I did full send/receive on testnet. This update has not been debugged yet. Debug is sometime (always?) a pain and Model integrationSimple caseFor almost original function that need to be delegated a new one is defined. e.g., for the original function: namespace foo {
retType func(type1 arg1, type2 arg2) {
//Some code computing retval;
return retval
}
} it is duplicated using parametric polymorphism namespace foo {
retType func(type1 arg1, type2 arg2) {
//Some code computing retval;
return retval
retType func(type1 arg1, type2 arg2, hw::Device &device) {
retType retval;
device.fun(arg1, arg2, retval);
return retval
}
} The hw::Device class is basically a error class defined as: namespace hw {
class Device {
namespace {
//device funcion not supported
inline bool dfns() {throw std::runtime_error("device function not supported");return false;}
}
virtual bool func(type1 &arg1, type2 &arg2, retType &retval) { return dfns();}
}
} For now two real class are defined: The former is basically a proxy forwarder : namespace hw {
namespace core {
class DefaultDevice :: public hw::Device{
bool func(type1 &arg1, type2 &arg2, retType &retval) {
retval = foo::func1(arg1,arg2)
return true;
}
} The Thus the original function is untouched and still be available for normal usage. More over the common Device abstraction allow future Complex caseFor some complex case of the followig form: namespace foo {
retType func(type1 arg1, type2 arg2) {
//long complex code
//code to delegate
//more long complex code
}
} the function is not duplicated, but only the code to delegate is moved. namespace foo {
retType func(type1 arg1, type2 arg2, hw::Device &device) {
//long complex code
device->func(arg1,arg2)
//more long complex code
}
} and device is so: namespace hw {
namespace core {
class DefaultDevice :: public hw::Device{
bool func(type1 &arg1, type2 &arg2) {
//code to delegate
}
} A typical example is MLSAG_Gen in rctSigs.cpp. |
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 agree with the new approach, thanks.
At a quick glance, half the changes are spurious changes (adding an unnecessary namespace::, whitespace, swappines lines without need, etc), makes review harder, and these will need removing (not necessarily now, but hopefully before actual review :)).
Thank you moneromooo. This is is a very great great news! Yes I will make a full review with no space removing/adding, new line...and other things |
hi @cslashm, was wondering if you had an update on this for github or reddit as it's been about 2 weeks. thanks |
Hi @rndbr, Was busy last weeks on other ledger stuff. Restart the integration since yesterday. |
hi @cslashm, just pinging you again on behalf of the monero community. :) I figure I do it every 2 weeks or so, so it's not too annoying, but the community has a better sense where things currently are. thank you for your work on this. |
Replaced by #3303 |
+duplicate |
Goals
The goal of this PR is to propose code modifications to integrate the ledger HW into
monero-wallet-cli. This code mod is not ready to merge (see below) but it is
time to ask to the dev team and tech guys a first review for opinion.
Approach
The basic approach it to delegate all sensitive data (master key, secret ephemeral key, key derivation, ....) and related operations to the device. As the device as low
memory the device do not keep itself the values (except for view/spend keys) but once
computed there are encrypted (AES) and return back to monero-wallet-cli. When
they need to be manipulated by the device, they are decrypted on receive.
The code is modified at tow main levels:
. crypto operations, mainly crypto.cpp and cryptonote_format_utils.cpp
. signature level, mainly rctSigs.cpp and cryptonote_tx_utils.cpp
The pdf (https://github.com/LedgerHQ/blue-app-monero/blob/master/doc/developer/blue-app-monero.pdf) gives more details (but is a little be obsolete)
The application device is located at https://github.com/LedgerHQ/blue-app-monero.
As the transaction is constructed several times, the device signature operates in tow
modes: "FAKE" and "REAL". In fake mode, the device signs the the transaction with
random key without asking anything to the use. In real mode, the device use the
right keys and ask the user to valid (on the HW screen) fees, destinations, amounts.
For now this is handled in wallet2.cpp:7015.
NEEDHELP: some time the real mode switch and resign fails and the correct signature is not generated. There is certainly a better way to do that :)
What is supported
Creation and Restore
A new command line option has been added: --generate-from-device
*Spend/receive money
Subaddress
Seems to work.
More to come
I will look at Export/Import Keyimage
I hope to add multi-sig.
LightWallet need be more studied
more....
DO NOT MERGE
There is some clean up to do. NEEDHELP to fix. Review to merge. Commands to
lock when device is used (unsupported commands ....). TX mutex to manage. Security to add. ....