p2p_v2_misbehaving.py raw

   1  #!/usr/bin/env python3
   2  # Copyright (c) 2022-present The Bitcoin Core developers
   3  # Distributed under the MIT software license, see the accompanying
   4  # file COPYING or http://www.opensource.org/licenses/mit-license.php.
   5  
   6  import random
   7  import time
   8  from enum import Enum
   9  
  10  from test_framework.messages import MAGIC_BYTES
  11  from test_framework.p2p import P2PInterface
  12  from test_framework.test_framework import BitcoinTestFramework
  13  from test_framework.util import random_bitflip
  14  from test_framework.v2_p2p import (
  15      EncryptedP2PState,
  16      MAX_GARBAGE_LEN,
  17  )
  18  
  19  
  20  class TestType(Enum):
  21      """ Scenarios to be tested:
  22  
  23      1. EARLY_KEY_RESPONSE - The responder needs to wait until one byte is received which does not match the 16 bytes
  24      consisting of network magic followed by "version\x00\x00\x00\x00\x00" before sending out its ellswift + garbage bytes
  25      2. EXCESS_GARBAGE - Disconnection happens when > MAX_GARBAGE_LEN bytes garbage is sent
  26      3. WRONG_GARBAGE_TERMINATOR - Disconnection happens when incorrect garbage terminator is sent
  27      4. WRONG_GARBAGE - Disconnection happens when garbage bytes that is sent is different from what the peer receives
  28      5. SEND_NO_AAD - Disconnection happens when AAD of first encrypted packet after the garbage terminator is not filled
  29      6. SEND_NON_EMPTY_VERSION_PACKET - non-empty version packet is simply ignored
  30      """
  31      EARLY_KEY_RESPONSE = 0
  32      EXCESS_GARBAGE = 1
  33      WRONG_GARBAGE_TERMINATOR = 2
  34      WRONG_GARBAGE = 3
  35      SEND_NO_AAD = 4
  36      SEND_NON_EMPTY_VERSION_PACKET = 5
  37  
  38  
  39  class EarlyKeyResponseState(EncryptedP2PState):
  40      """ Modify v2 P2P protocol functions for testing EARLY_KEY_RESPONSE scenario"""
  41      def __init__(self, initiating, net):
  42          super().__init__(initiating=initiating, net=net)
  43          self.can_data_be_received = False  # variable used to assert if data is received on recvbuf.
  44  
  45      def initiate_v2_handshake(self):
  46          """Send ellswift and garbage bytes in 2 parts when TestType = (EARLY_KEY_RESPONSE)"""
  47          self.generate_keypair_and_garbage()
  48          return b""
  49  
  50  
  51  class ExcessGarbageState(EncryptedP2PState):
  52      """Generate > MAX_GARBAGE_LEN garbage bytes"""
  53      def generate_keypair_and_garbage(self):
  54          garbage_len = MAX_GARBAGE_LEN + random.randrange(1, MAX_GARBAGE_LEN + 1)
  55          return super().generate_keypair_and_garbage(garbage_len)
  56  
  57  
  58  class WrongGarbageTerminatorState(EncryptedP2PState):
  59      """Add option for sending wrong garbage terminator"""
  60      def generate_keypair_and_garbage(self):
  61          garbage_len = random.randrange(MAX_GARBAGE_LEN//2)
  62          return super().generate_keypair_and_garbage(garbage_len)
  63  
  64      def complete_handshake(self, response):
  65          length, handshake_bytes = super().complete_handshake(response)
  66          # first 16 bytes returned by complete_handshake() is the garbage terminator
  67          wrong_garbage_terminator = random_bitflip(handshake_bytes[:16])
  68          return length, wrong_garbage_terminator + handshake_bytes[16:]
  69  
  70  
  71  class WrongGarbageState(EncryptedP2PState):
  72      """Generate tampered garbage bytes"""
  73      def generate_keypair_and_garbage(self):
  74          garbage_len = random.randrange(1, MAX_GARBAGE_LEN)
  75          ellswift_garbage_bytes = super().generate_keypair_and_garbage(garbage_len)
  76          # assume that garbage bytes sent to TestNode were tampered with
  77          return ellswift_garbage_bytes[:64] + random_bitflip(ellswift_garbage_bytes[64:])
  78  
  79  
  80  class NoAADState(EncryptedP2PState):
  81      """Add option for not filling first encrypted packet after garbage terminator with AAD"""
  82      def generate_keypair_and_garbage(self):
  83          garbage_len = random.randrange(1, MAX_GARBAGE_LEN)
  84          return super().generate_keypair_and_garbage(garbage_len)
  85  
  86      def complete_handshake(self, response):
  87          self.sent_garbage = b''  # do not authenticate the garbage which is sent
  88          return super().complete_handshake(response)
  89  
  90  
  91  class NonEmptyVersionPacketState(EncryptedP2PState):
  92      """"Add option for sending non-empty transport version packet."""
  93      def complete_handshake(self, response):
  94          self.transport_version = random.randbytes(5)
  95          return super().complete_handshake(response)
  96  
  97  
  98  class MisbehavingV2Peer(P2PInterface):
  99      """Custom implementation of P2PInterface which uses modified v2 P2P protocol functions for testing purposes."""
 100      def __init__(self, test_type):
 101          super().__init__()
 102          self.test_type = test_type
 103  
 104      def connection_made(self, transport):
 105          if self.test_type == TestType.EARLY_KEY_RESPONSE:
 106              self.v2_state = EarlyKeyResponseState(initiating=True, net='regtest')
 107          elif self.test_type == TestType.EXCESS_GARBAGE:
 108              self.v2_state = ExcessGarbageState(initiating=True, net='regtest')
 109          elif self.test_type == TestType.WRONG_GARBAGE_TERMINATOR:
 110              self.v2_state = WrongGarbageTerminatorState(initiating=True, net='regtest')
 111          elif self.test_type == TestType.WRONG_GARBAGE:
 112              self.v2_state = WrongGarbageState(initiating=True, net='regtest')
 113          elif self.test_type == TestType.SEND_NO_AAD:
 114              self.v2_state = NoAADState(initiating=True, net='regtest')
 115          elif TestType.SEND_NON_EMPTY_VERSION_PACKET:
 116              self.v2_state = NonEmptyVersionPacketState(initiating=True, net='regtest')
 117          super().connection_made(transport)
 118  
 119      def data_received(self, t):
 120          if self.test_type == TestType.EARLY_KEY_RESPONSE:
 121              # check that data can be received on recvbuf only when mismatch from V1_PREFIX happens
 122              assert self.v2_state.can_data_be_received
 123          else:
 124              super().data_received(t)
 125  
 126  
 127  class EncryptedP2PMisbehaving(BitcoinTestFramework):
 128      def set_test_params(self):
 129          self.num_nodes = 1
 130          self.extra_args = [["-v2transport=1", "-peertimeout=3"]]
 131  
 132      def run_test(self):
 133          self.test_earlykeyresponse()
 134          self.test_v2disconnection()
 135  
 136      def test_earlykeyresponse(self):
 137          self.log.info('Sending ellswift bytes in parts to ensure that response from responder is received only when')
 138          self.log.info('ellswift bytes have a mismatch from the 16 bytes(network magic followed by "version\\x00\\x00\\x00\\x00\\x00")')
 139          node0 = self.nodes[0]
 140          node0.setmocktime(int(time.time()))
 141          self.log.info('Sending first 4 bytes of ellswift which match network magic')
 142          self.log.info('If a response is received, assertion failure would happen in our custom data_received() function')
 143          with node0.wait_for_new_peer():
 144              peer1 = node0.add_p2p_connection(MisbehavingV2Peer(TestType.EARLY_KEY_RESPONSE), wait_for_verack=False, send_version=False, supports_v2_p2p=True, wait_for_v2_handshake=False)
 145          peer1.send_raw_message(MAGIC_BYTES['regtest'])
 146          self.log.info('Sending remaining ellswift and garbage which are different from V1_PREFIX. Since a response is')
 147          self.log.info('expected now, our custom data_received() function wouldn\'t result in assertion failure')
 148          peer1.v2_state.can_data_be_received = True
 149          self.wait_until(lambda: peer1.v2_state.ellswift_ours)
 150          peer1.send_raw_message(peer1.v2_state.ellswift_ours[4:] + peer1.v2_state.sent_garbage)
 151          # Ensure that the bytes sent after 4 bytes network magic are actually received.
 152          self.wait_until(lambda: node0.getpeerinfo()[-1]["bytesrecv"] > 4)
 153          self.wait_until(lambda: node0.getpeerinfo()[-1]["bytessent"] > 0)
 154          with node0.assert_debug_log(['V2 handshake timeout, disconnecting peer=0']):
 155              node0.bumpmocktime(4)  # `InactivityCheck()` triggers now
 156              peer1.wait_for_disconnect(timeout=1)
 157          self.log.info('successful disconnection since modified ellswift was sent as response')
 158  
 159      def test_v2disconnection(self):
 160          # test v2 disconnection scenarios
 161          node0 = self.nodes[0]
 162          expected_debug_message = [
 163              [],  # EARLY_KEY_RESPONSE
 164              ["V2 transport error: missing garbage terminator"],  # EXCESS_GARBAGE
 165              ["V2 handshake timeout, disconnecting peer"],  # WRONG_GARBAGE_TERMINATOR
 166              ["V2 transport error: packet decryption failure"],  # WRONG_GARBAGE
 167              ["V2 transport error: packet decryption failure"],  # SEND_NO_AAD
 168              [],  # SEND_NON_EMPTY_VERSION_PACKET
 169          ]
 170          for test_type in TestType:
 171              if test_type == TestType.EARLY_KEY_RESPONSE:
 172                  continue
 173              elif test_type == TestType.SEND_NON_EMPTY_VERSION_PACKET:
 174                  node0.add_p2p_connection(MisbehavingV2Peer(test_type), wait_for_verack=True, send_version=True, supports_v2_p2p=True)
 175                  self.log.info(f"No disconnection for {test_type.name}")
 176              else:
 177                  with node0.assert_debug_log(expected_debug_message[test_type.value], timeout=5):
 178                      node0.setmocktime(int(time.time()))
 179                      peer1 = node0.add_p2p_connection(MisbehavingV2Peer(test_type), wait_for_verack=False, send_version=False, supports_v2_p2p=True, expect_success=False)
 180                      # Make a passing connection for more robust disconnection checking.
 181                      peer2 = node0.add_p2p_connection(P2PInterface())
 182                      assert peer2.is_connected
 183                      node0.bumpmocktime(4)  # `InactivityCheck()` triggers now
 184                      peer1.wait_for_disconnect()
 185                  self.log.info(f"Expected disconnection for {test_type.name}")
 186  
 187  
 188  if __name__ == '__main__':
 189      EncryptedP2PMisbehaving(__file__).main()
 190