p2p节点连接中的秘钥交换RLPX (代码篇)

不了解RLPX的可以查看上篇博客。
p2p节点连接中的秘钥交换RLPX (理论篇)

Enode

enode://6ee7f15fbe4a60585a54722b11727652f3ac54ce7a9a4c5977fd554200d7f7a1e636e90bd0b953f73c0bdd29720211b083600e5f73c7d1c765b3263f9509dfc3@39.99.195.63:30313

enode 由3部分组成,

  • 公钥
  • IP
  • 端口

如果想连接节点,首先需要知道enode,TCP Dial的时候其实只需要IP和端口公钥是在秘钥交换的时候才启作用,加密发送给对方的数据

接收方是先用私钥解密数据,通过签名和摘要算出对方PK,用此PK加密数据发送给对方。

RLPX只需要一轮rtt即可实现秘钥交换。

秘钥共享第一阶段

发起方发消息:

  1. 发起方(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)
  1. 发起方生成一个随机数initNonce,和token异或生成一个待签名信息: unsigned := initNonce ⊕ token
	signed := xor(token, h.initNonce)
  1. 发起方用随机生成的私钥iRandPrv对待签名信息进行签名: signature := Sign(unsigned, iRandPrv)
	signature, err := crypto.Sign(signed, h.randomPrivKey.ExportECDSA())
  1. 发起方将·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)

接收方收消息:

  1. 接收方(receiver)收到数据请求,先解密数据尝试解密使用的哪种加密方式
    • sealPlain. 先尝试用普通方式解密,成功会把 msg.gotPlain = true设置为true。
	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)
  1. 用自己的私钥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)
  1. 接收方用自己生成的token与发起方发送过来的initNonce异或得到签名前的信息unsigned,用unsigned和signature可以导出发送方的随机公钥iRandPub.
	signedMsg := xor(token, h.initNonce)
	remoteRandomPub, err := crypto.Ecrecover(signedMsg, msg.Signature[:])
	h.remoteRandomPub, _ = importPublicKey(remoteRandomPub)

秘钥共享第二阶段

接收方发送消息:

  1. 接收方生成自己的随机私钥rRandPrv和随机数respNonce
	_, err = rand.Read(h.respNonce)
	msg = new(authRespV4)
	copy(msg.Nonce[:], h.respNonce)
	copy(msg.RandomPubkey[:], exportPubkey(&h.randomPrivKey.PublicKey))
  1. 将随机公钥rRandPub和respNonce选择使用sealEIP8或是sealPlain封装成respAuthMsg用对方公钥加密发送给发起方.
	if authMsg.gotPlain {
		authRespPacket, err = authRespMsg.sealPlain(h)
	} else {
		authRespPacket, err = sealEIP8(authRespMsg, h)
	}

此时接收方秘钥已建立完成,只是对方尚未完成。

发起方接收消息

  1. 发起方(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")
	}

猜你喜欢

转载自blog.csdn.net/JIYILANZHOU/article/details/106569329