Browse Source

make key derivation reasonable

no more hardcoded secrets, no more key-reuse
dependabot/pip/contrib/deterministic-build/ecdsa-0.13.3
SomberNight 6 years ago
committed by ThomasV
parent
commit
17457327ef
  1. 2
      electrum/commands.py
  2. 2
      electrum/gui/qt/channels_list.py
  3. 80
      electrum/lnbase.py
  4. 19
      electrum/lnutil.py
  5. 38
      electrum/lnworker.py

2
electrum/commands.py

@ -785,7 +785,7 @@ class Commands:
@command('wn') @command('wn')
def nodeid(self): def nodeid(self):
return bh2u(self.wallet.lnworker.pubkey) return bh2u(self.wallet.lnworker.node_keypair.pubkey)
@command('wn') @command('wn')
def listchannels(self): def listchannels(self):

2
electrum/gui/qt/channels_list.py

@ -82,7 +82,7 @@ class ChannelsList(MyTreeWidget):
vbox = QVBoxLayout(d) vbox = QVBoxLayout(d)
h = QGridLayout() h = QGridLayout()
local_nodeid = QLineEdit() local_nodeid = QLineEdit()
local_nodeid.setText(bh2u(lnworker.pubkey)) local_nodeid.setText(bh2u(lnworker.node_keypair.pubkey))
local_nodeid.setReadOnly(True) local_nodeid.setReadOnly(True)
local_nodeid.setCursorPosition(0) local_nodeid.setCursorPosition(0)
remote_nodeid = QLineEdit() remote_nodeid = QLineEdit()

80
electrum/lnbase.py

@ -4,38 +4,34 @@
Derived from https://gist.github.com/AdamISZ/046d05c156aaeb56cc897f85eecb3eb8 Derived from https://gist.github.com/AdamISZ/046d05c156aaeb56cc897f85eecb3eb8
""" """
from collections import namedtuple, defaultdict, OrderedDict, defaultdict from collections import 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
import json import json
import asyncio import asyncio
from concurrent.futures import FIRST_COMPLETED
import os import os
import time import time
import binascii
import hashlib import hashlib
import hmac import hmac
from functools import partial
import cryptography.hazmat.primitives.ciphers.aead as AEAD import cryptography.hazmat.primitives.ciphers.aead as AEAD
import aiorpcx import aiorpcx
from functools import partial
from . import bitcoin from . import bitcoin
from . import ecc 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 .crypto import sha256
from . import constants from . import constants
from . import transaction
from .util import PrintError, bh2u, print_error, bfh, aiosafe 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 .lnonion import new_onion_packet, OnionHopsDataSingle, OnionPerHop, decode_onion_error, ONION_FAILURE_CODE_MAP
from .lnaddr import lndecode from .lnaddr import lndecode
from .lnhtlc import HTLCStateMachine, RevokeAndAck 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): def channel_id_from_funding_tx(funding_txid, funding_index):
funding_txid_bytes = bytes.fromhex(funding_txid)[::-1] funding_txid_bytes = bytes.fromhex(funding_txid)[::-1]
@ -277,7 +273,7 @@ class Peer(PrintError):
self.pubkey = pubkey self.pubkey = pubkey
self.peer_addr = LNPeerAddr(host, port, pubkey) self.peer_addr = LNPeerAddr(host, port, pubkey)
self.lnworker = lnworker self.lnworker = lnworker
self.privkey = lnworker.privkey self.privkey = lnworker.node_keypair.privkey
self.network = lnworker.network self.network = lnworker.network
self.lnwatcher = lnworker.network.lnwatcher self.lnwatcher = lnworker.network.lnwatcher
self.channel_db = lnworker.network.channel_db self.channel_db = lnworker.network.channel_db
@ -484,50 +480,37 @@ class Peer(PrintError):
chan.set_state('DISCONNECTED') chan.set_state('DISCONNECTED')
self.network.trigger_callback('channel', chan) self.network.trigger_callback('channel', chan)
def make_local_config(self, funding_sat, push_msat, initiator: HTLCOwner, password): def make_local_config(self, funding_sat, push_msat, initiator: HTLCOwner):
# see lnd/keychain/derivation.go
keyfamilymultisig = 0
keyfamilyrevocationbase = 1
keyfamilyhtlcbase = 2
keyfamilypaymentbase = 3
keyfamilydelaybase = 4
keyfamilyrevocationroot = 5
keyfamilynodekey = 6 # TODO currently unused
# key derivation # 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: if initiator == LOCAL:
initial_msat = funding_sat * 1000 - push_msat initial_msat = funding_sat * 1000 - push_msat
else: else:
initial_msat = push_msat initial_msat = push_msat
local_config=ChannelConfig( local_config=ChannelConfig(
payment_basepoint=keypair_generator(keyfamilypaymentbase, 0), payment_basepoint=keypair_generator(LnKeyFamily.PAYMENT_BASE),
multisig_key=keypair_generator(keyfamilymultisig, 0), multisig_key=keypair_generator(LnKeyFamily.MULTISIG),
htlc_basepoint=keypair_generator(keyfamilyhtlcbase, 0), htlc_basepoint=keypair_generator(LnKeyFamily.HTLC_BASE),
delayed_basepoint=keypair_generator(keyfamilydelaybase, 0), delayed_basepoint=keypair_generator(LnKeyFamily.DELAY_BASE),
revocation_basepoint=keypair_generator(keyfamilyrevocationbase, 0), revocation_basepoint=keypair_generator(LnKeyFamily.REVOCATION_BASE),
to_self_delay=143, to_self_delay=143,
dust_limit_sat=546, dust_limit_sat=546,
max_htlc_value_in_flight_msat=0xffffffffffffffff, max_htlc_value_in_flight_msat=0xffffffffffffffff,
max_accepted_htlcs=5, max_accepted_htlcs=5,
initial_msat=initial_msat, initial_msat=initial_msat,
) )
return local_config per_commitment_secret_seed = keypair_generator(LnKeyFamily.REVOCATION_ROOT).privkey
return local_config, per_commitment_secret_seed
def make_per_commitment_secret_seed(self):
# TODO
return 0x1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100.to_bytes(32, 'big')
@aiosafe @aiosafe
async def channel_establishment_flow(self, password, funding_sat, push_msat, temp_channel_id, sweep_address): async def channel_establishment_flow(self, password, funding_sat, push_msat, temp_channel_id, sweep_address):
await self.initialized 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 # amounts
local_feerate = self.current_feerate_per_kw() 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 # 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')) per_commitment_point_first = secret_to_pubkey(int.from_bytes(per_commitment_secret_first, 'big'))
msg = gen_msg( msg = gen_msg(
"open_channel", "open_channel",
@ -656,13 +639,10 @@ class Peer(PrintError):
initial_msat=funding_sat * 1000 - push_msat, initial_msat=funding_sat * 1000 - push_msat,
) )
temp_chan_id = payload['temporary_channel_id'] temp_chan_id = payload['temporary_channel_id']
password = None # TODO local_config, per_commitment_secret_seed = self.make_local_config(funding_sat * 1000, push_msat, REMOTE)
local_config = self.make_local_config(funding_sat * 1000, push_msat, REMOTE, password)
per_commitment_secret_seed = self.make_per_commitment_secret_seed()
per_commitment_secret_index = RevocationStore.START_INDEX
# for the first commitment transaction # 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')) per_commitment_point_first = secret_to_pubkey(int.from_bytes(per_commitment_secret_first, 'big'))
min_depth = 3 min_depth = 3
@ -884,7 +864,7 @@ class Peer(PrintError):
chan.set_state("OPEN") chan.set_state("OPEN")
self.network.trigger_callback('channel', chan) self.network.trigger_callback('channel', chan)
# add channel to database # 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] bitcoin_keys = [chan.local_config.multisig_key.pubkey, chan.remote_config.multisig_key.pubkey]
sorted_node_ids = list(sorted(node_ids)) sorted_node_ids = list(sorted(node_ids))
if sorted_node_ids != node_ids: if sorted_node_ids != node_ids:
@ -931,8 +911,8 @@ class Peer(PrintError):
) )
to_hash = chan_ann[256+2:] to_hash = chan_ann[256+2:]
h = bitcoin.Hash(to_hash) h = bitcoin.Hash(to_hash)
bitcoin_signature = ecc.ECPrivkey(chan.local_config.multisig_key.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, sigencode_string_canonize, sigdecode_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", self.send_message(gen_msg("announcement_signatures",
channel_id=chan.channel_id, channel_id=chan.channel_id,
short_channel_id=chan.short_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 chan.get_state() == "OPEN", chan.get_state()
assert amount_msat > 0, "amount_msat is not greater zero" assert amount_msat > 0, "amount_msat is not greater zero"
height = self.network.get_local_height() 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 = [] hops_data = []
sum_of_deltas = sum(route_edge.channel_policy.cltv_expiry_delta for route_edge in route[1:]) sum_of_deltas = sum(route_edge.channel_policy.cltv_expiry_delta for route_edge in route[1:])
total_fee = 0 total_fee = 0

19
electrum/lnutil.py

@ -1,4 +1,4 @@
from enum import IntFlag from enum import IntFlag, IntEnum
import json import json
from collections import namedtuple from collections import namedtuple
from typing import NamedTuple, List, Tuple from typing import NamedTuple, List, Tuple
@ -14,6 +14,7 @@ from .bitcoin import push_script
from . import segwit_addr from . import segwit_addr
from .i18n import _ from .i18n import _
from .lnaddr import lndecode from .lnaddr import lndecode
from .keystore import BIP32_KeyStore
HTLC_TIMEOUT_WEIGHT = 663 HTLC_TIMEOUT_WEIGHT = 663
HTLC_SUCCESS_WEIGHT = 703 HTLC_SUCCESS_WEIGHT = 703
@ -526,3 +527,19 @@ def extract_nodeid(connect_contents: str) -> Tuple[bytes, str]:
except: except:
raise ConnStringFormatError(_('Invalid node ID, must be 33 bytes and hexadecimal')) raise ConnStringFormatError(_('Invalid node ID, must be 33 bytes and hexadecimal'))
return node_id, rest 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))

38
electrum/lnworker.py

@ -12,6 +12,9 @@ import dns.resolver
import dns.exception import dns.exception
from . import constants from . import constants
from . import keystore
from . import bitcoin
from .keystore import BIP32_KeyStore
from .bitcoin import sha256, COIN from .bitcoin import sha256, COIN
from .util import bh2u, bfh, PrintError, InvoiceError, resolve_dns_srv, is_ip_address from .util import bh2u, bfh, PrintError, InvoiceError, resolve_dns_srv, is_ip_address
from .lnbase import Peer, privkey_to_pubkey, aiosafe 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 .lnhtlc import HTLCStateMachine
from .lnutil import (Outpoint, calc_short_channel_id, LNPeerAddr, from .lnutil import (Outpoint, calc_short_channel_id, LNPeerAddr,
get_compressed_pubkey_from_bech32, extract_nodeid, 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 electrum.lnaddr import lndecode
from .i18n import _ from .i18n import _
@ -41,13 +45,8 @@ class LNWorker(PrintError):
self.network = network self.network = network
self.channel_db = self.network.channel_db self.channel_db = self.network.channel_db
self.lock = threading.RLock() self.lock = threading.RLock()
pk = wallet.storage.get('lightning_privkey') self.ln_keystore = self._read_ln_keystore()
if pk is None: self.node_keypair = generate_keypair(self.ln_keystore, LnKeyFamily.NODE_KEY, 0)
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.config = network.config self.config = network.config
self.peers = {} # pubkey -> Peer self.peers = {} # pubkey -> Peer
self.channels = {x.channel_id: x for x in map(HTLCStateMachine, wallet.storage.get("channels", []))} 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 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) 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): def _add_peers_from_config(self):
peer_list = self.config.get('lightning_peers', []) peer_list = self.config.get('lightning_peers', [])
for host, port, pubkey in peer_list: for host, port, pubkey in peer_list:
@ -217,7 +235,7 @@ class LNWorker(PrintError):
if amount_sat is None: if amount_sat is None:
raise InvoiceError(_("Missing amount")) raise InvoiceError(_("Missing amount"))
amount_msat = int(amount_sat * 1000) 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: if path is None:
raise PaymentFailure(_("No path found")) raise PaymentFailure(_("No path found"))
node_id, short_channel_id = path[0] node_id, short_channel_id = path[0]
@ -236,7 +254,7 @@ class LNWorker(PrintError):
payment_preimage = os.urandom(32) payment_preimage = os.urandom(32)
RHASH = sha256(payment_preimage) RHASH = sha256(payment_preimage)
amount_btc = amount_sat/Decimal(COIN) if amount_sat else None 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.invoices[bh2u(payment_preimage)] = pay_req
self.wallet.storage.put('lightning_invoices', self.invoices) self.wallet.storage.put('lightning_invoices', self.invoices)
self.wallet.storage.write() self.wallet.storage.write()

Loading…
Cancel
Save