tbolt-04: implement processing of onion packets - electrum - Electrum Bitcoin wallet (HTM) git clone https://git.parazyd.org/electrum (DIR) Log (DIR) Files (DIR) Refs (DIR) Submodules --- (DIR) commit a58a345dc3253576edb8e5fa51c8e575d51d7b01 (DIR) parent 053c571d7479370b8dce912918451823cad00abf (HTM) 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) self.assertEqual(bfh('0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619e5f14350c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a71da571226458c510bbadd1276f045c21c520a07d35da256ef75b4367962437b0dd10f7d61ab590531cf08000178a333a347f8b4072e216400406bdf3bf038659793a86cae5f52d32f3438527b47a1cfc54285a8afec3a4c9f3323db0c946f5d4cb2ce721caad69320c3a469a202f3e468c67eaf7a7cda226d0fd32f7b48084dca885d15222e60826d5d971f64172d98e0760154400958f00e86697aa1aa9d41bee8119a1ec866abe044a9ad635778ba61fc0776dc832b39451bd5d35072d2269cf9b040d6ba38b54ec35f81d7fc67678c3be47274f3c4cc472aff005c3469eb3bc140769ed4c7f0218ff8c6c7dd7221d189c65b3b9aaa71a01484b122846c7c7b57e02e679ea8469b70e14fe4f70fee4d87b910cf144be6fe48eef24da475c0b0bcc6565ae82cd3f4e3b24c76eaa5616c6111343306ab35c1fe5ca4a77c0e314ed7dba39d6f1e0de791719c241a939cc493bea2bae1c1e932679ea94d29084278513c77b899cc98059d06a27d171b0dbdf6bee13ddc4fc17a0c4d2827d488436b57baa167544138ca2e64a11b43ac8a06cd0c2fba2d4d900ed2d9205305e2d7383cc98dacb078133de5f6fb6bed2ef26ba92cea28aafc3b9948dd9ae5559e8bd6920b8cea462aa445ca6a95e0e7ba52961b181c79e73bd581821df2b10173727a810c92b83b5ba4a0403eb710d2ca10689a35bec6c3a708e9e92f7d78ff3c5d9989574b00c6736f84c199256e76e19e78f0c98a9d580b4a658c84fc8f2096c2fbea8f5f8c59d0fdacb3be2802ef802abbecb3aba4acaac69a0e965abd8981e9896b1f6ef9d60f7a164b371af869fd0e48073742825e9434fc54da837e120266d53302954843538ea7c6c3dbfb4ff3b2fdbe244437f2a153ccf7bdb4c92aa08102d4f3cff2ae5ef86fab4653595e6a5837fa2f3e29f27a9cde5966843fb847a4a61f1e76c281fe8bb2b0a181d096100db5a1a5ce7a910238251a43ca556712eaadea167fb4d7d75825e440f3ecd782036d7574df8bceacb397abefc5f5254d2722215c53ff54af8299aaaad642c6d72a14d27882d9bbd539e1cc7a527526ba89b8c037ad09120e98ab042d3e8652b31ae0e478516bfaf88efca9f3676ffe99d2819dcaeb7610a626695f53117665d267d3f7abebd6bbd6733f645c72c389f03855bdf1e4b8075b516569b118233a0f0971d24b83113c0b096f5216a207ca99a7cddc81c130923fe3d91e7508c9ac5f2e914ff5dccab9e558566fa14efb34ac98d878580814b94b73acbfde9072f30b881f7f0fff42d4045d1ace6322d86a97d164aa84d93a60498065cc7c20e636f5862dc81531a88c60305a2e59a985be327a6902e4bed986dbf4a0b50c217af0ea7fdf9ab37f9ea1a1aaa72f54cf40154ea9b269f1a7c09f9f43245109431a175d50e2db0132337baa0ef97eed0fcf20489da36b79a1172faccc2f7ded7c60e00694282d93359c4682135642bc81f433574aa8ef0c97b4ade7ca372c5ffc23c7eddd839bab4e0f14d6df15c9dbeab176bec8b5701cf054eb3072f6dadc98f88819042bf10c407516ee58bce33fbe3b3d86a54255e577db4598e30a135361528c101683a5fcde7e8ba53f3456254be8f45fe3a56120ae96ea3773631fcb3873aa3abd91bcff00bd38bd43697a2e789e00da6077482e7b1b1a677b5afae4c54e6cbdf7377b694eb7d7a5b913476a5be923322d3de06060fd5e819635232a2cf4f0731da13b8546d1d6d4f8d75b9fce6c2341a71b0ea6f780df54bfdb0dd5cd9855179f602f917265f21f9190c70217774a6fbaaa7d63ad64199f4664813b955cff954949076dcf'), packet.to_bytes()) + + def test_process_onion_packet(self): + # this test is not from bolt-04, but is based on the one there; + # except here we have the privkeys for these pubkeys + payment_path_pubkeys = [ + bfh('03d75c0ee70f68d73d7d13aeb6261d8ace11416800860c7e59407afe4e2e2d42bb'), + bfh('03960a0b830c7b8e76de745b819f252c62508346196b916f5e813cdb0773283cce'), + bfh('0385620e0a571cbc3552620f8bf1bdcdab2d1a4a59c36fa10b8249114ccbdda40d'), + bfh('02ee242cf6c38b7285f0152c33804ff777f5c51fd352ca8132e845e2cf23b3d8ba'), + bfh('025c585fd2e174bf8245b2b4a119e52a417688904228643ea3edaa1728bf2a258e'), + ] + payment_path_privkeys = [ + bfh('3463a278617b3dd83f79bda7f97673f12609c54386e1f0d2b67b1c6354fda14e'), + bfh('7e1255fddb52db1729fc3ceb21a46f95b8d9fe94cc83425e936a6c5223bb679d'), + bfh('c7ce8c1462c311eec24dff9e2532ac6241e50ae57e7d1833af21942136972f23'), + bfh('3d885f374d79a5e777459b083f7818cdc9493e5c4994ac9c7b843de8b70be661'), + bfh('dd72ab44729527b7942e195e7a835e7c71f9c0ff61844eb21274d9c26166a8f8'), + ] + session_key = bfh('4141414141414141414141414141414141414141414141414141414141414141') + associated_data = bfh('4242424242424242424242424242424242424242424242424242424242424242') + hops_data = [ + OnionHopsDataSingle(OnionPerHop( + bfh('0000000000000000'), bfh('0000000000000000'), bfh('00000000') + )), + OnionHopsDataSingle(OnionPerHop( + bfh('0101010101010101'), bfh('0000000000000001'), bfh('00000001') + )), + OnionHopsDataSingle(OnionPerHop( + bfh('0202020202020202'), bfh('0000000000000002'), bfh('00000002') + )), + OnionHopsDataSingle(OnionPerHop( + bfh('0303030303030303'), bfh('0000000000000003'), bfh('00000003') + )), + OnionHopsDataSingle(OnionPerHop( + bfh('0404040404040404'), bfh('0000000000000004'), bfh('00000004') + )), + ] + packet = new_onion_packet(payment_path_pubkeys, session_key, hops_data, associated_data) + self.assertEqual(bfh('0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f28368661954176cd9869da33d713aa219fcef1e5c806fef11e696bcc66844de8271c27974a0fd57c2dbcb2c6dd4e8ef35d96db28d5a0e49b6ab3d6de31af65950723b8cddc108390bebf8d149002e31bdc283056477ba27c8054c248ad7306de31663a7c99ec659da15d0f6fbc7e1687485b39e9be0ec3b70164cb3618a9b546317e7c2d62ae9f0f840704535729262d30c6132d1b390f073edec8fa057176c6268b6ad06a82ff0d16d4c662194873e8b4ecf46eb2c9d4d58d2ee2021adb19840605ac5afd8bd942dd71e8244c83e28b2ed5a3b09e9e7df5c8c747e5765ba366a4f7407a6c6b0a32f74bc5e428f7fa4c3cf70e13ed91563177d94190d5149aa4b9c96d00e40d2ac35ab9c4a621ce0f6f5df7d64a9c8d435db19de192d9db522c7f7b4e201fc1b61a9bd3efd062ae24455d463818b01e2756c7d0691bc3ac4c017be34c9a8b2913bb1b937e31e0ae40f650a7cd820bcb4996825b1cbad1ff7ccc2b513b1104524c34f6573e1b59201c005a632ee5dccd3711a32e3ba1ff00fcffbe636e4b3a84bbe491b836a57ccec138b8cc2ec733846904d872f305d538d51db8e56232ec6e07877075328874cb7b09c7e799100a9ff085dead253886b174fc408a0ea7b48bce2c5d8992285011960af088f7e006ef60089d46ac9aa15acfac6c87c3cf6904764dd785419292fbafa9cca09c8ade24a6cd63f12d1cfc83fa35cf2f1cf503c39cbf78293f06c68a3cece7177169cd872bb49bf69d933a27a887dd9daefa9239fca9f0c3e309ec61d9df947211da98cf11a6e0fb77252629cdf9f2226dd69ca73fa51be4df224592f8d471b69a1aebbdaa2f3a798b3581253d97feb0a12e6606043ca0fc5efc0f49b8061d6796eff31cd8638499e2f25ffb96eec32837438ed7ebebbe587886648f63e35d80f41869f4c308f2e6970bd65fead5e8544e3239a6acc9d996b08d1546455bcafbe88ed3ed547714841946fe2e77180e4d7bf1452414e4b1745a7897184a2c4cbc3ac46f83342a55a48e29dc8f17cf595dd28f51e297ba89fd25ed0dbd1c0081a810beaab09758a36fbfd16fbdc3daa9fe05c8a73195f244ef2743a5df761f01ee6e693eb6c7f1a7834fab3671391e5ddebf611e119a2ae4456e2cee7a6d4f27a2246cdb1f8ef35f0b3d7044b3799d8d0ed0a6470557fd807c065d6d83acba07e96e10770ada8c0b4d4921522944188d5f30086a6ee0a4795331273f32beaaa43363fc58208a257e5c5c434c7325b583642219d81c7d67b908d5263b42ac1991edc69a777da60f38eff138c844af9e549374e8b29b166211bfded24587a29394e33828b784da7e7b62ab7e49ea2693fcdd17fa96186a5ef11ef1a8adffa50f93a3119e95e6c09014f3e3b0709183fa08a826ced6deb4608b7d986ebbcf99ad58e25451d4d9d38d0059734d8501467b97182cd11e0c07c91ca50f61cc31255a3147ade654976a5989097281892aafd8df595c63bd14f1e03f5955a9398d2dd6368bbcae833ae1cc2df31eb0980b4817dfd130020ffb275743fcc01df40e3ecda1c5988e8e1bde965353b0b1bf34ea05f095000c45b6249618d275905a24d3eb58c600aeab4fb552fbf1ccdb2a5c80ace220310f89829d7e53f78c126037b6d8d500220c7a118d9621b4d6bd5379edd7e24bcf540e87aba6b88862db16fa4ee00b009fda80577be67ab94910fd8a7807dfe4ebe66b8fdcd040aa2dc17ec22639298be56b2a2c9d8940647b75f2f6d81746df16e1cb2f05e23397a8c63baea0803441ff4b7d517ff172980a056726235e2f6af85e8aa9b91ba85f14532272d6170df3166b91169dc09d4f4a251610f57ff0885a93364cfaf650bdf436c89795efed5ca934bc7ffc0a4'), + packet.to_bytes()) + for i, privkey in enumerate(payment_path_privkeys): + processed_packet = lnbase.process_onion_packet(packet, associated_data, privkey) + self.assertEqual(hops_data[i].per_hop.to_bytes(), processed_packet.hop_data.per_hop.to_bytes()) + packet = processed_packet.next_packet