Skip to content

RLPx加密传输协议

erick yan edited this page Sep 2, 2019 · 3 revisions

RLPx 加密传输协议

RLPx 基于 TCP 传输协议用于 ftnode 加密通信。RLPx 的名字来自于 RLP 序列化。

符号

X || Y
    表示X和Y的拼接
X ^ Y
    X和Y按位异或
X[:N]
    X的前N个字节
[X, Y, Z, ...]
    [X, Y, Z, ...]的RLP编码
keccak256(MESSAGE)
    keccak256哈希算法
ecies.encrypt(PUBKEY, MESSAGE, AUTHDATA)
    RLPx使用的非对称身份验证加密函数。
    AUTHDATA是身份认证的数据,并非密文的一部分。
    但是AUTHDATA会在生成消息tag前,写入HMAC-256哈希函数。
ecdh.agree(PRIVKEY, PUBKEY)
    ECDH密钥协商函数。
ecdsa.sign(PRIVKEY, signed)
    ecdsa签名,PRIVKEY是签名用的私钥,signed是消息的哈希。

ECIES 加密

ECIES 非对称加密用于 RLPx 握手。RLPx 用了该加密体系的参数:

  • 椭圆曲线 secp256k1 基点G
  • KDF(k, len): 密钥推导函数 《NIST SP 800-56 Concatenation Key Derivation Function》SEC 5.8.1
  • MAC(k, m): HMAC 函数,使用了 SHA-256 哈希
  • AES(k, iv, m): AES-128 对称加密,CTR 模式。

Alice 想发送加密消息给 Bob,并期望 Bob 可以用他的私钥kB解密。Alice 知道 Bob 的公钥KB

Alice 为了加密消息m:

  1. 生成一个随机数r并生成对应的公钥R = r * G.
  2. 计算共享密码S = Px,其中 (Px, Py) = r * KB.
  3. 推导加密认证用的 keykE || kM = KDF(S, 32)以及随机向量iv.
  4. 使用 AES 加密 c = AES(kE, iv, m).
  5. 计算 MAC 校验 d = MAC(keccak256(kM), iv || c).
  6. 发送完整密文R || iv || c || d给 Bob.

Bob 解密密文R || iv || c || d:

  1. 推导共享密码S = Px, 其中(Px, Py) = r _ KB = kB _ R.
  2. 推导加密认证用的 keykE || kM = KDF(S, 32).
  3. 计算并检查 MACd = MAC(keccak256(kM), iv || c).
  4. 解密明文m = AES(kE, iv || c).

节点身份

所有的加密操作都是基于 secp256k1 椭圆曲线。每个节点维护一个静态的 secp256k1 私钥。该私钥只能被手动重置(删除私钥文件)。

握手流程

RLPx 基于 TCP 通信,并且每次通信都会生成随机的临时密钥用于加密。

握手消息:

发起方(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: 随机生成的私钥
  • initiator-nonce: 随机数
  • NetID: 网络 ID
  • static-shared-secret: 计算方式见下文

接收方(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)
  • auth-versionack-version不同不会导致错误(用于协议升级)
  • auth-bodyack-body多余的字段会被忽略(用于协议升级)
  • NetID不同将导致握手失败

握手密钥生成

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 = 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

RLPx 中的消息认证(Message authentication)使用了两个 keccak256 状态,每个传输方向一个。egress-macingress-mac分别代表发送和接收状态,每次发送或者接收密文,其状态都会更新。初始握手后,MAC 状态初始化如下:

发送方初始状态:

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

接收方初始状态:

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

当发送一帧数据时,通过发送的数据更新egress-mac状态,然后计算相应的 MAC 值。

计算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]

计算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]

只要发送者和接受者按相同的方式更新egress-macingress-mac,就可以对密文进行校验。

消息结构

Hello消息

frame-data = msg-id || msg-data
frame-size = length of frame-data, encoded as a 24bit big-endian integer
  • msg-id是一个 RLP 编码后的整数。
  • msg-data是一个 RLP 编码后的消息列表。

Hello消息是握手完成后的第一包数据,所有Hello消息之后的消息,都会使用 Snappy 算法压缩。

压缩后的消息:

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

基于msg-id的消息分类

虽然 frame 中支持capability-idcontext-id, 但是这两个字段并没有被利用。当前的版本使用 msg-id 来区分不同的消息。

msg-id应用层消息的值大于 0x11(0x00-0x10 保留用于p2p capability)

p2p capability

在握手协商完成后,连接的双方需要发送Hello消息。

任何时候,都可能会收到Disconnect消息。

Hello(0x00)

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

握手完成后,双方发送的第一包数据。在收到Hello消息前,不可以发送任何其他的消息(Disconnect除外)。

  • Hello消息中多余的字段会被忽略(用于协议升级)
  • protocolVersion: 当前版本是 5
  • clientId: 节点名,人类可读的字符串, 比如"Fractal-P2P"
  • capabilities: 支持的子协议列表,名称及其版本
  • listenPort: 节点的监听端口。0 表示没有监听。
  • nodeId: secp256k1 的公钥,对应节点身份的私钥.

Disconnect(0x01)

[reason: P]

通知节点断开连接。收到该消息后,节点会立刻断开连接。如果是发送,正常的主机会在发送后断开连接。

  • reason: 一个可选的整数,表示连接断开的原因:
    • 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)

[]

心跳,请求回复Pong

Pong(0x03)

[]

回复Ping