URI: 
       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