async def test_handshake(): # TODO: this test should be re-written to not depend on functionality in the `ETHPeer` class. cancel_token = CancelToken("test_handshake") use_eip8 = False initiator_remote = kademlia.Node( keys.PrivateKey(test_values["receiver_private_key"]).public_key, kademlia.Address("0.0.0.0", 0, 0), ) initiator = HandshakeInitiator( initiator_remote, keys.PrivateKey(test_values["initiator_private_key"]), use_eip8, cancel_token, ) initiator.ephemeral_privkey = keys.PrivateKey( test_values["initiator_ephemeral_private_key"]) responder_remote = kademlia.Node( keys.PrivateKey(test_values["initiator_private_key"]).public_key, kademlia.Address("0.0.0.0", 0, 0), ) responder = HandshakeResponder( responder_remote, keys.PrivateKey(test_values["receiver_private_key"]), use_eip8, cancel_token, ) responder.ephemeral_privkey = keys.PrivateKey( test_values["receiver_ephemeral_private_key"]) # Check that the auth message generated by the initiator is what we expect. Notice that we # can't use the auth_init generated here because the non-deterministic prefix would cause the # derived secrets to not match the expected values. _auth_init = initiator.create_auth_message(test_values["initiator_nonce"]) assert len(_auth_init) == len(test_values["auth_plaintext"]) assert (_auth_init[65:] == test_values["auth_plaintext"][65:] ) # starts with non deterministic k # Check that encrypting and decrypting the auth_init gets us the orig msg. _auth_init_ciphertext = initiator.encrypt_auth_message(_auth_init) assert _auth_init == ecies.decrypt(_auth_init_ciphertext, responder.privkey) # Check that the responder correctly decodes the auth msg. auth_msg_ciphertext = test_values["auth_ciphertext"] initiator_ephemeral_pubkey, initiator_nonce, _ = decode_authentication( auth_msg_ciphertext, responder.privkey) assert initiator_nonce == test_values["initiator_nonce"] assert initiator_ephemeral_pubkey == (keys.PrivateKey( test_values["initiator_ephemeral_private_key"]).public_key) # Check that the auth_ack msg generated by the responder is what we expect. auth_ack_msg = responder.create_auth_ack_message( test_values["receiver_nonce"]) assert auth_ack_msg == test_values["authresp_plaintext"] # Check that the secrets derived from ephemeral key agreements match the expected values. auth_ack_ciphertext = test_values["authresp_ciphertext"] aes_secret, mac_secret, egress_mac, ingress_mac = responder.derive_secrets( initiator_nonce, test_values["receiver_nonce"], initiator_ephemeral_pubkey, auth_msg_ciphertext, auth_ack_ciphertext, ) assert aes_secret == test_values["aes_secret"] assert mac_secret == test_values["mac_secret"] # Test values are from initiator perspective, so they're reversed here. assert ingress_mac.digest() == test_values["initial_egress_MAC"] assert egress_mac.digest() == test_values["initial_ingress_MAC"] # Check that the initiator secrets match as well. responder_ephemeral_pubkey, responder_nonce = initiator.decode_auth_ack_message( test_values["authresp_ciphertext"]) ( initiator_aes_secret, initiator_mac_secret, initiator_egress_mac, initiator_ingress_mac, ) = initiator.derive_secrets( initiator_nonce, responder_nonce, responder_ephemeral_pubkey, auth_msg_ciphertext, auth_ack_ciphertext, ) assert initiator_aes_secret == aes_secret assert initiator_mac_secret == mac_secret assert initiator_ingress_mac.digest() == test_values["initial_ingress_MAC"] assert initiator_egress_mac.digest() == test_values["initial_egress_MAC"] # Finally, check that two Peers configured with the secrets generated above understand each # other. ( (responder_reader, responder_writer), (initiator_reader, initiator_writer), ) = get_directly_connected_streams() initiator_connection = PeerConnection( reader=initiator_reader, writer=initiator_writer, aes_secret=initiator_aes_secret, mac_secret=initiator_mac_secret, egress_mac=initiator_egress_mac, ingress_mac=initiator_ingress_mac, ) initiator_peer = ParagonPeer( remote=initiator.remote, privkey=initiator.privkey, connection=initiator_connection, context=ParagonContext(), ) initiator_peer.base_protocol.send_handshake() responder_connection = PeerConnection( reader=responder_reader, writer=responder_writer, aes_secret=aes_secret, mac_secret=mac_secret, egress_mac=egress_mac, ingress_mac=ingress_mac, ) responder_peer = ParagonPeer( remote=responder.remote, privkey=responder.privkey, connection=responder_connection, context=ParagonContext(), ) responder_peer.base_protocol.send_handshake() # The handshake msgs sent by each peer (above) are going to be fed directly into their remote's # reader, and thus the read_msg() calls will return immediately. responder_hello, _ = await responder_peer.read_msg() initiator_hello, _ = await initiator_peer.read_msg() assert isinstance(responder_hello, Hello) assert isinstance(initiator_hello, Hello)
async def _receive_handshake(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None: msg = await self.wait(reader.read(ENCRYPTED_AUTH_MSG_LEN), timeout=REPLY_TIMEOUT) ip, socket, *_ = writer.get_extra_info("peername") remote_address = Address(ip, socket) self.logger.debug("Receiving handshake from %s", remote_address) got_eip8 = False try: ephem_pubkey, initiator_nonce, initiator_pubkey = decode_authentication( msg, self.privkey) except DecryptionError: # Try to decode as EIP8 got_eip8 = True msg_size = big_endian_to_int(msg[:2]) remaining_bytes = msg_size - ENCRYPTED_AUTH_MSG_LEN + 2 msg += await self.wait(reader.read(remaining_bytes), timeout=REPLY_TIMEOUT) try: ephem_pubkey, initiator_nonce, initiator_pubkey = decode_authentication( msg, self.privkey) except DecryptionError as e: self.logger.debug("Failed to decrypt handshake: %s", e) return initiator_remote = Node(initiator_pubkey, remote_address) responder = HandshakeResponder(initiator_remote, self.privkey, got_eip8, self.cancel_token) responder_nonce = numpy.random.bytes(HASH_LEN) auth_ack_msg = responder.create_auth_ack_message(responder_nonce) auth_ack_ciphertext = responder.encrypt_auth_ack_message(auth_ack_msg) # Use the `writer` to send the reply to the remote writer.write(auth_ack_ciphertext) await self.wait(writer.drain()) # Call `HandshakeResponder.derive_shared_secrets()` and use return values to create `Peer` aes_secret, mac_secret, egress_mac, ingress_mac = responder.derive_secrets( initiator_nonce=initiator_nonce, responder_nonce=responder_nonce, remote_ephemeral_pubkey=ephem_pubkey, auth_init_ciphertext=msg, auth_ack_ciphertext=auth_ack_ciphertext, ) connection = PeerConnection( reader=reader, writer=writer, aes_secret=aes_secret, mac_secret=mac_secret, egress_mac=egress_mac, ingress_mac=ingress_mac, ) # Create and register peer in peer_pool peer = self.peer_pool.get_peer_factory().create_peer( remote=initiator_remote, connection=connection, inbound=True) if self.peer_pool.is_full: await peer.disconnect(DisconnectReason.too_many_peers) return elif not self.peer_pool.is_valid_connection_candidate(peer.remote): await peer.disconnect(DisconnectReason.useless_peer) return total_peers = len(self.peer_pool) inbound_peer_count = len([ peer for peer in self.peer_pool.connected_nodes.values() if peer.inbound ]) if total_peers > 1 and inbound_peer_count / total_peers > DIAL_IN_OUT_RATIO: # make sure to have at least 1/4 outbound connections await peer.disconnect(DisconnectReason.too_many_peers) else: # We use self.wait() here as a workaround for # https://github.com/ethereum/py-evm/issues/670. await self.wait(self.do_handshake(peer))
async def test_handshake_eip8(): cancel_token = CancelToken("test_handshake_eip8") use_eip8 = True initiator_remote = kademlia.Node( keys.PrivateKey(eip8_values["receiver_private_key"]).public_key, kademlia.Address("0.0.0.0", 0, 0), ) initiator = HandshakeInitiator( initiator_remote, keys.PrivateKey(eip8_values["initiator_private_key"]), use_eip8, cancel_token, ) initiator.ephemeral_privkey = keys.PrivateKey( eip8_values["initiator_ephemeral_private_key"]) responder_remote = kademlia.Node( keys.PrivateKey(eip8_values["initiator_private_key"]).public_key, kademlia.Address("0.0.0.0", 0, 0), ) responder = HandshakeResponder( responder_remote, keys.PrivateKey(eip8_values["receiver_private_key"]), use_eip8, cancel_token, ) responder.ephemeral_privkey = keys.PrivateKey( eip8_values["receiver_ephemeral_private_key"]) auth_init_ciphertext = eip8_values["auth_init_ciphertext"] # Check that we can decrypt/decode the EIP-8 auth init message. initiator_ephemeral_pubkey, initiator_nonce, _ = decode_authentication( auth_init_ciphertext, responder.privkey) assert initiator_nonce == eip8_values["initiator_nonce"] assert initiator_ephemeral_pubkey == (keys.PrivateKey( eip8_values["initiator_ephemeral_private_key"]).public_key) responder_nonce = eip8_values["receiver_nonce"] auth_ack_ciphertext = eip8_values["auth_ack_ciphertext"] aes_secret, mac_secret, egress_mac, ingress_mac = responder.derive_secrets( initiator_nonce, responder_nonce, initiator_ephemeral_pubkey, auth_init_ciphertext, auth_ack_ciphertext, ) # Check that the secrets derived by the responder match the expected values. assert aes_secret == eip8_values["expected_aes_secret"] assert mac_secret == eip8_values["expected_mac_secret"] # Also according to https://github.com/ethereum/EIPs/blob/master/EIPS/eip-8.md, running B's # ingress-mac keccak state on the string "foo" yields the following hash: ingress_mac_copy = ingress_mac.copy() ingress_mac_copy.update(b"foo") assert ingress_mac_copy.digest().hex() == ( "0c7ec6340062cc46f5e9f1e3cf86f8c8c403c5a0964f5df0ebd34a75ddc86db5") responder_ephemeral_pubkey, responder_nonce = initiator.decode_auth_ack_message( auth_ack_ciphertext) ( initiator_aes_secret, initiator_mac_secret, initiator_egress_mac, initiator_ingress_mac, ) = initiator.derive_secrets( initiator_nonce, responder_nonce, responder_ephemeral_pubkey, auth_init_ciphertext, auth_ack_ciphertext, ) # Check that the secrets derived by the initiator match the expected values. assert initiator_aes_secret == eip8_values["expected_aes_secret"] assert initiator_mac_secret == eip8_values["expected_mac_secret"] # Finally, check that two Peers configured with the secrets generated above understand each # other. ( (responder_reader, responder_writer), (initiator_reader, initiator_writer), ) = get_directly_connected_streams() initiator_connection = PeerConnection( reader=initiator_reader, writer=initiator_writer, aes_secret=initiator_aes_secret, mac_secret=initiator_mac_secret, egress_mac=initiator_egress_mac, ingress_mac=initiator_ingress_mac, ) initiator_peer = ParagonPeer( remote=initiator.remote, privkey=initiator.privkey, connection=initiator_connection, context=ParagonContext(), ) initiator_peer.base_protocol.send_handshake() responder_connection = PeerConnection( reader=responder_reader, writer=responder_writer, aes_secret=aes_secret, mac_secret=mac_secret, egress_mac=egress_mac, ingress_mac=ingress_mac, ) responder_peer = ParagonPeer( remote=responder.remote, privkey=responder.privkey, connection=responder_connection, context=ParagonContext(), ) responder_peer.base_protocol.send_handshake() # The handshake msgs sent by each peer (above) are going to be fed directly into their remote's # reader, and thus the read_msg() calls will return immediately. responder_hello, _ = await responder_peer.read_msg() initiator_hello, _ = await initiator_peer.read_msg() assert isinstance(responder_hello, Hello) assert isinstance(initiator_hello, Hello)