Commit 51e01cce authored by Felix Lange's avatar Felix Lange

p2p: encrypted and authenticated RLPx frame I/O

parent 936dd0f3
This diff is collapsed.
...@@ -2,8 +2,6 @@ package p2p ...@@ -2,8 +2,6 @@ package p2p
import ( import (
"bytes" "bytes"
"crypto/ecdsa"
"crypto/rand"
"net" "net"
"reflect" "reflect"
"testing" "testing"
...@@ -69,102 +67,46 @@ func TestSharedSecret(t *testing.T) { ...@@ -69,102 +67,46 @@ func TestSharedSecret(t *testing.T) {
} }
} }
func TestCryptoHandshake(t *testing.T) {
testCryptoHandshake(newkey(), newkey(), nil, t)
}
func TestCryptoHandshakeWithToken(t *testing.T) {
sessionToken := make([]byte, shaLen)
rand.Read(sessionToken)
testCryptoHandshake(newkey(), newkey(), sessionToken, t)
}
func testCryptoHandshake(prv0, prv1 *ecdsa.PrivateKey, sessionToken []byte, t *testing.T) {
var err error
// pub0 := &prv0.PublicKey
pub1 := &prv1.PublicKey
// pub0s := crypto.FromECDSAPub(pub0)
pub1s := crypto.FromECDSAPub(pub1)
// simulate handshake by feeding output to input
// initiator sends handshake 'auth'
auth, initNonce, randomPrivKey, err := authMsg(prv0, pub1s, sessionToken)
if err != nil {
t.Errorf("%v", err)
}
// t.Logf("-> %v", hexkey(auth))
// receiver reads auth and responds with response
response, remoteRecNonce, remoteInitNonce, _, remoteRandomPrivKey, remoteInitRandomPubKey, err := authResp(auth, sessionToken, prv1)
if err != nil {
t.Errorf("%v", err)
}
// t.Logf("<- %v\n", hexkey(response))
// initiator reads receiver's response and the key exchange completes
recNonce, remoteRandomPubKey, _, err := completeHandshake(response, prv0)
if err != nil {
t.Errorf("completeHandshake error: %v", err)
}
// now both parties should have the same session parameters
initSessionToken, err := newSession(initNonce, recNonce, randomPrivKey, remoteRandomPubKey)
if err != nil {
t.Errorf("newSession error: %v", err)
}
recSessionToken, err := newSession(remoteInitNonce, remoteRecNonce, remoteRandomPrivKey, remoteInitRandomPubKey)
if err != nil {
t.Errorf("newSession error: %v", err)
}
// fmt.Printf("\nauth (%v) %x\n\nresp (%v) %x\n\n", len(auth), auth, len(response), response)
// fmt.Printf("\nauth %x\ninitNonce %x\nresponse%x\nremoteRecNonce %x\nremoteInitNonce %x\nremoteRandomPubKey %x\nrecNonce %x\nremoteInitRandomPubKey %x\ninitSessionToken %x\n\n", auth, initNonce, response, remoteRecNonce, remoteInitNonce, remoteRandomPubKey, recNonce, remoteInitRandomPubKey, initSessionToken)
if !bytes.Equal(initNonce, remoteInitNonce) {
t.Errorf("nonces do not match")
}
if !bytes.Equal(recNonce, remoteRecNonce) {
t.Errorf("receiver nonces do not match")
}
if !bytes.Equal(initSessionToken, recSessionToken) {
t.Errorf("session tokens do not match")
}
}
func TestEncHandshake(t *testing.T) { func TestEncHandshake(t *testing.T) {
defer testlog(t).detach() defer testlog(t).detach()
prv0, _ := crypto.GenerateKey() prv0, _ := crypto.GenerateKey()
prv1, _ := crypto.GenerateKey() prv1, _ := crypto.GenerateKey()
pub0s, _ := exportPublicKey(&prv0.PublicKey)
pub1s, _ := exportPublicKey(&prv1.PublicKey)
rw0, rw1 := net.Pipe() rw0, rw1 := net.Pipe()
tokens := make(chan []byte) secrets := make(chan secrets)
go func() { go func() {
token, err := outboundEncHandshake(rw0, prv0, pub1s, nil) pub1s, _ := exportPublicKey(&prv1.PublicKey)
s, err := outboundEncHandshake(rw0, prv0, pub1s, nil)
if err != nil { if err != nil {
t.Errorf("outbound side error: %v", err) t.Errorf("outbound side error: %v", err)
} }
tokens <- token id1 := discover.PubkeyID(&prv1.PublicKey)
if s.RemoteID != id1 {
t.Errorf("outbound side remote ID mismatch")
}
secrets <- s
}() }()
go func() { go func() {
token, remotePubkey, err := inboundEncHandshake(rw1, prv1, nil) s, err := inboundEncHandshake(rw1, prv1, nil)
if err != nil { if err != nil {
t.Errorf("inbound side error: %v", err) t.Errorf("inbound side error: %v", err)
} }
if !bytes.Equal(remotePubkey, pub0s) { id0 := discover.PubkeyID(&prv0.PublicKey)
t.Errorf("inbound side returned wrong remote pubkey\n got: %x\n want: %x", remotePubkey, pub0s) if s.RemoteID != id0 {
t.Errorf("inbound side remote ID mismatch")
} }
tokens <- token secrets <- s
}() }()
t1, t2 := <-tokens, <-tokens // get computed secrets from both sides
if !bytes.Equal(t1, t2) { t1, t2 := <-secrets, <-secrets
t.Error("session token mismatch") // don't compare remote node IDs
t1.RemoteID, t2.RemoteID = discover.NodeID{}, discover.NodeID{}
// flip MACs on one of them so they compare equal
t1.EgressMAC, t1.IngressMAC = t1.IngressMAC, t1.EgressMAC
if !reflect.DeepEqual(t1, t2) {
t.Errorf("secrets mismatch:\n t1: %#v\n t2: %#v", t1, t2)
} }
} }
......
...@@ -13,24 +13,44 @@ import ( ...@@ -13,24 +13,44 @@ import (
) )
var ( 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} zeroHeader = []byte{0xC2, 0x80, 0x80}
zero16 = make([]byte, 16)
// sixteen zero bytes
zero16 = make([]byte, 16)
) )
type rlpxFrameRW struct { type rlpxFrameRW struct {
conn io.ReadWriter conn io.ReadWriter
enc cipher.Stream
dec cipher.Stream
macCipher cipher.Block macCipher cipher.Block
egressMAC hash.Hash egressMAC hash.Hash
ingressMAC hash.Hash ingressMAC hash.Hash
} }
func newRlpxFrameRW(conn io.ReadWriter, macSecret []byte, egressMAC, ingressMAC hash.Hash) *rlpxFrameRW { func newRlpxFrameRW(conn io.ReadWriter, s secrets) *rlpxFrameRW {
cipher, err := aes.NewCipher(macSecret) macc, err := aes.NewCipher(s.MAC)
if err != nil {
panic("invalid MAC secret: " + err.Error())
}
encc, err := aes.NewCipher(s.AES)
if err != nil { 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 { func (rw *rlpxFrameRW) WriteMsg(msg Msg) error {
...@@ -41,13 +61,14 @@ func (rw *rlpxFrameRW) WriteMsg(msg Msg) error { ...@@ -41,13 +61,14 @@ func (rw *rlpxFrameRW) WriteMsg(msg Msg) error {
fsize := uint32(len(ptype)) + msg.Size fsize := uint32(len(ptype)) + msg.Size
putInt24(fsize, headbuf) // TODO: check overflow putInt24(fsize, headbuf) // TODO: check overflow
copy(headbuf[3:], zeroHeader) 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])) copy(headbuf[16:], updateHeaderMAC(rw.egressMAC, rw.macCipher, headbuf[:16]))
if _, err := rw.conn.Write(headbuf); err != nil { if _, err := rw.conn.Write(headbuf); err != nil {
return err return err
} }
// write frame, updating the egress MAC while writing to conn. // write encrypted frame, updating the egress MAC while writing to conn.
tee := io.MultiWriter(rw.conn, rw.egressMAC) tee := cipher.StreamWriter{S: rw.enc, W: io.MultiWriter(rw.conn, rw.egressMAC)}
if _, err := tee.Write(ptype); err != nil { if _, err := tee.Write(ptype); err != nil {
return err return err
} }
...@@ -62,7 +83,8 @@ func (rw *rlpxFrameRW) WriteMsg(msg Msg) error { ...@@ -62,7 +83,8 @@ func (rw *rlpxFrameRW) WriteMsg(msg Msg) error {
// write packet-mac. egress MAC is up to date because // write packet-mac. egress MAC is up to date because
// frame content was written to it as well. // 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 return err
} }
...@@ -72,34 +94,40 @@ func (rw *rlpxFrameRW) ReadMsg() (msg Msg, err error) { ...@@ -72,34 +94,40 @@ func (rw *rlpxFrameRW) ReadMsg() (msg Msg, err error) {
if _, err := io.ReadFull(rw.conn, headbuf); err != nil { if _, err := io.ReadFull(rw.conn, headbuf); err != nil {
return msg, err return msg, err
} }
fsize := readInt24(headbuf) // verify header mac
// ignore protocol type for now
shouldMAC := updateHeaderMAC(rw.ingressMAC, rw.macCipher, headbuf[:16]) shouldMAC := updateHeaderMAC(rw.ingressMAC, rw.macCipher, headbuf[:16])
if !hmac.Equal(shouldMAC[:16], headbuf[16:]) { if !hmac.Equal(shouldMAC[:16], headbuf[16:]) {
return msg, errors.New("bad header MAC") 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 // 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 { if _, err := io.ReadFull(rw.conn, framebuf); err != nil {
return msg, err 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. // 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 { if _, err := io.ReadFull(rw.conn, headbuf); err != nil {
return msg, err 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") return msg, errors.New("bad frame MAC")
} }
// decrypt frame content
rw.dec.XORKeyStream(framebuf, framebuf)
// decode message code // decode message code
content := bytes.NewReader(framebuf) content := bytes.NewReader(framebuf[:fsize])
if err := rlp.Decode(content, &msg.Code); err != nil { if err := rlp.Decode(content, &msg.Code); err != nil {
return msg, err return msg, err
} }
......
...@@ -16,14 +16,18 @@ import ( ...@@ -16,14 +16,18 @@ import (
func TestRlpxFrameFake(t *testing.T) { func TestRlpxFrameFake(t *testing.T) {
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
secret := crypto.Sha3()
hash := fakeHash([]byte{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}) hash := fakeHash([]byte{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1})
rw := newRlpxFrameRW(buf, secret, hash, hash) rw := newRlpxFrameRW(buf, secrets{
AES: crypto.Sha3(),
MAC: crypto.Sha3(),
IngressMAC: hash,
EgressMAC: hash,
})
golden := unhex(` golden := unhex(`
000006C2808000000000000000000000 00828ddae471818bb0bfa6b551d1cb42
01010101010101010101010101010101 01010101010101010101010101010101
08C40102030400000000000000000000 ba628a4ba590cb43f7848f41c4382885
01010101010101010101010101010101 01010101010101010101010101010101
01010101010101010101010101010101 01010101010101010101010101010101
`) `)
...@@ -75,27 +79,35 @@ func unhex(str string) []byte { ...@@ -75,27 +79,35 @@ func unhex(str string) []byte {
func TestRlpxFrameRW(t *testing.T) { func TestRlpxFrameRW(t *testing.T) {
var ( var (
aesSecret = make([]byte, 16)
macSecret = make([]byte, 16) macSecret = make([]byte, 16)
egressMACinit = make([]byte, 32) egressMACinit = make([]byte, 32)
ingressMACinit = make([]byte, 32) ingressMACinit = make([]byte, 32)
) )
for _, s := range [][]byte{macSecret, egressMACinit, ingressMACinit} { for _, s := range [][]byte{aesSecret, macSecret, egressMACinit, ingressMACinit} {
rand.Read(s) rand.Read(s)
} }
conn := new(bytes.Buffer) conn := new(bytes.Buffer)
em1 := sha3.NewKeccak256() s1 := secrets{
em1.Write(egressMACinit) AES: aesSecret,
im1 := sha3.NewKeccak256() MAC: macSecret,
im1.Write(ingressMACinit) EgressMAC: sha3.NewKeccak256(),
rw1 := newRlpxFrameRW(conn, macSecret, em1, im1) IngressMAC: sha3.NewKeccak256(),
}
em2 := sha3.NewKeccak256() s1.EgressMAC.Write(egressMACinit)
em2.Write(ingressMACinit) s1.IngressMAC.Write(ingressMACinit)
im2 := sha3.NewKeccak256() rw1 := newRlpxFrameRW(conn, s1)
im2.Write(egressMACinit)
rw2 := newRlpxFrameRW(conn, macSecret, em2, im2) s2 := secrets{
AES: aesSecret,
MAC: macSecret,
EgressMAC: sha3.NewKeccak256(),
IngressMAC: sha3.NewKeccak256(),
}
s2.EgressMAC.Write(ingressMACinit)
s2.IngressMAC.Write(egressMACinit)
rw2 := newRlpxFrameRW(conn, s2)
// send some messages // send some messages
for i := 0; i < 10; i++ { for i := 0; i < 10; i++ {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment