0324 - Final - Standards - March 8, 2019 (7 years, 4 months ago)
8
2019
BIP: 324
Layer: Peer Services
Title: Version 2 P2P Encrypted Transport Protocol
Author: Dhruv Mehta <[email protected]>
Tim Ruffing <[email protected]>
Jonas Schnelli <[email protected]>
Pieter Wuille <[email protected]>
Comments-URI: https://github.com/bitcoin/bips/wiki/Comments:BIP 324
Status: Final
Type: Standards Track
Created: 2019-03-08
License: BSD-3-Clause
Replaces: 151
This document proposes a new Bitcoin P2P transport protocol, which features opportunistic encryption, a mild bandwidth reduction, and the ability to negotiate upgrades before exchanging application messages.
This document is licensed under the 3-clause BSD license.
Bitcoin is a permissionless network whose purpose is to reach consensus over public data. Since all data relayed in the Bitcoin P2P network is inherently public, and the protocol lacks a notion of cryptographic identities, peers talk to each other over unencrypted and unauthenticated connections. Nevertheless, this plaintext nature of the current P2P protocol (referred to as v1 in this document) has severe drawbacks in the presence of attackers:
This proposal for a new P2P protocol version (v2) aims to improve upon this by raising the costs for performing these attacks substantially, primarily through the use of unauthenticated, opportunistic transport encryption. In addition, the bytestream on the wire is made pseudorandom (i.e., indistinguishable from uniformly random bytes) to a passive eavesdropper.
Why encrypt without authentication?
As we have argued above, unauthenticated encryption 1 (one in each direction) using HKDF-SHA256.
#** Send their 16-byte garbage terminator. 2
#** Receive up to 4111 bytes, stopping when encountering the garbage terminator.
#* At this point, both parties have the same keys, and all further communication proceeds in the form of encrypted packets .
#** Encrypted packets have an ignore bit , which makes them decoy packets if set. Decoy packets are to be ignored by the receiver apart from verifying they decrypt correctly. Either peer may send such decoy packets at any point from here on. These form the primary shapability mechanism in the protocol. How and when to use them is out of scope for this document.
#** For each of the two directions, the first encrypted packet that will be sent in that direction (regardless of it being a decoy packet or not) will make use of the associated authenticated data (AAD) feature of the AEAD to authenticate the garbage that has been sent in that direction. 3
To avoid the recognizable pattern of first messages being at least 64 bytes, a future backwards-compatible upgrade to this protocol may allow both peers to send their public key + garbage + garbage terminator in multiple rounds, slicing those bytes up into messages arbitrarily, as long as progress is guaranteed. 6
Note that the version negotiation phase does not need to wait for the key exchange phase to complete; version packets can be sent immediately after sending the garbage terminator. So the first two phases together, jointly called the handshake , comprise just 1.5 roundtrips:
Packet encryption overview
All data on the wire after the garbage terminators takes the form of encrypted packets. Every packet encodes an encrypted variable-length byte array, called the contents , as well as an ignore bit as mentioned before. The total size of a packet is 20 bytes plus the length of its contents.
Each packet consists of:
The encryption of the plaintext uses ChaCha20Poly1305 9 , an authenticated encryption with associated data (AEAD) cipher specified in RFC 8439 . Every packet's plaintext is treated as a separate AEAD message, with a different nonce for each.
The length must be dealt with specially, as it is needed to determine packet boundaries before the whole packet is received and authenticated. As we want a stream that is pseudorandom to a passive attacker, it still needs encryption. We use unauthenticated 10 ChaCha20 encryption for this, with an independent key. Note that the plaintext length is still implicitly authenticated by the encryption of the plaintext, but this can only be verified after receiving the whole packet. This design is inspired by that of the ChaCha20Poly1305 cipher suite in OpenSSH . 11 Because only fixed-length chunks (3-byte length fields) are encrypted, we do not need to treat all length chunks as separate messages. Instead, a single cipher (with the same nonce) is used for multiple consecutive length fields. This avoids wasting 61 pseudorandom bytes per packet, and makes the cost of having a separate cipher for length encryption negligible. 12
In order to provide forward security 13 14 , the encryption keys for both plaintext and length encryption are cycled every 224 messages, by switching to a new key that is generated by the key stream using the old key.
Next we specify the handshake of a connection in detail.
As explained before, these messages are sent to set up the connection:
----------------------------------------------------------------------------------------------------
| Initiator Responder |
| |
| x, ellswift_X = ellswift_create() |
| |
| ---- ellswift_X + initiator_garbage (initiator_garbage_len bytes; max 4095) ---> |
| |
| y, ellswift_Y = ellswift_create() |
| ecdh_secret = v2_ecdh( |
| y, ellswift_X, ellswift_Y, initiating=False) |
| v2_initialize(initiator, ecdh_secret, initiating=False) |
| |
| <--- ellswift_Y + responder_garbage (responder_garbage_len bytes; max 4095) + |
| responder_garbage_terminator (16 bytes) + |
| v2_enc_packet(initiator, RESPONDER_TRANSPORT_VERSION, aad=responder_garbage) ---- |
| |
| ecdh_secret = v2_ecdh(x, ellswift_Y, ellswift_X, initiating=True) |
| v2_initialize(responder, ecdh_secret, initiating=True) |
| |
| ---- initiator_garbage_terminator (16 bytes) + |
| v2_enc_packet(responder, INITIATOR_TRANSPORT_VERSION, aad=initiator_garbage) ---> |
| |
----------------------------------------------------------------------------------------------------
The peers derive their shared secret through X-only ECDH, hashed together with the exactly 64-byte public keys' encodings sent over the wire.
def v2_ecdh(priv, ellswift_theirs, ellswift_ours, initiating):
ecdh_point_x32 = ellswift_ecdh_xonly(ellswift_theirs, priv)
if initiating:
# Initiating, place our public key encoding first.
return sha256_tagged("bip324_ellswift_xonly_ecdh", ellswift_ours + ellswift_theirs + ecdh_point_x32)
else:
# Responding, place their public key encoding first.
return sha256_tagged("bip324_ellswift_xonly_ecdh", ellswift_theirs + ellswift_ours + ecdh_point_x32)
Here,
sha256_tagged(tag, x)
returns a tagged hash value
SHA256(SHA256(tag) || SHA256(tag) || x)
as in
BIP
340
.
The functions
ellswift_create
and
ellswift_ecdh_xonly
encapsulate the construction of
ElligatorSwift-encoded public keys, and the computation of X-only ECDH
with ElligatorSwift-encoded public keys.
First we define a constant:
To define the needed functions, we first introduce a helper function,
matching the
XSwiftEC
function from the
SwiftEC
paper,
instantiated for the secp256k1 curve, with minor modifications. It maps
pairs of integers
(u, t)
(both in range
0..p-1
) to
valid X coordinates on the curve. Note that the specification here does
not attempt to be constant time, as it does not operate on secret data.
In what follows, we use the notation from
BIP
340
.
To find encodings of a given X coordinate x , we first need the inverse of XSwiftEC . The function XSwiftECInv(x, u, case) either returns t such that XSwiftEC(u, t) = x , or None . The case variable is an integer in range 0..7 , which selects which of the up to 8 valid such t values to return:
The overall XElligatorSwift algorithm, matching the name used in the paper, then uses this inverse to randomly 19 sample encodings of x'':
This is used to define the
ellswift_create
algorithm
used in the previous section; it generates a random private key, along
with a uniformly sampled 64-byte ElligatorSwift-encoded public key
corresponding to it:
Finally the
ellswift_ecdh_xonly
algorithm is:
The authenticated encryption construction proposed here requires two 32-byte keys per communication direction. These (in addition to a session ID) are computed using HKDF 21 as specified in RFC 5869 with SHA256 as the hash function:
def initialize_v2_transport(peer, ecdh_secret, initiating):
# Include NETWORK_MAGIC to ensure a connection between nodes on different networks will immediately fail
prk = HKDF_Extract(Hash=sha256, salt=b'bitcoin_v2_shared_secret' + NETWORK_MAGIC, ikm=ecdh_secret)
peer.session_id = HKDF_Expand(Hash=sha256, PRK=prk, info=b'session_id', L=32)
# Initialize the packet encryption ciphers.
initiator_L = HKDF_Expand(Hash=sha256, PRK=prk, info=b'initiator_L', L=32)
initiator_P = HKDF_Expand(Hash=sha256, PRK=prk, info=b'initiator_P', L=32)
responder_L = HKDF_Expand(Hash=sha256, PRK=prk, info=b'responder_L', L=32)
responder_P = HKDF_Expand(Hash=sha256, PRK=prk, info=b'responder_P', L=32)
garbage_terminators = HKDF_Expand(Hash=sha256, PRK=prk, info=b'garbage_terminators', L=32)
initiator_garbage_terminator = garbage_terminators[:16]
responder_garbage_terminator = garbage_terminators[16:]
if initiating:
peer.send_L = FSChaCha20(initiator_L)
peer.send_P = FSChaCha20Poly1305(initiator_P)
peer.send_garbage_terminator = initiator_garbage_terminator
peer.recv_L = FSChaCha20(responder_L)
peer.recv_P = FSChaCha20Poly1305(responder_P)
peer.recv_garbage_terminator = responder_garbage_terminator
else:
peer.send_L = FSChaCha20(responder_L)
peer.send_P = FSChaCha20Poly1305(responder_P)
peer.send_garbage_terminator = responder_garbage_terminator
peer.recv_L = FSChaCha20(initiator_L)
peer.recv_P = FSChaCha20Poly1305(initiator_P)
peer.recv_garbage_terminator = initiator_garbage_terminator
# To achieve forward secrecy we must wipe the key material used to initialize the ciphers:
memory_cleanse(ecdh_secret, prk, initiator_L, initiator_P, responder_L, responder_K)
The session ID uniquely identifies the encrypted channel. v2 clients supporting this proposal may present the entire session ID (encoded as a hex string) to the node operator to allow for manual, out of band comparison with the peer node operator. Future transport versions may introduce optional authentication methods that compare the session ID as seen by the two endpoints in order to bind the encrypted channel to the authentication.
To establish a v2 encrypted connection, the initiator generates an
ephemeral secp256k1 keypair and sends an unencrypted ElligatorSwift
encoding of the public key to the responding peer followed by
unencrypted pseudorandom bytes
initiator_garbage
of length
garbage_len < 4096
.
def initiate_v2_handshake(peer, garbage_len):
peer.privkey_ours, peer.ellswift_ours = ellswift_create()
peer.sent_garbage = rand_bytes(garbage_len)
send(peer, peer.ellswift_ours + peer.sent_garbage)
The responder generates an ephemeral keypair for itself and derives
the shared ECDH secret (using the first 64 received bytes) which enables
it to instantiate the encrypted transport. It then sends 64 bytes of the
unencrypted ElligatorSwift encoding of its own public key and its own
responder_garbage
also of length
garbage_len < 4096
. If the first 16 bytes received match
the v1 prefix, the v1 protocol is used instead.
TRANSPORT_VERSION = b''
NETWORK_MAGIC = b'\xf9\xbe\xb4\xd9' # Mainnet network magic; differs on other networks.
V1_PREFIX = NETWORK_MAGIC + b'version\x00\x00\x00\x00\x00'
def respond_v2_handshake(peer, garbage_len):
peer.received_prefix = b""
while len(peer.received_prefix) < len(V1_PREFIX):
peer.received_prefix += receive(peer, 1)
if peer.received_prefix[-1] != V1_PREFIX[len(peer.received_prefix) - 1]:
peer.privkey_ours, peer.ellswift_ours = ellswift_create()
peer.sent_garbage = rand_bytes(garbage_len)
send(peer, ellswift_Y + peer.sent_garbage)
return
use_v1_protocol()
Upon receiving the encoded responder public key, the initiator
derives the shared ECDH secret and instantiates the encrypted transport.
It then sends the derived 16-byte
initiator_garbage_terminator
, optionally followed by an
arbitrary number of decoy packets. Afterwards, it receives the
responder's garbage (delimited by the garbage terminator). The responder
performs very similar steps but includes the earlier received prefix
bytes in the public key. Both the initiator and the responder set the
AAD of the first encrypted packet they send after the garbage terminator
(i.e., either an optional decoy packet or the version packet) to the
garbage they have just sent, not including the garbage terminator.
def complete_handshake(peer, initiating, decoy_content_lengths=[]):
received_prefix = b'' if initiating else peer.received_prefix
ellswift_theirs = receive(peer, 64 - len(received_prefix))
if not initiating and ellswift_theirs[4:16] == V1_PREFIX[4:16]:
# Looks like a v1 peer from the wrong network.
disconnect(peer)
ecdh_secret = v2_ecdh(peer.privkey_ours, ellswift_theirs, peer.ellswift_ours,
initiating=initiating)
initialize_v2_transport(peer, ecdh_secret, initiating=True)
# Send garbage terminator
send(peer, peer.send_garbage_terminator)
# Optionally send decoy packets after garbage terminator.
aad = peer.sent_garbage
for decoy_content_len in decoy_content_lengths:
send(v2_enc_packet(peer, decoy_content_len * b'\x00', aad=aad))
aad = b''
# Send version packet.
send(v2_enc_packet(peer, TRANSPORT_VERSION, aad=aad))
# Skip garbage, until encountering garbage terminator.
received_garbage = recv(peer, 16)
for i in range(4096):
if received_garbage[-16:] == peer.recv_garbage_terminator:
# Receive, decode, and ignore version packet.
# This includes skipping decoys and authenticating the received garbage.
v2_receive_packet(peer, aad=received_garbage[:-16])
return
else:
received_garbage += recv(peer, 1)
# Garbage terminator was not seen after 4 KiB of garbage.
disconnect(peer)
Lastly, we specify the packet encryption cipher in detail.
Packet encryption is built on two existing primitives:
AEAD_CHACHA20_POLY1305
in
RFC
8439 section 2.8
. It is an authenticated encryption protocol with
associated data (AEAD), taking a 256-bit key, 96-bit nonce, and an
arbitrary-length byte array of associated authenticated data (AAD). Due
to the built-in authentication tag, ciphertexts are 16 bytes longer than
the corresponding plaintext. In what follows:
aead_chacha20_poly1305_encrypt(key, nonce, aad, plaintext)
refers to a function that takes as input a 32-byte array
key
, a
12-byte array
nonce
, an arbitrary-length byte array
aad
, and an arbitrary-length byte array
plaintext
, and
returns a byte array
ciphertext
, 16 bytes longer than the
plaintext.
aead_chacha20_poly1305_decrypt(key, nonce, aad, ciphertext)
refers to a function that takes as input a 32-byte array
key
, a
12-byte array
nonce
, an arbitrary-length byte array
aad
, and an arbitrary-length byte array
ciphertext
,
and returns either a byte array
plaintext
(16 bytes shorter
than the ciphertext), or
None
in case the ciphertext was not a
valid ChaCha20Poly1305 encryption of any plaintext with the specified
key
,
nonce
, and
aad
.
chacha20_block(key, nonce, count)
refers to a function
that takes as input a 32-byte array
key
, a 12-byte array
nonce
, and an integer
count
in range
0..2
32
-1
, and returns a byte array of length
64.
These will be used for plaintext encryption and length encryption, respectively.
To provide re-keying every 224 packets, we specify two wrappers.
The first is FSChaCha20Poly1305 , which represents a ChaCha20Poly1305 AEAD, which automatically changes the nonce after every message, and rekeys every 224 messages by encrypting 32 zero bytes 22 , and using the first 32 bytes of the result. Each message will be used for one packet. Note that in our protocol, any FSChaCha20Poly1305 instance is always either exclusively encryption or exclusively decryption, as separate instances are used for each direction of the protocol. The nonce used for a message is composed of the 32-bit little-endian encoding of the number of messages with the current key, followed by the 64-bit little-endian encoding of the number of rekeyings performed. For rekeying, the first 32-bit integer is set to 0xffffffff .
REKEY_INTERVAL = 224
class FSChaCha20Poly1305:
"""Rekeying wrapper AEAD around ChaCha20Poly1305."""
def __init__(self, initial_key):
self.key = initial_key
self.packet_counter = 0
def crypt(self, aad, text, is_decrypt):
nonce = ((self.packet_counter % REKEY_INTERVAL).to_bytes(4, 'little') +
(self.packet_counter // REKEY_INTERVAL).to_bytes(8, 'little'))
if is_decrypt:
ret = aead_chacha20_poly1305_decrypt(self.key, nonce, aad, text)
else:
ret = aead_chacha20_poly1305_encrypt(self.key, nonce, aad, text)
if (self.packet_counter + 1) % REKEY_INTERVAL == 0:
rekey_nonce = b"\xFF\xFF\xFF\xFF" + nonce[4:]
self.key = aead_chacha20_poly1305_encrypt(self.key, rekey_nonce, b"", b"\x00" * 32)[:32]
self.packet_counter += 1
return ret
def decrypt(self, aad, ciphertext):
return self.crypt(aad, ciphertext, True)
def encrypt(self, aad, plaintext):
return self.crypt(aad, plaintext, False)
The second is
FSChaCha20
, a (single) stream cipher
which is used for the lengths of all packets. Encryption and decryption
are identical here, so a single function
crypt
is exposed.
It XORs the input with bytes generated using the ChaCha20 block
function, rekeying every 224 chunks using the next 32 bytes of the block
function output as new key. A
chunk
refers here to a single
invocation of
crypt
. As explained before, the same cipher
is used for 224 consecutive chunks, to avoid wasting cipher output. The
nonce used for these batches of 224 chunks is composed of 4 zero bytes
followed by the 64-bit little-endian encoding of the number of rekeyings
performed. The block counter is reset to 0 after every rekeying.
class FSChaCha20:
"""Rekeying wrapper stream cipher around ChaCha20."""
def __init__(self, initial_key):
self.key = initial_key
self.block_counter = 0
self.chunk_counter = 0
self.keystream = b''
def get_keystream_bytes(self, nbytes):
while len(self.keystream) < nbytes:
nonce = ((0).to_bytes(4, 'little') +
(self.chunk_counter // REKEY_INTERVAL).to_bytes(8, 'little'))
self.keystream += chacha20_block(self.key, nonce, self.block_counter)
self.block_counter += 1
ret = self.keystream[:nbytes]
self.keystream = self.keystream[nbytes:]
return ret
def crypt(self, chunk):
ks = self.get_keystream_bytes(len(chunk))
ret = bytes([ks[i] ^ chunk[i] for i in range(len(chunk))])
if ((self.chunk_counter + 1) % REKEY_INTERVAL) == 0:
self.key = self.get_keystream_bytes(32)
self.block_counter = 0
self.chunk_counter += 1
return ret
Encryption and decryption of packets then follow by composing the ciphers from the previous section as building blocks.
LENGTH_FIELD_LEN = 3
HEADER_LEN = 1
IGNORE_BIT_POS = 7
def v2_enc_packet(peer, contents, aad=b'', ignore=False):
assert len(contents) <= 2**24 - 1
header = (ignore << IGNORE_BIT_POS).to_bytes(HEADER_LEN, 'little')
plaintext = header + contents
aead_ciphertext = peer.send_P.encrypt(aad, plaintext)
enc_contents_len = peer.send_L.encrypt(len(contents).to_bytes(LENGTH_FIELD_LEN, 'little'))
return enc_contents_len + aead_ciphertext
CHACHA20POLY1305_EXPANSION = 16
def v2_receive_packet(peer, aad=b''):
while True:
enc_contents_len = receive(peer, LENGTH_FIELD_LEN)
contents_len = int.from_bytes(peer.recv_L.crypt(enc_contents_len), 'little')
aead_ciphertext = receive(peer, HEADER_LEN + contents_len + CHACHA20POLY1305_EXPANSION)
plaintext = peer.recv_P.decrypt(aad, aead_ciphertext)
if plaintext is None:
disconnect(peer)
break
# Only the first packet is expected to have non-empty AAD.
aad = b''
header = plaintext[:HEADER_LEN]
if not (header[0] & (1 << IGNORE_BIT_POS)):
return plaintext[HEADER_LEN:]
Each v1 P2P message uses a double-SHA256 checksum truncated to 4 bytes. Roughly the same amount of computation power is required for encrypting and authenticating a v2 P2P message as proposed.
v2 Bitcoin P2P transport layer packets use the encrypted message structure shown above. An unencrypted application layer contents is composed of:
| Field | Size in bytes | Comments |
|---|---|---|
|
|
1 or 13 |
either a one byte ID in range
1..255
or
|
|
|
|
message payload |
If the first byte of
message_type
is
b'\x00'
, the following 12 bytes are interpreted as an ASCII
message type (as in the v1 P2P protocol), trailing padded with
b'\x00'
as necessary. If the first byte of
message_type
is in the range
1..255
, it is
interpreted as a message type ID. This structure results in smaller
messages than the v1 protocol, as most messages sent/received will have
a message type ID. We recommend reserving 1-byte type IDs for message
types that are sent more than once per direction per connection.
23
<ref
name"fixed_length_long_ids">
Why not allow variable length
long message type IDs?
Allowing for variable length long IDs
reduces the available 1-byte ID space by 12 (to encode the length
itself) and incentivizes less descriptive message types. In addition,
limiting message types to fixed lengths of 1 or 13 hampers traffic
analysis.
The following table lists currently defined message type IDs:
| 0 | 1 | 2 | 3 | |
|---|---|---|---|---|
|
+0 |
(12 bytes follow) |
|
|
|
|
+4 |
|
|
|
|
|
+8 |
|
|
|
|
|
+12 |
|
|
|
|
|
+16 |
|
|
|
|
|
+20 |
|
|
|
|
|
+24 |
|
|
|
|
|
+28 |
|
|||
|
≥29 |
| colspan="4" | (undefined) |
Additional message types may be added separately after BIP finalization.
Peers supporting the v2 transport protocol signal support by
advertising the
NODE_P2P_V2 = (1 << 11)
service flag
in addr relay. If met with immediate disconnection when establishing a
v2 connection, clients implementing this proposal are encouraged to
retry connecting using the v1 protocol.
24
For development and testing purposes, we provide a collection of test vectors in CSV format, and a naive, highly inefficient, reference implementation of the relevant algorithms. This code is for demonstration purposes only:
Thanks to everyone (last name order) that helped invent and develop the ideas in this proposal:
What does authentication mean in this context? Unfortunately, the term authentication in the context of secure channel protocols is ambiguous. It can refer to:
provides strictly better security than no encryption. Thus, all connections should use encryption, even if they are unauthenticated.
When it comes to authentication, the situation is not as clear as for encryption. Due to Bitcoin's permissionless nature, authentication will always be restricted to specific scenarios (e.g., connections between peers belonging to the same operator), and whether some form of (possibly partially anonymous) authentication is desired depends on the specific requirements of the involved peers. As a consequence, we believe that authentication should be addressed separately (if desired), and this proposal aims to provide a solid technical basis for future protocol upgrades, including the addition of optional authentication (see Private authentication protocols ).
Why have a pseudorandom bytestream when traffic analysis is still possible?
Traffic analysis, e.g., observing packet lengths and timing, as well as active attacks can still reveal that the Bitcoin v2 P2P protocol is in use. Nevertheless, a pseudorandom bytestream raises the cost of fingerprinting the protocol substantially, and may force some intermediaries to attack any protocol they cannot identify, causing collateral cost.
A pseudorandom bytestream is not self-identifying. Moreover, it is unopinionated and thus a canonical choice for similar protocols. As a result, Bitcoin P2P traffic will be indistinguishable from traffic of other protocols which make the same choice (e.g., obfs4 and a recently proposed cTLS extension ). Moreover, traffic shapers and protocol wrappers (for example, making the traffic look like HTTPS or SSH) can further mitigate traffic analysis and active attacks but are out of scope for this proposal.
Why not use a secure tunnel protocol?
Our goal includes making opportunistic encryption ubiquitously available, as that provides the best defense against large-scale attacks. That implies protecting both the manual, deliberate connections node operators instruct their software to make, and the automatic connections Bitcoin nodes make with each other based on IP addresses obtained via gossip. While encryption per se is already possible with proxy networks or VPN protocols, these are not desirable or applicable for automatic connections at scale:
Thus, to achieve our goal, we need a solution that has minimal costs, works without configuration, and is always enabled – on top of any network layer rather than be part of the network layer.
Why not use a general-purpose transport encryption protocol?
While it would be possible to rely on an off-the-shelf transport encryption protocol such as TLS or Noise, the specific requirements of the Bitcoin P2P network laid out above make these protocols an unsuitable choice.
The primary requirement which existing protocols fail to meet is a sufficiently modular treatment of encryption and authentication. As we argue above, whether and which form of authentication is desired in the Bitcoin P2P network will depend on the specific requirements of the involved peers (resulting in a mix of authenticated and unauthenticated connections), and thus the question of authentication should be decoupled from encryption. However, native support for a handful of standard authentication scenarios (e.g., using digital signatures and certificates) is at the core of the design of existing general-purpose transport encryption protocols. This focus on authentication would not provide clear benefits for the Bitcoin P2P network but would come with a large amount of additional complexity.
In contrast, our proposal instead aims for a simple modular design that makes it possible to address authentication separately. Our proposal provides a foundation for authentication by exporting a session ID that uniquely identifies the encrypted channel. After an encrypted channel has been established, the two endpoints are able to use any authentication protocol to confirm that they have the same session ID. (This is sometimes called channel binding because the session ID binds the encrypted channel to the authentication protocol.) Since in our proposal, any authentication needs to run after an encrypted connection has been established, the price we pay for this modularity is a possibly higher number of roundtrips as opposed to other protocols that perform authentication alongside the Diffie-Hellman key exchange. 2 However, the resulting increase in connection establishment latency is a not a concern for Bitcoin's long-lived connections, which typically live for hours or even weeks .
Besides this fundamentally different treatment of authentication, further technical issues arise when applying TLS or Noise to our desired use case:
While existing protocols could be amended to address all of the aforementioned issues, this would negate the benefits of using them as off-the-shelf solution, e.g., the possibility to re-use existing implementations and security analyses.
This proposal aims to achieve the following properties:
The specification consists of three parts:
In this section, we define the encryption protocol for messages between peers.
We first give an informal overview of the entire protocol flow and packet encryption.
Protocol flow overview
Given a newly established connection (typically TCP/IP) between two v2 P2P nodes, there are 3 phases the connection goes through. The first starts immediately, i.e. there are no v1 messages or any other bytes exchanged on the link beforehand. The two parties are called the initiator (who established the connection) and the responder (who accepted the connection).
How can progress be guaranteed in a backwards-compatible way? In order to guarantee progress, it must be ensured that no deadlock occurs, i.e., no state is reached in which each party waits for the other party indefinitely. For example, any upgrade that adheres to the following conditions will guarantee progress:
Since the protocol as specified here adheres to these conditions, any upgrade which also adheres to these conditions will be backwards-compatible. ↩︎
How does packet encryption differ from the OpenSSH design? The differences are: