RLPx 传输协议

  • ethereum
  • 发布于 2023-11-28 18:36
  • 阅读 11

RLPx是以太坊节点间通信的TCP传输协议,支持加密消息和能力协商。它详细定义了ECIES加密、初始握手、消息帧以及MAC机制,并描述了P2P能力中的Hello、Disconnect等消息,旨在确保节点间通信的安全性和效率。

RLPx 传输协议

本规范定义了 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 加密

ECIES(椭圆曲线集成加密方案)是 RLPx 握手中使用的一种非对称加密方法。RLPx 使用的密码系统是

  • 椭圆曲线 secp256k1,生成器为 $G$。
  • $\text{KDF}(k, \text{len})$: NIST SP 800-56 串联密钥派生函数。
  • $\text{MAC}(k, m)$: 使用 SHA-256 哈希函数的 HMAC。
  • $\text{AES}(k, iv, m)$: CTR 模式下的 AES-128 加密函数。

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 连接的节点)和“接收者”(接受连接的节点)之间进行。

  1. 发起者连接到接收者并发送其 auth 消息
  2. 接收者接受、解密并验证 auth(检查签名恢复 == $\text{keccak256}(\text{ephemeral-pubk})$)
  3. 接收者根据 remote-ephemeral-pubknonce 生成 auth-ack 消息
  4. 接收者派生密钥并发送包含 Hello 消息的第一个加密帧
  5. 发起者接收 auth-ack 并派生密钥
  6. 发起者发送包含发起者 Hello 消息的第一个加密帧
  7. 接收者接收并认证第一个加密帧
  8. 发起者接收并认证第一个加密帧
  9. 如果双方第一个加密帧的 MAC 有效,则密码握手完成

如果第一个成帧数据包的认证失败,任何一方都可以断开连接。

握手消息:

$\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}$ 的定义。

MAC

RLPx 中的消息认证使用两个 keccak256 状态,每个通信方向一个。egress-macingress-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-macframe-mac 值进行比较来完成的。这应该在解密 header-ciphertextframe-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 未压缩数据的消息应通过关闭连接来拒绝。

基于消息 ID 的复用

虽然成帧层支持 capability-id,但当前版本的 RLPx 不使用该字段在不同能力之间进行复用。相反,复用纯粹依赖于消息 ID。

每个能力都可以获得所需的消息 ID 空间。所有此类能力必须静态指定它们需要的消息 ID 数量。在连接和接收 Hello 消息时,双方都具有关于它们共享哪些能力(包括版本)的等效信息,并且能够就消息 ID 空间的组成达成共识。

消息 ID 被假定为从 0x10 开始是紧凑的(0x00-0x0f 保留给“p2p”能力),并按字母顺序分配给每个共享的(版本相同、名称相同)能力。能力名称区分大小写。不共享的能力将被忽略。如果共享相同(名称相同)能力的多个版本,则数值最高的版本胜出,其他版本将被忽略。

“p2p”能力

“p2p”能力存在于所有连接上。初始握手后,连接的两端都必须发送 HelloDisconnect 消息。收到 Hello 消息后,会话激活,可以发送任何其他消息。为了向前兼容,实现必须忽略协议版本中的任何差异。当与较低版本的对等节点通信时,实现应尝试模仿该版本。

在协议协商之后的任何时候,都可以发送 Disconnect 消息。

Hello (0x00)

$[\text{protocolVersion}: P, \text{clientId}: B, \text{capabilities}, \text{listenPort}: P, \text{nodeKey}: B_{64}, ...]$

通过连接发送的第一个数据包,双方各发送一次。在收到 Hello 之前,不得发送其他消息。实现必须忽略 Hello 中任何额外的列表元素,因为它们可能被未来的版本使用。

  • $\text{protocolVersion}$ “p2p”能力的版本,5
  • $\text{clientId}$ 指定客户端软件身份,作为人类可读的字符串(例如,“Ethereum(++)/1.0.0”)。
  • $\text{capabilities}$ 是支持的能力及其版本的列表:$[[\text{cap1}, \text{capVersion1}], [\text{cap2}, \text{capVersion2}], ...]$。
  • $\text{listenPort}$ (旧版)指定客户端正在监听的端口(在当前连接遍历的接口上)。如果为 0,则表示客户端未监听。此字段应被忽略。
  • $\text{nodeId}$ 是与节点私钥对应的 secp256k1 公钥。

Disconnect (0x01)

$[\text{reason}: P]$

通知对等节点即将断开连接;如果收到,对等节点应立即断开连接。发送时,行为良好的主机会在断开连接之前给它们的对等节点一个机会(即等待 2 秒)来断开连接。

$\text{reason}$ 是一个可选整数,指定断开连接的多种原因之一:

原因 含义
0x00 请求断开连接
0x01 TCP 子系统错误
0x02 协议违规,例如格式错误的消息、错误的 RLP 等
0x03 无用的对等节点
0x04 对等节点过多
0x05 已连接
0x06 不兼容的 P2P 协议版本
0x07 收到空节点身份 – 这会自动失效
0x08 客户端退出
0x09 握手时出现意外身份
0x0a 身份与此节点相同(即连接到自身)
0x0b Ping 超时
0x10 特定于子协议的其他原因

Ping (0x02)

[]

请求对等节点立即回复 Pong

Pong (0x03)

[]

回复对等节点的 Ping 数据包。

更改日志

当前版本中的已知问题

  • 帧加密/MAC 方案被认为是“有缺陷的”,因为 aes-secretmac-secret 在读写时都被重用。RLPx 连接的两端从相同的密钥、nonce 和 IV 生成两个 CTR 流。如果攻击者知道一个明文,他们就可以解密重用密钥流的未知明文。
  • 审阅者的普遍反馈是,使用 keccak256 状态作为 MAC 累加器以及在 MAC 算法中使用 AES 是一种不常见且过于复杂的消息认证方式,但可以认为是安全的。
  • 帧编码提供了用于复用目的的 capability-idcontext-id 字段,但这些字段未被使用。

版本 5 (EIP-706, 2017 年 9 月)

EIP-706 添加了 Snappy 消息压缩。

版本 4 (EIP-8, 2015 年 12 月)

EIP-8 更改了初始握手时 auth-bodyack-body 的 RLP 编码,在握手中添加了版本号,并强制要求实现应忽略握手消息和 Hello 中额外的列表元素。

参考

版权所有 © 2014 Alex Leverington。\ <a rel="license" href="http://creativecommons.org/licenses/by-nc-sa/4.0/"> 本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可</a>。

  • 原文链接: github.com/ethereum/devp...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
ethereum
ethereum
江湖只有他的大名,没有他的介绍。