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

How does the actual ed25519 derivation work #61

Closed
jacogr opened this issue Jan 26, 2021 · 8 comments
Closed

How does the actual ed25519 derivation work #61

jacogr opened this issue Jan 26, 2021 · 8 comments
Assignees

Comments

@jacogr
Copy link

jacogr commented Jan 26, 2021

So I have an issue logged from a user around supporting Ledger seeds as an import. Generally this is not such a great idea since you really want to keep the secret safe and locked up.

Having said that, addressing it will also be self-serving for me - i.e. I just had a little episode in which I had to replace my Ledger and while waiting for a replacement (6+ weeks) didn't have access to that specific stash at all. So at least in my recent experience there is an argument to be made that keys should be derivable in a couple of places. (Not available/exposed, just the tools should be there)

So this brings me to how it is done on the Ledger. Did a bit of research and it seems the Tezos (and actual Trezor HW as well) implementation uses the bip32-ed25519. Tested that via others and libraries and it works quite well, but doesn't seem to be compatible with what is outputted on the Ledger.

For earlier posts elsewhere I picked up that (and it may be misleading) that the Ledger does the normal derivation, then hashes the secrete and feeds it into the ed25519 curve when not using the bip25519 approach.

So effectively, from my understanding, the steps are -

  • mnemonic -> seed
  • derive path e.g. m/44'/354'/0'/0'/0' (assuming account 0 & index 0)
  • do something ...
  • create a ed25519 secret/public from this as an input seed
@jacogr
Copy link
Author

jacogr commented Jan 28, 2021

This is what I have, which doesn't seem to match for my mnemonics -

// standard BTC libs
const bp39 = require('bip39');

// also use by Stellar, Tezos (or adapted from)
const { derivePath, getPublicKey } = require('ed25519-hd-key');

// inputs
const accountIndex = 0;
const addressIndex = 0;
const mnemonic = 'abandon ...';

// using 1b2 here, the one I'm looking at is on Kusama
const path = `m/${0x2c}'/${0x01b2}'/${accountIndex}'/0'/${addressIndex}'`;

// mnemonic -> bip39 seed
const seed = bip39.mnemonicToSeedSync(mnemonic);

// ed25519 + bip39
const { key } = derivePath(path, seed);

// log
console.log('private', key.toString('hex'))
console.log('public', getPublicKey(key, false).toString('hex'));

Also attempted the approach of bip39+bip32->ed (which I believe is incorrect and certainly not bip32+ed25519 nor anywhere close to passing public testcases for that type). Just adding it for the sake of completeness for attempts -

const bp39 = require('bip39');
const bip32 = require('bip32');
const EdDSA = require('elliptic').eddsa;

const seed = bip39.mnemonicToSeedSync(mnemonic);
const root = bip32.fromSeed(seed);
const child = root.derivePath(path);

// using tweetnacl here has the same outputs at the end
const ec = new EdDSA('ed25519');
const eckeypair = ec.keyFromSecret(child.privateKey);

console.log("private:",  new Buffer.from(eckeypair.getSecret()).toString('hex'));
console.log("public:", new Buffer.from(eckeypair.getPublic()).toString('hex'));

@jacogr
Copy link
Author

jacogr commented Jan 29, 2021

Also tried the (most-probably also wrong approach) of ed25519_scalar = sc_reduce32(SHA3(derivation)) method (as originally used n XMR, i.e. non-slip-0010), but yes also not a match -

const bp39 = require('bip39');
const bip32 = require('bip32');
const EdDSA = require('elliptic').eddsa;
const keccak256 = require('keccak256');

//sc_reduce32 algorithm function with any hex string
function sc_reduce32 (key) {
  const l = 7237005577332262213973186563042994240857116359379907606001950938285454250989n

  // reverse (little endian), run mod l, pad from the start with '0', reverse (again) and get the bytes
  return Buffer.from(
    (BigInt('0x' + key.reverse().toString('hex')) % l)
      .toString(16)
      .padStart(64, '0'),
    'hex'
  ).reverse();
}

const seed = bip39.mnemonicToSeedSync(mnemonic);
const root = bip32.fromSeed(seed);
const child = root.derivePath(path);
const privateHash = keccak256(child.privateKey);
const privateSpendKey = sc_reduce32(privateHash);

const ec = new EdDSA('ed25519');
const eckeypair = ec.keyFromSecret(privateSpendKey);

console.log("private:",  new Buffer.from(eckeypair.getSecret()).toString('hex'));
console.log("public:", new Buffer.from(eckeypair.getPublic()).toString('hex'));

Obviously I could have gotten something small wrong in all 3 attempts here. But alas, none the wiser as the the approach followed.

@gorgos
Copy link

gorgos commented Jan 30, 2021

@jacogr Did you have a look at #43? Might be helpful.

Also paritytech/substrate#7824 seems to be relevant here.

Edit: In particular

Ledger's "NORMAL" derivation approach for ed25519 is to use ed25519+bip32 (cardano like) not SLIP0010

and https://github.com/alepop/ed25519-hd-key is apparently using SLIP-0010.

@jacogr
Copy link
Author

jacogr commented Jan 30, 2021

Thank you, missed that issue in #43 . Will adapt and report back :)

@jacogr
Copy link
Author

jacogr commented Jan 30, 2021

@gorgos Thanks a million for the pointer. Ok, have the extraction working, will cleanup and push an example as to "how-to". (tested it and it indeed yields the correct results)

EDIT: Sample here - https://github.com/jacogr/sample-ledger-ed25519 (not 100% clean, but working)

@jleni
Copy link
Member

jleni commented Jan 30, 2021

We are working on a library to do this in a safe and consistent way.

Nevertheless, we VERY STRONGLY advise against using this approach and entering your mnemonic in web or deskop apps.

As described here:
polkadot-js/apps#4487 (comment)

We are very committed to this community and do not worry if you sent funds to your own (but incorrect addresses). These funds are safe and the recovery tool will be available ASAP.

@jacogr
Copy link
Author

jacogr commented Jan 30, 2021

I do agree with keeping the mnemonic safe and on the Ledger only. At the same time, it is good that there is a way to extract the seeds so it is not a complete black box at all.

The above POC is also runnable via the command-line so you don't need to add it in the web,, so if I ever I do lose the Ledger (again) there is a command-line util available to recover the raw secret.

Looking forward to a ZondaX-lib.

Closing this, since the question has been resolved as to "how".

@jacogr jacogr closed this as completed Jan 30, 2021
@Kwaskoff
Copy link

@gorgosОгромное спасибо за указатель. Хорошо, пусть извлечение работает, мы очистим и отправим пример «как сделать». (проверил, и он действительно дает правильные результаты)

РЕДАКТИРОВАТЬ: образец здесь - https://github.com/jacogr/sample-ledger-ed25519 (не на 100% чистый, но работает)

thank you, kind man! I've been trying to solve this problem for more than a day! Thanks to your tool, I solved it!

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

No branches or pull requests

5 participants