p2p节点连接中的秘钥交换RLPX (代码篇)
程序员文章站
2022-07-01 15:28:28
...
不了解RLPX的可以查看上篇博客。
p2p节点连接中的秘钥交换RLPX (理论篇)
秘钥共享第一阶段
发起方发消息:
- 发起方(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")
}