aboutsummaryrefslogtreecommitdiffstats
path: root/p2p/rlpx.go
diff options
context:
space:
mode:
Diffstat (limited to 'p2p/rlpx.go')
-rw-r--r--p2p/rlpx.go351
1 files changed, 209 insertions, 142 deletions
diff --git a/p2p/rlpx.go b/p2p/rlpx.go
index 8f429d6ec..9d6cba5b6 100644
--- a/p2p/rlpx.go
+++ b/p2p/rlpx.go
@@ -24,11 +24,14 @@ import (
"crypto/elliptic"
"crypto/hmac"
"crypto/rand"
+ "encoding/binary"
"errors"
"fmt"
"hash"
"io"
+ mrand "math/rand"
"net"
+ "os"
"sync"
"time"
@@ -51,9 +54,10 @@ const (
authMsgLen = sigLen + shaLen + pubLen + shaLen + 1
authRespLen = pubLen + shaLen + 1
- eciesBytes = 65 + 16 + 32
- encAuthMsgLen = authMsgLen + eciesBytes // size of the final ECIES payload sent as initiator's handshake
- encAuthRespLen = authRespLen + eciesBytes // size of the final ECIES payload sent as receiver's handshake
+ eciesOverhead = 65 /* pubkey */ + 16 /* IV */ + 32 /* MAC */
+
+ encAuthMsgLen = authMsgLen + eciesOverhead // size of encrypted pre-EIP-8 initiator handshake
+ encAuthRespLen = authRespLen + eciesOverhead // size of encrypted pre-EIP-8 handshake reply
// total timeout for encryption handshake and protocol
// handshake in both directions.
@@ -151,10 +155,6 @@ func readProtocolHandshake(rw MsgReader, our *protoHandshake) (*protoHandshake,
if err := msg.Decode(&hs); err != nil {
return nil, err
}
- // validate handshake info
- if hs.Version != our.Version {
- return nil, DiscIncompatibleVersion
- }
if (hs.ID == discover.NodeID{}) {
return nil, DiscInvalidIdentity
}
@@ -200,6 +200,29 @@ type secrets struct {
Token []byte
}
+// RLPx v4 handshake auth (defined in EIP-8).
+type authMsgV4 struct {
+ gotPlain bool // whether read packet had plain format.
+
+ Signature [sigLen]byte
+ InitiatorPubkey [pubLen]byte
+ Nonce [shaLen]byte
+ Version uint
+
+ // Ignore additional fields (forward-compatibility)
+ Rest []rlp.RawValue `rlp:"tail"`
+}
+
+// RLPx v4 handshake response (defined in EIP-8).
+type authRespV4 struct {
+ RandomPubkey [pubLen]byte
+ Nonce [shaLen]byte
+ Version uint
+
+ // Ignore additional fields (forward-compatibility)
+ Rest []rlp.RawValue `rlp:"tail"`
+}
+
// secrets is called after the handshake is completed.
// It extracts the connection secrets from the handshake values.
func (h *encHandshake) secrets(auth, authResp []byte) (secrets, error) {
@@ -215,7 +238,6 @@ func (h *encHandshake) secrets(auth, authResp []byte) (secrets, error) {
RemoteID: h.remoteID,
AES: aesSecret,
MAC: crypto.Sha3(ecdheSecret, aesSecret),
- Token: crypto.Sha3(sharedSecret),
}
// setup sha3 instances for the MACs
@@ -234,114 +256,89 @@ func (h *encHandshake) secrets(auth, authResp []byte) (secrets, error) {
return s, nil
}
-func (h *encHandshake) ecdhShared(prv *ecdsa.PrivateKey) ([]byte, error) {
+// staticSharedSecret returns the static shared secret, the result
+// of key agreement between the local and remote static node key.
+func (h *encHandshake) staticSharedSecret(prv *ecdsa.PrivateKey) ([]byte, error) {
return ecies.ImportECDSA(prv).GenerateShared(h.remotePub, sskLen, sskLen)
}
+var configSendEIP = os.Getenv("RLPX_EIP8") != ""
+
// initiatorEncHandshake negotiates a session token on conn.
// it should be called on the dialing side of the connection.
//
// prv is the local client's private key.
-// token is the token from a previous session with this node.
func initiatorEncHandshake(conn io.ReadWriter, prv *ecdsa.PrivateKey, remoteID discover.NodeID, token []byte) (s secrets, err error) {
- h, err := newInitiatorHandshake(remoteID)
+ h := &encHandshake{initiator: true, remoteID: remoteID}
+ authMsg, err := h.makeAuthMsg(prv, token)
if err != nil {
return s, err
}
- auth, err := h.authMsg(prv, token)
+ var authPacket []byte
+ if configSendEIP {
+ authPacket, err = sealEIP8(authMsg, h)
+ } else {
+ authPacket, err = authMsg.sealPlain(h)
+ }
if err != nil {
return s, err
}
- if _, err = conn.Write(auth); err != nil {
+ if _, err = conn.Write(authPacket); err != nil {
return s, err
}
- response := make([]byte, encAuthRespLen)
- if _, err = io.ReadFull(conn, response); err != nil {
+ authRespMsg := new(authRespV4)
+ authRespPacket, err := readHandshakeMsg(authRespMsg, encAuthRespLen, prv, conn)
+ if err != nil {
return s, err
}
- if err := h.decodeAuthResp(response, prv); err != nil {
+ if err := h.handleAuthResp(authRespMsg); err != nil {
return s, err
}
- return h.secrets(auth, response)
+ return h.secrets(authPacket, authRespPacket)
}
-func newInitiatorHandshake(remoteID discover.NodeID) (*encHandshake, error) {
- rpub, err := remoteID.Pubkey()
+// makeAuthMsg creates the initiator handshake message.
+func (h *encHandshake) makeAuthMsg(prv *ecdsa.PrivateKey, token []byte) (*authMsgV4, error) {
+ rpub, err := h.remoteID.Pubkey()
if err != nil {
return nil, fmt.Errorf("bad remoteID: %v", err)
}
- // generate random initiator nonce
- n := make([]byte, shaLen)
- if _, err := rand.Read(n); err != nil {
+ h.remotePub = ecies.ImportECDSAPublic(rpub)
+ // Generate random initiator nonce.
+ h.initNonce = make([]byte, shaLen)
+ if _, err := rand.Read(h.initNonce); err != nil {
return nil, err
}
- // generate random keypair to use for signing
- randpriv, err := ecies.GenerateKey(rand.Reader, secp256k1.S256(), nil)
+ // Generate random keypair to for ECDH.
+ h.randomPrivKey, err = ecies.GenerateKey(rand.Reader, secp256k1.S256(), nil)
if err != nil {
return nil, err
}
- h := &encHandshake{
- initiator: true,
- remoteID: remoteID,
- remotePub: ecies.ImportECDSAPublic(rpub),
- initNonce: n,
- randomPrivKey: randpriv,
- }
- return h, nil
-}
-
-// authMsg creates an encrypted initiator handshake message.
-func (h *encHandshake) authMsg(prv *ecdsa.PrivateKey, token []byte) ([]byte, error) {
- var tokenFlag byte
- if token == nil {
- // no session token found means we need to generate shared secret.
- // ecies shared secret is used as initial session token for new peers
- // generate shared key from prv and remote pubkey
- var err error
- if token, err = h.ecdhShared(prv); err != nil {
- return nil, err
- }
- } else {
- // for known peers, we use stored token from the previous session
- tokenFlag = 0x01
- }
- // sign known message:
- // ecdh-shared-secret^nonce for new peers
- // token^nonce for old peers
+ // Sign known message: static-shared-secret ^ nonce
+ token, err = h.staticSharedSecret(prv)
+ if err != nil {
+ return nil, err
+ }
signed := xor(token, h.initNonce)
signature, err := crypto.Sign(signed, h.randomPrivKey.ExportECDSA())
if err != nil {
return nil, err
}
- // encode auth message
- // signature || sha3(ecdhe-random-pubk) || pubk || nonce || token-flag
- msg := make([]byte, authMsgLen)
- n := copy(msg, signature)
- n += copy(msg[n:], crypto.Sha3(exportPubkey(&h.randomPrivKey.PublicKey)))
- n += copy(msg[n:], crypto.FromECDSAPub(&prv.PublicKey)[1:])
- n += copy(msg[n:], h.initNonce)
- msg[n] = tokenFlag
-
- // encrypt auth message using remote-pubk
- return ecies.Encrypt(rand.Reader, h.remotePub, msg, nil, nil)
+ msg := new(authMsgV4)
+ copy(msg.Signature[:], signature)
+ copy(msg.InitiatorPubkey[:], crypto.FromECDSAPub(&prv.PublicKey)[1:])
+ copy(msg.Nonce[:], h.initNonce)
+ msg.Version = 4
+ return msg, nil
}
-// decodeAuthResp decode an encrypted authentication response message.
-func (h *encHandshake) decodeAuthResp(auth []byte, prv *ecdsa.PrivateKey) error {
- msg, err := crypto.Decrypt(prv, auth)
- if err != nil {
- return fmt.Errorf("could not decrypt auth response (%v)", err)
- }
- h.respNonce = msg[pubLen : pubLen+shaLen]
- h.remoteRandomPub, err = importPublicKey(msg[:pubLen])
- if err != nil {
- return err
- }
- // ignore token flag for now
- return nil
+func (h *encHandshake) handleAuthResp(msg *authRespV4) (err error) {
+ h.respNonce = msg.Nonce[:]
+ h.remoteRandomPub, err = importPublicKey(msg.RandomPubkey[:])
+ return err
}
// receiverEncHandshake negotiates a session token on conn.
@@ -350,99 +347,165 @@ func (h *encHandshake) decodeAuthResp(auth []byte, prv *ecdsa.PrivateKey) error
// prv is the local client's private key.
// token is the token from a previous session with this node.
func receiverEncHandshake(conn io.ReadWriter, prv *ecdsa.PrivateKey, token []byte) (s secrets, err error) {
- // read remote auth sent by initiator.
- auth := make([]byte, encAuthMsgLen)
- if _, err := io.ReadFull(conn, auth); err != nil {
+ authMsg := new(authMsgV4)
+ authPacket, err := readHandshakeMsg(authMsg, encAuthMsgLen, prv, conn)
+ if err != nil {
return s, err
}
- h, err := decodeAuthMsg(prv, token, auth)
- if err != nil {
+ h := new(encHandshake)
+ if err := h.handleAuthMsg(authMsg, prv); err != nil {
return s, err
}
- // send auth response
- resp, err := h.authResp(prv, token)
+ authRespMsg, err := h.makeAuthResp()
if err != nil {
return s, err
}
- if _, err = conn.Write(resp); err != nil {
- return s, err
+ var authRespPacket []byte
+ if authMsg.gotPlain {
+ authRespPacket, err = authRespMsg.sealPlain(h)
+ } else {
+ authRespPacket, err = sealEIP8(authRespMsg, h)
}
-
- return h.secrets(auth, resp)
-}
-
-func decodeAuthMsg(prv *ecdsa.PrivateKey, token []byte, auth []byte) (*encHandshake, error) {
- var err error
- h := new(encHandshake)
- // generate random keypair for session
- h.randomPrivKey, err = ecies.GenerateKey(rand.Reader, secp256k1.S256(), nil)
if err != nil {
- return nil, err
- }
- // generate random nonce
- h.respNonce = make([]byte, shaLen)
- if _, err = rand.Read(h.respNonce); err != nil {
- return nil, err
+ return s, err
}
-
- msg, err := crypto.Decrypt(prv, auth)
- if err != nil {
- return nil, fmt.Errorf("could not decrypt auth message (%v)", err)
+ if _, err = conn.Write(authRespPacket); err != nil {
+ return s, err
}
+ return h.secrets(authPacket, authRespPacket)
+}
- // decode message parameters
- // signature || sha3(ecdhe-random-pubk) || pubk || nonce || token-flag
- h.initNonce = msg[authMsgLen-shaLen-1 : authMsgLen-1]
- copy(h.remoteID[:], msg[sigLen+shaLen:sigLen+shaLen+pubLen])
+func (h *encHandshake) handleAuthMsg(msg *authMsgV4, prv *ecdsa.PrivateKey) error {
+ // Import the remote identity.
+ h.initNonce = msg.Nonce[:]
+ h.remoteID = msg.InitiatorPubkey
rpub, err := h.remoteID.Pubkey()
if err != nil {
- return nil, fmt.Errorf("bad remoteID: %#v", err)
+ return fmt.Errorf("bad remoteID: %#v", err)
}
h.remotePub = ecies.ImportECDSAPublic(rpub)
- // recover remote random pubkey from signed message.
- if token == nil {
- // TODO: it is an error if the initiator has a token and we don't. check that.
-
- // no session token means we need to generate shared secret.
- // ecies shared secret is used as initial session token for new peers.
- // generate shared key from prv and remote pubkey.
- if token, err = h.ecdhShared(prv); err != nil {
- return nil, err
+ // Generate random keypair for ECDH.
+ // If a private key is already set, use it instead of generating one (for testing).
+ if h.randomPrivKey == nil {
+ h.randomPrivKey, err = ecies.GenerateKey(rand.Reader, secp256k1.S256(), nil)
+ if err != nil {
+ return err
}
}
+
+ // Check the signature.
+ token, err := h.staticSharedSecret(prv)
+ if err != nil {
+ return err
+ }
signedMsg := xor(token, h.initNonce)
- remoteRandomPub, err := secp256k1.RecoverPubkey(signedMsg, msg[:sigLen])
+ remoteRandomPub, err := secp256k1.RecoverPubkey(signedMsg, msg.Signature[:])
if err != nil {
+ return err
+ }
+ h.remoteRandomPub, _ = importPublicKey(remoteRandomPub)
+ return nil
+}
+
+func (h *encHandshake) makeAuthResp() (msg *authRespV4, err error) {
+ // Generate random nonce.
+ h.respNonce = make([]byte, shaLen)
+ if _, err = rand.Read(h.respNonce); err != nil {
return nil, err
}
- // validate the sha3 of recovered pubkey
- remoteRandomPubMAC := msg[sigLen : sigLen+shaLen]
- shaRemoteRandomPub := crypto.Sha3(remoteRandomPub[1:])
- if !bytes.Equal(remoteRandomPubMAC, shaRemoteRandomPub) {
- return nil, fmt.Errorf("sha3 of recovered ephemeral pubkey does not match checksum in auth message")
+ msg = new(authRespV4)
+ copy(msg.Nonce[:], h.respNonce)
+ copy(msg.RandomPubkey[:], exportPubkey(&h.randomPrivKey.PublicKey))
+ msg.Version = 4
+ return msg, nil
+}
+
+func (msg *authMsgV4) sealPlain(h *encHandshake) ([]byte, error) {
+ buf := make([]byte, authMsgLen)
+ n := copy(buf, msg.Signature[:])
+ n += copy(buf[n:], crypto.Sha3(exportPubkey(&h.randomPrivKey.PublicKey)))
+ n += copy(buf[n:], msg.InitiatorPubkey[:])
+ n += copy(buf[n:], msg.Nonce[:])
+ buf[n] = 0 // token-flag
+ return ecies.Encrypt(rand.Reader, h.remotePub, buf, nil, nil)
+}
+
+func (msg *authMsgV4) decodePlain(input []byte) {
+ n := copy(msg.Signature[:], input)
+ n += shaLen // skip sha3(initiator-ephemeral-pubk)
+ n += copy(msg.InitiatorPubkey[:], input[n:])
+ n += copy(msg.Nonce[:], input[n:])
+ msg.Version = 4
+ msg.gotPlain = true
+}
+
+func (msg *authRespV4) sealPlain(hs *encHandshake) ([]byte, error) {
+ buf := make([]byte, authRespLen)
+ n := copy(buf, msg.RandomPubkey[:])
+ n += copy(buf[n:], msg.Nonce[:])
+ return ecies.Encrypt(rand.Reader, hs.remotePub, buf, nil, nil)
+}
+
+func (msg *authRespV4) decodePlain(input []byte) {
+ n := copy(msg.RandomPubkey[:], input)
+ n += copy(msg.Nonce[:], input[n:])
+ msg.Version = 4
+}
+
+var padSpace = make([]byte, 300)
+
+func sealEIP8(msg interface{}, h *encHandshake) ([]byte, error) {
+ buf := new(bytes.Buffer)
+ if err := rlp.Encode(buf, msg); err != nil {
+ return nil, err
}
+ // pad with random amount of data. the amount needs to be at least 100 bytes to make
+ // the message distinguishable from pre-EIP-8 handshakes.
+ pad := padSpace[:mrand.Intn(len(padSpace)-100)+100]
+ buf.Write(pad)
+ prefix := make([]byte, 2)
+ binary.BigEndian.PutUint16(prefix, uint16(buf.Len()+eciesOverhead))
- h.remoteRandomPub, _ = importPublicKey(remoteRandomPub)
- return h, nil
-}
-
-// authResp generates the encrypted authentication response message.
-func (h *encHandshake) authResp(prv *ecdsa.PrivateKey, token []byte) ([]byte, error) {
- // responder auth message
- // E(remote-pubk, ecdhe-random-pubk || nonce || 0x0)
- resp := make([]byte, authRespLen)
- n := copy(resp, exportPubkey(&h.randomPrivKey.PublicKey))
- n += copy(resp[n:], h.respNonce)
- if token == nil {
- resp[n] = 0
- } else {
- resp[n] = 1
+ enc, err := ecies.Encrypt(rand.Reader, h.remotePub, buf.Bytes(), nil, prefix)
+ return append(prefix, enc...), err
+}
+
+type plainDecoder interface {
+ decodePlain([]byte)
+}
+
+func readHandshakeMsg(msg plainDecoder, plainSize int, prv *ecdsa.PrivateKey, r io.Reader) ([]byte, error) {
+ buf := make([]byte, plainSize)
+ if _, err := io.ReadFull(r, buf); err != nil {
+ return buf, err
+ }
+ // Attempt decoding pre-EIP-8 "plain" format.
+ key := ecies.ImportECDSA(prv)
+ if dec, err := key.Decrypt(rand.Reader, buf, nil, nil); err == nil {
+ msg.decodePlain(dec)
+ return buf, nil
}
- // encrypt using remote-pubk
- return ecies.Encrypt(rand.Reader, h.remotePub, resp, nil, nil)
+ // Could be EIP-8 format, try that.
+ prefix := buf[:2]
+ size := binary.BigEndian.Uint16(prefix)
+ if size < uint16(plainSize) {
+ return buf, fmt.Errorf("size underflow, need at least %d bytes", plainSize)
+ }
+ buf = append(buf, make([]byte, size-uint16(plainSize)+2)...)
+ if _, err := io.ReadFull(r, buf[plainSize:]); err != nil {
+ return buf, err
+ }
+ dec, err := key.Decrypt(rand.Reader, buf[2:], nil, prefix)
+ if err != nil {
+ return buf, err
+ }
+ // Can't use rlp.DecodeBytes here because it rejects
+ // trailing data (forward-compatibility).
+ s := rlp.NewStream(bytes.NewReader(dec), 0)
+ return buf, s.Decode(msg)
}
// importPublicKey unmarshals 512 bit public keys.
@@ -458,7 +521,11 @@ func importPublicKey(pubKey []byte) (*ecies.PublicKey, error) {
return nil, fmt.Errorf("invalid public key length %v (expect 64/65)", len(pubKey))
}
// TODO: fewer pointless conversions
- return ecies.ImportECDSAPublic(crypto.ToECDSAPub(pubKey65)), nil
+ pub := crypto.ToECDSAPub(pubKey65)
+ if pub.X == nil {
+ return nil, fmt.Errorf("invalid public key")
+ }
+ return ecies.ImportECDSAPublic(pub), nil
}
func exportPubkey(pub *ecies.PublicKey) []byte {