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)
示例#2
0
    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)