URI: 
       tmake key derivation reasonable - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit 17457327eff3966d0170eee5fcb0348471b9f822
   DIR parent 585905409567245c062a8ead744890fe81f579a4
  HTML Author: SomberNight <somber.night@protonmail.com>
       Date:   Fri,  5 Oct 2018 15:37:47 +0200
       
       make key derivation reasonable
       
       no more hardcoded secrets, no more key-reuse
       
       Diffstat:
         M electrum/commands.py                |       2 +-
         M electrum/gui/qt/channels_list.py    |       2 +-
         M electrum/lnbase.py                  |      80 ++++++++++++-------------------
         M electrum/lnutil.py                  |      19 ++++++++++++++++++-
         M electrum/lnworker.py                |      38 +++++++++++++++++++++++--------
       
       5 files changed, 78 insertions(+), 63 deletions(-)
       ---
   DIR diff --git a/electrum/commands.py b/electrum/commands.py
       t@@ -785,7 +785,7 @@ class Commands:
        
            @command('wn')
            def nodeid(self):
       -        return bh2u(self.wallet.lnworker.pubkey)
       +        return bh2u(self.wallet.lnworker.node_keypair.pubkey)
        
            @command('wn')
            def listchannels(self):
   DIR diff --git a/electrum/gui/qt/channels_list.py b/electrum/gui/qt/channels_list.py
       t@@ -82,7 +82,7 @@ class ChannelsList(MyTreeWidget):
                vbox = QVBoxLayout(d)
                h = QGridLayout()
                local_nodeid = QLineEdit()
       -        local_nodeid.setText(bh2u(lnworker.pubkey))
       +        local_nodeid.setText(bh2u(lnworker.node_keypair.pubkey))
                local_nodeid.setReadOnly(True)
                local_nodeid.setCursorPosition(0)
                remote_nodeid = QLineEdit()
   DIR diff --git a/electrum/lnbase.py b/electrum/lnbase.py
       t@@ -4,38 +4,34 @@
          Derived from https://gist.github.com/AdamISZ/046d05c156aaeb56cc897f85eecb3eb8
        """
        
       -from collections import namedtuple, defaultdict, OrderedDict, defaultdict
       -from .lnutil import Outpoint, ChannelConfig, LocalState, RemoteState, Keypair, OnlyPubkeyKeypair, ChannelConstraints, RevocationStore
       -from .lnutil import sign_and_get_sig_string, funding_output_script, get_ecdh, get_per_commitment_secret_from_seed
       -from .lnutil import secret_to_pubkey, LNPeerAddr, PaymentFailure
       -from .lnutil import LOCAL, REMOTE, HTLCOwner
       -from .bitcoin import COIN
       -
       -from ecdsa.util import sigdecode_der, sigencode_string_canonize, sigdecode_string
       -import queue
       +from collections import OrderedDict, defaultdict
        import json
        import asyncio
       -from concurrent.futures import FIRST_COMPLETED
        import os
        import time
       -import binascii
        import hashlib
        import hmac
       +from functools import partial
       +
        import cryptography.hazmat.primitives.ciphers.aead as AEAD
        import aiorpcx
       -from functools import partial
        
        from . import bitcoin
        from . import ecc
       -from . import crypto
       +from .ecc import sig_string_from_r_and_s, get_r_and_s_from_sig_string
        from .crypto import sha256
        from . import constants
       -from . import transaction
        from .util import PrintError, bh2u, print_error, bfh, aiosafe
       -from .transaction import opcodes, Transaction, TxOutput
       +from .transaction import Transaction, TxOutput
        from .lnonion import new_onion_packet, OnionHopsDataSingle, OnionPerHop, decode_onion_error, ONION_FAILURE_CODE_MAP
        from .lnaddr import lndecode
        from .lnhtlc import HTLCStateMachine, RevokeAndAck
       +from .lnutil import (Outpoint, ChannelConfig, LocalState,
       +                     RemoteState, OnlyPubkeyKeypair, ChannelConstraints, RevocationStore,
       +                     funding_output_script, get_ecdh, get_per_commitment_secret_from_seed,
       +                     secret_to_pubkey, LNPeerAddr, PaymentFailure,
       +                     LOCAL, REMOTE, HTLCOwner, generate_keypair, LnKeyFamily)
       +
        
        def channel_id_from_funding_tx(funding_txid, funding_index):
            funding_txid_bytes = bytes.fromhex(funding_txid)[::-1]
       t@@ -277,7 +273,7 @@ class Peer(PrintError):
                self.pubkey = pubkey
                self.peer_addr = LNPeerAddr(host, port, pubkey)
                self.lnworker = lnworker
       -        self.privkey = lnworker.privkey
       +        self.privkey = lnworker.node_keypair.privkey
                self.network = lnworker.network
                self.lnwatcher = lnworker.network.lnwatcher
                self.channel_db = lnworker.network.channel_db
       t@@ -484,50 +480,37 @@ class Peer(PrintError):
                    chan.set_state('DISCONNECTED')
                    self.network.trigger_callback('channel', chan)
        
       -    def make_local_config(self, funding_sat, push_msat, initiator: HTLCOwner, password):
       -        # see lnd/keychain/derivation.go
       -        keyfamilymultisig = 0
       -        keyfamilyrevocationbase = 1
       -        keyfamilyhtlcbase = 2
       -        keyfamilypaymentbase = 3
       -        keyfamilydelaybase = 4
       -        keyfamilyrevocationroot = 5
       -        keyfamilynodekey = 6 # TODO currently unused
       +    def make_local_config(self, funding_sat, push_msat, initiator: HTLCOwner):
                # key derivation
       -        keypair_generator = lambda family, i: Keypair(*self.lnworker.wallet.keystore.get_keypair([family, i], password))
       +        channel_counter = self.lnworker.get_and_inc_counter_for_channel_keys()
       +        keypair_generator = lambda family: generate_keypair(self.lnworker.ln_keystore, family, channel_counter)
                if initiator == LOCAL:
                    initial_msat = funding_sat * 1000 - push_msat
                else:
                    initial_msat = push_msat
                local_config=ChannelConfig(
       -            payment_basepoint=keypair_generator(keyfamilypaymentbase, 0),
       -            multisig_key=keypair_generator(keyfamilymultisig, 0),
       -            htlc_basepoint=keypair_generator(keyfamilyhtlcbase, 0),
       -            delayed_basepoint=keypair_generator(keyfamilydelaybase, 0),
       -            revocation_basepoint=keypair_generator(keyfamilyrevocationbase, 0),
       +            payment_basepoint=keypair_generator(LnKeyFamily.PAYMENT_BASE),
       +            multisig_key=keypair_generator(LnKeyFamily.MULTISIG),
       +            htlc_basepoint=keypair_generator(LnKeyFamily.HTLC_BASE),
       +            delayed_basepoint=keypair_generator(LnKeyFamily.DELAY_BASE),
       +            revocation_basepoint=keypair_generator(LnKeyFamily.REVOCATION_BASE),
                    to_self_delay=143,
                    dust_limit_sat=546,
                    max_htlc_value_in_flight_msat=0xffffffffffffffff,
                    max_accepted_htlcs=5,
                    initial_msat=initial_msat,
                )
       -        return local_config
       -
       -    def make_per_commitment_secret_seed(self):
       -        # TODO
       -        return 0x1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100.to_bytes(32, 'big')
       +        per_commitment_secret_seed = keypair_generator(LnKeyFamily.REVOCATION_ROOT).privkey
       +        return local_config, per_commitment_secret_seed
        
            @aiosafe
            async def channel_establishment_flow(self, password, funding_sat, push_msat, temp_channel_id, sweep_address):
                await self.initialized
       -        local_config = self.make_local_config(funding_sat, push_msat, LOCAL, password)
       +        local_config, per_commitment_secret_seed = self.make_local_config(funding_sat, push_msat, LOCAL)
                # amounts
                local_feerate = self.current_feerate_per_kw()
       -        # TODO derive this?
       -        per_commitment_secret_seed = self.make_per_commitment_secret_seed()
       -        per_commitment_secret_index = RevocationStore.START_INDEX
                # for the first commitment transaction
       -        per_commitment_secret_first = get_per_commitment_secret_from_seed(per_commitment_secret_seed, per_commitment_secret_index)
       +        per_commitment_secret_first = get_per_commitment_secret_from_seed(per_commitment_secret_seed, RevocationStore.START_INDEX)
                per_commitment_point_first = secret_to_pubkey(int.from_bytes(per_commitment_secret_first, 'big'))
                msg = gen_msg(
                    "open_channel",
       t@@ -656,13 +639,10 @@ class Peer(PrintError):
                    initial_msat=funding_sat * 1000 - push_msat,
                )
                temp_chan_id = payload['temporary_channel_id']
       -        password = None # TODO
       -        local_config = self.make_local_config(funding_sat * 1000, push_msat, REMOTE, password)
       +        local_config, per_commitment_secret_seed = self.make_local_config(funding_sat * 1000, push_msat, REMOTE)
        
       -        per_commitment_secret_seed = self.make_per_commitment_secret_seed()
       -        per_commitment_secret_index = RevocationStore.START_INDEX
                # for the first commitment transaction
       -        per_commitment_secret_first = get_per_commitment_secret_from_seed(per_commitment_secret_seed, per_commitment_secret_index)
       +        per_commitment_secret_first = get_per_commitment_secret_from_seed(per_commitment_secret_seed, RevocationStore.START_INDEX)
                per_commitment_point_first = secret_to_pubkey(int.from_bytes(per_commitment_secret_first, 'big'))
        
                min_depth = 3
       t@@ -884,7 +864,7 @@ class Peer(PrintError):
                chan.set_state("OPEN")
                self.network.trigger_callback('channel', chan)
                # add channel to database
       -        node_ids = [self.pubkey, self.lnworker.pubkey]
       +        node_ids = [self.pubkey, self.lnworker.node_keypair.pubkey]
                bitcoin_keys = [chan.local_config.multisig_key.pubkey, chan.remote_config.multisig_key.pubkey]
                sorted_node_ids = list(sorted(node_ids))
                if sorted_node_ids != node_ids:
       t@@ -931,8 +911,8 @@ class Peer(PrintError):
                )
                to_hash = chan_ann[256+2:]
                h = bitcoin.Hash(to_hash)
       -        bitcoin_signature = ecc.ECPrivkey(chan.local_config.multisig_key.privkey).sign(h, sigencode_string_canonize, sigdecode_string)
       -        node_signature = ecc.ECPrivkey(self.privkey).sign(h, sigencode_string_canonize, sigdecode_string)
       +        bitcoin_signature = ecc.ECPrivkey(chan.local_config.multisig_key.privkey).sign(h, sig_string_from_r_and_s, get_r_and_s_from_sig_string)
       +        node_signature = ecc.ECPrivkey(self.privkey).sign(h, sig_string_from_r_and_s, get_r_and_s_from_sig_string)
                self.send_message(gen_msg("announcement_signatures",
                    channel_id=chan.channel_id,
                    short_channel_id=chan.short_channel_id,
       t@@ -991,7 +971,7 @@ class Peer(PrintError):
                assert chan.get_state() == "OPEN", chan.get_state()
                assert amount_msat > 0, "amount_msat is not greater zero"
                height = self.network.get_local_height()
       -        route = self.network.path_finder.create_route_from_path(path, self.lnworker.pubkey)
       +        route = self.network.path_finder.create_route_from_path(path, self.lnworker.node_keypair.pubkey)
                hops_data = []
                sum_of_deltas = sum(route_edge.channel_policy.cltv_expiry_delta for route_edge in route[1:])
                total_fee = 0
   DIR diff --git a/electrum/lnutil.py b/electrum/lnutil.py
       t@@ -1,4 +1,4 @@
       -from enum import IntFlag
       +from enum import IntFlag, IntEnum
        import json
        from collections import namedtuple
        from typing import NamedTuple, List, Tuple
       t@@ -14,6 +14,7 @@ from .bitcoin import push_script
        from . import segwit_addr
        from .i18n import _
        from .lnaddr import lndecode
       +from .keystore import BIP32_KeyStore
        
        HTLC_TIMEOUT_WEIGHT = 663
        HTLC_SUCCESS_WEIGHT = 703
       t@@ -526,3 +527,19 @@ def extract_nodeid(connect_contents: str) -> Tuple[bytes, str]:
            except:
                raise ConnStringFormatError(_('Invalid node ID, must be 33 bytes and hexadecimal'))
            return node_id, rest
       +
       +
       +# key derivation
       +# see lnd/keychain/derivation.go
       +class LnKeyFamily(IntEnum):
       +    MULTISIG = 0
       +    REVOCATION_BASE = 1
       +    HTLC_BASE = 2
       +    PAYMENT_BASE = 3
       +    DELAY_BASE = 4
       +    REVOCATION_ROOT = 5
       +    NODE_KEY = 6
       +
       +
       +def generate_keypair(ln_keystore: BIP32_KeyStore, key_family: LnKeyFamily, index: int) -> Keypair:
       +    return Keypair(*ln_keystore.get_keypair([key_family, 0, index], None))
   DIR diff --git a/electrum/lnworker.py b/electrum/lnworker.py
       t@@ -12,6 +12,9 @@ import dns.resolver
        import dns.exception
        
        from . import constants
       +from . import keystore
       +from . import bitcoin
       +from .keystore import BIP32_KeyStore
        from .bitcoin import sha256, COIN
        from .util import bh2u, bfh, PrintError, InvoiceError, resolve_dns_srv, is_ip_address
        from .lnbase import Peer, privkey_to_pubkey, aiosafe
       t@@ -20,7 +23,8 @@ from .ecc import der_sig_from_sig_string
        from .lnhtlc import HTLCStateMachine
        from .lnutil import (Outpoint, calc_short_channel_id, LNPeerAddr,
                             get_compressed_pubkey_from_bech32, extract_nodeid,
       -                     PaymentFailure, split_host_port, ConnStringFormatError)
       +                     PaymentFailure, split_host_port, ConnStringFormatError,
       +                     generate_keypair, LnKeyFamily)
        from electrum.lnaddr import lndecode
        from .i18n import _
        
       t@@ -41,13 +45,8 @@ class LNWorker(PrintError):
                self.network = network
                self.channel_db = self.network.channel_db
                self.lock = threading.RLock()
       -        pk = wallet.storage.get('lightning_privkey')
       -        if pk is None:
       -            pk = bh2u(os.urandom(32))
       -            wallet.storage.put('lightning_privkey', pk)
       -            wallet.storage.write()
       -        self.privkey = bfh(pk)
       -        self.pubkey = privkey_to_pubkey(self.privkey)
       +        self.ln_keystore = self._read_ln_keystore()
       +        self.node_keypair = generate_keypair(self.ln_keystore, LnKeyFamily.NODE_KEY, 0)
                self.config = network.config
                self.peers = {}  # pubkey -> Peer
                self.channels = {x.channel_id: x for x in map(HTLCStateMachine, wallet.storage.get("channels", []))}
       t@@ -62,6 +61,25 @@ class LNWorker(PrintError):
                self.network.register_callback(self.on_network_update, ['network_updated', 'verified', 'fee'])  # thread safe
                asyncio.run_coroutine_threadsafe(self.network.main_taskgroup.spawn(self.main_loop()), self.network.asyncio_loop)
        
       +    def _read_ln_keystore(self) -> BIP32_KeyStore:
       +        xprv = self.wallet.storage.get('lightning_privkey')
       +        if xprv is None:
       +            # TODO derive this deterministically from wallet.keystore at keystore generation time
       +            # probably along a hardened path ( lnd-equivalent would be m/1017'/coinType'/ )
       +            seed = os.urandom(32)
       +            xprv, xpub = bitcoin.bip32_root(seed, xtype='standard')
       +            self.wallet.storage.put('lightning_privkey', xprv)
       +            self.wallet.storage.write()
       +        return keystore.from_xprv(xprv)
       +
       +    def get_and_inc_counter_for_channel_keys(self):
       +        with self.lock:
       +            ctr = self.wallet.storage.get('lightning_channel_key_der_ctr', -1)
       +            ctr += 1
       +            self.wallet.storage.put('lightning_channel_key_der_ctr', ctr)
       +            self.wallet.storage.write()
       +            return ctr
       +
            def _add_peers_from_config(self):
                peer_list = self.config.get('lightning_peers', [])
                for host, port, pubkey in peer_list:
       t@@ -217,7 +235,7 @@ class LNWorker(PrintError):
                if amount_sat is None:
                    raise InvoiceError(_("Missing amount"))
                amount_msat = int(amount_sat * 1000)
       -        path = self.network.path_finder.find_path_for_payment(self.pubkey, invoice_pubkey, amount_msat)
       +        path = self.network.path_finder.find_path_for_payment(self.node_keypair.pubkey, invoice_pubkey, amount_msat)
                if path is None:
                    raise PaymentFailure(_("No path found"))
                node_id, short_channel_id = path[0]
       t@@ -236,7 +254,7 @@ class LNWorker(PrintError):
                payment_preimage = os.urandom(32)
                RHASH = sha256(payment_preimage)
                amount_btc = amount_sat/Decimal(COIN) if amount_sat else None
       -        pay_req = lnencode(LnAddr(RHASH, amount_btc, tags=[('d', message)]), self.privkey)
       +        pay_req = lnencode(LnAddr(RHASH, amount_btc, tags=[('d', message)]), self.node_keypair.privkey)
                self.invoices[bh2u(payment_preimage)] = pay_req
                self.wallet.storage.put('lightning_invoices', self.invoices)
                self.wallet.storage.write()