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