RLPx是以太坊节点间通信的TCP传输协议,支持加密消息和能力协商。它详细定义了ECIES加密、初始握手、消息帧以及MAC机制,并描述了P2P能力中的Hello、Disconnect等消息,旨在确保节点间通信的安全性和效率。
本规范定义了 RLPx 传输协议,这是一个基于 TCP 的传输协议,用于以太坊节点之间的通信。该协议承载一个或多个在连接建立期间协商的“能力”的加密消息。RLPx 以 RLP 序列化格式命名。这个名字不是一个缩写,也没有特殊的含义。
当前协议版本是 5。你可以在本文档末尾找到过去版本中的更改列表。
X || Y\
表示 $\text{X} || \text{Y}$ 的连接。\
X ^ Y\
是 $\text{X} \oplus \text{Y}$ 的逐字节 XOR。\
X[:N]\
表示 $\text{X}$ 的 $X[:N]$ 前缀。\
[X, Y, Z, ...]\
表示作为 RLP 列表的递归编码:$[\text{X}, \text{Y}, \text{Z}, ...]$。\
keccak256(MESSAGE)\
是以太坊使用的 $\text{keccak256}(\text{MESSAGE})$ 哈希函数。\
ecies.encrypt(PUBKEY, MESSAGE, AUTHDATA)\
是 RLPx 使用的非对称认证加密函数:$\text{ecies.encrypt}(\text{PUBKEY}, \text{MESSAGE}, \text{AUTHDATA})$。\
$\text{AUTHDATA}$ 是认证数据,它不是结果密文的一部分,但会在生成消息标签之前写入 HMAC-256。\
ecdh.agree(PRIVKEY, PUBKEY)\
是 $\text{PRIVKEY}$ 和 $\text{PUBKEY}$ 之间的椭圆曲线 Diffie-Hellman 密钥协商:$\text{ecdh.agree}(\text{PRIVKEY}, \text{PUBKEY})$。
ECIES(椭圆曲线集成加密方案)是 RLPx 握手中使用的一种非对称加密方法。RLPx 使用的密码系统是
Alice 想要发送一条加密消息,该消息可以由 Bob 的静态私钥 $k_B$ 解密。Alice 知道 Bob 的静态公钥 $K_B$。
为了加密消息 $m$,Alice 生成一个随机数 $r$ 和相应的椭圆曲线公钥 $R = r G$,并计算共享密钥 $S = P_x$,其中 $(P_x, P_y) = r K_B$。她将密钥材料派生用于加密和认证,形式为 $k_E || k_M = \text{KDF}(S, 32)$,以及一个随机初始化向量 $iv$。Alice 将加密消息 $R || iv || c || d$ 发送给 Bob,其中 $c = \text{AES}(k_E, iv , m)$ 且 $d = \text{MAC}(\text{sha256}(k_M), iv || c)$。
为了让 Bob 解密消息 $R || iv || c || d$,他派生共享密钥 $S = P_x$,其中 $(P_x, P_y) = k_B * R$,以及加密和认证密钥 $k_E || k_M = \text{KDF}(S, 32)$。Bob 通过检查 $d == \text{MAC}(\text{sha256}(k_M), iv || c)$ 来验证消息的真实性,然后获得明文 $m = \text{AES}(k_E, iv || c)$。
所有密码操作都基于 secp256k1 椭圆曲线。每个节点都应维护一个静态的 secp256k1 私钥,该私钥在会话之间保存和恢复。建议私钥只能通过手动方式重置,例如,通过删除文件或数据库条目。
RLPx 连接通过建立 TCP 连接并协商临时密钥材料来建立,以进行进一步的加密和认证通信。创建这些会话密钥的过程称为“握手”,在“发起者”(打开 TCP 连接的节点)和“接收者”(接受连接的节点)之间进行。
auth 消息auth(检查签名恢复 == $\text{keccak256}(\text{ephemeral-pubk})$)remote-ephemeral-pubk 和 nonce 生成 auth-ack 消息auth-ack 并派生密钥如果第一个成帧数据包的认证失败,任何一方都可以断开连接。
握手消息:
$\text{auth} = \text{auth-size} || \text{enc-auth-body}$
$\text{auth-size} = \text{enc-auth-body 的大小,编码为大端 16 位整数}$
$\text{auth-vsn} = 4$
$\text{auth-body} = [\text{sig}, \text{initiator-pubk}, \text{initiator-nonce}, \text{auth-vsn}, ...]$
$\text{enc-auth-body} = \text{ecies.encrypt}(\text{recipient-pubk}, \text{auth-body} || \text{auth-padding}, \text{auth-size})$
$\text{auth-padding} = \text{任意数据}$
$\text{ack} = \text{ack-size} || \text{enc-ack-body}$
$\text{ack-size} = \text{enc-ack-body 的大小,编码为大端 16 位整数}$
$\text{ack-vsn} = 4$
$\text{ack-body} = [\text{recipient-ephemeral-pubk}, \text{recipient-nonce}, \text{ack-vsn}, ...]$
$\text{enc-ack-body} = \text{ecies.encrypt}(\text{initiator-pubk}, \text{ack-body} || \text{ack-padding}, \text{ack-size})$
$\text{ack-padding} = \text{任意数据}$
实现必须忽略 $\text{auth-vsn}$ 和 $\text{ack-vsn}$ 中的任何不匹配。实现也必须忽略 $\text{auth-body}$ 和 $\text{ack-body}$ 中的任何额外列表元素。
握手消息交换后生成的密钥:
$\text{static-shared-secret} = \text{ecdh.agree}(\text{privkey}, \text{remote-pubk})$
$\text{ephemeral-key} = \text{ecdh.agree}(\text{ephemeral-privkey}, \text{remote-ephemeral-pubk})$
$\text{shared-secret} = \text{keccak256}(\text{ephemeral-key} || \text{keccak256}(\text{nonce} || \text{initiator-nonce}))$
$\text{aes-secret} = \text{keccak256}(\text{ephemeral-key} || \text{shared-secret})$
$\text{mac-secret} = \text{keccak256}(\text{ephemeral-key} || \text{aes-secret})$
初始握手后的所有消息都经过成帧处理。一个帧承载一个属于某个能力的加密消息。
成帧的目的是通过单个连接复用多个能力。其次,由于成帧消息为消息认证码提供了合理的边界点,支持加密和认证流变得简单直接。帧通过握手期间生成的密钥材料进行加密和认证。
帧头提供有关消息大小和消息源能力的信息。使用填充是为了防止缓冲区饥饿,使得帧组件与密码的块大小字节对齐。
$\text{frame} = \text{header-ciphertext} || \text{header-mac} || \text{frame-ciphertext} || \text{frame-mac}$
$\text{header-ciphertext} = \text{aes}(\text{aes-secret}, \text{header})$
$\text{header} = \text{frame-size} || \text{header-data} || \text{header-padding}$
$\text{header-data} = [\text{capability-id}, \text{context-id}]$
$\text{capability-id} = \text{整数,总是零}$
$\text{context-id} = \text{整数,总是零}$
$\text{header-padding} = \text{将 header 填充零到 16 字节边界}$
$\text{frame-ciphertext} = \text{aes}(\text{aes-secret}, \text{frame-data} || \text{frame-padding})$
$\text{frame-padding} = \text{将 frame-data 填充零到 16 字节边界}$
请参阅 Capability Messaging 部分以获取 $\text{frame-data}$ 和 $\text{frame-size}$ 的定义。
RLPx 中的消息认证使用两个 keccak256 状态,每个通信方向一个。egress-mac 和 ingress-mac keccak 状态会随着发送(egress)或接收(ingress)的字节密文而持续更新。初始握手后,MAC 状态初始化如下:
发起者:
$\text{egress-mac} = \text{keccak256.init}((\text{mac-secret} \oplus \text{recipient-nonce}) || \text{auth})$
$\text{ingress-mac} = \text{keccak256.init}((\text{mac-secret} \oplus \text{initiator-nonce}) || \text{ack})$
接收者:
$\text{egress-mac} = \text{keccak256.init}((\text{mac-secret} \oplus \text{initiator-nonce}) || \text{ack})$
$\text{ingress-mac} = \text{keccak256.init}((\text{mac-secret} \oplus \text{recipient-nonce}) || \text{auth})$
发送帧时,通过使用要发送的数据更新 egress-mac 状态来计算相应的 MAC 值。更新通过将 header 与其相应 MAC 的加密输出进行 XOR 运算来执行。这样做是为了确保对明文 MAC 和密文都执行统一的操作。所有 MAC 都以明文形式发送。
$\text{header-mac-seed} = \text{aes}(\text{mac-secret}, \text{keccak256.digest}(\text{egress-mac})[:16]) \oplus \text{header-ciphertext}$
$\text{egress-mac} = \text{keccak256.update}(\text{egress-mac}, \text{header-mac-seed})$
$\text{header-mac} = \text{keccak256.digest}(\text{egress-mac})[:16]$
计算 frame-mac:
$\text{egress-mac} = \text{keccak256.update}(\text{egress-mac}, \text{frame-ciphertext})$
$\text{frame-mac-seed} = \text{aes}(\text{mac-secret}, \text{keccak256.digest}(\text{egress-mac})[:16]) \oplus \text{keccak256.digest}(\text{egress-mac})[:16]$
$\text{egress-mac} = \text{keccak256.update}(\text{egress-mac}, \text{frame-mac-seed})$
$\text{frame-mac} = \text{keccak256.digest}(\text{egress-mac})[:16]$
在接收帧上验证 MAC 是通过以与 egress-mac 相同的方式更新 ingress-mac 状态,并与接收帧中的 header-mac 和 frame-mac 值进行比较来完成的。这应该在解密 header-ciphertext 和 frame-ciphertext 之前进行。
初始握手之后的所有消息都与一个“能力”相关联。在单个 RLPx 连接上可以同时使用任意数量的能力。
能力由一个简短的 ASCII 名称(最多八个字符)和版本号标识。连接两端支持的能力在属于“p2p”能力(所有连接都必须提供)的 Hello 消息中交换。
初始 Hello 消息的编码如下:
$\text{frame-data} = \text{msg-id} || \text{msg-data}$
$\text{frame-size} = \text{frame-data 的长度,编码为 24 位大端整数}$
其中 $\text{msg-id}$ 是一个标识消息的 RLP 编码整数,$\text{msg-data}$ 是一个包含消息数据的 RLP 列表。
Hello 之后的所有消息都使用 Snappy 算法压缩。
$\text{frame-data} = \text{msg-id} || \text{snappyCompress}(\text{msg-data})$
$\text{frame-size} = \text{frame-data 的长度,编码为 24 位大端整数}$
请注意,压缩消息的 $\text{frame-size}$ 指的是 $\text{msg-data}$ 的压缩大小。由于压缩消息在解压缩后可能会膨胀到非常大的尺寸,因此实现应在解码消息之前检查数据的未压缩大小。这是可能的,因为 snappy format 包含一个长度头。携带超过 16 MiB 未压缩数据的消息应通过关闭连接来拒绝。
虽然成帧层支持 capability-id,但当前版本的 RLPx 不使用该字段在不同能力之间进行复用。相反,复用纯粹依赖于消息 ID。
每个能力都可以获得所需的消息 ID 空间。所有此类能力必须静态指定它们需要的消息 ID 数量。在连接和接收 Hello 消息时,双方都具有关于它们共享哪些能力(包括版本)的等效信息,并且能够就消息 ID 空间的组成达成共识。
消息 ID 被假定为从 0x10 开始是紧凑的(0x00-0x0f 保留给“p2p”能力),并按字母顺序分配给每个共享的(版本相同、名称相同)能力。能力名称区分大小写。不共享的能力将被忽略。如果共享相同(名称相同)能力的多个版本,则数值最高的版本胜出,其他版本将被忽略。
“p2p”能力存在于所有连接上。初始握手后,连接的两端都必须发送 Hello 或 Disconnect 消息。收到 Hello 消息后,会话激活,可以发送任何其他消息。为了向前兼容,实现必须忽略协议版本中的任何差异。当与较低版本的对等节点通信时,实现应尝试模仿该版本。
在协议协商之后的任何时候,都可以发送 Disconnect 消息。
$[\text{protocolVersion}: P, \text{clientId}: B, \text{capabilities}, \text{listenPort}: P, \text{nodeKey}: B_{64}, ...]$
通过连接发送的第一个数据包,双方各发送一次。在收到 Hello 之前,不得发送其他消息。实现必须忽略 Hello 中任何额外的列表元素,因为它们可能被未来的版本使用。
$[\text{reason}: P]$
通知对等节点即将断开连接;如果收到,对等节点应立即断开连接。发送时,行为良好的主机会在断开连接之前给它们的对等节点一个机会(即等待 2 秒)来断开连接。
$\text{reason}$ 是一个可选整数,指定断开连接的多种原因之一:
| 原因 | 含义 |
|---|---|
0x00 |
请求断开连接 |
0x01 |
TCP 子系统错误 |
0x02 |
协议违规,例如格式错误的消息、错误的 RLP 等 |
0x03 |
无用的对等节点 |
0x04 |
对等节点过多 |
0x05 |
已连接 |
0x06 |
不兼容的 P2P 协议版本 |
0x07 |
收到空节点身份 – 这会自动失效 |
0x08 |
客户端退出 |
0x09 |
握手时出现意外身份 |
0x0a |
身份与此节点相同(即连接到自身) |
0x0b |
Ping 超时 |
0x10 |
特定于子协议的其他原因 |
[]
请求对等节点立即回复 Pong。
[]
回复对等节点的 Ping 数据包。
aes-secret 和 mac-secret 在读写时都被重用。RLPx 连接的两端从相同的密钥、nonce 和 IV 生成两个 CTR 流。如果攻击者知道一个明文,他们就可以解密重用密钥流的未知明文。capability-id 和 context-id 字段,但这些字段未被使用。EIP-706 添加了 Snappy 消息压缩。
EIP-8 更改了初始握手时 auth-body 和 ack-body 的 RLP 编码,在握手中添加了版本号,并强制要求实现应忽略握手消息和 Hello 中额外的列表元素。
Elaine Barker, Don Johnson, and Miles Smid. NIST 特别出版物 800-56A 第 5.8.1 节,串联密钥派生函数。2017 年。\ URL <https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-56ar.pdf>
Victor Shoup. 关于公钥加密 ISO 标准的提案,版本 2.1。2001 年。\ URL <http://www.shoup.net/papers/iso-2_1.pdf>
Mike Belshe and Roberto Peon. SPDY 协议 - 草案 3。2014 年。\ URL <http://www.chromium.org/spdy/spdy-protocol/spdy-protocol-draft3>
Snappy 压缩格式描述。2011 年。\ URL <https://github.com/google/snappy/blob/master/format_description.txt>
版权所有 © 2014 Alex Leverington。\ <a rel="license" href="http://creativecommons.org/licenses/by-nc-sa/4.0/"> 本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可</a>。
- 原文链接: github.com/ethereum/devp...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!