tbolt-04: implement processing of onion packets - electrum - Electrum Bitcoin wallet HTML git clone https://git.parazyd.org/electrum DIR Log DIR Files DIR Refs DIR Submodules --- DIR commit a58a345dc3253576edb8e5fa51c8e575d51d7b01 DIR parent 053c571d7479370b8dce912918451823cad00abf HTML Author: SomberNight <somber.night@protonmail.com> Date: Fri, 4 May 2018 18:13:59 +0200 bolt-04: implement processing of onion packets Diffstat: M lib/lnbase.py | 95 ++++++++++++++++++++++++++++++- M lib/tests/test_lnbase.py | 47 ++++++++++++++++++++++++++++++- 2 files changed, 138 insertions(+), 4 deletions(-) --- DIR diff --git a/lib/lnbase.py b/lib/lnbase.py t@@ -242,7 +242,7 @@ def get_bolt8_hkdf(salt, ikm): assert len(T1 + T2) == 64 return T1, T2 -def get_ecdh(priv, pub): +def get_ecdh(priv: bytes, pub: bytes) -> bytes: s = string_to_number(priv) pk = ser_to_point(pub) pt = point_to_ser(pk * s) t@@ -1309,6 +1309,10 @@ NUM_STREAM_BYTES = HOPS_DATA_SIZE + PER_HOP_FULL_SIZE PER_HOP_HMAC_SIZE = 32 +class UnsupportedOnionPacketVersion(Exception): pass +class InvalidOnionMac(Exception): pass + + class OnionPerHop: def __init__(self, short_channel_id: bytes, amt_to_forward: bytes, outgoing_cltv_value: bytes): t@@ -1321,12 +1325,24 @@ class OnionPerHop: ret += self.amt_to_forward ret += self.outgoing_cltv_value ret += bytes(12) # padding + if len(ret) != 32: + raise Exception('unexpected length {}'.format(len(ret))) return ret + @classmethod + def from_bytes(cls, b: bytes): + if len(b) != 32: + raise Exception('unexpected length {}'.format(len(b))) + return OnionPerHop( + short_channel_id=b[:8], + amt_to_forward=b[8:16], + outgoing_cltv_value=b[16:20] + ) + -class OnionHopsDataSingle: +class OnionHopsDataSingle: # called HopData in lnd - def __init__(self, per_hop: OnionPerHop): + def __init__(self, per_hop: OnionPerHop = None): self.realm = 0 self.per_hop = per_hop self.hmac = None t@@ -1335,6 +1351,20 @@ class OnionHopsDataSingle: ret = bytes([self.realm]) ret += self.per_hop.to_bytes() ret += self.hmac if self.hmac is not None else bytes(PER_HOP_HMAC_SIZE) + if len(ret) != PER_HOP_FULL_SIZE: + raise Exception('unexpected length {}'.format(len(ret))) + return ret + + @classmethod + def from_bytes(cls, b: bytes): + if len(b) != PER_HOP_FULL_SIZE: + raise Exception('unexpected length {}'.format(len(b))) + ret = OnionHopsDataSingle() + ret.realm = b[0] + if ret.realm != 0: + raise Exception('only realm 0 is supported') + ret.per_hop = OnionPerHop.from_bytes(b[1:33]) + ret.hmac = b[33:] return ret t@@ -1351,8 +1381,23 @@ class OnionPacket: ret += self.public_key ret += self.hops_data ret += self.hmac + if len(ret) != 1366: + raise Exception('unexpected length {}'.format(len(ret))) return ret + @classmethod + def from_bytes(cls, b: bytes): + if len(b) != 1366: + raise Exception('unexpected length {}'.format(len(b))) + version = b[0] + if version != 0: + raise UnsupportedOnionPacketVersion('version {} is not supported'.format(version)) + return OnionPacket( + public_key=b[1:34], + hops_data=b[34:1334], + hmac=b[1334:] + ) + def get_bolt04_onion_key(key_type: bytes, secret: bytes) -> bytes: if key_type not in (b'rho', b'mu', b'um'): t@@ -1421,3 +1466,47 @@ def generate_cipher_stream(stream_key: bytes, num_bytes: int) -> bytes: cipher = Cipher(algo, mode=None, backend=default_backend()) encryptor = cipher.encryptor() return encryptor.update(bytes(num_bytes)) + + +ProcessedOnionPacket = namedtuple("ProcessedOnionPacket", ["are_we_final", "hop_data", "next_packet"]) + + +# TODO replay protection +def process_onion_packet(onion_packet: OnionPacket, associated_data: bytes, + our_onion_private_key: bytes) -> ProcessedOnionPacket: + shared_secret = get_ecdh(our_onion_private_key, onion_packet.public_key) + + # check message integrity + mu_key = get_bolt04_onion_key(b'mu', shared_secret) + calculated_mac = hmac.new(mu_key, msg=onion_packet.hops_data+associated_data, + digestmod=hashlib.sha256).digest() + if onion_packet.hmac != calculated_mac: + raise InvalidOnionMac() + + # peel an onion layer off + rho_key = get_bolt04_onion_key(b'rho', shared_secret) + stream_bytes = generate_cipher_stream(rho_key, NUM_STREAM_BYTES) + padded_header = onion_packet.hops_data + bytes(PER_HOP_FULL_SIZE) + next_hops_data = xor_bytes(padded_header, stream_bytes) + + # calc next ephemeral key + blinding_factor = H256(onion_packet.public_key + shared_secret) + blinding_factor_int = int.from_bytes(blinding_factor, byteorder="big") + next_public_key_int = ser_to_point(onion_packet.public_key) * blinding_factor_int + next_public_key = point_to_ser(next_public_key_int) + + hop_data = OnionHopsDataSingle.from_bytes(next_hops_data[:PER_HOP_FULL_SIZE]) + next_onion_packet = OnionPacket( + public_key=next_public_key, + hops_data=next_hops_data[PER_HOP_FULL_SIZE:], + hmac=hop_data.hmac + ) + if hop_data.hmac == bytes(PER_HOP_HMAC_SIZE): + # we are the destination / exit node + are_we_final = True + else: + # we are an intermediate node; forwarding + are_we_final = False + return ProcessedOnionPacket(are_we_final, hop_data, next_onion_packet) + + DIR diff --git a/lib/tests/test_lnbase.py b/lib/tests/test_lnbase.py t@@ -12,7 +12,7 @@ from lib import bitcoin import ecdsa.ellipticcurve from ecdsa.curves import SECP256k1 from lib.util import bfh -from lib import bitcoin +from lib import bitcoin, lnbase funding_tx_id = '8984484a580b825b9972d7adb15050b3ab624ccd731946b3eeddb92f4e7ef6be' funding_output_index = 0 t@@ -309,6 +309,7 @@ class Test_LNBase(unittest.TestCase): get_per_commitment_secret_from_seed(0x0101010101010101010101010101010101010101010101010101010101010101.to_bytes(byteorder="big", length=32), 1)) def test_new_onion_packet(self): + # test vector from bolt-04 payment_path_pubkeys = [ bfh('02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619'), bfh('0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c'), t@@ -338,3 +339,47 @@ class Test_LNBase(unittest.TestCase): packet = new_onion_packet(payment_path_pubkeys, session_key, hops_data, associated_data) parazyd.org:70 /git/electrum/commit/a58a345dc3253576edb8e5fa51c8e575d51d7b01.gph:182: line too long