Skip to content

test/BIP324: functional tests for v2 P2P encryption#24748

Merged
achow101 merged 15 commits intobitcoin:masterfrom
stratospher:p2p-encryption-test
Jan 29, 2024
Merged

test/BIP324: functional tests for v2 P2P encryption#24748
achow101 merged 15 commits intobitcoin:masterfrom
stratospher:p2p-encryption-test

Conversation

@stratospher
Copy link
Contributor

@stratospher stratospher commented Apr 3, 2022

This PR introduces support for v2 P2P encryption(BIP 324) in the existing functional test framework and adds functional tests for the same.

commits overview

  1. introduces a new class EncryptedP2PState to store the keys, functions for performing the initial v2 handshake and encryption/decryption.

  2. this class is used by P2PConnection in inbound/outbound connections to perform the initial v2 handshake before the v1 version handshake. Only after the initial v2 handshake is performed do application layer P2P messages(version, verack etc..) get exchanged. (in a v2 connection)

    • v2_state is the object of class EncryptedP2PState in P2PConnection used to store its keys, session-id etc.
    • a node advertising support for v2 P2P is different from a node actually supporting v2 P2P (differ when false advertisement of services occur)
      • introduce a boolean variable supports_v2_p2p in P2PConnection to denote if it supports v2 P2P.
      • introduce a boolean variable advertises_v2_p2p to denote whether P2PConnection which mimics peer behaviour advertises V2 P2P support. Default option is False.
    • In the test framework, you can create Inbound and Outbound connections to TestNode
      1. During Inbound Connections, P2PConnection is the initiator [TestNode <--------- P2PConnection]
        • Case 1:
          • if the TestNode advertises/signals v2 P2P support (means self.nodes[i] set up with "-v2transport=1"), different behaviour will be exhibited based on whether:
            1. P2PConnection supports v2 P2P
            2. P2PConnection does not support v2 P2P
          • In a real world scenario, the initiator node would intrinsically know if they support v2 P2P based on whatever code they choose to run. However, in the test scenario where we mimic peer behaviour, we have no way of knowing if P2PConnection should support v2 P2P or not. So supports_v2_p2p boolean variable is used as an option to enable support for v2 P2P in P2PConnection.
          • Since the TestNode advertises v2 P2P support (using "-v2transport=1"), our initiator P2PConnection would send:
            1. (if the P2PConnection supports v2 P2P) ellswift + garbage bytes to initiate the connection
            2. (if the P2PConnection does not support v2 P2P) version message to initiate the connection
        • Case 2:
          • if the TestNode doesn't signal v2 P2P support; P2PConnection being the initiator would send version message to initiate a connection.
      2. During Outbound Connections [TestNode --------> P2PConnection]
        • initiator TestNode would send:
          • (if the P2PConnection advertises v2 P2P) ellswift + garbage bytes to initiate the connection
          • (if the P2PConnection advertises v2 P2P) version message to initiate the connection
        • Suppose P2PConnection advertises v2 P2P support when it actually doesn't support v2 P2P (false advertisement scenario)
          • TestNode sends ellswift + garbage bytes
          • P2PConnection receives but can't process it and disconnects.
          • TestNode then tries using v1 P2P and sends version message
          • P2PConnection receives/processes this successfully and they communicate on v1 P2P
  3. the encrypted P2P messages follow a different format - 3 byte length + 1-13 byte message_type + payload + 16 byte MAC

  4. includes support for testing decoy messages and v2 connection downgrade(using false advertisement - when a v2 node makes an outbound connection to a node which doesn't support v2 but is advertised as v2 by some malicious
    intermediary)

run the tests

  • functional test - test/functional/p2p_v2_encrypted.py test/functional/p2p_v2_earlykeyresponse.py

I'm also super grateful to @ dhruv for his really valuable feedback on this branch.
Also written a more elaborate explanation here - https://github.com/stratospher/blogosphere/blob/main/integration_test_bip324.md

@DrahtBot
Copy link
Contributor

DrahtBot commented Apr 5, 2022

The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

Code Coverage

For detailed information about the code coverage, see the test coverage report.

Reviews

See the guideline for information on the review process.

Type Reviewers
ACK mzumsande, theStack, naumenkogs, glozow
Concept ACK jonatack, brunoerg
Approach ACK sr-gi

If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update.

Conflicts

Reviewers, this pull request conflicts with the following ones:

  • #29279 (test: p2p: check disconnect due to lack of desirable service flags by theStack)
  • #29016 (RPC: add new listmempooltransactions by niftynei)
  • #28463 (p2p: Increase inbound capacity for block-relay only connections by mzumsande)

If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

@jonatack
Copy link
Member

jonatack commented Apr 7, 2022

Concept ACK

@brunoerg
Copy link
Contributor

brunoerg commented Apr 8, 2022

Concept ACK

@stratospher stratospher marked this pull request as draft October 3, 2022 17:17
@stratospher
Copy link
Contributor Author

converting this PR into a draft. I'll push the updated version which includes the new spec changes in BIP 324 soon.

stratospher and others added 11 commits January 25, 2024 11:09
Instantiate this object when the connection supports v2 P2P transport
protocol.

- When a P2PConnection is opened, perform initiate_v2_handshake() if the
connection is an initiator. application layer messages are only sent after
the initial v2 handshake is over (for both initiator and responder).
Messages are built, encrypted and sent over the socket in v2
connections. If a race condition happens between python's main
thread and p2p thread with both of them trying to send a message,
it's possible that the messages get encrypted with wrong keystream.

Messages are built and sent over the socket in v1 connections.
So there's no problem if messages are sent in the wrong order.

Co-authored-by: Martin Zumsande <[email protected]>
Co-authored-by: theStack <[email protected]>
…te mismatch

- When a v2 TestNode makes an outbound connection to a P2PInterface node
which doesn't support v2 but is advertised as v2 by some malicious
intermediary, the TestNode sends 64 bytes ellswift. The v1 node doesn't
understand this and disconnects. Then the v2 TestNode reconnects by
sending a v1/version message.
- Add an optional `supports_v2_p2p` parameter to specify if the inbound
and outbound connections support v2 P2P protocol.
- In the `addconnection_callback` which gets called when creating
outbound connections, call the `addconnection` RPC with v2 P2P protocol
support enabled.
Also allow P2PConnection::send_message() to send decoy messages for
writing tests.
…P 324

- A node initiates a v2 connection by sending 64 bytes ellswift
- In BIP 324 "The responder waits until one byte is received which does not match the
  V1_PREFIX (16 bytes consisting of the network magic followed by "version\x00\x00\x00\x00\x00".)"
- It's possible that the 64 bytes ellswift sent by an initiator starts with a prefix of V1_PREFIX
- Example form of 64 bytes ellswift could be:
	4 bytes network magic + 60 bytes which aren't prefixed with remaining V1_PREFIX
- We test this behaviour:
	- when responder receives 4 byte network magic -> no response received by initiator
	- when first mismatch happens -> response received by initiator
@stratospher
Copy link
Contributor Author

There are many more tests possible, right? E.g. sending more than 4095 bytes of garbage.
I assume the goal here wasn't to cover everything?

yes! goal here was to just introduce support for v2 in the test framework. we need more tests after this.

Copy link
Contributor

@mzumsande mzumsande left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review ACK bc9283c

Copy link
Contributor

@theStack theStack left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code-review ACK bc9283c

Went through another review round and only found some minor nits, mostly naming suggestions. If you retouch (or follow-up material):

  • should use multi-line imports (see style guidelines in test/functional/README.md)
  • commit 382894c has a leading space in the commit subject

@naumenkogs
Copy link
Contributor

ACK bc9283c

Copy link
Member

@glozow glozow left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK bc9283c

Did some light code review of the P2PConnection functions, mutation testing of EncryptedP2PState, and manually changing other functional tests to use v2 connections.

@maflcko
Copy link
Member

maflcko commented Jan 30, 2024

The test fails for me:

146/294 - p2p_v2_earlykeyresponse.py failed, Duration: 1 s

stdout:
2024-01-30T13:33:10.051000Z TestFramework (INFO): PRNG seed is: 5518845862592986813
2024-01-30T13:33:10.052000Z TestFramework (INFO): Initializing test directory /tmp/test_runner_₿_🏃_20240130_142948/p2p_v2_earlykeyresponse_133
2024-01-30T13:33:10.370000Z TestFramework (INFO): Sending ellswift bytes in parts to ensure that response from responder is received only when
2024-01-30T13:33:10.370000Z TestFramework (INFO): ellswift bytes have a mismatch from the 16 bytes(network magic followed by "version\x00\x00\x00\x00\x00")
2024-01-30T13:33:10.371000Z TestFramework (INFO): Sending first 4 bytes of ellswift which match network magic
2024-01-30T13:33:10.371000Z TestFramework (INFO): If a response is received, assertion failure would happen in our custom data_received() function
2024-01-30T13:33:10.429000Z TestFramework (INFO): Sending remaining ellswift and garbage which are different from V1_PREFIX. Since a response is
2024-01-30T13:33:10.429000Z TestFramework (INFO): expected now, our custom data_received() function wouldn't result in assertion failure
2024-01-30T13:33:10.480000Z TestFramework.p2p (WARNING): Connection lost to 127.0.0.1:12596 due to 
2024-01-30T13:33:10.518000Z TestFramework (INFO): successful disconnection when MITM happens in the key exchange phase
2024-01-30T13:33:10.519000Z TestFramework (INFO): Stopping nodes
2024-01-30T13:33:10.625000Z TestFramework (INFO): Cleaning up /tmp/test_runner_₿_🏃_20240130_142948/p2p_v2_earlykeyresponse_133 on exit
2024-01-30T13:33:10.625000Z TestFramework (INFO): Tests successful


stderr:
Fatal error: protocol.data_received() call failed.
protocol: <__main__.PeerEarlyKey object at 0x7fe7925dccb0>
transport: <_SelectorSocketTransport fd=10 read=polling write=<idle, bufsize=0>>
Traceback (most recent call last):
  File "python3.12/asyncio/selector_events.py", line 1023, in _read_ready__data_received
    self._protocol.data_received(data)
  File "test/functional/p2p_v2_earlykeyresponse.py", line 60, in data_received
    assert self.v2_state.can_data_be_received and not self.v2_state.send_net_magic
AssertionError


@stratospher
Copy link
Contributor Author

hmm weird, looking into it.

i also have a WIP branch which uses this test file and adds more v2 tests for sending excess garbage bytes, wrong garbage terminator, incorrect ellswift and non-empty version packet.

@sr-gi
Copy link
Member

sr-gi commented Jan 30, 2024

WIP branch

Is this consistently failing for you (or can you at least reproduce it)? I can see it also failing on some of the current open PRs but I can not get my head around why (nor can I reproduce it).

This should only happen if PeerEarlyKey receives any data from the node before sending anything at all, or after sending the network magic but before sending the remaining initial bytes + garbage.

To my understanding, the node should not have been able to parse enough data to be replying (with either v1 or v2) 😕

In case you can reproduce it, it may be useful to log what data_received is receiving

@mzumsande
Copy link
Contributor

mzumsande commented Jan 30, 2024

The test fails for me:

see #29352 for a fix (and a way to reproduce it).

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

Projects

No open projects
Status: Needs review

Development

Successfully merging this pull request may close these issues.