RLPx Encrypted Transmission Protocol - Second-Earth/setchain GitHub Wiki

RLPx Encrypted Transmission Protocol

RLPx is based on the TCP transmission protocol for setnode encrypted communication. The name RLPx comes from RLP serialization.

Symbol

X || Y
    Represents the splicing of X and Y
X ^ Y
    X and Y bitwise exclusive OR
X[:N]
    The first N bytes of X
[X, Y, Z, ...]
    [X, Y, Z, ...] RLP encoding
keccak256(MESSAGE)
    keccak256 hash algorithm
ecies.encrypt(PUBKEY, MESSAGE, AUTHDATA)
    Asymmetric authentication encryption function used by RLPx.
    AUTHDATA is the data for identity authentication, not part of the ciphertext.
    But AUTHDATA will write the HMAC-256 hash function before generating the message tag.
ecdh.agree(PRIVKEY, PUBKEY)
    ECDH key agreement function.
ecdsa.sign(PRIVKEY, signed)
    ecdsa signature, PRIVKEY is the private key for signing, and signed is the hash of the message.

ECIES encryption

ECIES asymmetric encryption is used for RLPx handshake. RLPx uses the parameters of the encryption system:

-Elliptic curve secp256k1 base point G. -KDF(k, len): Key Derivation Function 《NIST SP 800-56 Concatenation Key Derivation Function》SEC 5.8.1 -MAC(k, m): HMAC function, using SHA-256 hash -AES(k, iv, m): AES-128 symmetric encryption, CTR mode.

Alice wants to send an encrypted message to Bob, and expects Bob to be able to decrypt it with his private key kB. Alice knows Bob's public key KB.

Alice wants to encrypt the message m:

  1. Generate a random number r and generate the corresponding public key R = r * G.
  2. Calculate the shared password S = Px, where (Px, Py) = r * KB.
  3. Derive the keykE || kM = KDF(S, 32) and the random vector iv for encryption authentication.
  4. Use AES encryption c = AES(kE, iv, m).
  5. Calculate the MAC check d = MAC(keccak256(kM), iv || c).
  6. Send the complete ciphertext R || iv || c || d to Bob.

Bob decrypts the ciphertext R || iv || c || d:

  1. Derive the shared password S = Px, where (Px, Py) = r _ KB = kB _ R.
  2. Derive the key for encryption authentication kE || kM = KDF(S, 32).
  3. Calculate and check MACd = MAC(keccak256(kM), iv || c).
  4. Decrypt the plaintext m = AES(kE, iv || c).

Node identity

All encryption operations are based on secp256k1 elliptic curve. Each node maintains a static secp256k1 private key. The private key can only be reset manually (delete the private key file).

Handshake process

RLPx is based on TCP communication, and each communication generates a random temporary key for encryption.

Handshake message:

Initiator

auth = auth-size || enc-auth-body
auth-size = size of enc-auth-body, encoded as a big-endian 16-bit integer
auth-version = 5
auth-body = [sig, initiator-pubk, initiator-nonce, auth-version, NetID, ...]
sig = ecdsa.sign(initiator-ephemeral-privkey, static-shared-secret ^ initiator-nonce)
enc-auth-body = ecies.encrypt(recipient-pubk, auth-body, auth-size)

-initiator-ephemeral-privkey: randomly generated private key -initiator-nonce: random number -NetID: Network ID -static-shared-secret: See below for calculation method

Recipient

ack = ack-size || enc-ack-body
ack-size = size of enc-ack-body, encoded as a big-endian 16-bit integer
ack-version = 5
ack-body = [recipient-ephemeral-pubk, recipient-nonce, ack-version, NetID, ...]
enc-ack-body = ecies.encrypt(initiator-pubk, ack-body, ack-size)

-The difference between auth-version and ack-version will not cause an error (used for protocol upgrade) -The extra fields of auth-body and ack-body will be ignored (used for protocol upgrade) -Different NetID will cause handshake failure

Handshake key generation

static-shared-secret = ecdh.agree(privkey, remote-pubk)
ephemeral-key = ecdh.agree(ephemeral-privkey, remote-ephemeral-pubk)
shared-secret = keccak256(ephemeral-key || keccak256(nonce || initiator-nonce))
aes-secret = keccak256(ephemeral-key || shared-secret)
mac-secret = keccak256(ephemeral-key || aes-secret)

Frame structure

After the handshake, all messages are transmitted in frames. One frame of data carries a packet of encrypted messages.

The frame header provides information about the size of the message and the function of the message source. Padding is used to align the encrypted data block (depending on the smallest encrypted block of the encryption algorithm).

frame = header-ciphertext || header-mac || frame-ciphertext || frame-mac
header-ciphertext = aes(aes-secret, header)
header = frame-size || header-data || header-padding
header-data = [capability-id, context-id]
capability-id = integer, always zero
context-id = integer, always zero
header-padding = zero-fill header to 16-byte boundary
frame-ciphertext = aes(aes-secret, frame-data || frame-padding)
frame-padding = zero-fill frame-data to 16-byte boundary

MAC

Message authentication in RLPx uses two keccak256 states, one for each transmission direction. egress-mac and ingress-mac represent the sending and receiving status respectively. Each time a ciphertext is sent or received, its status will be updated. After the initial handshake, the MAC state is initialized as follows:

The initial state of the sender:

egress-mac = keccak256.init((mac-secret ^ recipient-nonce) || auth)
ingress-mac = keccak256.init((mac-secret ^ initiator-nonce) || ack)

The initial state of the receiver:

egress-mac = keccak256.init((mac-secret ^ initiator-nonce) || ack)
ingress-mac = keccak256.init((mac-secret ^ recipient-nonce) || auth)

When sending a frame of data, update the status of egress-mac through the sent data, and then calculate the corresponding MAC value.

Calculate header-mac:

header-mac-seed = aes(mac-secret, keccak256.digest(egress-mac)[:16]) ^ header-ciphertext
egress-mac = keccak256.update(egress-mac, header-mac-seed)
header-mac = keccak256.digest(egress-mac)[:16]

Calculate frame-mac:

egress-mac = keccak256.update(egress-mac, frame-ciphertext)
frame-mac-seed = aes(mac-secret, keccak256.digest(egress-mac)[:16]) ^ keccak256.digest(egress-mac)[:16]
egress-mac = keccak256.update(egress-mac, frame-mac-seed)
frame-mac = keccak256.digest(egress-mac)[:16]

As long as the sender and receiver update egress-mac and ingress-mac in the same way, the ciphertext can be verified.

Message structure

Hello message

frame-data = msg-id || msg-data
frame-size = length of frame-data, encoded as a 24bit big-endian integer

-msg-id is an RLP-encoded integer. -msg-data is a message list after RLP encoding.

The Hello message is the first packet of data after the handshake is completed. All messages after the Hello message will be compressed using the Snappy algorithm.

Compressed message:

frame-data = msg-id || snappyCompress(msg-data)
frame-size = length of (msg-id || snappyCompress(msg-data)) encoded as a 24bit 
big-endian integer

Message classification based on msg-id

Although capability-id and context-id are supported in the frame, these two fields are not used. The current version uses msg-id to distinguish different messages.

The value of msg-id application layer message is greater than 0x11 (0x00-0x10 are reserved for p2p capability)

p2p capability

After the handshake negotiation is completed, the connected parties need to send a Hello message.

At any time, you may receive a Disconnect message.

Hello(0x00)

[protocolVersion: P, clientId: B, capabilities, listenPort: P, nodeKey: B_64, ...]

After the handshake is completed, the first packet of data sent by both parties. Before receiving the Hello message, no other messages (except Disconnect) can be sent.

-Extra fields in the Hello message will be ignored (used for protocol upgrade) -protocolVersion: The current version is 5 -clientId: node name, a human-readable string, such as "set-P2P" -capabilities: list of supported sub-protocols, names and versions -listenPort: The listening port of the node. 0 means no monitoring. -nodeId: The public key of secp256k1, corresponding to the private key of the node identity.

Disconnect(0x01)

[reason: P]

Notify the node to disconnect. After receiving this message, the node will immediately disconnect. If it is sending, the normal host will disconnect after sending.

-reason: An optional integer that indicates the reason for the disconnection: -0x00 Disconnect requested; -0x01 TCP sub-system error; -0x02 Breach of protocol, e.g. a malformed message, bad RLP, incorrect magic number; -0x03 Useless peer; -0x04 Too many peers; -0x05 Already connected; -0x06 Incompatible P2P protocol version; -0x07 Null node identity received-this is automatically invalid; -0x08 Client quitting; -0x09 Unexpected identity (i.e. a different identity to a previous connection/what a trusted peer told us). -0x0a Identity is the same as this node (i.e. connected to itself); -0x0b Timeout on receiving a message (i.e. nothing received since sending last ping); -0x0c Peer is in blacklist. -0x10 Some other reason specific to a subprotocol.

Ping(0x02)

[]

Heartbeat, request reply Pong package

Pong(0x03)

[]

Reply to Ping package

⚠️ **GitHub.com Fallback** ⚠️