timplement bolt-04 onion packet construction - electrum - Electrum Bitcoin wallet HTML git clone https://git.parazyd.org/electrum DIR Log DIR Files DIR Refs DIR Submodules --- DIR commit 47b1bed53933dd1f24f8e68ff97ae8ab778ea9cc DIR parent 60b77f6a005e45e95435420d305163b151a4684f HTML Author: SomberNight <somber.night@protonmail.com> Date: Thu, 3 May 2018 18:29:02 +0200 implement bolt-04 onion packet construction Diffstat: M lib/lnbase.py | 129 +++++++++++++++++++++++++++++++ M lib/tests/test_lnbase.py | 32 ++++++++++++++++++++++++++++++- 2 files changed, 160 insertions(+), 1 deletion(-) --- DIR diff --git a/lib/lnbase.py b/lib/lnbase.py t@@ -19,7 +19,10 @@ import time import binascii import hashlib import hmac +from typing import Sequence import cryptography.hazmat.primitives.ciphers.aead as AEAD +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms +from cryptography.hazmat.backends import default_backend from .bitcoin import (public_key_from_private_key, ser_to_point, point_to_ser, string_to_number, deserialize_privkey, EC_KEY, rev_hex, int_to_hex, t@@ -1295,3 +1298,129 @@ class LNPathFinder(PrintError): path += [(cur_node, edge_taken)] path.reverse() return path + + +# bolt 04, "onion" -----> + +NUM_MAX_HOPS_IN_PATH = 20 +HOPS_DATA_SIZE = 1300 # also sometimes called routingInfoSize in bolt-04 +PER_HOP_FULL_SIZE = 65 # HOPS_DATA_SIZE / 20 +NUM_STREAM_BYTES = HOPS_DATA_SIZE + PER_HOP_FULL_SIZE +PER_HOP_PAYLOAD_SIZE = 32 # PER_HOP_FULL_SIZE - len(realm) - len(HMAC) +PER_HOP_HMAC_SIZE = 32 + + +class OnionPerHop: + + def __init__(self, short_channel_id: bytes, amt_to_forward: bytes, outgoing_cltv_value: bytes): + self.short_channel_id = short_channel_id + self.amt_to_forward = amt_to_forward + self.outgoing_cltv_value = outgoing_cltv_value + + def to_bytes(self) -> bytes: + ret = self.short_channel_id + ret += self.amt_to_forward + ret += self.outgoing_cltv_value + ret += bytes(12) # padding + return ret + + +class OnionHopsDataSingle: + + def __init__(self, per_hop: OnionPerHop): + self.realm = 0 + self.per_hop = per_hop + self.hmac = None + + def to_bytes(self) -> bytes: + 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) + return ret + + +class OnionPacket: + + def __init__(self, public_key: bytes, hops_data: bytes, hmac: bytes): + self.version = 0 + self.public_key = public_key + self.hops_data = hops_data # also called RoutingInfo in bolt-04 + self.hmac = hmac + + def to_bytes(self) -> bytes: + ret = bytes([self.version]) + ret += self.public_key + ret += self.hops_data + ret += self.hmac + return ret + + +def get_bolt04_onion_key(key_type: bytes, secret: bytes) -> bytes: + if key_type not in (b'rho', b'mu', b'um'): + raise Exception('invalid key_type {}'.format(key_type)) + key = hmac.new(key_type, msg=secret, digestmod=hashlib.sha256).digest() + return key + + +def new_onion_packet(payment_path_pubkeys: Sequence[bytes], session_key: bytes, + hops_data: Sequence[OnionHopsDataSingle], associated_data: bytes) -> OnionPacket: + num_hops = len(payment_path_pubkeys) + hop_shared_secrets = num_hops * [b''] + ephemeral_key = session_key + + # compute shared key for each hop + for i in range(0, num_hops): + hop_shared_secrets[i] = get_ecdh(ephemeral_key, payment_path_pubkeys[i]) + ephemeral_pubkey = bfh(EC_KEY(ephemeral_key).get_public_key()) + blinding_factor = H256(ephemeral_pubkey + hop_shared_secrets[i]) + blinding_factor_int = int.from_bytes(blinding_factor, byteorder="big") + ephemeral_key_int = int.from_bytes(ephemeral_key, byteorder="big") + ephemeral_key_int = ephemeral_key_int * blinding_factor_int % SECP256k1.order + ephemeral_key = ephemeral_key_int.to_bytes(32, byteorder="big") + + filler = generate_filler(b'rho', num_hops, PER_HOP_FULL_SIZE, hop_shared_secrets) + mix_header = bytearray(HOPS_DATA_SIZE) + next_hmac = bytearray(PER_HOP_HMAC_SIZE) + + # compute routing info and MAC for each hop + for i in range(num_hops-1, -1, -1): + rho_key = get_bolt04_onion_key(b'rho', hop_shared_secrets[i]) + mu_key = get_bolt04_onion_key(b'mu', hop_shared_secrets[i]) + hops_data[i].hmac = next_hmac + stream_bytes = generate_cipher_stream(rho_key, NUM_STREAM_BYTES) + mix_header = mix_header[:-PER_HOP_FULL_SIZE] + mix_header = hops_data[i].to_bytes() + mix_header + mix_header = ((int.from_bytes(mix_header, "big") ^ int.from_bytes(stream_bytes[:HOPS_DATA_SIZE], "big")) + .to_bytes(HOPS_DATA_SIZE, "big")) + if i == num_hops - 1: + mix_header = mix_header[:-len(filler)] + filler + packet = mix_header + associated_data + next_hmac = hmac.new(mu_key, msg=packet, digestmod=hashlib.sha256).digest() + + return OnionPacket( + public_key=bfh(EC_KEY(session_key).get_public_key()), + hops_data=bytes(mix_header), + hmac=next_hmac) + + +def generate_filler(key_type: bytes, num_hops: int, hop_size: int, + shared_secrets: Sequence[bytes]) -> bytes: + filler_size = (NUM_MAX_HOPS_IN_PATH + 1) * hop_size + filler = bytearray(filler_size) + + for i in range(0, num_hops-1): # -1, as last hop does not obfuscate + filler = filler[hop_size:] + filler += bytearray(hop_size) + stream_key = get_bolt04_onion_key(key_type, shared_secrets[i]) + stream_bytes = generate_cipher_stream(stream_key, filler_size) + filler = ((int.from_bytes(filler, "big") ^ int.from_bytes(stream_bytes, "big")) + .to_bytes(filler_size, "big")) + + return filler[(NUM_MAX_HOPS_IN_PATH-num_hops+2)*hop_size:] + + +def generate_cipher_stream(stream_key: bytes, num_bytes: int) -> bytes: + algo = algorithms.ChaCha20(stream_key, nonce=bytes(16)) + cipher = Cipher(algo, mode=None, backend=default_backend()) + encryptor = cipher.encryptor() + return encryptor.update(bytes(num_bytes)) DIR diff --git a/lib/tests/test_lnbase.py b/lib/tests/test_lnbase.py t@@ -6,7 +6,7 @@ from lib.util import bh2u, bfh from lib.lnbase import make_commitment, get_obscured_ctn, Peer, make_offered_htlc, make_received_htlc, make_htlc_tx from lib.lnbase import secret_to_pubkey, derive_pubkey, derive_privkey, derive_blinded_pubkey, overall_weight from lib.lnbase import make_htlc_tx_output, make_htlc_tx_inputs, get_per_commitment_secret_from_seed -from lib.lnbase import make_htlc_tx_witness +from lib.lnbase import make_htlc_tx_witness, OnionHopsDataSingle, new_onion_packet, OnionPerHop from lib.transaction import Transaction from lib import bitcoin import ecdsa.ellipticcurve t@@ -308,3 +308,33 @@ class Test_LNBase(unittest.TestCase): self.assertEqual(0x915c75942a26bb3a433a8ce2cb0427c29ec6c1775cfc78328b57f6ba7bfeaa9c.to_bytes(byteorder="big", length=32), get_per_commitment_secret_from_seed(0x0101010101010101010101010101010101010101010101010101010101010101.to_bytes(byteorder="big", length=32), 1)) + def test_new_onion_packet(self): + payment_path_pubkeys = [ + bfh('02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619'), + bfh('0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c'), + bfh('027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007'), + bfh('032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991'), + bfh('02edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145'), + ] + 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) parazyd.org:70 /git/electrum/commit/47b1bed53933dd1f24f8e68ff97ae8ab778ea9cc.gph:205: line too long