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