Ejemplo n.º 1
0
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
Ejemplo n.º 2
0
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
Ejemplo n.º 3
0
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)
Ejemplo n.º 4
0
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)
Ejemplo n.º 5
0
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