Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?

  BIP: 324
  Layer: Peer Services
  Title: Version 2 P2P Encrypted Transport Protocol
  Author: Dhruv Mehta <dhruv@bip324.com>
          Tim Ruffing <crypto@timruffing.de>
          Jonas Schnelli <dev@jonasschnelli.ch>
          Pieter Wuille <bitcoin-dev@wuille.net>
  Status: Draft
  Type: Standards Track
  Created: 2019-03-08
  License: BSD-3-Clause

Table of Contents

Introduction

Abstract

This document proposes a new Bitcoin P2P protocol, which features transport encryption and slightly lower bandwidth usage.

Copyright

This document is licensed under the 3-clause BSD license.

Motivation

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:

  • While the relayed data itself is public in nature, the associated metadata may reveal private information and hamper privacy of users. For example, a global passive attacker eavesdropping on all Bitcoin P2P connections can trivially identify the source and timing of a transaction.
  • Since connections are unauthenticated, they can be tampered with at a low cost and often even with a low risk of detection. For example, an attacker can alter specific bytes of a connection (such as node flags) on-the-fly without the need to keep any state.
  • The protocol is self-revealing. For example, deep packet inspection can identify a P2P connection trivially because connections start with a fixed sequence of magic bytes. The ability to detect connections enables censorship and facilitates the aforementioned attacks as well as other attacks which require the attacker to control the connections of victims, e.g., eclipse attacks targeted at miners.
This proposal of a new P2P protocol version (v2) aims to mitigate these issues by means of unauthenticated, opportunistic transport encryption. As a secondary goal beside the concealment of the transferred data, the entire bytestream on the wire should be pseudorandom (i.e., indistinguishable from uniformly random bytes) to a passive eavesdropper.

This countermeasure increases the costs of the aforementioned attacks substantially. Encryption, even when it is unauthenticated and used only when both endpoints support v2, impedes eavesdropping or tampering by forcing the attacker to either perform a persistent man-in-the-middle attack or downgrade connections to v1. Since both of these are active attacks, they are in principle detectable by the endpoints and thus hard to perform covertly at scale: Even very basic checks, e.g., operators manually comparing protocol versions and session IDs (as supported by the proposed protocol), will expose the attacker. Moreover, a pseudorandom bytestream excludes identification techniques based on pattern matching, and makes it easier to shape the bytestream in order to mimic other protocols used on the Internet.

Why encrypt without authentication?

As we have argued above, unauthenticated encryption 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.

Why have a pseudorandom bytestream when traffic analysis is still possible?

Traffic analysis, e.g., observing message 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 unopinionated, canonical and does not self-identify. If other commonly used protocols choose similarly, their bytestreams will become indistinguishable from Bitcoin P2P traffic. 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, as well as the 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 networks, these are not desirable or applicable for automatic connections at scale:

  • Proxy networks like Tor or I2P introduce a separate address space, independent from network topology, with a very low cost per address. Thus, it is not desirable[1] to have a substantial portion of nodes be exclusively connected this way, as it would significantly reduce Eclipse attack costs. Additionally, Tor connections come with significant bandwidth and latency costs that may not be desirable for all network users.
  • VPN networks like WireGuard or OpenVPN inherently define a private network, which requires manual configuration, which isn't a realistic avenue for automatic connections.
Thus, to achieve our goal we need a solution that has minimal costs, works without configuration, and can always be 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 existing general-purpose 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 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 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 with 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 with off-the-shelf transport layer encryption protocols arise when applied to our desired use case:

  • Noise and TLS do not offer a pseudorandom bytestream.
  • Noise and TLS do not offer native support for elliptic curve cryptography on the curve secp256k1 as otherwise used in Bitcoin. While using secp256k1 is not strictly necessary, it is the obvious choice is for any new asymmetric cryptography in Bitcoin because it minimizes the cryptographic hardness assumptions as well as the dependencies that Bitcoin software will need.
  • Both TLS and Noise provide a stream-based interface to the application-layer whereas Bitcoin requires a packet-based interface, resulting in the need for an additional thin layer to perform packet serialization and deserialization.
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.

Design

This proposal aims to achieve the following properties:

  • Confidentiality against passive attacks: A passive attacker having recorded a v2 P2P bytestream (without timing information and packet metadata) must not be able to determine the plaintext being exchanged by the nodes.
  • Observability of active attacks: A session ID identifiying the encrypted channel uniquely is derived deterministically from a Diffie-Hellman negotiation. An active man-in-the-middle attacker is forced to incur a risk of being detected as peer operators can compare session IDs manually, or using optional authentication methods possibly introduced in future protocol versions.
  • Pseudorandom bytestream: A passive attacker having recorded a v2 P2P bytestream (without timing information and packet metadata) must not be able to distinguish it from a uniformly random bytestream.
  • Shapable bytestream: It should be possible to shape the bytestream using decoy traffic to increase resistance to traffic analysis (for example, to conceal block propagation).
  • Forward secrecy: An eavesdropping attacker who compromises a peer's sessions secrets should not be able to decrypt past session traffic, except for the latest few messages.
  • Upgradability: The proposal provides an upgrade path using transport versioning which can be used to add features like authentication, PQC handshake upgrade, etc. in the future.
  • Compatibility: v2 clients will allow inbound v1 connections to minimize risk of network partitions.
  • Low overhead: the introduction of a new P2P transport protocol should not substantially increase computational cost or bandwidth for nodes that implement it, compared to the current protocol.
The overall proposed design is:

  • Both peers generate ephemeral keypairs and exchange public keys.
    • The public keys exchanged on wire use an encoding which makes the handshake (and therefore, the entire bytestream, as what follows is encrypted) indistinguishable from random.
  • Both peers then deterministically derive the ECDH secret and further derive encryption keys, a session ID and rekeying salt from the shared secret.
  • We use a two layer cipher suite. The inner layer uses the ChaCha20/Poly1305 AEAD defined in RFC8439 to provide confidentiality and authenticity of individual messages, using a battle-tested authenticated encryption suite. The outer layer prefixes every message with lengths encrypted using ChaCha20 directly to provide confidentiality against passive attackers only and a pseudorandom bytestream.
  • The keys for both layers are rotated after a fixed number of messages for forward secrecy.

Signaling specification

Signaling v2 support

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.

Transport layer specification

v2 encrypted message structure

The structure of the v2 encrypted messages is as follows:

Field Size in bytes Comments
length 3 Encrypt(LittleEndian(len(header) + ciphertext_length))
header 1 Encrypt(ignore_bit << 7)
ciphertext_payload ciphertext_length Encrypted payload. ciphertext_length < 2^24-1
rfc8439_mac 16 RFC8439_AUTH_TAG(authenticated_bytes=header + ciphertext_payload, aad=b'')

Initial handshake

 ----------------------------------------------------------------------------------------------
 | Initiator                         Responder                                                |
 |                                                                                            |
 | x, ellsq_X = v2_keygen(initiating=True)                                                    |
 |                                                                                            |
 |           --- ellsq_X --->                                                                 |
 |                                                                                            |
 |                                   y, ellsq_Y = v2_keygen(initiating=False)                 |
 |                                   ECDH_SECRET = secp256k1_ellsq_ecdh_secret(               |
 |                                                     y, ellsq_X, ellsq_Y, initiating=False) |
 |                                                                                            |
 |           <-- ellsq_Y + v2_enc_msg(initiator, RESPONDER_TRANSPORT_VERSION) ---             |
 |                                                                                            |
 | ECDH_SECRET = secp256k1_ellsq_ecdh_secret(x, ellsq_X, ellsq_Y, initiating=True)            |
 |                                                                                            |
 |            --- v2_enc_msg(responder, INITIATOR_TRANSPORT_VERSION) --->                     |
 |                                                                                            |
 ----------------------------------------------------------------------------------------------

To establish a v2 encrypted connection, the initiator generates an ephemeral secp256k1 keypair and sends an unencrypted Elligator-squared[3] [4] encoding of the public key to the responding peer.

def initiate_v2_handshake(responder):
    x, ellsq_X = v2_keygen(initiating=True)
    send(responder, ellsq_X)

The responder generates an ephemeral keypair for itself and derives the shared ECDH secret which enables it to instantiate the encrypted transport. It then sends 64 bytes of the unencrypted Elligator-squared encoding of its own public key appended with a v2 protocol encrypted message where the payload is set to a transport version number. v2.0 clients implementing this proposal will send an empty payload which should be interpreted as version 0. This design choice allows deferral of the transport version number encoding until version 1. If the received version number is malformed, it will be interpreted as empty.

def respond_v2_handshake(initiator, ellsq_X):
    y, ellsq_Y = v2_keygen(initiating=False)
    ecdh_secret = secp256k1_ellsq_ecdh_secret(y, ellsq_X, ellsq_Y, initiating=False)
    initialize_v2_transport(initiator, ecdh_secret, initiating=False)
    send_bytes = ellsq_Y + v2_enc_msg(initiator, TRANSPORT_VERSION)
    send(initiator, send_bytes)

Upon receiving the encoded responder public key, the initiator derives the shared ECDH secret, instantiates the encrypted transport and sends its own transport version number as well. The transport session version[5] is set to the minimum of the supported versions.

def initiator_complete_handshake(responder, response):
    ellsq_Y = response[:64]
    ecdh_secret = secp256k1_ellsq_ecdh_secret(x, ellsq_X, ellsq_Y, initiating=True)
    initialize_v2_transport(responder, ecdh_secret, initiating=True)
    responder_transport_version = v2_dec_msg(responder, response[64:])
    send(responder, v2_enc_msg(responder, TRANSPORT_VERSION))
    set_transport_version(responder, min(responder_transport_version, TRANSPORT_VERSION))

The responder also similarly sets the session version:

def responder_complete_handshake(initiator, msg):
    initiator_transport_version = v2_dec_msg(initiator, msg)
    set_transport_version(initiator, min(initiator_transport_version, TRANSPORT_VERSION))

ECDH keypair generation and shared secret computation

To aid disambiguation of v1 and v2 handshakes, public keys with Elligator-squared encodings starting with the 12-bytes of NETWORK_MAGIC || "version\x00" are forbidden for use by the initiator[6]. This restriction does not apply to the responder.

def v2_keygen(initiating):
    while True:
        priv, ellsq_pub = secp256k1_ellsq_ecdh_keygen()
        if not (initiating and ellsq_pub[:12] == NETWORK_MAGIC + "version\x00"):
            break
    return priv, ellsq_pub

def secp256k1_ellsq_ecdh_keygen():
    priv, pub = secp256k1_keygen()
    ellsq_pub = secp256k1_ellsq_encode(pub)
    return priv, ellsq_pub

The peers derive their shared ECDH secret only from the x-coordinate of the secret curve point (ignoring the y-coordinate).[7]

def secp256k1_ellsq_ecdh_secret(priv, ellsq_X, ellsq_Y, initiating):
    ellsq_theirs = ellsq_Y if initiating else ellsq_X
    pub_theirs = secp256k1_ellsq_decode(ellsq_theirs)
    xonly_ecdh = secp256k1_xonly_ecdh_raw(pub_theirs, priv)
    return sha256_tagged("secp256k1_ellsq_xonly_ecdh", ellsq_X + ellsq_Y + xonly_ecdh)

Here, sha256_tagged(tag, x) returns a tagged hash value SHA256(SHA256(tag) || SHA256(tag) || x) as in BIP340, and secp256k1_xonly_ecdh_raw(X, y) returns the 32-byte big-endian encoding of the x-coordinate of the point pt = y*X on the secp256k1 curve.

Elligator-squared mapping and encoding of field elements

Elligator-squared prescribes the properties of the mapping function from field elements to curve points and provides one such choice in section 4.3. The mapping function used in this proposal is described in another paper by Fouque and Tibouchi. Let f be the function from field elements to curve points, defined as follows:

def f(t):
    c = 0xa2d2ba93507f1df233770c2a797962cc61f6d15da14ecd47d8d27ae1cd5f852
    x1 = (c - 1)/2 - c*t^2 / (t^2 + 8) (mod p)
    x2 = (-c - 1)/2 + c*t^2 / (t^2 + 8) (mod p)
    x3 = 1 - (t^2 + 8)^2 / (3*t^2) (mod p)

    # At least one of (x1, x2, x3) is guaranteed to be a valid x-coordinate on the curve for any t
    for x_candidate in (x1, x2, x3):
        if secp256k1_is_valid_x_coord(x_candidate):
            x = x_candidate
            break

    # Pick the curve point where the Y co-ordinate is the same parity(even/odd) as t
    if t % 2:
        y = secp256k1_odd_y(x)
    else:
        y = secp256k1_even_y(x)

    return secp256k1_point(x, y)

The Elligator-squared encoding of a curve point P(public key) consists of the 32-byte big-endian encodings of field elements u1 and u2 concatenated, where f(u1)+f(u2) = P. The encoding algorithm is described in the paper, and effectively picks a uniformly random pair (u1,u2) among those which encode P. For completeness, to make the encoding able to deal with all inputs, if f(u1)+f(u2) is the point at infinity, the decoding is defined to be f(u1) instead. A detailed writeup on the encoding algorithm we use can be found here.

Keys and Session ID Derivation

The authenticated encryption construction proposed here requires two 32-byte keys per communication direction and a rekey salt. These (in addition to a session ID) are computed using HKDF per RFC 5869 as shown below.

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="bitcoin_v2_shared_secret" + NETWORK_MAGIC, ikm=ecdh_secret)

    # We no longer need the ECDH secret
    memory_cleanse(ecdh_secret)

    initiator_L  = HKDF_Expand(Hash=sha256, PRK=prk, info="initiator_L",  L=32)
    initiator_P  = HKDF_Expand(Hash=sha256, PRK=prk, info="initiator_P",  L=32)
    responder_L  = HKDF_Expand(Hash=sha256, PRK=prk, info="responder_L",  L=32)
    responder_P  = HKDF_Expand(Hash=sha256, PRK=prk, info="responder_P",  L=32)

    if initiating:
        peer.send_L = initiator_L
        peer.send_P = initiator_P
        peer.recv_L = responder_L
        peer.recv_P = responder_P
    else:
        peer.recv_L = initiator_L
        peer.recv_P = initiator_P
        peer.send_L = responder_L
        peer.send_P = responder_P

    peer.session_id   = HKDF_Expand(Hash=sha256, PRK=prk, info="session_id",   L=32)
    peer.rekey_salt   = HKDF_Expand(Hash=sha256, PRK=prk, info="rekey_salt", L=32)

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.

BIP324 Cipher Suite

The BIP324 cipher suite uses a two layer cipher suite. We use RFC8439 for the inner-layer which gives us the ciphertext (for a 1-byte header and the application layer payload) and a MAC tag authenticating the ciphertext. Since we use a single bytestream to send multiple messages, we still need to provide the peer with the length of the ciphertext to locate the message boundary. We use FSChaCha20, a new cryptogtraphic primitive introduced below (keyed separately) to encrypt the ciphertext length.[8] This results in an entirely pseudorandom bytestream.

Background: Existing cryptographic primitives

rfc8439_encrypt_and_auth(key, nonce, plaintext) is defined as chacha20_aead_encrypt(aad, key, nonce, constant, plaintext) according to RFC8439 with constant and aad set to be empty[9]. It takes a 256-bit key, 96-bit nonce and returns a ciphertext that is the same length as the plaintext along with a 128-bit message authentication code tag.

ChaCha20PRF(key, nonce, ctr) is the pseudorandom function (PRF) defined in this paper. We use it with a 256-bit key, a 96-bit nonce (to stay similar to how RFC8439 uses ChaCha20) and a 32-bit counter as inputs, and obtain 512 pseudorandom bits.

The PRF can be composed into a deterministic random bit generator (DRBG) producing a pseudorandom keystream as shown below.

def ChaCha20DRBG(key, nonce):
    ctr = 0
    while ctr < 2**32:
        yield ChaCha20PRF(key, nonce, ctr)
        ctr += 1

The stream cipher derived from ChaCha20DRBG(key, nonce) (by XORing the keystream with the plaintext) does not provide forward secrecy[10] in case of a compromised key. This proposal outlines a new FSChaCha20(key) stream cipher construction to permit implementations that offer forward secrecy.

New primitive: FSChaCha20

The FSChaCha20(key) stream cipher is composed by re-setting the key on the underlying ChaCha20DRBG(key) instance using key = SHA256(rekey_salt + key) every 256 messages. [11] [12] The nonce is incremented upon re-key.

CHACHA20_BLOCKSIZE = 64 # bytes
REKEY_INTERVAL = 256 # messages

def get_nonce(rekey_ctr, msg_ctr):
    return msg_ctr.to_bytes(4, 'little') + rekey_ctr.to_bytes(8, 'little')

class FSChaCha20:
    def __init__(self, key, rekey_salt):
        self.key = key
        self.rekey_ctr = 0 # internal 64-bit re-key counter
        self.msg_ctr = 0
        self.keystream = b""
        self.rekey_salt = rekey_salt

    def encipher(input):
        # We do not use the message counter in the nonce to reduce wasted pseudorandom bytes in the outer layer
        nonce = get_nonce(self.rekey_ctr, 0)
        if len(self.keystream) < len(input):
            for _ in range(((len(input) - 1) // CHACHA20_BLOCKSIZE) + 1):
                self.keystream += ChaCha20DRBG(self.key, nonce)
        output = input ^ self.keystream[:len(input)]
        self.keystream = self.keystream[len(input):]

        self.msg_ctr += 1
        if self.msg_ctr == REKEY_INTERVAL:
            self.rekey_ctr += 1
            self.msg_ctr = 0
            self.key = hashlib.sha256(self.rekey_salt + self.key).digest()

        return output

Detailed construction

As mentioned above, our approach uses two layers. We will call the outer layer, L(FSChaCha20 used for encrypting the 3-byte length per message) and the inner layer, P(RFC8439 instance used for encrypting and authenticating the 1-byte header and application layer payload). P is re-keyed every 256 messages similar to L.

Encrypting

Before encrypting or decrypting the first message:

rekey_ctr = 0 # 64-bits
msg_ctr = 0 # 32-bits
L = FSChaCha20(k_L, rekey_salt)

When encrypting a message M of length len(M) bytes:

  • Set a 96-bit nonce nonce = rekey_ctr + msg_ctr
  • Verify len(M) < 2^24 - 1. If not, the message cannot be encrypted[13]
  • Set ignore to 1 if you want the receiver to ignore this message, else set it to 0
  • Set header = ignore << 7
  • Compute (C, mac_tag) = rfc8439_encrypt_and_auth(k_P, nonce, header + M) where C is the cipertext and mac_tag is the authentication tag
  • Set length to the 3-byte little endian encoding for len(C) (which is the same as 1 + len(M) since the header is 1-byte)
  • Set C = L.encipher(length) + C
  • C || mac_tag is now the content that can be transmitted to maintain confidentiality and integrity.
  • Increment msg_ctr. If it is 256, re-key P using k_P = SHA256(rekey_salt + k_P), increment the rekey_ctr and reset the message counter msg_ctr = 0

Decrypting

  • Set a 96-bit nonce nonce = rekey_ctr + msg_ctr
  • The encrypted length can be decrypted as soon as 3 bytes of ciphertext, C have been received. Set length = L.encipher(C[:3]) and interpret length as the little-endian encoded length of the ciphertext payload, i.e. len(C) which is also 1 + len(M).
  • Compute the mac tag for C[3:3+len(C)] according to RFC8439 using k_P and nonce with aad and constant set to empty and compare it in constant time to the 16 bytes starting at C[3+len(C)]. The byte comparison must be done in a constant time manner. If the expected MAC tag differs from the provided MAC tag, the connection MUST be immediately terminated.
  • Decrypt C[3:3+len(C)] per RFC8439. Interpret the first byte of the obtained plaintext as header and set ignore = header >> 7.
  • If ignore is non-zero, ignore the message, else, pass the remaining len(M) bytes of the plaintext to the application layer.
  • Increment msg_ctr. If it is 256, re-key P using k_P = SHA256(rekey_salt + k_P), increment the rekey_ctr and reset the message counter msg_ctr = 0
LENGTH_FIELD_LEN = 3
HEADER_LEN = 1
RFC8439_TAG_LEN = 32
IGNORE_BIT_POS = 7
REKEY_INTERVAL = 256

# Yields (disconnect, ignore_message, bytes)
def BIP324CipherSuite(key_L, key_P, rekey_salt, is_encrypt, crypt_bytes, set_ignore=False):
    L = FSChaCha20(key_L, rekey_salt)
    rekey_ctr = 0
    msg_ctr = 0

    while True:
        nonce = get_nonce(rekey_ctr, msg_ctr)
        ret = b""
        ignore = False
        disconnect = False

        if is_encrypt:
            if len(crypt_bytes) >= 2**24:
                raise MessageTooLongErr

            payload_len = HEADER_LEN + len(crypt_bytes)
            header = 1 << IGNORE_BIT_POS if set_ignore else 0
            ciphertext, mac_tag = rfc8439_encrypt_and_auth(key_P, nonce, header.to_bytes(HEADER_LEN, 'big') + crypt_bytes)
            ret += L.encipher(payload_len.to_bytes(LENGTH_FIELD_LEN, 'little')) + ciphertext + mac_tag
        else:
            payload_len = int.from_bytes(crypt_bytes[:LENGTH_FIELD_LEN], 'little')
            mac_tag = crypt_bytes[LENGTH_FIELD_LEN + payload_len: LENGTH_FIELD_LEN + payload_len + RFC8439_TAG_LEN]
            authenticated, plaintext = rfc8439_auth_and_decrypt(key_P, nonce, crypt_bytes[LENGTH_FIELD_LEN:LENGTH_FIELD_LEN + payload_len], mac_tag)
            # Terminate connection if authentication fails
            if not authenticated:
                disconnect = True
            else:
                assert len(crypt_bytes) == LENGTH_FIELD_LEN + payload_len + RFC8439_TAG_LEN
                header = plaintext[:HEADER_LEN]
                ignore = header >> IGNORE_BIT_POS
                if not ignore:
                    ret += plaintext[HEADER_LEN:]

        msg_ctr += 1
        if msg_ctr == REKEY_INTERVAL:
            key_P = hashlib.sha256(rekey_salt + key_P).digest()
            msg_ctr = 0
            rekey_ctr += 1

        yield (disconnect, ignore, ret)

def v2_enc_msg(peer, msg_bytes, ignore=False):
    _, _, ret = BIP324CipherSuite(peer.send_L, peer.send_P, peer.rekey_salt, True, msg_bytes, ignore)
    return ret

def v2_dec_msg(peer, encrypted_bytes):
    disconnect, ignore, ret = BIP324CipherSuite(peer.recv_L, peer.recv_P, peer.rekey_salt, False, encrypted_bytes)
    if disconnect:
        peer.disconnect = True
        return None
    if ignore:
        return b""
    return ret

Computation overhead

TODO: Update the numbers here after changing the code.

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.[14]

Application layer specification

v2 Bitcoin P2P message structure

v2 Bitcoin P2P messages use the v2 encrypted message structure shown above. The ciphertext payload is composed of:

Field Size in bytes Comments
message_type 1-13 Encrypted one byte message type ID or ASCII message type
payload payload_length Encrypted payload

If the first byte is in the range [1, 12], it is interpreted as the number of ASCII bytes that follow for the message type. If it is in the range [13, 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. [15]

The message type IDs for existing P2P message types are:

ADDR:13
BLOCK:14
BLOCKTXN:15
CMPCTBLOCK:16
FEEFILTER:17
FILTERADD:18
FILTERCLEAR:19
FILTERLOAD:20
GETADDR:21
GETBLOCKS:22
GETBLOCKTXN:23
GETDATA:24
GETHEADERS:25
HEADERS:26
INV:27
MEMPOOL:28
MERKLEBLOCK:29
NOTFOUND:30
PING:31
PONG:32
SENDCMPCT:33
SENDHEADERS:34
TX:35
VERACK:36
VERSION:37
GETCFILTERS:38
CFILTER:39
GETCFHEADERS:40
CFHEADERS:41
GETCFCHECKPT:42
CFCHECKPT:43
WTXIDRELAY:44
ADDRV2:45
SENDADDRV2:46

The message types may be updated separately after BIP finalization.

Test Vectors

TODO: Update test vectors after updating the code.

The test vectors below illustrate the lifecycle of the authenticated encryption scheme proposed in this document. While most fields are self explanatory, ciphertext_mac_0 is the ciphertext and MAC obtained by encrypting the plaintext one time and ciphertext_mac_999 is the ciphertext and MAC obtained on the 1000th iteration of encrypting the same payload.

initiator_privkey: 9cdfc7df74056ddebee98e3310026ecb11578cad9c5d09457194cc2162a1973b
responder_privkey: 2030aaaf44a1437c07c938aa33c58751a6aee0c0e48e285f8031b137f498921d
initiator_ellsq_r32: c1efb3a6738a6d612f5f27dc35959c7e5c7d3ec15ffae3ca3159abd1582e8db7
responder_ellsq_r32: cb9dfe3802ae4caf320971d52f36f284ad88ddb976976cc2deb6bb39d0a79fde
initiator_ellsq: c6e4580be2a41dd5bfe632c46bc77c184908feb169b5d54afa79413ee48b56c6630d17f20004fc8c3de11013e979079f76066ed14a7cd774a642f0aaa5297691
responder_ellsq: 9e3fae2f60318dfa19a05120be6e44923052999d873f49f44a2bd87454d2dcada92a7871e7bfb52ea3f5f6a6c2f77c8e9d72afbb52d6d43db16a4aae9e8e2ddd
shared_ecdh_secret: bfd8c535f857d7497d29e48b157b9e3d7a88686824ee8f840971be70a7be5920
initiator_F: 9700dfcbf5184af88ef064cff42eedb18b1afcc2ca50789350f5a4ede06cff86
initiator_V: 3a94fe3415bc019459a7f63a9d289f4216bbfddba15b0fcdf4c23ca4ea9a3b37
responder_F: 9112e0dc3d5b8eb2e541174fde11d206010f5476e1ce1cfa88cf47d9ba36f3c8
responder_V: aec27acf4b74660d1e2c84c12ff3abfb7fd37b663862fcc3cb774742f5f30ad8
session_id: c601328e586f36ab09d9645d5585b1666bf08c4586355c043bf5f987e214f638
initiator_plaintext: 9bc0f24442a76af47b9daa9f0c99d41381c0c06698ffc4ad069acf3d20928277433818565904cdd66ea93b1b755a3293d1d154110faa8add3dcafed2328fffea
initiator_ciphertext_mac_0: 721aa1b6d699142d723b0e0c80279e7609d97204b06b45de55a3619488c76e3674a73beb702de27e71fc08c5120c6efe07d737704f5aa49b751d32b9022d86074af4e38985438193ed633f9932221834
initiator_ciphertext_mac_999: 74278bacb4201e2937839039ca292aa4dc9585ccc7cfe4ff22dbdacca871fa8cd0fc6f5a7ad9be439abc4ee0a6be0ef7bee7cbb6091f6c4628f9620849def1ab866760313d94d7a032599f0f14388117
responder_plaintext: 2822acbe87f9e1a284f2eaa9a56f948fc0de0c91f342ae541722b758d7956c9a8fe25b082789a2fb5b23da639d05e438461e7fcf92262dbeaeebbacbf01dcfb2
responder_ciphertext_mac_0: e731c8ec08bc3d5b65bab054a8e9e8ab8c23b65b85f09eef8d72eab5325d65b321ccbda552a1035c85f6f5e8e17778a1332b16f637335db39e04dbc4bdfa3cc8a131d10d66ad6ed49bc91b204fa7a41a
responder_ciphertext_mac_999: 523e22e00ef15bae2066dc4b9ac343b8e25261027d19f95b9b4c4917616365984093ea3d9d4505797d5b348bc50a1038418413b2164f472ba4a7062ee9cd7fb4d2934a8350a42af41e695b574d510494

initiator_privkey: 44e5fe764c43d1f7a60ead0acd0e74a4b14f7b7fc056984a993dede99e04c743
responder_privkey: fe6065b12cdfd53b9cd9b55c491063d60abdccc3090d2cdba17bf093fe363f09
initiator_ellsq_r32: f54a836324dcb9c5701c3f73edf96ebfea053a2af1be4e7bb178bf721bad5e4d
responder_ellsq_r32: ef7cd5de28f2b6b77f59ef3b4d00939841e0ab9ab5fdd351e83ce0626c90e866
initiator_ellsq: 673dbb95454e936c66e87ad6febc84386218e71cb677b6a5dee7301fdc7838148928194c153c17b0d4a279034d3f470932fc3fbed996e4fc026963a5124a2e9f
responder_ellsq: fe50e78b30f2ffee97740c6b5a0788677b70d57876a0c54a7e447413406e79e91c8a756b83592d9297deb884687302ecef77e32c02d74ca52cf0548542cde1e0
shared_ecdh_secret: f3652ea42e8bf49137128c1a19cbfecb246a2dd70e76e7ab63acca54cdc2d8f0
initiator_F: e3b4c4944ddc3ac5155957b7a0cf2950fcc4e3de100868af82ff33873c96230f
initiator_V: a437bb9995bcaeb53bd9cbdb18ea754af2442150d05aa9b5224f492794b60978
responder_F: 77593b41779de3f9e16fa8e708a7d33710b7a5c8c37a60a7456403133061513c
responder_V: 5254941ce174f447d639aac2e6984130a82580317e0761d0fe85e5600177e4a1
session_id: 9522bb78e9e23c7e4d8dc3e54cbe90aaf01123abafd796764e83fc345029eb75
initiator_plaintext: a4874343279f3ca57427a8649833a7d276023e1035f85a7bfe19597055657192b9d2c102c69f0c8b2fdaeb064cc7432e549614e5aef603f9cf41e44a2f0b41b0
initiator_ciphertext_mac_0: 6ff5d19e58165c774b2f698712b55f06bafe393457cbec42c94b07f13e50f0f7482ffc5650c08a4133ceb8bc4bb1140ae3f0f33c42b30675ed3f5f47faf2c40a173ff87b62fec3c543d7f96745658f5d
initiator_ciphertext_mac_999: 75c5d09f8db9d969815d21c217a9308e5d5a89f208f6b9cb53566def79d43f2e170b6a41674abf443bdf3d0c1eaa9a0375378450d702c1f17e7fdfa65bac4144de87cd9118098e655598e4493ccb939d
responder_plaintext: dc77639158727bd733b8accc7c4bd27d329653bceace8be353b02fa56dda8598ff52c833e6aa826c9b7458d978490b24e6cd267afe6f4f1f47edf732e6d08beb
responder_ciphertext_mac_0: 2089ce8cd6fec8975d4cbd82fcc36a5368e5bea528a90068fc822c850dccae127a36b1b38aba6bd72e60fb98bc696d349ad3b2eed8cf10b95b31baf9c28eafbbd6f1412f5bc3179e0c8f797ba4777353
responder_ciphertext_mac_999: 614ab91b337bee5973e8e95847736f3e2c82c198b14cc6254b0a367466b2328504e69b994c7553270057097146133ccc5b5f1ae87583520f2ec468e946c01dd6e08fd9d5f961c9b0aa5ec38e3e73bf54

initiator_privkey: 2e26264ea126f08b8baa90f394defc2af8e5e1a3392c1cf6456ba7879494cc29
responder_privkey: 9fc639ee5a340b6d646d3c0ab35e634c565d25d8dde5fa2ca2e79fad07c9a6d5
initiator_ellsq_r32: 794d7f24d16def675f37c41277349fc7186bfa38943f3e349fb98a9c28cd92a8
responder_ellsq_r32: 2b8e3b215a08098a43604a9f0305a2ec5f5dc0cc624293fc285c6e5f1ad412f9
initiator_ellsq: 96bb686b49a706bed07dd1e124ab0cd7257e29d3f8b4174e804e944aff8ad5bc8460784d65e716b2a6669ffc35afcc7ab9d6f4d4ce4aaa948cae292c0ccfccfa
responder_ellsq: 6381f713bab8b2fc1f1221670a8c02e6abe280aac6e54098366aba04c37f3680eed2cd477d93fbc8ef888e0bdca4e1fb077bd82586a391d7683f5c7cbdc58ea4
shared_ecdh_secret: 9d87e83bb4282e32e03c3c3dfafdd0a99c908f34563b8023f4e317036851ded1
initiator_F: 325fdbc587a13b7294845feb4d9ec66741f133d7d87f159c949394ad9165da35
initiator_V: a3f77893d7c3874babb0d3389900a4fcc74668b2416ebdce6804ac9198f0c751
responder_F: e31d3f38d4ed8fbb4ddee0d930a74a2a15523de59086e48ca1b795dd73edac92
responder_V: 4884f94ba80d6ec5a48530aad1060f6e54f10dd8bba5960995507e9816135370
session_id: 2d401c456cc77c97c2adb3a0d2bc280c4c629850675cf682528ef3a4fc8b9c3f
initiator_plaintext: 868d3648fbd816c72c54c12f403127503ba305eeb1a65a144009fae2b6e3bea4c76034a88ccee3a3396c69a970b4fff2f381097d33943a0d455f6f3303a4d3dd
initiator_ciphertext_mac_0: c2dd81a1b58d227e593e222299d4365c15af30e22ef3d7ededc5ceb3f5c7ef9d19f432c1801799101ff4f8d7eeb50e02fb3968eff2b1dd1468309dfb91d172961c70c78826242ba6086f1aee0447baef
initiator_ciphertext_mac_999: d02ae3450d2c627ef7ce1f6d9c79033c0a120ae3bc3a81b40a32265a56a941d275a9e69d93e39d566176d606bf6985af2d2604b7c5c713ded491b4687243c780e86954ba123de33ed59d53a9579dae4c
responder_plaintext: ba67e844fa8d7aa6b77dbb1737c3080da7b65c36b219d91da499b2fb58b6e6e711e7d2960ce744d1e15351badf205a829f7b55b74e971e0a9547d88ec3c30686
responder_ciphertext_mac_0: cdbd75b66a6e328bb6caa980e7fdbb4a6c29280d1c183b0aa3115d364091878f35389dcef92a3beabc54afe30802b0344df32b6f9a12673dab8ae373cd56bed6ca9f19825f75b2b562b079826655e40a
responder_ciphertext_mac_999: 385c2140033bf344028b282baec542346ce8116b1820f79c07d7e3e2a496bd6dac54bba7237a659803d8c94e3f803829dec71729d9f6e9e8063bcef9776d5992e3003c3ff3f4f5ebaa310dbd49a07c8c

initiator_privkey: a371e20223e60e967233fe079f052aeabd30f6c6781314f3e7c44e049c648b7d
responder_privkey: 8063aec031db643874c6629942c402e48f7d74abaf97a8faf8d4628010e46ba4
initiator_ellsq_r32: ec23b3eab32028a9981ff20851abdd10846951b88989950cc31565bd9a3cda79
responder_ellsq_r32: 546bfd88292d90a9bbf697380c68f017fdf911d20acad6c3c7e900eff0205a83
initiator_ellsq: 838423b90876431a97d31c881bf168d3e64dc5b9ce2feb3d344910a3394aec57c0c701c5d4b99febb62c637319369e02fcb5a0af3f5879e65ea0892e08ed3704
responder_ellsq: 8ac47b65a6f9670216a4ce1b25a04caab0db3383d2ba940cd335c5a5953ffc4de1dec70c8606e0ee009f49dff607b5c72240f67e1574893a7b4997b99a39d29c
shared_ecdh_secret: 0275605a44d65f47d81ec714315e8b92731b1b063daec2b3125dd455d3099b7f
initiator_F: 162698894238972d7c851908961235f530890ef251e6da6c20f8e64fcb122ff2
initiator_V: ed0650d151c90c106d8d1578aa9c27f7ebc13adc08e2b4f4ef642859546fc8cb
responder_F: 70d28c04c85fdc62d20cb982e329a976f18e3c98e8f4c769b0d5504bb8a975e2
responder_V: 1b5b7b526af54a45c1aebf2e43cf30742798e638a0e3ca5ce4e4373fcf472b7c
session_id: dde1588198203a7d2309df7bc3b67941004f97a63cadd83ec1b8859cc3e0ff9e
initiator_plaintext: 3e7443578c300b7210860c17168c9e11414781f6168710968777b567f1c27165bc8118ef402150549c18de9b567b85d4046fbef91f502f8cf4c298888ddd434b
initiator_ciphertext_mac_0: 5ab6cb5ec037e6eb84a6c411f68e0d7bda6b17da949f03cb8521bebb825f7d9e72699989961ea29b2c4d253d3636dbc96c45f261ef883592282922160949772fb43e63a8084afb808063c7b7a186feb7
initiator_ciphertext_mac_999: 4804238eef14c76a1bd602c0755ea53bc0790f252dff4ca9996d0ed2497db7226ec631e412e07d715922cb03921d907f02e5f1daa82d0e97a730edb8a84f628b9f7c9f4c4889867a6c95c16586e592ee
responder_plaintext: 7f6c9fbae0c003bb38ee2e73c31b248d639cc63b0d5d57b05f57c8b82122d61e401af33d481304a7d956b9ca730500890908682b14933cde958bf497cbcbbd45
responder_ciphertext_mac_0: c9fa4d4c1a404d1e707b8412d5e668129fd7a0fd2ae85be93c1b5f81bed223d9ce54d4369ada34d94bb1b2650a4e175ae0d3fc30a0fde3fd8b43ba37d84382cff5e5d1f0f0a956bd678e96c99041d5f4
responder_ciphertext_mac_999: e89ca85ef9edbc1fce19b1af7972201070359c94cbe7c2f26fcbc76c97d7187730ab5e6470483b8b49f22680cbf8d6973fafab181aa5bd0bfeff0e57b56e33cfc505ec09b94647c4eedcde989104bfae

initiator_privkey: 928861cf12421b8174bce71bdbdf4397213e17977e40116d79fd42372dfce856
responder_privkey: 1b06ce10bfdeb76e002d370df40120eb0472b432c5f6535d6a47cff44e126255
initiator_ellsq_r32: 1f909dc3ba59acbc6d24f589712cba5ac3926d7c8bc79f02316f4d1adb4f1b26
responder_ellsq_r32: 8bc6a59833a8e94810665ac0360b8c976d3f6dfec9573ae8333759e7d5fa8af8
initiator_ellsq: 4b681cf9aced2d5dbc515a1b82229e8fd9109d25827a8788064957f1578203aab31a5f52bfdcd625b0deead1d6172bbfead0be81365ab986584dfbff6620d403
responder_ellsq: ae43ccfeb45f7acc8a6e40d6c202d3845e8d19c8e1233030770b394c0ef5e239da0bb9ef921d5d7fcc6835f5d88ce538728919b5a465b25da786a19018c2891f
shared_ecdh_secret: 1832945b258ae71af6fd14266047629ae4363587ee0e7ec8bc5d94a4aee2cfb7
initiator_F: 9663c2dedeb0cac8d16696944b5da3b726f900dc8dd8fcb59704d1c0c22968ca
initiator_V: dbd218da57787a363f2fad7b797ab124e09dace125fb03930c750e651cd348b7
responder_F: 16d67432eb7866746ffdcfa660bdcedfe3b964aa99cb50b9fa426cfe8b09c276
responder_V: 002f859dc28fce49f9bdaada974688ae872deb089c200b5d00101abb2b74389b
session_id: 0c77bd7b101a9f2da27da6ea0aba3bffb0679c7bdeff3ddeb5e00a375570ec93
initiator_plaintext: 7ab5826761ecf971d0aeff4c14eed091a206d29ddd84681c206b333bf0e121fcc5f8d45a266ce9ded4f7476edd0ab941c59cf4bca47f9327cf26a78ab4c9e7d6
initiator_ciphertext_mac_0: f85759a7cea0ea0f1ee5296866aadb04bfa84bf1c4ac99136e0af930a60057569a6f58b4ea6baac2d02ff876d783c8941af563b3b172e81d381698ba624e053029365d1ae71e2b2938e20c55900ef547
initiator_ciphertext_mac_999: afb98425ca1fad833bcc5f7a0f82d35e7e695957b220b24ae7c58c3b29e89cfa030b01e58015340c1476d39f53e9a6594b7f230f91e9ccb16513d91d2d840920ad76623430825083b4739ca09fa4268f
responder_plaintext: dee314b076651a31f0e7451f4e0c3cebddeb6ce82d937b14e036cfa8ae8a91d3afd2760351c0c146fe8740874a3e281fb298cb00a1d9e58a1081f173466ceed6
responder_ciphertext_mac_0: 174770b958408c64838759f470480cacaed1f9034c80254763a0aa0648a5cd1c09df983da2f848c9c7f05e6c69df3a8547fa88b545d6b8b497d34ac4885f3fd4656e5baee10c99f1325f651723a1e0c9
responder_ciphertext_mac_999: 458304181686c4deaf25b223a59951edeaf5ca95202875c50f4fbc4549dde8c098bb6021a543a324a88f0d832079fb7cdf10c606472385e946aa5d8c7d16aa19b440416542848b12b215b759079ac987

Rationale and References

  1. ^ For more information, see the Bitcoin over Tor isn't a Good Idea paper
  2. ^ While Noise and TLS (as a draft) offer similar protocol extensions for exporting session IDs, using channel binding for authentication is not at the focus of their design and would not avoid the bulk of additional complexity due to the native support of authentication methods.
  3. ^ Elligator-squared is an encoding for elliptic curve points that allows us to make the serialization indistinguishable from a uniformly distributed bitstream. While a random 256-bit string has about 50% chance of being a valid x-coordinate on the secp256k1 curve, every 512-bit string is a valid Elligator-squared encoding of a curve point, making the encoded point indistinguishable from random when using an encoder that can sample uniformly.
  4. ^ In writing this proposal, the running time of Elligator-squared encoding was benchmarked. Encoded ECDH is about ~2.72x as expensive than the unencoded ECDH. Given the fast performance of ECDH and the low frequency of new connections, we found the performance trade-off acceptable for the pseudorandom bytestream and future censorship resistance it can enable.
  5. ^ Once an encrypted transport and its session version is established, further properties of the transport can be negotiated. Examples include post-quantum cryptography upgrades to the handshake and optional authentication. While this proposal does not include any such features, it allows for an upgrade path using transport version numbers. As a hypothetical example, v2.1 clients would exchange the transport version number 1. If both peers are v2.1, the transport session version would be 1. This could imply that before any P2P messages are exchanged, a post-quantum key exchange mechanism must be used (as defined in the hypothetical v2.1 protocol) to upgrade the security of the derived encryption keys against quantum computers.
  6. ^ Disallowing elligator-squared encodings that begin with the specified prefix does result in a less than perfectly pseudorandom bytestream. While the 2^-96 bias this introduces implies a security level that is below our 128-bit security level, the low frequency of connections makes this practically unexploitable, while significantly simplifying compatibility.
  7. ^ Using only the x-coordinate provides the same security as using a full encoding of the secret curve point but might allow for more efficient ECDH implementations in the future, e.g., based on x-only multiplication ladders. Nevertheless, the function secp256k1_ellsq_ecdh_secret can be implemented by calling the existing secp256k1_ecdh function in libsecp256k1 with an appropriate secp256k1_ecdh_hash_function that simply ignores the y-coordinate, and thus implementing this proposal does not require an entirely new ECDH implementation in libsecp256k1.
  8. ^ An MITM attacker should be assumed to have knowledge of the plaintext corresponding to the encrypted 3-byte length. This can lead to malleability in the encrypted length ciphertext since it is used prior to checking the MAC. We use two cipher instances with separate keys to avoid any consequences of such malleability leaking into the payload portion of the ciphertext. This is loosely inspired by a [similar](http://bxr.su/OpenBSD/usr.bin/ssh/PROTOCOL.chacha20poly1305). The two layered approach makes it easier to reason about the security of the suite. The cost in creating an additional ChaCha20 instance is very low.
  9. ^ The associated data is empty in our proposal as the length of the ciphertext (separately encrypted and transmit in the first 3 bytes) is implicitly authenticated in RFC8439. Even though the outer layer does not authenticate the encrypted message length, the authentication is provided by the inner layer.
  10. ^ If an attacker passively collects ciphertext and later learns the key, they can decrypt all the historical ciphertext.
  11. ^ Assuming a node sends only ping messages every 20 minutes (the timeout interval for post-BIP31 connections) on a connection, the node will transmit 256 messages in about 3.56 days. This means soft rekeying after a fixed number of messages automatically translates to an upper-bound of time interval for rekeying.
  12. ^ Re-keying ensures forward secrecy within the DRBG , i.e., an attacker compromising the current DRBG key cannot derive past DRBG keys.
  13. ^ Even if future evolutions of the protocol need messages longer than 2^24-2 bytes, they should be split into multiple application-layer messages anyway, to avoid needing very large receive buffers per peer.
  14. ^ As of the writing of this document, BIP324CipherSuite for 1MB payloads (encrypt + decrypt) was about 1.5x faster than 2 * (SHA256(1MB) + SHA256(32 bytes)).
  15. ^ Here are some length comparisons between v1 and v2 messages:
    v1 inv: 4(Magic)+12(Message-Type)+4(MessageSize)+4(Checksum)+37(Payload) == 61
    v2 inv: 3(Length)+1(Header)+1(Message-Type)+37(Payload)+16(MAC) == 58
    
    v1 ping: 4(Magic)+12(Message-Type)+4(MessageSize)+4(Checksum)+8(Payload) == 32
    v2 ping: 3(Length)+1(Header)+1(Message-Type)+8(Payload)+16(MAC) == 29
    
    v1 block: 4(Magic)+12(Message-Type)+4(MessageSize)+4(Checksum)+1’048’576(Payload) = 1’048’600
    v2 block: 3(Length)+1(Header)+1(Message-Type)+1’048’576(Payload)+16(MAC) = 1’048’597
    

Acknowledgements

Thanks to everyone (alphabetical order) that helped invent and develop the ideas in this proposal:

  • Lloyd Fournier
  • Gregory Maxwell
@LLFourn
Copy link

LLFourn commented Aug 18, 2021

Comments on the new text (which are mostly the same as the old but less speculative):

ODD secp256k1 public keys MUST be used (public keys starting with 0x03). If the public key from the generated ephemeral key is an EVEN public key (starting with 0x02), its private key SHOULD be negated and then recalculated. Only using ODD public keys makes it more complex to identify the handshake based by analyzing the traffic and looking for 33 bytes that start with 0x02 or 0x03. This in turn increases the cost of censorship.

I maintain it would be better if this was EVEN instead of ODD because that's what's done in BIP340 and it doesn't make sense to introduce a new x-only key that behaves differently to the existing one. BIP340 should be referenced but unfortunately it doesn't look like you can explicitly reference any particular bit of it because it doesn't seem to have a notion of key-pair generation like this document does (which works well here IMO so I would keep it). For me, the justification for only sending 32 byte keys can safely be omitted since it sounds odd to justify why you're not sending a useless byte when that is now more established practice with BIP340.

Two separate cipher instances are used here so as to keep the packet lengths confidential (best effort; for passive observing) but not create an oracle for the packet payload cipher by decrypting and using the packet length prior to checking the MAC. By using an independently-keyed cipher instance to encrypt the length, an active attacker seeking to exploit the packet input handling as a decryption oracle can learn nothing about the payload contents or its MAC (assuming key derivation, ChaCha20 and Poly1305 are secure). Active observers can still obtain the message length (ex. active ciphertext bit flipping or traffic semantics analysis)

I still find this explanation very unhelpful. The only purpose of the length field is to find the end of the payload and check the MAC so why would you use the payload length for anything "prior to checking the MAC". If we didn't have MACs there would be no need for length fields at the transport encryption layer at all afaict -- checking the MAC is its sole purpose. I think the dual stream design is justifiable because it's easier to implement with two streams and easier to get an intuition for the security without having to think carefully about it.

Thanks for working on this @dhruv I think I will have to implement myself to offer further feedback.

@dhruv
Copy link
Author

dhruv commented Aug 23, 2021

Thank you for the review @LLFourn!

I maintain it would be better if this was EVEN instead of ODD because that's what's done in BIP340 and it doesn't make sense to introduce a new x-only key that behaves differently to the existing one.

Agreed. Updated.

For me, the justification for only sending 32 byte keys can safely be omitted

I'd like to keep that in case any reader is unaware of BIP340 specifics.

I still find this explanation very unhelpful.

I completely agree. It's the one paragraph I trip over again and again. I've updated it to:

Even with end-to-end encryption, the Bitcoin p2p protocol can be fingerprinted (using public information like new blocks) and an MITM attacker should be assumed to have knowledge of the plaintext corresponding to the encrypted length. This can lead to malleability in the encrypted length portion of the ciphertext since it is used prior to checking the MAC. We use two cipher instance to avoid any consequences of such malleability leaking into the payload portion of the ciphertext. The two cipher instances make it easier to reason about the security of the suite. The cost in creating additional ChaCha20 and Poly1305 instances is very low.

@real-or-random
Copy link

real-or-random commented Aug 24, 2021

I think there are still some bigger open questions and this needs a long way. Unfortunately, noone really had the time to contribute, so it's really nice to have @dhruv push the BIP forward now. :)

Here are some points:

  • Is it a goal to hide that this is the Bitcoin protocol? It's not stated as a goal but the text uses this as justification for 32-byte keys. But anyway, this is not effective: Even an attacker who ignores ports, timing etc, can just check for evenness of the 32-bytes. If we want to counter the guaranteed evenness, we could do so using the Elligator Squared techniques. But before we can design a proper protocol that achieves the desired objectives, we need to be clear about the objectives.
  • Post-quantum key exchange: this was discussed a few years ago. We could add a post-quantum key exchange to make sure confidentiality holds up even after an attacker has a quantum computer and can break old recorded connections. That may be overkill (this was my position a few years ago) but connections are long-lived and we don't really care about performance of connection establishment, so this may be a valid thing to do.
  • No matter if we want to integrate the post-quantum key exchange, we should think about extensibility. If we don't integrate that now, what is the proper way to add this or other features in the future -- without having fallback attacks? (TLS has done a lot here and I discussed this a while ago with @sipa but I don't think I can remember all of the details).
  • This BIP is about encryption only. We should design it carefully such that authentication is possible in an established encryption session. I think the BIP achieves this with the session ID but we need to look at this. (This is stated as a goal: "Peer operators can compare encryption session IDs").
  • (I'm sure there's more.)

I suggest we create a repo for it (can be private for now) because this would make discussion much easier: We can have separate issues. And even more important, we can have PRs, which makes it much easier to make updates and improve wording etc.

@sipa
Copy link

sipa commented Aug 24, 2021

I agree with creating a repository, so separate aspects can be discussed separately.

FWIW, I've discussed the idea of a generic extension/feature negotiation mechanism after the ECDH step, and before starting the application level protocol with @dhruv. It's probably easiest to work on that as part of a repository too.

I have some thoughts on @real-or-random's discussion points, but I'll keep them for discussing them as issues if we go that route, to avoid spreading everything out.

@dhruv
Copy link
Author

dhruv commented Aug 25, 2021

I've created a repo for now to get the large outstanding questions under discussion. Once we are in reasonable shape, I will update again here or make that repo public.

@dhruv
Copy link
Author

dhruv commented Oct 8, 2021

Pushed revision 92 removing the BIP 61 REJECT p2p message which is no longer used.

@GeneFerneau
Copy link

GeneFerneau commented Nov 5, 2021

On rekey, it may help strengthen entropy to feed the last 32 bytes of keystream into the HKDF:

... The IV is initialized to 0 and incremented on every re-key event.

k0 = key
iv = 0
k0 = HKDF_EXPAND(prk=k0, hash=SHA256, info="BitcoinK_Rekey", L=32)
ks0, k1 = ChaCha20DRBG(k0, iv)[0:4064], ChaCha20DRBG(k0, iv)[4064:4096]
iv = iv + 1
k1 = HKDF_EXPAND(prk=k1, hash=SHA256, info="BitcoinK_Rekey", L=32)
ks1, k2 = ChaCha20DRBG(k1, iv)[0:4064], ChaCha20DRBG(k1, iv)[4064:4096]
iv = iv + 1
k2 = HKDF_EXPAND(prk=k2, hash=SHA256, info="BitcoinK_Rekey", L=32)
ChaCha20Forward4064DRBG(key) = ks0 || ks1 || ks2 || ...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment