1. 程式人生 > >死磕以太坊原始碼分析之rlpx協議

死磕以太坊原始碼分析之rlpx協議

> 死磕以太坊原始碼分析之rlpx協議 > 本文主要參考自eth官方文件:[rlpx協議](https://github.com/blockchainGuide) ## 符號 - `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)`:是PRIVKEY和PUBKEY之間的橢圓曲線Diffie-Hellman協商函式 -------- ## ECIES加密 ECIES (Elliptic Curve Integrated Encryption Scheme) 非對稱加密用於RLPx握手。RLPx使用的加密系統: - 橢圓曲線secp256k1基點`G` - `KDF(k, len)`:金鑰推導函式 NIST SP 800-56 Concatenation - `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. 推導加密及認證所需的金鑰`kE || 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. 推導加密認證用的金鑰`kE || kM = KDF(S, 32)` 3. 驗證MAC`d = MAC(keccak256(kM), iv || c)` 4. 獲得明文`m = AES(kE, iv || c)` ----- ## 節點身份 所有的加密操作都基於**secp256k1**橢圓曲線。每個節點維護一個靜態的**secp256k1**私鑰。建議該私鑰只能進行手動重置(例如刪除檔案或資料庫條目)。 ----- ## 握手流程 RLPx連線基於TCP通訊,並且每次通訊都會生成隨機的臨時金鑰用於加密和驗證。生成臨時金鑰的過程被稱作“握手” (handshake),握手在發起端(initiator, 發起TCP連線請求的節點)和接收端(recipient, 接受連線的節點)之間進行。 1. 發起端向接收端發起TCP連線,傳送`auth`訊息 2. 接收端接受連線,解密、驗證`auth`訊息(檢查recovery of signature == `keccak256(ephemeral-pubk)`) 3. 接收端通過`remote-ephemeral-pubk` 和 `nonce`生成`auth-ack`訊息 4. 接收端推導金鑰,傳送首個包含[Hello](https://github.com/ethereum/devp2p/blob/master/rlpx.md#hello-0x00)訊息的資料幀 (frame) 5. 發起端接收到`auth-ack`訊息,匯出金鑰 6. 發起端傳送首個加密後的資料幀,包含發起端[Hello](https://github.com/ethereum/devp2p/blob/master/rlpx.md#hello-0x00)訊息 7. 接收端接收並驗證首個加密後的資料幀 8. 發起端接收並驗證首個加密後的資料幀 9. 如果兩邊的首個加密資料幀的MAC都驗證通過,則加密握手完成 如果首個數據幀的驗證失敗,則任意一方都可以斷開連線。 ### 握手訊息 **傳送端:** ```go auth = auth-size || enc-auth-body auth-size = size of enc-auth-body, encoded as a big-endian 16-bit integer auth-vsn = 4 auth-body = [sig, initiator-pubk, initiator-nonce, auth-vsn, ...] enc-auth-body = ecies.encrypt(recipient-pubk, auth-body || auth-padding, auth-size) auth-padding = arbitrary data ``` **接收端:** ```go ack = ack-size || enc-ack-body ack-size = size of enc-ack-body, encoded as a big-endian 16-bit integer ack-vsn = 4 ack-body = [recipient-ephemeral-pubk, recipient-nonce, ack-vsn, ...] enc-ack-body = ecies.encrypt(initiator-pubk, ack-body || ack-padding, ack-size) ack-padding = arbitrary data ``` 實現必須忽略`auth-vsn` 和 `ack-vsn`中的所有不匹配。 實現必須忽略`auth-body` 和 `ack-body`中的所有額外列表元素。 握手訊息互換後,金鑰生成: ```go 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) 傳輸。一幀資料攜帶屬於某一功能的一條加密訊息。 分幀傳輸的主要目的是在單一連線上實現可靠的支援多路複用協議。其次,因資料包分幀,為訊息認證碼產生了適當的分界點,使得加密流變得簡單了。通過握手生成的金鑰對資料幀進行加密和驗證。 幀頭提供關於訊息大小和訊息源功能的資訊。填充位元組用於防止快取區不足,使得幀元件按指定區塊位元組大小對齊。 ```go 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-mac`和`ingress-mac`分別代表傳送和接收狀態,每次傳送或者接收密文,其狀態都會更新。初始握手後,MAC狀態初始化如下: **傳送端:** ```go egress-mac = keccak256.init((mac-secret ^ recipient-nonce) || auth) ingress-mac = keccak256.init((mac-secret ^ initiator-nonce) || ack) ``` **接收端:** ```go egress-mac = keccak256.init((mac-secret ^ initiator-nonce) || ack) ingress-mac = keccak256.init((mac-secret ^ recipient-nonce) || auth) ``` 當傳送一幀資料時,通過即將傳送的資料更新`egress-mac`狀態,然後計算相應的MAC值。通過將幀頭與其對應MAC值的加密輸出異或來進行更新。這樣做是為了確保對明文MAC和密文執行統一操作。所有的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`,並且在ingress幀中比對`header-mac` 和 `frame-mac`的值,就能對ingress幀中的MAC值進行校驗。這一步應當在解密`header-ciphertext` 和 `frame-ciphertext`之前完成。 ---- ## 功能訊息 初始握手後的所有訊息均與“功能”相關。單個RLPx連線上就可以同時使用任何數量的功能。 功能由簡短的ASCII名稱和版本號標識。連線兩端都支援的功能在隸屬於“ p2p”功能的[Hello](https://github.com/ethereum/devp2p/blob/master/rlpx.md#hello-0x00)訊息中進行交換,p2p功能需要在所有連線中都可用。 ### 訊息編碼 初始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之後的所有訊息均使用Snappy演算法壓縮。請注意,壓縮訊息的`frame-size`指`msg-data`壓縮前的大小。訊息的壓縮編碼為: ``` frame-data = msg-id || snappyCompress(msg-data) frame-size = length of (msg-id || msg-data) encoded as a 24bit big-endian integer ``` ## 基於`msg-id`的複用 frame中雖然支援`capability-id`,但是在本RLPx版本中並沒有將該欄位用於不同功能之間的複用(當前版本僅使用msg-id來實現複用)。 每種功能都會根據需要分配儘可能多的msg-id空間。所有這些功能所需的msg-id空間都必須通過靜態指定。在連線和接收[Hello](https://github.com/ethereum/devp2p/blob/master/rlpx.md#hello-0x00)訊息時,兩端都具有共享功能(包括版本)的對等資訊,並且能夠就msg-id空間達成共識。 msg-id應當大於0x11(0x00-0x10保留用於“ p2p”功能)。 ----- ## p2p功能 所有連線都具有“p2p”功能。初始握手後,連線的兩端都必須傳送[Hello](https://github.com/ethereum/devp2p/blob/master/rlpx.md#hello-0x00)或[Disconnect](https://github.com/ethereum/devp2p/blob/master/rlpx.md#disconnect-0x01)訊息。在接收到Hello訊息後,會話就進入啟用狀態,並且可以開始傳送其他訊息。由於前向相容性,實現必須忽略協議版本中的所有差異。與處於較低版本的節點通訊時,實現應嘗試靠近該版本。 任何時候都可能會收到[Disconnect](https://github.com/ethereum/devp2p/blob/master/rlpx.md#disconnect-0x01)訊息。 ### Hello (0x00) ``` [protocolVersion: P, clientId: B, capabilities, listenPort: P, nodeKey: B_64, ...] ``` 握手完成後,雙方傳送的第一包資料。在收到Hello訊息前,不能傳送任何其他訊息。實現必須忽略Hello訊息中所有其他列表元素,因為可能會在未來版本中用到。 - `protocolVersion`當前p2p功能版本為第5版 - `clientId`表示客戶端軟體身份,人類可讀字串, 比如"Ethereum(++)/1.0.0“ - `capabilities`支援的子協議列表,名稱及其版本:`[[cap1, capVersion1], [cap2, capVersion2], ...]` - `listenPort`節點的收聽埠 (位於當前連線路徑的介面),0表示沒有收聽 - `nodeId`secp256k1的公鑰,對應節點私鑰 ### Disconnect (0x01) ``` [reason: P] ``` 通知節點斷開連線。收到該訊息後,節點應當立即斷開連線。如果是傳送,正常的主機會給節點2秒鐘讀取時間,使其主動斷開連線。 `reason` 一個可選整數,表示斷開連線的原因: | Reason | Meaning | | ------ | ------------------------------------------------------------ | | `0x00` | Disconnect requested | | `0x01` | TCP sub-system error | | `0x02` | Breach of protocol, e.g. a malformed message, bad RLP, ... | | `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 in handshake | | `0x0a` | Identity is the same as this node (i.e. connected to itself) | | `0x0b` | Ping timeout | | `0x10` | Some other reason specific to a subprotocol | ### Ping (0x02) ``` [] ``` 要求節點立即進行[Pong](https://github.com/ethereum/devp2p/blob/master/rlpx.md#pong-0x03)回覆。 ### Pong (0x03) ``` [] ``` 回覆節點的[Ping](https://github.com/ethereum/devp2p/blob/master/rlpx.md#ping-0x02)包。 ----- ## 原始碼分析 ### 主要功能 #### 返回傳輸物件 > 返回一個transport物件,連線持續5秒 ```go // handshakeTimeout 5 func newRLPX(fd net.Conn) transport { .... } ``` #### 讀取訊息 > 返回Msg物件,呼叫讀寫器的ReadMsg,連線持續30秒 ```go func (t *rlpx) ReadMsg() (Msg, error) { .. t.fd.SetReadDeadline(time.Now().Add(frameReadTimeout)) } ``` #### 寫入訊息 > 呼叫讀寫器的WriteMsg寫資訊,連線持續20秒 ```go func (t *rlpx) WriteMsg(msg Msg) error { ... t.fd.SetWriteDeadline(time.Now().Add(frameWriteTimeout)) } ``` #### 協議版本握手 > 協議握手,輸入輸出均是protoHandshake物件,包含了版本號、名稱、容量、埠號、ID和一個擴充套件屬性,握手時會對這些資訊進行驗證 #### 加密握手 > 握手時主動發起者叫**initiator** > > 接收方叫**receiver** > > 分別對應兩種處理方式**initiatorEncHandshake**和receiverEncHandshake > > 兩種處理方式成功以後都會得到一個**secrets**物件,儲存了共享金鑰資訊,它會跟原有的**net.Conn**物件一起生成一個幀處理器:**rlpxFrameRW** > > 握手雙方使用到的資訊有:各自的公私鑰地址對**(iPrv,iPub,rPrv,rPub)**、各自生成的隨機公私鑰對**(iRandPrv,iRandPub,rRandPrv,rRandPub)**、各自生成的臨時隨機數**(initNonce,respNonce).** > 其中i開頭的表示發起方**(initiator)**資訊,r開頭的表示接收方**(receiver)**資訊. ```go func (t *rlpx) doEncHandshake(prv *ecdsa.PrivateKey, dial *ecdsa.PublicKey) (*ecdsa.PublicKey, error) { var ( sec secrets err error ) if dial == nil { sec, err = receiverEncHandshake(t.fd, prv) // 接收者 } else { sec, err = initiatorEncHandshake(t.fd, prv, dial) //主動發起者 } ... t.rw = newRLPXFrameRW(t.fd, sec) t.wmu.Unlock() return sec.Remote.ExportECDSA(), nil } ``` 這裡我們就講解一下主動握手部分原始碼`initiatorEncHandshake`: ①:初始化握手物件 ```GO h := &encHandshake{initiator: true, remote: ecies.ImportECDSAPublic(remote)} ``` ②:生成驗證資訊 ```go authMsg, err := h.makeAuthMsg(prv) ``` ```GO func (h *encHandshake) makeAuthMsg(prv *ecdsa.PrivateKey) (*authMsgV4, error) { // 生成己方隨機數initNonce h.initNonce = make([]byte, shaLen) _, err := rand.Read(h.initNonce) ... } // 生成隨機的一組公私鑰對 h.randomPrivKey, err = ecies.GenerateKey(rand.Reader, crypto.S256(), nil) ... } // 生成靜態共享祕密token(用己方私鑰和對方公鑰進行有限域乘法) token, err := h.staticSharedSecret(prv) ... } // 和己方隨機數異或後用隨機生成的私鑰簽名 signed := xor(token, h.initNonce) signature, err := crypto.Sign(signed, h.randomPrivKey.ExportECDSA()) ... } ... return msg, nil } ``` ③:封包,將驗證資訊和握手進行rlp編碼並拼接字首資訊 ```go authPacket, err := sealEIP8(authMsg, h) ``` ④:通過conn傳送訊息 ```go conn.Write(authPacket) ``` ⑤:處理接收的資訊,得到響應包 > `readHandshakeMsg`比較簡單。 首先用一種格式嘗試解碼。如果不行就換另外一種。應該是一種相容性的設定。 基本上就是使用自己的私鑰進行解碼然後呼叫rlp解碼成結構體。 > > 結構體的描述就是下面的authRespV4,裡面最重要的就是對端的隨機公鑰。 雙方通過自己的私鑰和對端的隨機公鑰可以得到一樣的共享祕密。 而這個共享祕密是第三方拿不到的 ```GO authRespMsg := new(authRespV4) authRespPacket, err := readHandshakeMsg(authRespMsg, encAuthRespLen, prv, conn) ``` ⑥:填充響應的respNonce(對方隨機數,生成共享私鑰用)和remoteRandomPub(對方的隨機公鑰) ```GO h.handleAuthResp(authRespMsg) ``` ⑦:將請求包和響應包封裝成共享祕密(secrets) ```go h.secrets(authPacket, authRespPacket) ``` 到此RLPX 相關的比較重要的內容就解讀差不多了。 ------ ## 參考 > https://github.com/blockchainGuide/blockchainguide ☆ ☆ ☆ ☆ ☆ > > https://mindcarver.cn/ ☆ ☆ ☆ ☆ ☆ > > https://github.com/ethereum/devp2p/blob/master/r