v2_p2p.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  """Class for v2 P2P protocol (see BIP 324)"""
   6  
   7  import random
   8  
   9  from .crypto.bip324_cipher import FSChaCha20Poly1305
  10  from .crypto.chacha20 import FSChaCha20
  11  from .crypto.ellswift import ellswift_create, ellswift_ecdh_xonly
  12  from .crypto.hkdf import hkdf_sha256
  13  from .key import TaggedHash
  14  from .messages import MAGIC_BYTES
  15  from .util import assert_equal
  16  
  17  
  18  CHACHA20POLY1305_EXPANSION = 16
  19  HEADER_LEN = 1
  20  IGNORE_BIT_POS = 7
  21  LENGTH_FIELD_LEN = 3
  22  MAX_GARBAGE_LEN = 4095
  23  
  24  SHORTID = {
  25      1: b"addr",
  26      2: b"block",
  27      3: b"blocktxn",
  28      4: b"cmpctblock",
  29      5: b"feefilter",
  30      6: b"filteradd",
  31      7: b"filterclear",
  32      8: b"filterload",
  33      9: b"getblocks",
  34      10: b"getblocktxn",
  35      11: b"getdata",
  36      12: b"getheaders",
  37      13: b"headers",
  38      14: b"inv",
  39      15: b"mempool",
  40      16: b"merkleblock",
  41      17: b"notfound",
  42      18: b"ping",
  43      19: b"pong",
  44      20: b"sendcmpct",
  45      21: b"tx",
  46      22: b"getcfilters",
  47      23: b"cfilter",
  48      24: b"getcfheaders",
  49      25: b"cfheaders",
  50      26: b"getcfcheckpt",
  51      27: b"cfcheckpt",
  52      28: b"addrv2",
  53      37: b"feature",
  54  }
  55  
  56  # Dictionary which contains short message type ID for the P2P message
  57  MSGTYPE_TO_SHORTID = {msgtype: shortid for shortid, msgtype in SHORTID.items()}
  58  
  59  
  60  class EncryptedP2PState:
  61      """A class for managing the state when v2 P2P protocol is used. Performs initial v2 handshake and encrypts/decrypts
  62      P2P messages. P2PConnection uses an object of this class.
  63  
  64  
  65      Args:
  66          initiating (bool): defines whether the P2PConnection is an initiator or responder.
  67              - initiating = True for inbound connections in the test framework   [TestNode <------- P2PConnection]
  68              - initiating = False for outbound connections in the test framework [TestNode -------> P2PConnection]
  69  
  70          net (string): chain used (regtest, signet etc..)
  71  
  72      Methods:
  73          perform an advanced form of diffie-hellman handshake to instantiate the encrypted transport. before exchanging
  74          any P2P messages, 2 nodes perform this handshake in order to determine a shared secret that is unique to both
  75          of them and use it to derive keys to encrypt/decrypt P2P messages.
  76              - initial v2 handshakes is performed by: (see BIP324 section #overall-handshake-pseudocode)
  77                  1. initiator using initiate_v2_handshake(), complete_handshake() and authenticate_handshake()
  78                  2. responder using respond_v2_handshake(), complete_handshake() and authenticate_handshake()
  79              - initialize_v2_transport() sets various BIP324 derived keys and ciphers.
  80  
  81          encrypt/decrypt v2 P2P messages using v2_enc_packet() and v2_receive_packet().
  82      """
  83      def __init__(self, *, initiating, net):
  84          self.initiating = initiating  # True if initiator
  85          self.net = net
  86          self.peer = {}  # object with various BIP324 derived keys and ciphers
  87          self.privkey_ours = None
  88          self.ellswift_ours = None
  89          self.sent_garbage = b""
  90          self.received_garbage = b""
  91          self.received_prefix = b""  # received ellswift bytes till the first mismatch from 16 bytes v1_prefix
  92          self.tried_v2_handshake = False  # True when the initial handshake is over
  93          # stores length of packet contents to detect whether first 3 bytes (which contains length of packet contents)
  94          # has been decrypted. set to -1 if decryption hasn't been done yet.
  95          self.contents_len = -1
  96          self.found_garbage_terminator = False
  97          self.transport_version = b''
  98  
  99      @staticmethod
 100      def v2_ecdh(priv, ellswift_theirs, ellswift_ours, initiating):
 101          """Compute BIP324 shared secret.
 102  
 103          Returns:
 104          bytes - BIP324 shared secret
 105          """
 106          ecdh_point_x32 = ellswift_ecdh_xonly(ellswift_theirs, priv)
 107          if initiating:
 108              # Initiating, place our public key encoding first.
 109              return TaggedHash("bip324_ellswift_xonly_ecdh", ellswift_ours + ellswift_theirs + ecdh_point_x32)
 110          else:
 111              # Responding, place their public key encoding first.
 112              return TaggedHash("bip324_ellswift_xonly_ecdh", ellswift_theirs + ellswift_ours + ecdh_point_x32)
 113  
 114      def generate_keypair_and_garbage(self, garbage_len=None):
 115          """Generates ellswift keypair and 4095 bytes garbage at max"""
 116          self.privkey_ours, self.ellswift_ours = ellswift_create()
 117          if garbage_len is None:
 118              garbage_len = random.randrange(MAX_GARBAGE_LEN + 1)
 119          self.sent_garbage = random.randbytes(garbage_len)
 120          return self.ellswift_ours + self.sent_garbage
 121  
 122      def initiate_v2_handshake(self):
 123          """Initiator begins the v2 handshake by sending its ellswift bytes and garbage
 124  
 125          Returns:
 126          bytes - bytes to be sent to the peer when starting the v2 handshake as an initiator
 127          """
 128          return self.generate_keypair_and_garbage()
 129  
 130      def respond_v2_handshake(self, response):
 131          """Responder begins the v2 handshake by sending its ellswift bytes and garbage. However, the responder
 132          sends this after having received at least one byte that mismatches 16-byte v1_prefix.
 133  
 134          Returns:
 135          1. int - length of bytes that were consumed so that recvbuf can be updated
 136          2. bytes - bytes to be sent to the peer when starting the v2 handshake as a responder.
 137                   - returns b"" if more bytes need to be received before we can respond and start the v2 handshake.
 138                   - returns -1 to downgrade the connection to v1 P2P.
 139          """
 140          v1_prefix = MAGIC_BYTES[self.net] + b'version\x00\x00\x00\x00\x00'
 141          while len(self.received_prefix) < 16:
 142              byte = response.read(1)
 143              # return b"" if we need to receive more bytes
 144              if not byte:
 145                  return len(self.received_prefix), b""
 146              self.received_prefix += byte
 147              if self.received_prefix[-1] != v1_prefix[len(self.received_prefix) - 1]:
 148                  return len(self.received_prefix), self.generate_keypair_and_garbage()
 149          # return -1 to decide v1 only after all 16 bytes processed
 150          return len(self.received_prefix), -1
 151  
 152      def complete_handshake(self, response):
 153          """ Instantiates the encrypted transport and
 154          sends garbage terminator + optional decoy packets + transport version packet.
 155          Done by both initiator and responder.
 156  
 157          Returns:
 158          1. int - length of bytes that were consumed. returns 0 if all 64 bytes from ellswift haven't been received yet.
 159          2. bytes - bytes to be sent to the peer when completing the v2 handshake
 160          """
 161          ellswift_theirs = self.received_prefix + response.read(64 - len(self.received_prefix))
 162          # return b"" if we need to receive more bytes
 163          if len(ellswift_theirs) != 64:
 164              return 0, b""
 165          ecdh_secret = self.v2_ecdh(self.privkey_ours, ellswift_theirs, self.ellswift_ours, self.initiating)
 166          self.initialize_v2_transport(ecdh_secret)
 167          # Send garbage terminator
 168          msg_to_send = self.peer['send_garbage_terminator']
 169          # Optionally send decoy packets after garbage terminator.
 170          aad = self.sent_garbage
 171          for decoy_content_len in [random.randint(1, 100) for _ in range(random.randint(0, 10))]:
 172              msg_to_send += self.v2_enc_packet(decoy_content_len * b'\x00', aad=aad, ignore=True)
 173              aad = b''
 174          # Send version packet.
 175          msg_to_send += self.v2_enc_packet(self.transport_version, aad=aad)
 176          return 64 - len(self.received_prefix), msg_to_send
 177  
 178      def authenticate_handshake(self, response):
 179          """ Ensures that the received optional decoy packets and transport version packet are authenticated.
 180          Marks the v2 handshake as complete. Done by both initiator and responder.
 181  
 182          Returns:
 183          1. int - length of bytes that were processed so that recvbuf can be updated
 184          2. bool - True if the authentication was successful/more bytes need to be received and False otherwise
 185          """
 186          processed_length = 0
 187  
 188          # Detect garbage terminator in the received bytes
 189          if not self.found_garbage_terminator:
 190              received_garbage = response[:16]
 191              response = response[16:]
 192              processed_length = len(received_garbage)
 193              for i in range(MAX_GARBAGE_LEN + 1):
 194                  if received_garbage[-16:] == self.peer['recv_garbage_terminator']:
 195                      # Receive, decode, and ignore version packet.
 196                      # This includes skipping decoys and authenticating the received garbage.
 197                      self.found_garbage_terminator = True
 198                      self.received_garbage = received_garbage[:-16]
 199                      break
 200                  else:
 201                      # don't update recvbuf since more bytes need to be received
 202                      if len(response) == 0:
 203                          return 0, True
 204                      received_garbage += response[:1]
 205                      processed_length += 1
 206                      response = response[1:]
 207              else:
 208                  # disconnect since garbage terminator was not seen after 4 KiB of garbage.
 209                  return processed_length, False
 210  
 211          # Process optional decoy packets and transport version packet
 212          while not self.tried_v2_handshake:
 213              length, contents = self.v2_receive_packet(response, aad=self.received_garbage)
 214              if length == -1:
 215                  return processed_length, False
 216              elif length == 0:
 217                  return processed_length, True
 218              processed_length += length
 219              self.received_garbage = b""
 220              # decoy packets have contents = None. v2 handshake is complete only when version packet
 221              # (can be empty with contents = b"") with contents != None is received.
 222              if contents is not None:
 223                  assert_equal(contents, b"")  # currently TestNode sends an empty version packet
 224                  self.tried_v2_handshake = True
 225                  return processed_length, True
 226              response = response[length:]
 227  
 228      def initialize_v2_transport(self, ecdh_secret):
 229          """Sets the peer object with various BIP324 derived keys and ciphers."""
 230          peer = {}
 231          salt = b'bitcoin_v2_shared_secret' + MAGIC_BYTES[self.net]
 232          for name in ('initiator_L', 'initiator_P', 'responder_L', 'responder_P', 'garbage_terminators', 'session_id'):
 233              peer[name] = hkdf_sha256(salt=salt, ikm=ecdh_secret, info=name.encode('utf-8'), length=32)
 234          if self.initiating:
 235              self.peer['send_L'] = FSChaCha20(peer['initiator_L'])
 236              self.peer['send_P'] = FSChaCha20Poly1305(peer['initiator_P'])
 237              self.peer['send_garbage_terminator'] = peer['garbage_terminators'][:16]
 238              self.peer['recv_L'] = FSChaCha20(peer['responder_L'])
 239              self.peer['recv_P'] = FSChaCha20Poly1305(peer['responder_P'])
 240              self.peer['recv_garbage_terminator'] = peer['garbage_terminators'][16:]
 241          else:
 242              self.peer['send_L'] = FSChaCha20(peer['responder_L'])
 243              self.peer['send_P'] = FSChaCha20Poly1305(peer['responder_P'])
 244              self.peer['send_garbage_terminator'] = peer['garbage_terminators'][16:]
 245              self.peer['recv_L'] = FSChaCha20(peer['initiator_L'])
 246              self.peer['recv_P'] = FSChaCha20Poly1305(peer['initiator_P'])
 247              self.peer['recv_garbage_terminator'] = peer['garbage_terminators'][:16]
 248          self.peer['session_id'] = peer['session_id']
 249  
 250      def v2_enc_packet(self, contents, aad=b'', ignore=False):
 251          """Encrypt a BIP324 packet.
 252  
 253          Returns:
 254          bytes - encrypted packet contents
 255          """
 256          assert len(contents) <= 2**24 - 1
 257          header = (ignore << IGNORE_BIT_POS).to_bytes(HEADER_LEN, 'little')
 258          plaintext = header + contents
 259          aead_ciphertext = self.peer['send_P'].encrypt(aad, plaintext)
 260          enc_plaintext_len = self.peer['send_L'].crypt(len(contents).to_bytes(LENGTH_FIELD_LEN, 'little'))
 261          return enc_plaintext_len + aead_ciphertext
 262  
 263      def v2_receive_packet(self, response, aad=b''):
 264          """Decrypt a BIP324 packet
 265  
 266          Returns:
 267          1. int - number of bytes consumed (or -1 if error)
 268          2. bytes - contents of decrypted non-decoy packet if any (or None otherwise)
 269          """
 270          if self.contents_len == -1:
 271              if len(response) < LENGTH_FIELD_LEN:
 272                  return 0, None
 273              enc_contents_len = response[:LENGTH_FIELD_LEN]
 274              self.contents_len = int.from_bytes(self.peer['recv_L'].crypt(enc_contents_len), 'little')
 275          response = response[LENGTH_FIELD_LEN:]
 276          if len(response) < HEADER_LEN + self.contents_len + CHACHA20POLY1305_EXPANSION:
 277              return 0, None
 278          aead_ciphertext = response[:HEADER_LEN + self.contents_len + CHACHA20POLY1305_EXPANSION]
 279          plaintext = self.peer['recv_P'].decrypt(aad, aead_ciphertext)
 280          if plaintext is None:
 281              return -1, None  # disconnect
 282          header = plaintext[:HEADER_LEN]
 283          length = LENGTH_FIELD_LEN + HEADER_LEN + self.contents_len + CHACHA20POLY1305_EXPANSION
 284          self.contents_len = -1
 285          return length, None if (header[0] & (1 << IGNORE_BIT_POS)) else plaintext[HEADER_LEN:]
 286