From 936dd0f3bc19457c8496af00b181f0a8a2f18d6f Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Thu, 26 Feb 2015 22:30:34 +0000 Subject: p2p: add basic RLPx frame I/O --- p2p/rlpx.go | 129 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 p2p/rlpx.go (limited to 'p2p/rlpx.go') diff --git a/p2p/rlpx.go b/p2p/rlpx.go new file mode 100644 index 000000000..9fd1aed1f --- /dev/null +++ b/p2p/rlpx.go @@ -0,0 +1,129 @@ +package p2p + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/hmac" + "errors" + "hash" + "io" + + "github.com/ethereum/go-ethereum/rlp" +) + +var ( + zeroHeader = []byte{0xC2, 0x80, 0x80} + zero16 = make([]byte, 16) +) + +type rlpxFrameRW struct { + conn io.ReadWriter + + macCipher cipher.Block + egressMAC hash.Hash + ingressMAC hash.Hash +} + +func newRlpxFrameRW(conn io.ReadWriter, macSecret []byte, egressMAC, ingressMAC hash.Hash) *rlpxFrameRW { + cipher, err := aes.NewCipher(macSecret) + if err != nil { + panic("invalid macSecret: " + err.Error()) + } + return &rlpxFrameRW{conn: conn, macCipher: cipher, egressMAC: egressMAC, ingressMAC: ingressMAC} +} + +func (rw *rlpxFrameRW) WriteMsg(msg Msg) error { + ptype, _ := rlp.EncodeToBytes(msg.Code) + + // write header + headbuf := make([]byte, 32) + fsize := uint32(len(ptype)) + msg.Size + putInt24(fsize, headbuf) // TODO: check overflow + copy(headbuf[3:], zeroHeader) + copy(headbuf[16:], updateHeaderMAC(rw.egressMAC, rw.macCipher, headbuf[:16])) + if _, err := rw.conn.Write(headbuf); err != nil { + return err + } + + // write frame, updating the egress MAC while writing to conn. + tee := io.MultiWriter(rw.conn, rw.egressMAC) + if _, err := tee.Write(ptype); err != nil { + return err + } + if _, err := io.Copy(tee, msg.Payload); err != nil { + return err + } + if padding := fsize % 16; padding > 0 { + if _, err := tee.Write(zero16[:16-padding]); err != nil { + return err + } + } + + // write packet-mac. egress MAC is up to date because + // frame content was written to it as well. + _, err := rw.conn.Write(rw.egressMAC.Sum(nil)) + return err +} + +func (rw *rlpxFrameRW) ReadMsg() (msg Msg, err error) { + // read the header + headbuf := make([]byte, 32) + if _, err := io.ReadFull(rw.conn, headbuf); err != nil { + return msg, err + } + fsize := readInt24(headbuf) + // ignore protocol type for now + shouldMAC := updateHeaderMAC(rw.ingressMAC, rw.macCipher, headbuf[:16]) + if !hmac.Equal(shouldMAC[:16], headbuf[16:]) { + return msg, errors.New("bad header MAC") + } + + // read the frame content + framebuf := make([]byte, fsize) + if _, err := io.ReadFull(rw.conn, framebuf); err != nil { + return msg, err + } + rw.ingressMAC.Write(framebuf) + if padding := fsize % 16; padding > 0 { + if _, err := io.CopyN(rw.ingressMAC, rw.conn, int64(16-padding)); err != nil { + return msg, err + } + } + // read and validate frame MAC. we can re-use headbuf for that. + if _, err := io.ReadFull(rw.conn, headbuf); err != nil { + return msg, err + } + if !hmac.Equal(rw.ingressMAC.Sum(nil), headbuf) { + return msg, errors.New("bad frame MAC") + } + + // decode message code + content := bytes.NewReader(framebuf) + if err := rlp.Decode(content, &msg.Code); err != nil { + return msg, err + } + msg.Size = uint32(content.Len()) + msg.Payload = content + return msg, nil +} + +func updateHeaderMAC(mac hash.Hash, block cipher.Block, header []byte) []byte { + aesbuf := make([]byte, aes.BlockSize) + block.Encrypt(aesbuf, mac.Sum(nil)) + for i := range aesbuf { + aesbuf[i] ^= header[i] + } + mac.Write(aesbuf) + return mac.Sum(nil) +} + +func readInt24(b []byte) uint32 { + return uint32(b[2]) | uint32(b[1])<<8 | uint32(b[0])<<16 +} + +func putInt24(v uint32, b []byte) { + b[0] = byte(v >> 16) + b[1] = byte(v >> 8) + b[2] = byte(v) +} -- cgit v1.2.3 From 51e01cceca81bc5e82896815754b7c33bb6e6005 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Fri, 27 Feb 2015 02:09:53 +0000 Subject: p2p: encrypted and authenticated RLPx frame I/O --- p2p/rlpx.go | 66 +++++++++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 47 insertions(+), 19 deletions(-) (limited to 'p2p/rlpx.go') diff --git a/p2p/rlpx.go b/p2p/rlpx.go index 9fd1aed1f..761dc2ed9 100644 --- a/p2p/rlpx.go +++ b/p2p/rlpx.go @@ -13,24 +13,44 @@ import ( ) var ( + // this is used in place of actual frame header data. + // TODO: replace this when Msg contains the protocol type code. zeroHeader = []byte{0xC2, 0x80, 0x80} - zero16 = make([]byte, 16) + + // sixteen zero bytes + zero16 = make([]byte, 16) ) type rlpxFrameRW struct { conn io.ReadWriter + enc cipher.Stream + dec cipher.Stream macCipher cipher.Block egressMAC hash.Hash ingressMAC hash.Hash } -func newRlpxFrameRW(conn io.ReadWriter, macSecret []byte, egressMAC, ingressMAC hash.Hash) *rlpxFrameRW { - cipher, err := aes.NewCipher(macSecret) +func newRlpxFrameRW(conn io.ReadWriter, s secrets) *rlpxFrameRW { + macc, err := aes.NewCipher(s.MAC) + if err != nil { + panic("invalid MAC secret: " + err.Error()) + } + encc, err := aes.NewCipher(s.AES) if err != nil { - panic("invalid macSecret: " + err.Error()) + panic("invalid AES secret: " + err.Error()) + } + // we use an all-zeroes IV for AES because the key used + // for encryption is ephemeral. + iv := make([]byte, encc.BlockSize()) + return &rlpxFrameRW{ + conn: conn, + enc: cipher.NewCTR(encc, iv), + dec: cipher.NewCTR(encc, iv), + macCipher: macc, + egressMAC: s.EgressMAC, + ingressMAC: s.IngressMAC, } - return &rlpxFrameRW{conn: conn, macCipher: cipher, egressMAC: egressMAC, ingressMAC: ingressMAC} } func (rw *rlpxFrameRW) WriteMsg(msg Msg) error { @@ -41,13 +61,14 @@ func (rw *rlpxFrameRW) WriteMsg(msg Msg) error { fsize := uint32(len(ptype)) + msg.Size putInt24(fsize, headbuf) // TODO: check overflow copy(headbuf[3:], zeroHeader) + rw.enc.XORKeyStream(headbuf[:16], headbuf[:16]) // first half is now encrypted copy(headbuf[16:], updateHeaderMAC(rw.egressMAC, rw.macCipher, headbuf[:16])) if _, err := rw.conn.Write(headbuf); err != nil { return err } - // write frame, updating the egress MAC while writing to conn. - tee := io.MultiWriter(rw.conn, rw.egressMAC) + // write encrypted frame, updating the egress MAC while writing to conn. + tee := cipher.StreamWriter{S: rw.enc, W: io.MultiWriter(rw.conn, rw.egressMAC)} if _, err := tee.Write(ptype); err != nil { return err } @@ -62,7 +83,8 @@ func (rw *rlpxFrameRW) WriteMsg(msg Msg) error { // write packet-mac. egress MAC is up to date because // frame content was written to it as well. - _, err := rw.conn.Write(rw.egressMAC.Sum(nil)) + mac := updateHeaderMAC(rw.egressMAC, rw.macCipher, rw.egressMAC.Sum(nil)) + _, err := rw.conn.Write(mac) return err } @@ -72,34 +94,40 @@ func (rw *rlpxFrameRW) ReadMsg() (msg Msg, err error) { if _, err := io.ReadFull(rw.conn, headbuf); err != nil { return msg, err } - fsize := readInt24(headbuf) - // ignore protocol type for now + // verify header mac shouldMAC := updateHeaderMAC(rw.ingressMAC, rw.macCipher, headbuf[:16]) if !hmac.Equal(shouldMAC[:16], headbuf[16:]) { return msg, errors.New("bad header MAC") } + rw.dec.XORKeyStream(headbuf[:16], headbuf[:16]) // first half is now decrypted + fsize := readInt24(headbuf) + // ignore protocol type for now // read the frame content - framebuf := make([]byte, fsize) + var rsize = fsize // frame size rounded up to 16 byte boundary + if padding := fsize % 16; padding > 0 { + rsize += 16 - padding + } + framebuf := make([]byte, rsize) if _, err := io.ReadFull(rw.conn, framebuf); err != nil { return msg, err } - rw.ingressMAC.Write(framebuf) - if padding := fsize % 16; padding > 0 { - if _, err := io.CopyN(rw.ingressMAC, rw.conn, int64(16-padding)); err != nil { - return msg, err - } - } + // read and validate frame MAC. we can re-use headbuf for that. + rw.ingressMAC.Write(framebuf) if _, err := io.ReadFull(rw.conn, headbuf); err != nil { return msg, err } - if !hmac.Equal(rw.ingressMAC.Sum(nil), headbuf) { + shouldMAC = updateHeaderMAC(rw.ingressMAC, rw.macCipher, rw.ingressMAC.Sum(nil)) + if !hmac.Equal(shouldMAC, headbuf) { return msg, errors.New("bad frame MAC") } + // decrypt frame content + rw.dec.XORKeyStream(framebuf, framebuf) + // decode message code - content := bytes.NewReader(framebuf) + content := bytes.NewReader(framebuf[:fsize]) if err := rlp.Decode(content, &msg.Code); err != nil { return msg, err } -- cgit v1.2.3 From d344054e5a2844241bf0e4f64ccfc4d2ad259718 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Fri, 27 Feb 2015 14:08:57 +0100 Subject: p2p: make RLPx frame MAC 16 bytes as defined in the spec --- p2p/rlpx.go | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) (limited to 'p2p/rlpx.go') diff --git a/p2p/rlpx.go b/p2p/rlpx.go index 761dc2ed9..a041bb314 100644 --- a/p2p/rlpx.go +++ b/p2p/rlpx.go @@ -62,12 +62,15 @@ func (rw *rlpxFrameRW) WriteMsg(msg Msg) error { putInt24(fsize, headbuf) // TODO: check overflow copy(headbuf[3:], zeroHeader) rw.enc.XORKeyStream(headbuf[:16], headbuf[:16]) // first half is now encrypted - copy(headbuf[16:], updateHeaderMAC(rw.egressMAC, rw.macCipher, headbuf[:16])) + + // write header MAC + copy(headbuf[16:], updateMAC(rw.egressMAC, rw.macCipher, headbuf[:16])) if _, err := rw.conn.Write(headbuf); err != nil { return err } - // write encrypted frame, updating the egress MAC while writing to conn. + // write encrypted frame, updating the egress MAC hash with + // the data written to conn. tee := cipher.StreamWriter{S: rw.enc, W: io.MultiWriter(rw.conn, rw.egressMAC)} if _, err := tee.Write(ptype); err != nil { return err @@ -81,9 +84,10 @@ func (rw *rlpxFrameRW) WriteMsg(msg Msg) error { } } - // write packet-mac. egress MAC is up to date because + // write frame MAC. egress MAC hash is up to date because // frame content was written to it as well. - mac := updateHeaderMAC(rw.egressMAC, rw.macCipher, rw.egressMAC.Sum(nil)) + fmacseed := rw.egressMAC.Sum(nil) + mac := updateMAC(rw.egressMAC, rw.macCipher, fmacseed) _, err := rw.conn.Write(mac) return err } @@ -95,8 +99,8 @@ func (rw *rlpxFrameRW) ReadMsg() (msg Msg, err error) { return msg, err } // verify header mac - shouldMAC := updateHeaderMAC(rw.ingressMAC, rw.macCipher, headbuf[:16]) - if !hmac.Equal(shouldMAC[:16], headbuf[16:]) { + shouldMAC := updateMAC(rw.ingressMAC, rw.macCipher, headbuf[:16]) + if !hmac.Equal(shouldMAC, headbuf[16:]) { return msg, errors.New("bad header MAC") } rw.dec.XORKeyStream(headbuf[:16], headbuf[:16]) // first half is now decrypted @@ -115,11 +119,12 @@ func (rw *rlpxFrameRW) ReadMsg() (msg Msg, err error) { // read and validate frame MAC. we can re-use headbuf for that. rw.ingressMAC.Write(framebuf) - if _, err := io.ReadFull(rw.conn, headbuf); err != nil { + fmacseed := rw.ingressMAC.Sum(nil) + if _, err := io.ReadFull(rw.conn, headbuf[:16]); err != nil { return msg, err } - shouldMAC = updateHeaderMAC(rw.ingressMAC, rw.macCipher, rw.ingressMAC.Sum(nil)) - if !hmac.Equal(shouldMAC, headbuf) { + shouldMAC = updateMAC(rw.ingressMAC, rw.macCipher, fmacseed) + if !hmac.Equal(shouldMAC, headbuf[:16]) { return msg, errors.New("bad frame MAC") } @@ -136,14 +141,16 @@ func (rw *rlpxFrameRW) ReadMsg() (msg Msg, err error) { return msg, nil } -func updateHeaderMAC(mac hash.Hash, block cipher.Block, header []byte) []byte { +// updateMAC reseeds the given hash with encrypted seed. +// it returns the first 16 bytes of the hash sum after seeding. +func updateMAC(mac hash.Hash, block cipher.Block, seed []byte) []byte { aesbuf := make([]byte, aes.BlockSize) block.Encrypt(aesbuf, mac.Sum(nil)) for i := range aesbuf { - aesbuf[i] ^= header[i] + aesbuf[i] ^= seed[i] } mac.Write(aesbuf) - return mac.Sum(nil) + return mac.Sum(nil)[:16] } func readInt24(b []byte) uint32 { -- cgit v1.2.3 From 22659a7feaf4e939a33762c3f83b43d8bec757db Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Wed, 4 Mar 2015 16:27:37 +0100 Subject: p2p: restore read/write timeouts They got lost in the transition to rlpxFrameRW. --- p2p/rlpx.go | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'p2p/rlpx.go') diff --git a/p2p/rlpx.go b/p2p/rlpx.go index a041bb314..166bbb5e6 100644 --- a/p2p/rlpx.go +++ b/p2p/rlpx.go @@ -21,6 +21,11 @@ var ( zero16 = make([]byte, 16) ) +// rlpxFrameRW implements a simplified version of RLPx framing. +// chunked messages are not supported and all headers are equal to +// zeroHeader. +// +// rlpxFrameRW is not safe for concurrent use from multiple goroutines. type rlpxFrameRW struct { conn io.ReadWriter enc cipher.Stream -- cgit v1.2.3 From 429828cd9205a8db0024652fd9da96cfbdaeae86 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Wed, 4 Mar 2015 16:39:04 +0100 Subject: p2p: reject messages that cannot be written as simple RLPx frames Until chunked frames are implemented we cannot send messages with a size overflowing uint24. --- p2p/rlpx.go | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'p2p/rlpx.go') diff --git a/p2p/rlpx.go b/p2p/rlpx.go index 166bbb5e6..6b533e275 100644 --- a/p2p/rlpx.go +++ b/p2p/rlpx.go @@ -19,6 +19,8 @@ var ( // sixteen zero bytes zero16 = make([]byte, 16) + + maxUint24 = ^uint32(0) >> 8 ) // rlpxFrameRW implements a simplified version of RLPx framing. @@ -64,6 +66,9 @@ func (rw *rlpxFrameRW) WriteMsg(msg Msg) error { // write header headbuf := make([]byte, 32) fsize := uint32(len(ptype)) + msg.Size + if fsize > maxUint24 { + return errors.New("message size overflows uint24") + } putInt24(fsize, headbuf) // TODO: check overflow copy(headbuf[3:], zeroHeader) rw.enc.XORKeyStream(headbuf[:16], headbuf[:16]) // first half is now encrypted -- cgit v1.2.3