不瞭解RLPX的可以查看上篇博客。
p2p節點連接中的祕鑰交換RLPX (理論篇)
Enode
enode://6ee7f15fbe4a60585a54722b11727652f3ac54ce7a9a4c5977fd554200d7f7a1e636e90bd0b953f73c0bdd29720211b083600e5f73c7d1c765b3263f9509dfc3@39.99.195.63:30313
enode
由3部分組成,
- 公鑰
- IP
- 端口
如果想連接節點,首先需要知道enode,TCP Dial
的時候其實只需要IP和端口
,公鑰是在祕鑰交換的時候才啓作用,加密發送給對方的數據。
接收方是先用私鑰解密數據,通過簽名和摘要算出對方PK,用此PK加密數據發送給對方。
RLPX只需要一輪rtt即可實現祕鑰交換。
祕鑰共享第一階段
發起方發消息:
- 發起方(initiator)使用自己的
私鑰Prv
和對方的公鑰remotePub
(這個公鑰從enode
中獲取)生成一個靜態共享私密(token)。token是由本地私鑰和對方公鑰擴展而成的橢圓曲線上的點做有限域標量乘積得到(與私鑰產生公鑰的過程類似).
_, err := rand.Read(h.initNonce)
// Generate random keypair to for ECDH.
h.randomPrivKey, err = ecies.GenerateKey(rand.Reader, crypto.S256(), nil)
// Sign known message: static-shared-secret ^ nonce
token, err := h.staticSharedSecret(prv)
- 發起方生成一個隨機數i
nitNonce
,和token異或
生成一個待簽名信息: unsigned := initNonce ⊕ token
signed := xor(token, h.initNonce)
- 發起方用隨機生成的私鑰
iRandPrv
對待簽名信息進行簽名: signature := Sign(unsigned, iRandPrv)
signature, err := crypto.Sign(signed, h.randomPrivKey.ExportECDSA())
- 發起方將·
nitNonce、signature、iPub
打包成authMsg發送給接收方,這裏面有兩種實現- sealEIP8 這個是升級後的實現.先對數據做rlp,公鑰加密數據,把偏移量記錄在buf頭裏面,數據擴張性增強,可直接定義數據,無升級問題
err := rlp.Encode(buf, msg) binary.BigEndian.PutUint16(prefix, uint16(buf.Len()+eciesOverhead)) enc, err := ecies.Encrypt(rand.Reader, h.remote, buf.Bytes(), nil, prefix)
- sealPlain 簡單將數據拷貝到buf裏面用對方公鑰加密
n := copy(buf, msg.Signature[:])
crypto.Keccak256(exportPubkey(&h.randomPrivKey.PublicKey)))
n += copy(buf[n:], msg.InitiatorPubkey[:])
n += copy(buf[n:], msg.Nonce[:])
return ecies.Encrypt(rand.Reader, h.remote, buf, nil, nil)
接收方收消息:
接收方(receiver)
收到數據請求,先解密數據
嘗試解密使用的哪種加密方式- sealPlain. 先嚐試用普通方式解密,成功會把
msg.gotPlain = true
設置爲true。
- sealPlain. 先嚐試用普通方式解密,成功會把
key := ecies.ImportECDSA(prv)
if dec, err := key.Decrypt(buf, nil, nil); err == nil {
msg.decodePlain(dec)
return buf, nil
}
func (msg *authMsgV4) decodePlain(input []byte) {
n := copy(msg.Signature[:], input)
n += copy(msg.InitiatorPubkey[:], input[n:])
copy(msg.Nonce[:], input[n:])
msg.gotPlain = true
}
- sealEIP8 不成功使用sealEIP8解密數據。如果兩種都無法解密成功則斷開連接。
dec, err := key.Decrypt(buf[2:], nil, prefix)
// Can't use rlp.DecodeBytes here because it rejects
// trailing data (forward-compatibility).
s := rlp.NewStream(bytes.NewReader(dec), 0)
- 用自己的私鑰rPrv和發起方的公鑰iPub,生成一個token: token := rPrv(iPub.X, iPub.Y)
由於公鑰是由私鑰產生的,有(iPub.X, iPub.Y) = iPrvG(x0, y0),而G(x0, y0)是ECDSA橢圓曲線給定的初始點 從而接收方生成的token與發起方一致,它的值都是iPrvrPrvG(x0, y0)
.
rpub, err := importPublicKey(msg.InitiatorPubkey[:])
// Check the signature.
token, err := h.staticSharedSecret(prv)
- 接收方用自己生成的token與發起方發送過來的initNonce異或得到簽名前的信息unsigned,用unsigned和signature可以導出發送方的隨機公鑰iRandPub.
signedMsg := xor(token, h.initNonce)
remoteRandomPub, err := crypto.Ecrecover(signedMsg, msg.Signature[:])
h.remoteRandomPub, _ = importPublicKey(remoteRandomPub)
祕鑰共享第二階段
接收方發送消息:
- 接收方生成自己的隨機私鑰rRandPrv和隨機數respNonce
_, err = rand.Read(h.respNonce)
msg = new(authRespV4)
copy(msg.Nonce[:], h.respNonce)
copy(msg.RandomPubkey[:], exportPubkey(&h.randomPrivKey.PublicKey))
- 將隨機公鑰rRandPub和respNonce選擇使用sealEIP8或是sealPlain封裝成respAuthMsg用
對方公鑰加密
發送給發起方.
if authMsg.gotPlain {
authRespPacket, err = authRespMsg.sealPlain(h)
} else {
authRespPacket, err = sealEIP8(authRespMsg, h)
}
此時接收方祕鑰已建立完成,只是對方尚未完成。
發起方接收消息
發起方(initiator)
收到請求,先解密數據
嘗試解密使用的哪種加密方式sealEIP8或是sealPlain,用私鑰解密數據。
h.remoteRandomPub, err = importPublicKey(msg.RandomPubkey[:])
祕鑰交換流程結束。
雙方建立連接
握手完成雙方都會用各自得到的authMsg和respAuthMsg生成一組共享祕密(secrets).
h.secrets(authPacket, authRespPacket)
它其中包含了雙方一致認同的AES密鑰和MAC密鑰等,
ecdheSecret, err := h.randomPrivKey.GenerateShared(h.remoteRandomPub, sskLen, sskLen)
// derive base secrets from ephemeral key agreement
sharedSecret := crypto.Keccak256(ecdheSecret, crypto.Keccak256(h.respNonce, h.initNonce))
aesSecret := crypto.Keccak256(ecdheSecret, sharedSecret)
s := secrets{
Remote: h.remote,
AES: aesSecret,
MAC: crypto.Keccak256(ecdheSecret, aesSecret),
}
這樣以後信道上傳輸的信息都將用這組密鑰來加密解密; 共享祕密生成的過程類似於之前token產生的過程,雙方都使用自己本地的隨機私鑰和對方的隨機公鑰相乘得到一個相同的密鑰,再用這個密鑰進行一系列Keccak256加密後得到AES密鑰和MAC密鑰.
收發消息MAC判斷
如果發送方的EgressMAC和接收方的IngressMAC不一致,
s.EgressMAC, s.IngressMAC = mac1, mac2
消息將無法成功發送
shouldMAC := updateMAC(rw.ingressMAC, rw.macCipher, headbuf[:16])
if !hmac.Equal(shouldMAC, headbuf[16:]) {
return msg, errors.New("bad header MAC")
}