【译】RLPx

By | 2018年7月14日

RLPx:加密网络&传输协议

简介:

RLPx是一个加密的点对点网络和合约套装,它能够为通过p2p网络通信的应用提供一般用途的传输和接口。RLPx为去中心化应用的需求设计,且已经被以太坊所使用。

 

当前版本的RLPx为以太坊提供网络层,路线图:

已完成:

UDP单个协议的节点发现

ECDSA签名UDP

加密握手/身份认证

对等持久性

加密/身份认证的TCP

TCP framing

 

May ’15 Beta

单协议节点发现

功能完整的传输

加密UDP

 

July ’15 Beta

重访节点表算法

多协议发现支持

 

Winter ’15 Beta

多协议节点发现

功能完整的UDP

 

特性

节点发现和网络形成

加密握手

加密传输

协议复用器(framing)

流控制

节点偏好策略

节点声誉

安全

  • 认证连接(ECDH+ECDHE,AES128)
  • 认证发现协议(ECDSA)
  • 加密传输(AES256)
  • 为共享连接的协议提供统一的带宽(framing)
  • 节点可以访问统一的网络拓扑
  • 节点可以统一地连接到网络
  • 本地化节点声誉模型

 

传输

目标

  • 单个连接上的多个协议
  • 加密
  • 流控制

采用认证加密是为了提供保密性,并防止网络中断。这对于一个结构良好的网络来说尤为重要。在这个网络中,节点会对其他节点做出长期的决定,并产生非局部效应。

(This is particularly important for a well-formed network where nodes make long-term decisions about other nodes which yield non-local effects.)

动态framing和流控制确保为每个协议分配相同的带宽。

 

网络形成

目标

  • 新节点能可靠地寻找到其他节点并连接上
  • 节点有足够的网络拓扑信息,以一致地连接到其他节点
  • 节点标识(identifier)是随机的

RLPx使用经过修改的“类Kademlia”路由作为p2p邻居发现协议。RLPx discovery使用512位公钥作为节点id,并用sha3(node-id)作为异或度量。没有实现DHT特性。

 

实现概览

数据包会动态成帧,以一个RLP编码的头修饰,并被加密和认证。复用通过一个帧头实现,这个帧头指定了一个包的目标协议。

 

所有加密操作都是基于secp256k1的,且每个节点都被认为包含一个静态的、被存储并在会话之间被使用(restore)的私钥。我们建议,私钥只能被通过诸如删除文件或数据库入口的方式手动重置。

 

一个RLPx实现需要包含:

  • 节点发现
  • 加密传输
  • 成帧(Framing)
  • 流控制

 

节点发现

节点(Node):网络上的一个实体

参与者(Peer):当前正连接到host节点(host node)的节点

NodeId:节点的公钥

 

【节点发现】和【网络形成】通过一个类kademlia的UDP实现。和Kademlia的主要区别:

  • 数据包均被签名
  • 以公钥作为节点id
  • 去除了与DHT相关的特性。没有实现FIND_VALUE和STORE数据包。
  • 异或距离度量(xor distance metric)基于sha3(nodeid)

 

为Kademlia选用的参数是一个尺寸为16的桶(在Kademlia中记为k),并发性为3(concurrency of 3)(在Kademlia中记为alpha),每一跳8位路由(8 bits per hop for routing)(在Kademlia中记为b)。驱逐检查间隔为75ms,请求超时是300ms,空闲桶刷新间隔是3600秒。在加密实现之前,数据包都有一个时间戳属性来降低实施重放攻击的时间窗口。时间戳的处理方式由接收人决定,我们建议接收人只接受最近3秒之内创建的数据包;时间戳对Pong数据包而言是可以忽略的。为减少数据包破碎的机会,一个数据报的最大尺寸限制为1280byte。这是IPv6数据报的最小尺寸。

 

除前文描述的差异外,节点发现采用了Maymounkov和Mazieres描述的系统和协议。

 

数据包都是被签名的。验证由【从签名恢复公钥并【检查其是否与预期值相符】】来进行。数据包属性被【按他们【被定义的顺序】】序列化为RLP

 

RLPx提供一个基于距离组成的‘潜在’节点列表,并可以根据理想的参与者(peer)计数维持良好的连接。 (这个计数默认为5).此策略通过连接到每个连接的“close”节点的一个随机节点实现。

 

其他可以通过协议实现的连接策略——协议可以使用自己的元数据和策略来进行连接。

 

注意:这个协议的最终版本会使用与TCP数据包完全相同或类似的结构进行构建(frame)和加密。这个改变将会随着【节点发现协议、成帧、加密的第一步实现完成后】发生。

 

数据包封装:

hash || signature || packet-type || packet-data
  hash: sha3(signature || packet-type || packet-data)	// used to verify integrity of datagram
  signature: sign(privkey, sha3(packet-type || packet-data))
  signature: sign(privkey, sha3(pubkey || packet-type || packet-data)) // implementation w/MCD
  packet-type: single byte < 2**7 // valid values are [1,4]
  packet-data: RLP encoded list. Packet properties are serialized in the order in which they're defined. See packet-data below.

DRAFT 加密数据包封装

mac || header || frame
  header: frame-size || header-data
    frame-size: 3-byte integer size of frame, big endian encoded
    header-data:
        normal: rlp.list(protocol-type[, context-id])
        chunked-0: rlp.list(protocol-type, context-id, total-packet-size)
    chunked-n: rlp.list(protocol-type, context-id)
        values:
            protocol-type: < 2**16
            context-id: < 2**16 (this value is optional for normal frames)
            total-packet-size: < 2**32

数据包数据(packet-data)

All data structures are RLP encoded.
Total payload of packet (excluding IP headers) must be no greater than 1280 bytes.
NodeId: The node's public key.
inline: Properties are appened to current list instead of encoded as list.
Maximum byte size of packet is noted for reference.
timestamp: When packet was created (number of seconds since epoch).

PingNode packet-type: 0x01
struct PingNode
{
  h256 version = 0x3;
  Endpoint from;
  Endpoint to;
  uint32_t timestamp;
};

Pong packet-type: 0x02
struct Pong
{
  Endpoint to;
  h256 echo;
  uint32_t timestamp;
};

FindNeighbours packet-type: 0x03
struct FindNeighbours
{
  NodeId target; // Id of a node. The responding node will send back nodes closest to the target.
  uint32_t timestamp;
};

Neighbors packet-type: 0x04
struct Neighbours
{
  list nodes: struct Neighbour
  {
    inline Endpoint endpoint;
    NodeId node;
  };
  
  uint32_t timestamp;
};

struct Endpoint
{
  bytes address; // BE encoded 4-byte or 16-byte address (size determines ipv4 vs ipv6)
  uint16_t udpPort; // BE encoded 16-bit unsigned
  uint16_t tcpPort; // BE encoded 16-bit unsigned
}

加密握手

连接要通过握手建立,一旦连接建立成功,数据包就会被封装成帧(数据包使用CTR模式的AES-256加密)。会话的密钥材料(key material)通过使用派生的ECDHE的KDF派生。ECC使用secp256k1曲线(ECP)。注意:“远程(remote)”是接收到连接的host

 

握手分两阶段进行。第一阶段时密钥交换,第二阶段是验证和协议协商。密钥交换是在ECIES加密信息中进行的,它包含一个为PFS(Perfect Forward Security,完美转发安全)准备的临时密钥。握手的第二阶段是DEVp2p的一部分,即一个交换双方节点都支持的能力(capabilities)的过程。如何处理【握手第二阶段的输出】由具体实现决定。

 

ECIES技术有几种变体,其中一些可延展的模式是不能使用的。这个规范依赖于Shoup定义的ECIES实现。因此,如果信息身份验证失败,就无法进行解密。

http://en.wikipedia.org/wiki/Integrated_Encryption_Scheme

 

有两种可建立的连接。一个节点可以连接到一个已知的参与者(peer),也可以连接到一个新的参与者。一个已知的参与者:是曾经连接过,且与其对应的会话token对验证已请求过的连接仍有效。

 

若初始化连接【到一个已知的参与者的】握手失败,这个节点的信息就会被从节点表中去除,且决不能重新尝试连接。因为IPv4空间的限制和ISP的通常做法,这种情况是普遍且正常的,因此我们不会做其他的行动。如果一个【已经接收到的连接】握手失败,则不会对节点表进行修改。

 

握手:

New: authInitiator -> E(remote-pubk, S(ephemeral-privk, static-shared-secret ^ nonce) || H(ephemeral-pubk) || pubk || nonce || 0x0)
     authRecipient -> E(remote-pubk, remote-ephemeral-pubk || nonce || 0x0)
   
Known: authInitiator = E(remote-pubk, S(ephemeral-privk, token ^ nonce) || H(ephemeral-pubk) || pubk || nonce || 0x1)
       authRecipient = E(remote-pubk, remote-ephemeral-pubk || nonce || 0x1) // token found
       authRecipient = E(remote-pubk, remote-ephemeral-pubk || nonce || 0x0) // token not found

static-shared-secret = ecdh.agree(privkey, remote-pubk)
ephemeral-shared-secret = ecdh.agree(ephemeral-privk, remote-ephemeral-pubk)

 

紧接着握手而产生的值(按步阅读下面的部分)

ephemeral-shared-secret = ecdh.agree(ephemeral-privkey, remote-ephemeral-pubk)
shared-secret = sha3(ephemeral-shared-secret || sha3(nonce || initiator-nonce))
token = sha3(shared-secret)
aes-secret = sha3(ephemeral-shared-secret || shared-secret)
# destroy shared-secret
mac-secret = sha3(ephemeral-shared-secret || aes-secret)
# destroy ephemeral-shared-secret

Initiator:
egress-mac = sha3.update(mac-secret ^ recipient-nonce || auth-sent-init)
# destroy nonce
ingress-mac = sha3.update(mac-secret ^ initiator-nonce || auth-recvd-ack)
# destroy remote-nonce

Recipient:
egress-mac = sha3.update(mac-secret ^ initiator-nonce || auth-sent-ack)
# destroy nonce
ingress-mac = sha3.update(mac-secret ^ recipient-nonce || auth-recvd-init)
# destroy remote-nonce

创建认证了的连接:

1. initiator generates auth from ecdhe-random, static-shared-secret, and nonce (auth = authInitiator handshake)
2. initiator connects to remote and sends auth

3. optionally, remote decrypts and verifies auth (checks that recovery of signature == H(ephemeral-pubk))
4. remote generates authAck from remote-ephemeral-pubk and nonce (authAck = authRecipient handshake)

optional: remote derives secrets and preemptively sends protocol-handshake (steps 9,11,8,10)

5. initiator receives authAck
6. initiator derives shared-secret, aes-secret, mac-secret, ingress-mac, egress-mac
7. initiator sends protocol-handshake

8. remote receives protocol-handshake
9. remote derives shared-secret, aes-secret, mac-secret, ingress-mac, egress-mac
10. remote authenticates protocol-handshake
11. remote sends protocol-handshake

12. initiator receives protocol-handshake
13. initiator authenticates protocol-handshake
13. cryptographic handshake is complete if mac of protocol-handshakes are valid; permanent-token is replaced with token
14. begin sending/receiving data

All packets following auth, including protocol negotiation handshake, are framed.

当且仅当第一个成帧数据包认证失败时,两方中的一方可能断开连接。或,当协议握手不成功(如版本太旧)

 

成帧(Framing)

在把数据包封帧的主要目的,是能够在一个单独的连接之上稳定支持多路多协议复用。其次,因为成帧的数据包可以为信息身份验证代码产生合理的断点,以支持加密流直接转发。于是,帧被通过在握手阶段中生成的密钥材料进行验证

(The primary purpose behind framing packets is in order to robustly support multiplexing multiple protocols over a single connection. Secondarily, as framed packets yield reasonable demarcation points for message authentication codes, supporting an encrypted stream becomes straight-forward. Accordingly, frames are authenticated via key material which is generated during the handshake.)

 

当通过RLPx发送一个数据包时,数据包是被成帧的。帧头提供关于数据包的信息和数据包的源协议。有三种稍有区别的帧,取决于帧是否在传输一个多帧数据包。一个多帧数据包是一个被拆(又名分块)为多个帧的数据包,通常是因为包的尺寸比协议窗口尺寸更大(Protocol Window Size,pws,见Multiplexing)。当一个数据包被分块为多帧,第一个帧和后面的所有帧有隐含的差异。因此,三个帧类型都是正常的:chunked-0(多帧数据包的第一个帧),chunked-n(多帧数据包中除了第一个帧之外后面所有的帧)。

  normal = not chunked
  chunked-0 = First frame of a multi-frame packet
  chunked-n = Subsequent frames for multi-frame packet
  || is concatenate
  ^ is xor

Single-frame packet:
header || header-mac || frame || frame-mac

Multi-frame packet:
header || header-mac || frame-0 ||
[ header || header-mac || frame-n || ... || ]
header || header-mac || frame-last || frame-mac

header: frame-size || header-data || padding
frame-size: 3-byte integer size of frame, big endian encoded (excludes padding)
header-data:
    normal: rlp.list(protocol-type[, context-id])
    chunked-0: rlp.list(protocol-type, context-id, total-packet-size)
    chunked-n: rlp.list(protocol-type, context-id)
    values:
        protocol-type: < 2**16
        context-id: < 2**16 (optional for normal frames)
        total-packet-size: < 2**32
padding: zero-fill to 16-byte boundary

header-mac: right128 of egress-mac.update(aes(mac-secret,egress-mac) ^ header-ciphertext).digest

frame:
    normal: rlp(packet-type) [|| rlp(packet-data)] || padding
    chunked-0: rlp(packet-type) || rlp(packet-data...)
    chunked-n: rlp(...packet-data) || padding
padding: zero-fill to 16-byte boundary (only necessary for last frame)

frame-mac: right128 of egress-mac.update(aes(mac-secret,egress-mac) ^ right128(egress-mac.update(frame-ciphertext).digest))

egress-mac: h256, continuously updated with egress-bytes*
ingress-mac: h256, continuously updated with ingress-bytes*

消息认证通过连续地更新egress-mac或ingress-mac,他们带有数byte的发送出(egress)或接收到(ingress)的密文;对于headers,更新是通过将header与【其对应的mac的加密输出】进行异或来执行的(例子见上文的header-mac)。这是为了确保对纯文本mac和密文进行相同操作而进行的。两个协议可能复用相同的内容id。

 

Padding被用于缓解缓冲区的空闲(buffer starvation),这样帧组件就会与密码快大小位对齐。(Padding is used to prevent buffer starvation, such that frame components are byte-aligned to block size of cipher.)

 

context-id用于分开在同一个协议中同时传输的数据包。一个特定的数据包的所有分块了的帧共享同一个context-id。数据包id的指定是由具体实现定义的。这些id对于协议类型而言是本地的,而非连接的。两个协议可能会使用相同的context id。

 

术语注释:

使用“数据包(Packet)”一词,是因为并非所有的数据包(packet)都是消息(message);RLPx对于目的地地址没有自己的标记。“帧(Frame)”被用来指代要被通过RLPx传输的数据包。尽管有些令人困惑,术语“消息(message)”被用于指代通过消息验证码(authentication code)(MAC或mac)验证后的文本(纯文本或密文)

 

 

流控制(Flow Control)

注意:RLPx的初始版本会设定一个静态的8kb的窗口尺寸;公平的排队和流控制(DeltaUpdate packet)不会被实现。

 

动态成帧是一个过程,这个过程通过两方发送帧完成——这种帧受到发送者的窗口尺寸和可用的协议数量的限制。动态成帧提供流控制功能,其由发送者传输窗口和协议窗口实现。数据传输窗口是一个由发送者指定的32位值,也指定了发送者可以传输多少位的数据。协议窗口是由激活的协议分割的传输窗口。在一个连接建立后和开始进行帧的传输之前的这段时间中,发送者通过初始化窗口尺寸开始。这个窗口尺寸一种接收人缓冲capability的措施。发送者不能发送超过协议窗口大小的帧。在发送每个数据帧后,发送者根据已传输数据的量来逐渐减小窗口尺寸。当窗口尺寸小于等于0时,发送者必须暂停传输数据帧。在流的另一端,接收人反向发送一个DeltaUpdate数据包来告知发送者:接收人已经处理掉了一部分数据并为接收更多数据释放了缓冲空间。连接初次建立时,初始窗口尺寸是8kb。

pws = protocol-window-size = window-size / active-protocol-count

The initial window-size is 8KB.
A protocol is considered active if it's queue contains one or more packets.

DeltaUpdate protocol-type: 0x0, packet-type: 0x0
struct DeltaUpdate
{
  unsigned size; // < 2**31
}

合约的复用通过动态成帧和平等排队实现。出队的数据包在一个周期中被处理,这个周期是从每个活动协议的队列中取出一个或多个数据包的周期。多路复用器在每轮推出出列数据包之前,确定每个协议要发送的byte的量。

 

若帧的尺寸小于1kb,协议可以请求网络层优先发送数据包。若这个数据包必须在其他所有数据包之前被发送,就应该使用这个操作。发送者网络层中每个协议包含两个队列和三个缓冲区:一个正常数据包队列,一个优先数据包队列,一个分块帧缓冲区,一个正常帧缓冲区,一个优先帧缓冲区。

If priority packet and normal packet exist: send up to pws/2 bytes from each (priority first!)
else if priority packet and chunked-frame exist: send up to pws/2 bytes from each
else if normal packet and chunked-frame exist: send up to pws/2 bytes from each
else read pws bytes from active buffer

If there are bytes leftover -- for example, if the bytes sent is < pws, then repeat the cycle.

 

点击量:683

发表评论

邮箱地址不会被公开。 必填项已用*标注