token_flag = b"\x00" msg = self.ephemeral_pubkey.to_bytes() + nonce + token_flag return msg def encrypt_auth_ack_message(self, ack_message: bytes) -> bytes: if self.use_eip8: auth_ack = encrypt_eip8_msg(ack_message, self.remote.pubkey) else: auth_ack = ecies.encrypt(ack_message, self.remote.pubkey) return auth_ack eip8_ack_sedes = sedes.List( [ sedes.Binary(min_length=64, max_length=64), # ephemeral pubkey sedes.Binary(min_length=32, max_length=32), # nonce sedes.BigEndianInt(), # version ], strict=False, ) eip8_auth_sedes = sedes.List( [ sedes.Binary(min_length=65, max_length=65), # sig sedes.Binary(min_length=64, max_length=64), # pubkey sedes.Binary(min_length=32, max_length=32), # nonce sedes.BigEndianInt(), # version ], strict=False, ) def _pad_eip8_data(data: bytes) -> bytes:
class Status(Command): _cmd_id = 0 decode_strict = False # A list of (key, value) pairs is all a Status msg contains, but since the values can be of # any type, we need to use the raw sedes here and do the actual deserialization in # decode_payload(). structure = sedes.CountableList(sedes.List([sedes.binary, sedes.raw])) # The sedes used for each key in the list above. items_sedes = { 'protocolVersion': sedes.big_endian_int, 'networkId': sedes.big_endian_int, 'headTd': sedes.big_endian_int, 'headHash': sedes.binary, 'headNum': sedes.big_endian_int, 'genesisHash': sedes.binary, 'serveHeaders': None, 'serveChainSince': sedes.big_endian_int, 'serveStateSince': sedes.big_endian_int, 'txRelay': None, 'flowControl/BL': sedes.big_endian_int, 'flowControl/MRC': sedes.CountableList( sedes.List([ sedes.big_endian_int, sedes.big_endian_int, sedes.big_endian_int ])), 'flowControl/MRR': sedes.big_endian_int, } @to_dict def decode_payload(self, rlp_data): data = super(Status, self).decode_payload(rlp_data) # The LES/Status msg contains an arbitrary list of (key, value) pairs, where values can # have different types and unknown keys should be ignored for forward compatibility # reasons, so here we need an extra pass to deserialize each of the key/value pairs we # know about. for key, value in data: # The sedes.binary we use in .structure above will give us a bytes value here, but # using bytes as dictionary keys makes it impossible to use the dict() constructor # with keyword arguments, so we convert them to strings here. key = key.decode('ascii') if key not in self.items_sedes: continue item_sedes = self.items_sedes[key] if item_sedes is not None: yield key, item_sedes.deserialize(value) else: yield key, value def encode_payload(self, data): response = [(key, self.items_sedes[key].serialize(value)) for key, value in sorted(data.items())] return super(Status, self).encode_payload(response) def as_head_info(self, decoded: _DecodedMsgType) -> HeadInfo: return HeadInfo( block_number=decoded['headNum'], block_hash=decoded['headHash'], total_difficulty=decoded['headTd'], reorg_depth=0, )
class NewBlockHashes(BaseCommand[Tuple[NewBlockHash, ...]]): protocol_command_id = 1 serialization_codec: RLPCodec[Tuple[NewBlockHash, ...]] = RLPCodec( sedes=sedes.CountableList(sedes.List([hash_sedes, sedes.big_endian_int])), process_inbound_payload_fn=apply_formatter_to_array(lambda args: NewBlockHash(*args)), )
class Status(Command): _cmd_id = 0 decode_strict = False # A list of (key, value) pairs is all a Status msg contains, but since the values can be of # any type, we need to use the raw sedes here and do the actual deserialization in # decode_payload(). structure = sedes.CountableList(sedes.List([sedes.text, sedes.raw])) # The sedes used for each key in the list above. Keys that use None as their sedes are # optional and have no value -- IOW, they just need to be present in the msg when appropriate. items_sedes = { 'protocolVersion': sedes.big_endian_int, 'networkId': sedes.big_endian_int, 'headTd': sedes.big_endian_int, 'headHash': sedes.binary, 'headNum': sedes.big_endian_int, 'genesisHash': sedes.binary, 'serveHeaders': None, 'serveChainSince': sedes.big_endian_int, 'serveStateSince': sedes.big_endian_int, 'txRelay': None, 'flowControl/BL': sedes.big_endian_int, 'flowControl/MRC': sedes.CountableList( sedes.List([ sedes.big_endian_int, sedes.big_endian_int, sedes.big_endian_int ])), 'flowControl/MRR': sedes.big_endian_int, } @to_dict def decode_payload(self, rlp_data: bytes) -> Iterator[Tuple[str, Any]]: data = cast(List[Tuple[str, bytes]], super().decode_payload(rlp_data)) # The LES/Status msg contains an arbitrary list of (key, value) pairs, where values can # have different types and unknown keys should be ignored for forward compatibility # reasons, so here we need an extra pass to deserialize each of the key/value pairs we # know about. for key, value in data: if key not in self.items_sedes: continue yield key, self._deserialize_item(key, value) def encode_payload(self, data: Union[Payload, sedes.CountableList]) -> bytes: response = [ (key, self._serialize_item(key, value)) for key, value in sorted(cast(Dict[str, Any], data).items()) ] return super().encode_payload(response) def _deserialize_item(self, key: str, value: bytes) -> Any: sedes = self.items_sedes[key] if sedes is not None: return sedes.deserialize(value) else: # See comment in the definition of item_sedes as to why we do this. return b'' def _serialize_item(self, key: str, value: bytes) -> bytes: sedes = self.items_sedes[key] if sedes is not None: return sedes.serialize(value) else: # See comment in the definition of item_sedes as to why we do this. return b''
from trinity.rlp.block_body import BlockBody from trinity.rlp.sedes import HashOrNumber, hash_sedes from .forkid import ForkID from .payloads import ( StatusV63Payload, NewBlockHash, BlockFields, NewBlockPayload, StatusPayload, ) STATUS_V63_STRUCTURE = sedes.List(( sedes.big_endian_int, sedes.big_endian_int, sedes.big_endian_int, hash_sedes, hash_sedes, )) class StatusV63(BaseCommand[StatusV63Payload]): protocol_command_id = 0 serialization_codec: RLPCodec[StatusV63Payload] = RLPCodec( sedes=STATUS_V63_STRUCTURE, process_inbound_payload_fn=compose( lambda args: StatusV63Payload(*args), ), ) STATUS_STRUCTURE = sedes.List(
class NewBlockHashes(Command): _cmd_id = 1 structure = sedes.CountableList( sedes.List([sedes.binary, sedes.big_endian_int]))
class Status(Command): _cmd_id = 0 decode_strict = False # A list of (key, value) pairs is all a Status msg contains, but since the values can be of # any type, we need to use the raw sedes here and do the actual deserialization in # decode_payload(). structure = sedes.CountableList(sedes.List([sedes.text, sedes.raw])) # The sedes used for each key in the list above. items_sedes = { 'protocolVersion': sedes.big_endian_int, 'networkId': sedes.big_endian_int, 'headTd': sedes.big_endian_int, 'headHash': sedes.binary, 'headNum': sedes.big_endian_int, 'genesisHash': sedes.binary, 'serveHeaders': None, 'serveChainSince': sedes.big_endian_int, 'serveStateSince': sedes.big_endian_int, 'txRelay': None, 'flowControl/BL': sedes.big_endian_int, 'flowControl/MRC': sedes.CountableList( sedes.List([ sedes.big_endian_int, sedes.big_endian_int, sedes.big_endian_int ])), 'flowControl/MRR': sedes.big_endian_int, } @to_dict def decode_payload( self, rlp_data: bytes) -> Generator[Tuple[str, Any], None, None]: data = cast(List[Tuple[str, bytes]], super().decode_payload(rlp_data)) # The LES/Status msg contains an arbitrary list of (key, value) pairs, where values can # have different types and unknown keys should be ignored for forward compatibility # reasons, so here we need an extra pass to deserialize each of the key/value pairs we # know about. for key, value in data: if key not in self.items_sedes: continue item_sedes = self.items_sedes[key] if item_sedes is not None: yield key, item_sedes.deserialize(value) else: yield key, value def encode_payload( self, data: Union[_DecodedMsgType, sedes.CountableList]) -> bytes: response = [ (key, self.items_sedes[key].serialize(value)) for key, value in sorted(data.items()) # type: ignore ] return super().encode_payload(response) def as_head_info(self, decoded: _DecodedMsgType) -> HeadInfo: decoded = cast(Dict[str, Any], decoded) return HeadInfo( block_number=decoded['headNum'], block_hash=decoded['headHash'], total_difficulty=decoded['headTd'], reorg_depth=0, )
def test_handshake(): tv = test_values initiator = RLPxSession( ECCx(raw_privkey=tv['initiator_private_key']), is_initiator=True, ephemeral_privkey=tv['initiator_ephemeral_private_key']) initiator_pubkey = initiator.ecc.raw_pubkey responder = RLPxSession( ECCx(raw_privkey=tv['receiver_private_key']), ephemeral_privkey=tv['receiver_ephemeral_private_key']) responder_pubkey = responder.ecc.raw_pubkey # test encryption _enc = initiator.encrypt_auth_message(tv['auth_plaintext'], responder_pubkey) assert len(_enc) == len(tv['auth_ciphertext']) assert len(tv['auth_ciphertext']) == 113 + len(tv['auth_plaintext']) # len # test auth_msg plain auth_msg = initiator.create_auth_message( remote_pubkey=responder_pubkey, ephemeral_privkey=tv['initiator_ephemeral_private_key'], nonce=tv['initiator_nonce']) # test auth_msg plain assert len(auth_msg) == len(tv['auth_plaintext']) == 194 assert auth_msg[65:] == tv['auth_plaintext'][ 65:] # starts with non deterministic k _auth_msg_cipher = initiator.encrypt_auth_message(auth_msg, responder_pubkey) # test shared responder.ecc.get_ecdh_key(initiator_pubkey) == \ initiator.ecc.get_ecdh_key(responder_pubkey) # test decrypt assert auth_msg == responder.ecc.ecies_decrypt(_auth_msg_cipher) # check receive responder_ephemeral_pubkey = privtopub( tv['receiver_ephemeral_private_key']) auth_msg_cipher = tv['auth_ciphertext'] auth_msg = responder.ecc.ecies_decrypt(auth_msg_cipher) assert auth_msg[65:] == tv['auth_plaintext'][ 65:] # starts with non deterministic k responder.decode_authentication(auth_msg_cipher) auth_ack_msg = responder.create_auth_ack_message( responder_ephemeral_pubkey, nonce=tv['receiver_nonce']) assert auth_ack_msg == tv['authresp_plaintext'] auth_ack_msg_cipher = responder.encrypt_auth_ack_message( auth_ack_msg, remote_pubkey=responder.remote_pubkey) # set auth ack msg cipher (needed later for mac calculation) responder.auth_ack = tv['authresp_ciphertext'] responder.setup_cipher() assert responder.ecdhe_shared_secret == tv['ecdhe_shared_secret'] assert len(responder.token) == len(tv['token']) assert responder.token == tv['token'] assert responder.aes_secret == tv['aes_secret'] assert responder.mac_secret == tv['mac_secret'] assert responder.initiator_nonce == tv['initiator_nonce'] assert responder.responder_nonce == tv['receiver_nonce'] assert responder.auth_init == tv['auth_ciphertext'] assert responder.auth_ack == tv['authresp_ciphertext'] # test values are from initiator perspective? assert responder.ingress_mac.digest() == tv['initial_egress_MAC'] assert responder.ingress_mac.digest() == tv['initial_egress_MAC'] assert responder.egress_mac.digest() == tv['initial_ingress_MAC'] assert responder.egress_mac.digest() == tv['initial_ingress_MAC'] r = responder.decrypt(tv['initiator_hello_packet']) # unpack hello packet import struct import rlp import rlp.sedes as sedes from rlp.codec import consume_item header = r['header'] frame_length = struct.unpack(b'>I', b'\x00' + header[:3])[0] header_sedes = sedes.List([sedes.big_endian_int, sedes.big_endian_int]) header_data = rlp.decode(header[3:], strict=False, sedes=header_sedes) print('header', repr(header_data)) # frame frame = r['frame'] # normal: rlp(packet-type) [|| rlp(packet-data)] || padding packet_type, end = consume_item(frame, start=0) packet_type = rlp.decode(frame, sedes=sedes.big_endian_int, strict=False) print('packet_type', repr(packet_type)) # decode hello body _sedes_capabilites_tuple = sedes.List([sedes.binary, sedes.big_endian_int]) structure = [('version', sedes.big_endian_int), ('client_version_string', sedes.big_endian_int), ('capabilities', sedes.CountableList(_sedes_capabilites_tuple)), ('listen_port', sedes.big_endian_int), ('remote_pubkey', sedes.binary)] hello_sedes = sedes.List([x[1] for x in structure]) frame_data = rlp.decode(frame[end:], sedes=hello_sedes) frame_data = dict((structure[i][0], x) for i, x in enumerate(frame_data)) print('frame', frame_data)
'serveChainSince': sedes.big_endian_int, 'serveRecentChain': sedes.big_endian_int, 'serveStateSince': sedes.big_endian_int, 'serveRecentState': sedes.big_endian_int, 'txRelay': None, 'flowControl/BL': sedes.big_endian_int, 'flowControl/MRC': sedes.CountableList( sedes.List( [sedes.big_endian_int, sedes.big_endian_int, sedes.big_endian_int])), 'flowControl/MRR': sedes.big_endian_int, } STATUS_STRUCTURE = sedes.CountableList(sedes.List([sedes.text, sedes.raw])) class StatusSerializationCodec(SerializationCodecAPI[StatusPayload]): item_sedes: ClassVar[Dict[str, Any]] @to_tuple def _encode_items(self, *items: Tuple[str, Any]) -> Iterable[Tuple[str, bytes]]: for key, value in items:
class RLPxSession(object): ephemeral_ecc = None remote_ephemeral_pubkey = None initiator_nonce = None responder_nonce = None auth_init = None auth_ack = None aes_secret = None token = None aes_enc = None aes_dec = None mac_enc = None egress_mac = None ingress_mac = None is_ready = False remote_pubkey = None remote_version = 0 got_eip8_auth, got_eip8_ack = False, False def __init__(self, ecc, is_initiator=False, ephemeral_privkey=None): self.ecc = ecc self.is_initiator = is_initiator self.ephemeral_ecc = ECCx(raw_privkey=ephemeral_privkey) ### frame handling def encrypt(self, header, frame): assert self.is_ready is True assert len(header) == 16 assert len(frame) % 16 == 0 def aes(data=''): return self.aes_enc.update(data) def mac(data=''): self.egress_mac.update(data) return self.egress_mac.digest() # header header_ciphertext = aes(header) assert len(header_ciphertext) == 16 # egress-mac.update(aes(mac-secret,egress-mac) ^ header-ciphertext).digest header_mac = mac(sxor(self.mac_enc(mac()[:16]), header_ciphertext))[:16] # frame frame_ciphertext = aes(frame) assert len(frame_ciphertext) == len(frame) # egress-mac.update(aes(mac-secret,egress-mac) ^ # left128(egress-mac.update(frame-ciphertext).digest)) fmac_seed = mac(frame_ciphertext) frame_mac = mac(sxor(self.mac_enc(mac()[:16]), fmac_seed[:16]))[:16] return header_ciphertext + header_mac + frame_ciphertext + frame_mac def decrypt_header(self, data): assert self.is_ready is True assert len(data) == 32 def aes(data=''): return self.aes_dec.update(data) def mac(data=''): self.ingress_mac.update(data) return self.ingress_mac.digest() header_ciphertext = data[:16] header_mac = data[16:32] # ingress-mac.update(aes(mac-secret,ingress-mac) ^ header-ciphertext).digest expected_header_mac = mac( sxor(self.mac_enc(mac()[:16]), header_ciphertext))[:16] # expected_header_mac = self.updateMAC(self.ingress_mac, header_ciphertext) if not expected_header_mac == header_mac: raise AuthenticationError('invalid header mac') return aes(header_ciphertext) def decrypt_body(self, data, body_size): assert self.is_ready is True def aes(data=''): return self.aes_dec.update(data) def mac(data=''): self.ingress_mac.update(data) return self.ingress_mac.digest() # frame-size: 3-byte integer size of frame, big endian encoded (excludes padding) # frame relates to body w/o padding w/o mac read_size = ceil16(body_size) if not len(data) >= read_size + 16: raise FormatError('insufficient body length') # FIXME check frame length in header # assume datalen == framelen for now frame_ciphertext = data[:read_size] frame_mac = data[read_size:read_size + 16] assert len(frame_mac) == 16 # ingres-mac.update(aes(mac-secret,ingres-mac) ^ # left128(ingres-mac.update(frame-ciphertext).digest)) fmac_seed = mac(frame_ciphertext) expected_frame_mac = mac(sxor(self.mac_enc(mac()[:16]), fmac_seed[:16]))[:16] if not frame_mac == expected_frame_mac: raise AuthenticationError('invalid frame mac') return aes(frame_ciphertext)[:body_size] def decrypt(self, data): header = self.decrypt_header(data[:32]) body_size = struct.unpack('>I', '\x00' + header[:3])[0] if not len(data) >= 32 + ceil16(body_size) + 16: raise FormatError('insufficient body length') frame = self.decrypt_body(data[32:], body_size) return dict(header=header, frame=frame, bytes_read=32 + ceil16(len(frame)) + 16) ### handshake auth message handling def create_auth_message(self, remote_pubkey, ephemeral_privkey=None, nonce=None): """ 1. initiator generates ecdhe-random and nonce and creates auth 2. initiator connects to remote and sends auth New: E(remote-pubk, S(ephemeral-privk, ecdh-shared-secret ^ nonce) || H(ephemeral-pubk) || pubk || nonce || 0x0 ) Known: E(remote-pubk, S(ephemeral-privk, token ^ nonce) || H(ephemeral-pubk) || pubk || nonce || 0x1) """ assert self.is_initiator if not self.ecc.is_valid_key(remote_pubkey): raise InvalidKeyError('invalid remote pubkey') self.remote_pubkey = remote_pubkey ecdh_shared_secret = self.ecc.get_ecdh_key(remote_pubkey) token = ecdh_shared_secret flag = 0x0 self.initiator_nonce = nonce or sha3( ienc(random.randint(0, 2**256 - 1))) assert len(self.initiator_nonce) == 32 token_xor_nonce = sxor(token, self.initiator_nonce) assert len(token_xor_nonce) == 32 ephemeral_pubkey = self.ephemeral_ecc.raw_pubkey assert len(ephemeral_pubkey) == 512 / 8 if not self.ecc.is_valid_key(ephemeral_pubkey): raise InvalidKeyError('invalid ephemeral pubkey') # S(ephemeral-privk, ecdh-shared-secret ^ nonce) S = self.ephemeral_ecc.sign(token_xor_nonce) assert len(S) == 65 # S || H(ephemeral-pubk) || pubk || nonce || 0x0 auth_message = S + sha3(ephemeral_pubkey) + self.ecc.raw_pubkey + \ self.initiator_nonce + chr(flag) assert len(auth_message) == 65 + 32 + 64 + 32 + 1 == 194 return auth_message eip8_auth_sedes = sedes.List( [ sedes.Binary(min_length=65, max_length=65), # sig sedes.Binary(min_length=64, max_length=64), # pubkey sedes.Binary(min_length=32, max_length=32), # nonce sedes.BigEndianInt() # version ], strict=False) def encrypt_auth_message(self, auth_message, remote_pubkey=None): assert self.is_initiator remote_pubkey = remote_pubkey or self.remote_pubkey self.auth_init = self.ecc.ecies_encrypt(auth_message, remote_pubkey) assert len(self.auth_init) == 307 return self.auth_init def decode_authentication(self, ciphertext): """ 3. optionally, remote decrypts and verifies auth (checks that recovery of signature == H(ephemeral-pubk)) 4. remote generates authAck from remote-ephemeral-pubk and nonce (authAck = authRecipient handshake) optional: remote derives secrets and preemptively sends protocol-handshake (steps 9,11,8,10) """ assert not self.is_initiator assert len(ciphertext) >= 307 try: (size, sig, initiator_pubkey, nonce, version) = self.decode_auth_plain(ciphertext) except AuthenticationError: (size, sig, initiator_pubkey, nonce, version) = self.decode_auth_eip8(ciphertext) self.got_eip8_auth = True self.auth_init = ciphertext[:size] # recover initiator ephemeral pubkey from sig # S(ephemeral-privk, ecdh-shared-secret ^ nonce) token = self.ecc.get_ecdh_key(initiator_pubkey) self.remote_ephemeral_pubkey = ecdsa_recover(sxor(token, nonce), sig) if not self.ecc.is_valid_key(self.remote_ephemeral_pubkey): raise InvalidKeyError('invalid remote ephemeral pubkey') self.initiator_nonce = nonce self.remote_pubkey = initiator_pubkey self.remote_version = version return ciphertext[size:] def decode_auth_plain(self, ciphertext): """ decode legacy pre-EIP-8 auth message format """ try: message = self.ecc.ecies_decrypt(ciphertext[:307]) except RuntimeError as e: raise AuthenticationError(e) assert len(message) == 194 signature = message[:65] pubkey = message[65 + 32:65 + 32 + 64] if not self.ecc.is_valid_key(pubkey): raise InvalidKeyError('invalid initiator pubkey') nonce = message[65 + 32 + 64:65 + 32 + 64 + 32] known_flag = bool(ord(message[65 + 32 + 64 + 32:])) assert known_flag == 0 return (307, signature, pubkey, nonce, 4) def decode_auth_eip8(self, ciphertext): """ decode EIP-8 auth message format """ size = struct.unpack('>H', ciphertext[:2])[0] + 2 assert len(ciphertext) >= size try: message = self.ecc.ecies_decrypt(ciphertext[2:size], shared_mac_data=ciphertext[:2]) except RuntimeError as e: raise AuthenticationError(e) values = rlp.decode(message, sedes=self.eip8_auth_sedes, strict=False) assert len(values) >= 4 return (size, ) + values[:4] ### handshake ack message handling def create_auth_ack_message(self, version=supported_rlpx_version, eip8=False, ephemeral_pubkey=None, nonce=None): """ authRecipient = E(remote-pubk, remote-ephemeral-pubk || nonce || 0x1) // token found authRecipient = E(remote-pubk, remote-ephemeral-pubk || nonce || 0x0) // token not found nonce, ephemeral_pubkey, version are local! """ assert not self.is_initiator ephemeral_pubkey = ephemeral_pubkey or self.ephemeral_ecc.raw_pubkey self.responder_nonce = nonce or sha3( ienc(random.randint(0, 2**256 - 1))) if eip8 or self.got_eip8_auth: msg = self.create_eip8_auth_ack_message(ephemeral_pubkey, self.responder_nonce, version) assert len(msg) > 97 else: msg = ephemeral_pubkey + self.responder_nonce + '\x00' assert len(msg) == 97 return msg eip8_ack_sedes = sedes.List( [ sedes.Binary(min_length=64, max_length=64), # ephemeral pubkey sedes.Binary(min_length=32, max_length=32), # nonce sedes.BigEndianInt() # version ], strict=False) def create_eip8_auth_ack_message(self, ephemeral_pubkey, nonce, version): data = rlp.encode((ephemeral_pubkey, nonce, version), sedes=self.eip8_ack_sedes) pad = os.urandom(random.randint(100, 250)) return data + pad def encrypt_auth_ack_message(self, ack_message, eip8=False, remote_pubkey=None): assert not self.is_initiator remote_pubkey = remote_pubkey or self.remote_pubkey if eip8 or self.got_eip8_auth: # The EIP-8 version has an authenticated length prefix. prefix = struct.pack( '>H', len(ack_message) + self.ecc.ecies_encrypt_overhead_length) self.auth_ack = self.ecc.ecies_encrypt(ack_message, remote_pubkey, shared_mac_data=prefix) self.auth_ack = prefix + self.auth_ack else: self.auth_ack = self.ecc.ecies_encrypt(ack_message, remote_pubkey) assert len(self.auth_ack) == 210 return self.auth_ack def decode_auth_ack_message(self, ciphertext): assert self.is_initiator assert len(ciphertext) >= 210 try: (size, eph_pubkey, nonce, version) = self.decode_ack_plain(ciphertext) except AuthenticationError: (size, eph_pubkey, nonce, version) = self.decode_ack_eip8(ciphertext) self.got_eip8_ack = True self.auth_ack = ciphertext[:size] self.remote_ephemeral_pubkey = eph_pubkey[:64] self.responder_nonce = nonce self.remote_version = version if not self.ecc.is_valid_key(self.remote_ephemeral_pubkey): raise InvalidKeyError('invalid remote ephemeral pubkey') return ciphertext[size:] def decode_ack_plain(self, ciphertext): """ decode legacy pre-EIP-8 ack message format """ try: message = self.ecc.ecies_decrypt(ciphertext[:210]) except RuntimeError as e: raise AuthenticationError(e) assert len(message) == 64 + 32 + 1 eph_pubkey = message[:64] nonce = message[64:64 + 32] known = ord(message[-1]) assert known == 0 return (210, eph_pubkey, nonce, 4) def decode_ack_eip8(self, ciphertext): """ decode EIP-8 ack message format """ size = struct.unpack('>H', ciphertext[:2])[0] + 2 assert len(ciphertext) == size try: message = self.ecc.ecies_decrypt(ciphertext[2:size], shared_mac_data=ciphertext[:2]) except RuntimeError as e: raise AuthenticationError(e) values = rlp.decode(message, sedes=self.eip8_ack_sedes, strict=False) assert len(values) >= 3 return (size, ) + values[:3] ### handshake key derivation def setup_cipher(self): assert self.responder_nonce assert self.initiator_nonce assert self.auth_init assert self.auth_ack assert self.remote_ephemeral_pubkey if not self.ecc.is_valid_key(self.remote_ephemeral_pubkey): raise InvalidKeyError('invalid remote ephemeral pubkey') # derive base secrets from ephemeral key agreement # ecdhe-shared-secret = ecdh.agree(ephemeral-privkey, remote-ephemeral-pubk) ecdhe_shared_secret = self.ephemeral_ecc.get_ecdh_key( self.remote_ephemeral_pubkey) # shared-secret = sha3(ecdhe-shared-secret || sha3(nonce || initiator-nonce)) shared_secret = sha3(ecdhe_shared_secret + sha3(self.responder_nonce + self.initiator_nonce)) self.ecdhe_shared_secret = ecdhe_shared_secret # used in tests self.shared_secret = shared_secret # used in tests # token = sha3(shared-secret) self.token = sha3(shared_secret) # aes-secret = sha3(ecdhe-shared-secret || shared-secret) self.aes_secret = sha3(ecdhe_shared_secret + shared_secret) # mac-secret = sha3(ecdhe-shared-secret || aes-secret) self.mac_secret = sha3(ecdhe_shared_secret + self.aes_secret) # setup sha3 instances for the MACs # egress-mac = sha3.update(mac-secret ^ recipient-nonce || auth-sent-init) mac1 = sha3_256( sxor(self.mac_secret, self.responder_nonce) + self.auth_init) # ingress-mac = sha3.update(mac-secret ^ initiator-nonce || auth-recvd-ack) mac2 = sha3_256( sxor(self.mac_secret, self.initiator_nonce) + self.auth_ack) if self.is_initiator: self.egress_mac, self.ingress_mac = mac1, mac2 else: self.egress_mac, self.ingress_mac = mac2, mac1 ciphername = 'aes-256-ctr' iv = "\x00" * 16 assert len(iv) == 16 self.aes_enc = pyelliptic.Cipher(self.aes_secret, iv, 1, ciphername=ciphername) self.aes_dec = pyelliptic.Cipher(self.aes_secret, iv, 0, ciphername=ciphername) self.mac_enc = AES.new(self.mac_secret, AES.MODE_ECB).encrypt self.is_ready = True