-
Notifications
You must be signed in to change notification settings - Fork 36
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 非对称加密用于 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
:
- 生成一个随机数
r
并生成对应的公钥R = r * G
. - 计算共享密码
S = Px
,其中(Px, Py) = r * KB
. - 推导加密认证用的 key
kE || kM = KDF(S, 32)
以及随机向量iv
. - 使用 AES 加密
c = AES(kE, iv, m)
. - 计算 MAC 校验
d = MAC(keccak256(kM), iv || c)
. - 发送完整密文
R || iv || c || d
给 Bob.
Bob 解密密文R || iv || c || d
:
- 推导共享密码
S = Px
, 其中(Px, Py) = r _ KB = kB _ R
. - 推导加密认证用的 key
kE || kM = KDF(S, 32)
. - 计算并检查 MAC
d = MAC(keccak256(kM), iv || c)
. - 解密明文
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-version
和ack-version
不同不会导致错误(用于协议升级) -
auth-body
和ack-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
RLPx 中的消息认证(Message authentication)使用了两个 keccak256 状态,每个传输方向一个。egress-mac
和ingress-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-mac
和ingress-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
虽然 frame 中支持capability-id
和context-id
, 但是这两个字段并没有被利用。当前的版本使用 msg-id 来区分不同的消息。
msg-id
应用层消息的值大于 0x11(0x00-0x10 保留用于p2p capability
)
在握手协商完成后,连接的双方需要发送Hello
消息。
任何时候,都可能会收到Disconnect
消息。
[protocolVersion: P, clientId: B, capabilities, listenPort: P, nodeKey: B_64, ...]
握手完成后,双方发送的第一包数据。在收到Hello
消息前,不可以发送任何其他的消息(Disconnect
除外)。
-
Hello
消息中多余的字段会被忽略(用于协议升级) -
protocolVersion
: 当前版本是 5 -
clientId
: 节点名,人类可读的字符串, 比如"Fractal-P2P" -
capabilities
: 支持的子协议列表,名称及其版本 -
listenPort
: 节点的监听端口。0 表示没有监听。 -
nodeId
: secp256k1 的公钥,对应节点身份的私钥.
[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.
-
[]
心跳,请求回复Pong
包
[]
回复Ping
包