electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

commit 17457327eff3966d0170eee5fcb0348471b9f822
parent 585905409567245c062a8ead744890fe81f579a4
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:
Melectrum/commands.py | 2+-
Melectrum/gui/qt/channels_list.py | 2+-
Melectrum/lnbase.py | 80++++++++++++++++++++++++++++++-------------------------------------------------
Melectrum/lnutil.py | 19++++++++++++++++++-
Melectrum/lnworker.py | 38++++++++++++++++++++++++++++----------
5 files changed, 78 insertions(+), 63 deletions(-)

diff --git a/electrum/commands.py b/electrum/commands.py @@ -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): diff --git a/electrum/gui/qt/channels_list.py b/electrum/gui/qt/channels_list.py @@ -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() diff --git a/electrum/lnbase.py b/electrum/lnbase.py @@ -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] @@ -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 @@ -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", @@ -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 @@ -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: @@ -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, @@ -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 diff --git a/electrum/lnutil.py b/electrum/lnutil.py @@ -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 @@ -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 @@ -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)) diff --git a/electrum/lnworker.py b/electrum/lnworker.py @@ -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 @@ -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 _ @@ -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", []))} @@ -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: @@ -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] @@ -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()