def MultiplexerPairFactory(*, protocol_types: Tuple[Type[ProtocolAPI], ...] = (), transport_factory: Callable[..., TransportPair] = MemoryTransportPairFactory, # noqa: E501 alice_remote: NodeAPI = None, alice_private_key: keys.PrivateKey = None, alice_p2p_version: int = DEVP2P_V5, bob_remote: NodeAPI = None, bob_private_key: keys.PrivateKey = None, bob_p2p_version: int = DEVP2P_V5, cancel_token: CancelToken = None, ) -> Tuple[MultiplexerAPI, MultiplexerAPI]: if cancel_token is None: cancel_token = CancelTokenFactory(name='multiplexer-factory') alice_transport, bob_transport = transport_factory( alice_remote=alice_remote, alice_private_key=alice_private_key, bob_remote=bob_remote, bob_private_key=bob_private_key, ) snappy_support = alice_p2p_version >= DEVP2P_V5 and bob_p2p_version >= DEVP2P_V5 cmd_id_offsets = get_cmd_offsets(protocol_types) alice_protocols = tuple( protocol_class(alice_transport, offset, snappy_support) for protocol_class, offset in zip(protocol_types, cmd_id_offsets) ) bob_protocols = tuple( protocol_class(bob_transport, offset, snappy_support) for protocol_class, offset in zip(protocol_types, cmd_id_offsets) ) p2p_protocol_class: Type[BaseP2PProtocol] if snappy_support: p2p_protocol_class = P2PProtocolV5 else: p2p_protocol_class = P2PProtocolV4 alice_p2p_protocol = p2p_protocol_class(alice_transport, 0, snappy_support) alice_multiplexer = Multiplexer( transport=alice_transport, base_protocol=alice_p2p_protocol, protocols=alice_protocols, token=cancel_token, ) bob_p2p_protocol = p2p_protocol_class(bob_transport, 0, snappy_support) bob_multiplexer = Multiplexer( transport=bob_transport, base_protocol=bob_p2p_protocol, protocols=bob_protocols, token=cancel_token, ) return alice_multiplexer, bob_multiplexer
def MultiplexerPairFactory(*, protocol_types: Tuple[Type[ProtocolAPI], ...] = (), transport_factory: Callable[..., TransportPair] = MemoryTransportPairFactory, # noqa: E501 alice_remote: NodeAPI = None, alice_private_key: keys.PrivateKey = None, bob_remote: NodeAPI = None, bob_private_key: keys.PrivateKey = None, snappy_support: bool = False, cancel_token: CancelToken = None, ) -> Tuple[MultiplexerAPI, MultiplexerAPI]: alice_transport, bob_transport = transport_factory( alice_remote=alice_remote, alice_private_key=alice_private_key, bob_remote=bob_remote, bob_private_key=bob_private_key, ) cmd_id_offsets = get_cmd_offsets(protocol_types) alice_protocols = tuple( protocol_class(alice_transport, offset, snappy_support) for protocol_class, offset in zip(protocol_types, cmd_id_offsets) ) bob_protocols = tuple( protocol_class(bob_transport, offset, snappy_support) for protocol_class, offset in zip(protocol_types, cmd_id_offsets) ) alice_p2p_protocol = P2PProtocol(alice_transport, snappy_support) alice_multiplexer = Multiplexer( transport=alice_transport, base_protocol=alice_p2p_protocol, protocols=alice_protocols, token=cancel_token, ) bob_p2p_protocol = P2PProtocol(bob_transport, False) bob_multiplexer = Multiplexer( transport=bob_transport, base_protocol=bob_p2p_protocol, protocols=bob_protocols, token=cancel_token, ) return alice_multiplexer, bob_multiplexer
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() capabilities = (('paragon', 1), ) initiator_transport = Transport(remote=initiator_remote, private_key=initiator.privkey, 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_p2p_protocol = P2PProtocolV5(initiator_transport, 0, False) initiator_multiplexer = Multiplexer( transport=initiator_transport, base_protocol=initiator_p2p_protocol, protocols=(), ) initiator_multiplexer.get_base_protocol().send( Hello( HelloPayload( client_version_string='initiator', capabilities=capabilities, listen_port=30303, version=DEVP2P_V5, remote_public_key=initiator.privkey.public_key.to_bytes(), ))) responder_transport = Transport( remote=responder_remote, private_key=responder.privkey, reader=responder_reader, writer=responder_writer, aes_secret=aes_secret, mac_secret=mac_secret, egress_mac=egress_mac, ingress_mac=ingress_mac, ) responder_p2p_protocol = P2PProtocolV5(responder_transport, 0, False) responder_multiplexer = Multiplexer( transport=responder_transport, base_protocol=responder_p2p_protocol, protocols=(), ) responder_multiplexer.get_base_protocol().send( Hello( HelloPayload( client_version_string='responder', capabilities=capabilities, listen_port=30303, version=DEVP2P_V5, remote_public_key=responder.privkey.public_key.to_bytes(), ))) async with initiator_multiplexer.multiplex(): async with responder_multiplexer.multiplex(): initiator_stream = initiator_multiplexer.stream_protocol_messages( initiator_p2p_protocol, ) responder_stream = responder_multiplexer.stream_protocol_messages( responder_p2p_protocol, ) initiator_hello = await asyncio.wait_for( initiator_stream.asend(None), timeout=0.1) responder_hello = await asyncio.wait_for( responder_stream.asend(None), timeout=0.1) await initiator_stream.aclose() await responder_stream.aclose() assert isinstance(responder_hello, Hello) assert isinstance(initiator_hello, Hello)
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.hexdigest() == ( '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() capabilities = (('testing', 1), ) initiator_transport = Transport(remote=initiator_remote, private_key=initiator.privkey, 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_p2p_protocol = P2PProtocolV5(initiator_transport, 0, False) initiator_multiplexer = Multiplexer( transport=initiator_transport, base_protocol=initiator_p2p_protocol, protocols=(), ) initiator_multiplexer.get_base_protocol().send( Hello( HelloPayload( client_version_string='initiator', capabilities=capabilities, listen_port=30303, version=DEVP2P_V5, remote_public_key=initiator.privkey.public_key.to_bytes(), ))) responder_transport = Transport( remote=responder_remote, private_key=responder.privkey, reader=responder_reader, writer=responder_writer, aes_secret=aes_secret, mac_secret=mac_secret, egress_mac=egress_mac, ingress_mac=ingress_mac, ) responder_p2p_protocol = P2PProtocolV4(responder_transport, 0, False) responder_multiplexer = Multiplexer( transport=responder_transport, base_protocol=responder_p2p_protocol, protocols=(), ) responder_multiplexer.get_base_protocol().send( Hello( HelloPayload( client_version_string='responder', capabilities=capabilities, listen_port=30303, version=DEVP2P_V4, remote_public_key=responder.privkey.public_key.to_bytes(), ))) async with initiator_multiplexer.multiplex(): async with responder_multiplexer.multiplex(): initiator_stream = initiator_multiplexer.stream_protocol_messages( initiator_p2p_protocol, ) responder_stream = responder_multiplexer.stream_protocol_messages( responder_p2p_protocol, ) initiator_hello = await initiator_stream.asend(None) responder_hello = await responder_stream.asend(None) await initiator_stream.aclose() await responder_stream.aclose() assert isinstance(responder_hello, Hello) assert isinstance(initiator_hello, Hello)
async def negotiate_protocol_handshakes( transport: TransportAPI, p2p_handshake_params: DevP2PHandshakeParams, protocol_handshakers: Sequence[HandshakerAPI[ProtocolAPI]], ) -> Tuple[MultiplexerAPI, DevP2PReceipt, Tuple[HandshakeReceiptAPI, ...]]: # noqa: E501 """ Negotiate the handshakes for both the base `p2p` protocol and the appropriate sub protocols. The basic logic follows the following steps. * perform the base `p2p` handshake. * using the capabilities exchanged during the `p2p` handshake, select the appropriate sub protocols. * allow each sub-protocol to perform its own handshake. * return the established `Multiplexer` as well as the `HandshakeReceipt` objects from each handshake. """ # The `p2p` Protocol class that will be used. p2p_protocol_class = p2p_handshake_params.get_base_protocol_class() # Collect our local capabilities, the set of (name, version) pairs for all # of the protocols that we support. local_capabilities = tuple(handshaker.protocol_class.as_capability() for handshaker in protocol_handshakers) # Verify that there are no duplicated local or remote capabilities duplicate_capabilities = duplicates(local_capabilities) if duplicate_capabilities: raise Exception( f"Duplicate local capabilities: {duplicate_capabilities}") # We create an *ephemeral* version of the base `p2p` protocol with snappy # compression disabled for the handshake. As part of the handshake, a new # instance of this protocol will be created with snappy compression enabled # if it is supported by the protocol version. ephemeral_base_protocol = p2p_protocol_class( transport, command_id_offset=0, snappy_support=False, ) # Perform the actual `p2p` protocol handshake. We need the remote # capabilities data from the receipt to select the appropriate sub # protocols. devp2p_receipt, base_protocol = await _do_p2p_handshake( transport, local_capabilities, p2p_handshake_params, ephemeral_base_protocol, ) # This data structure is simply for easy retrieval of the proper # `Handshaker` for each selected protocol. protocol_handshakers_by_capability = dict( zip(local_capabilities, protocol_handshakers)) # Using our local capabilities and the ones transmitted by the remote # select the highest shared version of each shared protocol. selected_capabilities = _select_capabilities( devp2p_receipt.capabilities, local_capabilities, ) # If there are no capability matches throw an exception. if len(selected_capabilities) < 1: raise NoMatchingPeerCapabilities( "Found no matching capabilities between self and peer:\n" f" - local : {tuple(sorted(local_capabilities))}\n" f" - remote: {devp2p_receipt.capabilities}") # Retrieve the handshakers which correspond to the selected protocols. # These are needed to perform the actual handshake logic for each protocol. selected_handshakers = tuple(protocol_handshakers_by_capability[capability] for capability in selected_capabilities) # Grab the `Protocol` class for each of the selected protocols. We need # this to compute the offsets for each protocol's command ids, as well as # for instantiation of the protocol instances. selected_protocol_types = tuple(handshaker.protocol_class for handshaker in selected_handshakers) # Compute the offsets for each protocol's command ids protocol_cmd_offsets = get_cmd_offsets(selected_protocol_types) # Now instantiate instances of each of the protocol classes. selected_protocols = tuple( protocol_class(transport, command_id_offset, base_protocol.snappy_support) for protocol_class, command_id_offset in zip(selected_protocol_types, protocol_cmd_offsets)) # Create `Multiplexer` to abstract all of the protocols into a single # interface to stream only messages relevant to the given protocol. multiplexer = Multiplexer(transport, base_protocol, selected_protocols) # This context manager runs a background task which reads messages off of # the `Transport` and feeds them into protocol specific queues. Each # protocol is responsible for reading its own messages from that queue via # the `Multiplexer.stream_protocol_messages` API. await multiplexer.stream_in_background() # Concurrently perform the handshakes for each protocol, gathering up # the returned receipts. try: protocol_receipts = cast( Tuple[HandshakeReceiptAPI, ...], await asyncio.gather(*(handshaker.do_handshake(multiplexer, protocol) for handshaker, protocol in zip( selected_handshakers, selected_protocols)))) except BaseException as handshake_err: # If the multiplexer has a streaming error, that will certainly be the cause of # whatever handshake error we got, so raise that instead. multiplexer.raise_if_streaming_error() # Ok, no streaming error from the multiplexer, so stop it and raise the handshake error. await multiplexer.stop_streaming() raise handshake_err else: # The handshake was successful, but there's a chance the multiplexer's streaming stopped # after that, so we may raise that here to prevent an attempt to use a stopped multiplexer # further. multiplexer.raise_if_streaming_error() # Return the `Multiplexer` object as well as the handshake receipts. The # `Multiplexer` object acts as a container for the individual protocol # instances. return multiplexer, devp2p_receipt, protocol_receipts