From e82d0aa1cf0e9d7d526962649c6e1eb56d8013d2 Mon Sep 17 00:00:00 2001 From: zelig Date: Sun, 18 Jan 2015 07:59:54 +0000 Subject: [PATCH 01/31] initial hook for crypto handshake (void, off by default) --- p2p/peer.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/p2p/peer.go b/p2p/peer.go index 2380a3285b..886b95a80b 100644 --- a/p2p/peer.go +++ b/p2p/peer.go @@ -70,6 +70,7 @@ type Peer struct { // These fields maintain the running protocols. protocols []Protocol runBaseProtocol bool // for testing + cryptoHandshake bool // for testing runlock sync.RWMutex // protects running running map[string]*proto @@ -141,6 +142,20 @@ func (p *Peer) Identity() ClientIdentity { return p.identity } +func (self *Peer) Pubkey() (pubkey []byte) { + self.infolock.Lock() + defer self.infolock.Unlock() + switch { + case self.identity != nil: + pubkey = self.identity.Pubkey() + case self.dialAddr != nil: + pubkey = self.dialAddr.Pubkey + case self.listenAddr != nil: + pubkey = self.listenAddr.Pubkey + } + return +} + // Caps returns the capabilities (supported subprotocols) of the remote peer. func (p *Peer) Caps() []Cap { p.infolock.Lock() @@ -207,6 +222,12 @@ func (p *Peer) loop() (reason DiscReason, err error) { defer close(p.closed) defer p.conn.Close() + if p.cryptoHandshake { + if err := p.handleCryptoHandshake(); err != nil { + return DiscProtocolError, err // no graceful disconnect + } + } + // read loop readMsg := make(chan Msg) readErr := make(chan error) @@ -307,6 +328,11 @@ func (p *Peer) dispatch(msg Msg, protoDone chan struct{}) (wait bool, err error) return wait, nil } +func (p *Peer) handleCryptoHandshake() (err error) { + + return nil +} + func (p *Peer) startBaseProtocol() { p.runlock.Lock() defer p.runlock.Unlock() From a325b457af52999b29d750d8ea73f64fe37d07ca Mon Sep 17 00:00:00 2001 From: zelig Date: Sun, 18 Jan 2015 08:03:39 +0000 Subject: [PATCH 02/31] add privkey to clientIdentity + tests --- p2p/client_identity.go | 15 +++++++++++---- p2p/client_identity_test.go | 11 ++++++++++- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/p2p/client_identity.go b/p2p/client_identity.go index f15fd01bfc..fca2756bd6 100644 --- a/p2p/client_identity.go +++ b/p2p/client_identity.go @@ -7,8 +7,9 @@ import ( // ClientIdentity represents the identity of a peer. type ClientIdentity interface { - String() string // human readable identity - Pubkey() []byte // 512-bit public key + String() string // human readable identity + Pubkey() []byte // 512-bit public key + PrivKey() []byte // 512-bit private key } type SimpleClientIdentity struct { @@ -17,10 +18,11 @@ type SimpleClientIdentity struct { customIdentifier string os string implementation string + privkey []byte pubkey []byte } -func NewSimpleClientIdentity(clientIdentifier string, version string, customIdentifier string, pubkey []byte) *SimpleClientIdentity { +func NewSimpleClientIdentity(clientIdentifier string, version string, customIdentifier string, privkey []byte, pubkey []byte) *SimpleClientIdentity { clientIdentity := &SimpleClientIdentity{ clientIdentifier: clientIdentifier, version: version, @@ -28,6 +30,7 @@ func NewSimpleClientIdentity(clientIdentifier string, version string, customIden os: runtime.GOOS, implementation: runtime.Version(), pubkey: pubkey, + privkey: privkey, } return clientIdentity @@ -50,8 +53,12 @@ func (c *SimpleClientIdentity) String() string { c.implementation) } +func (c *SimpleClientIdentity) Privkey() []byte { + return c.privkey +} + func (c *SimpleClientIdentity) Pubkey() []byte { - return []byte(c.pubkey) + return c.pubkey } func (c *SimpleClientIdentity) SetCustomIdentifier(customIdentifier string) { diff --git a/p2p/client_identity_test.go b/p2p/client_identity_test.go index 7248a7b1a7..61c34fbf11 100644 --- a/p2p/client_identity_test.go +++ b/p2p/client_identity_test.go @@ -1,13 +1,22 @@ package p2p import ( + "bytes" "fmt" "runtime" "testing" ) func TestClientIdentity(t *testing.T) { - clientIdentity := NewSimpleClientIdentity("Ethereum(G)", "0.5.16", "test", []byte("pubkey")) + clientIdentity := NewSimpleClientIdentity("Ethereum(G)", "0.5.16", "test", []byte("privkey"), []byte("pubkey")) + key := clientIdentity.Privkey() + if !bytes.Equal(key, []byte("privkey")) { + t.Errorf("Expected Privkey to be %x, got %x", key, []byte("privkey")) + } + key = clientIdentity.Pubkey() + if !bytes.Equal(key, []byte("pubkey")) { + t.Errorf("Expected Pubkey to be %x, got %x", key, []byte("pubkey")) + } clientString := clientIdentity.String() expected := fmt.Sprintf("Ethereum(G)/v0.5.16/test/%s/%s", runtime.GOOS, runtime.Version()) if clientString != expected { From ac4e71f0512344d31be19f057da0173bb9729a5c Mon Sep 17 00:00:00 2001 From: zelig Date: Sun, 18 Jan 2015 09:44:49 +0000 Subject: [PATCH 03/31] fix protocol to accomodate privkey --- p2p/protocol.go | 4 ++++ p2p/protocol_test.go | 11 ++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/p2p/protocol.go b/p2p/protocol.go index 1d121a8855..62ada929d7 100644 --- a/p2p/protocol.go +++ b/p2p/protocol.go @@ -64,6 +64,10 @@ func (h *handshake) Pubkey() []byte { return h.NodeID } +func (h *handshake) PrivKey() []byte { + return nil +} + // Cap is the structure of a peer capability. type Cap struct { Name string diff --git a/p2p/protocol_test.go b/p2p/protocol_test.go index b1d10ac536..6804a9d40b 100644 --- a/p2p/protocol_test.go +++ b/p2p/protocol_test.go @@ -11,7 +11,7 @@ import ( ) type peerId struct { - pubkey []byte + privKey, pubkey []byte } func (self *peerId) String() string { @@ -27,6 +27,15 @@ func (self *peerId) Pubkey() (pubkey []byte) { return } +func (self *peerId) PrivKey() (privKey []byte) { + privKey = self.privKey + if len(privKey) == 0 { + privKey = crypto.GenerateNewKeyPair().PublicKey + self.privKey = privKey + } + return +} + func newTestPeer() (peer *Peer) { peer = NewPeer(&peerId{}, []Cap{}) peer.pubkeyHook = func(*peerAddr) error { return nil } From b8f0620af0c78a83f6b8b007188b15903a2a55f3 Mon Sep 17 00:00:00 2001 From: zelig Date: Sun, 18 Jan 2015 09:46:08 +0000 Subject: [PATCH 04/31] add crypto auth logic to p2p --- p2p/crypto.go | 174 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 p2p/crypto.go diff --git a/p2p/crypto.go b/p2p/crypto.go new file mode 100644 index 0000000000..9204fa9d0f --- /dev/null +++ b/p2p/crypto.go @@ -0,0 +1,174 @@ +package p2p + +import ( + "bytes" + "crypto/ecdsa" + "crypto/rand" + "fmt" + "io" + + "github.com/ethereum/go-ethereum/crypto" + "github.com/obscuren/ecies" + "github.com/obscuren/secp256k1-go" +) + +var ( + skLen int = 32 // ecies.MaxSharedKeyLength(pubKey) / 2 + sigLen int = 32 // elliptic S256 + pubKeyLen int = 32 // ECDSA + msgLen int = sigLen + 1 + pubKeyLen + skLen // 97 +) + +//, aesSecret, macSecret, egressMac, ingress +type secretRW struct { + aesSecret, macSecret, egressMac, ingressMac []byte +} + +type cryptoId struct { + prvKey *ecdsa.PrivateKey + pubKey *ecdsa.PublicKey + pubKeyR io.ReaderAt +} + +func newCryptoId(id ClientIdentity) (self *cryptoId, err error) { + // will be at server init + var prvKeyDER []byte = id.PrivKey() + if prvKeyDER == nil { + err = fmt.Errorf("no private key for client") + return + } + // initialise ecies private key via importing DER encoded keys (known via our own clientIdentity) + var prvKey = crypto.ToECDSA(prvKeyDER) + if prvKey == nil { + err = fmt.Errorf("invalid private key for client") + return + } + self = &cryptoId{ + prvKey: prvKey, + // initialise public key from the imported private key + pubKey: &prvKey.PublicKey, + // to be created at server init shared between peers and sessions + // for reuse, call wth ReadAt, no reset seek needed + } + self.pubKeyR = bytes.NewReader(id.Pubkey()) + return +} + +// +func (self *cryptoId) setupAuth(remotePubKeyDER, sessionToken []byte) (auth []byte, nonce []byte, sharedKnowledge []byte, err error) { + // session init, common to both parties + var remotePubKey = crypto.ToECDSAPub(remotePubKeyDER) + if remotePubKey == nil { + err = fmt.Errorf("invalid remote public key") + return + } + var sharedSecret []byte + // generate shared key from prv and remote pubkey + sharedSecret, err = ecies.ImportECDSA(self.prvKey).GenerateShared(ecies.ImportECDSAPublic(remotePubKey), skLen, skLen) + if err != nil { + return + } + // check previous session token + if sessionToken == nil { + err = fmt.Errorf("no session token for peer") + return + } + // allocate msgLen long message + var msg []byte = make([]byte, msgLen) + // generate skLen long nonce at the end + nonce = msg[msgLen-skLen:] + if _, err = rand.Read(nonce); err != nil { + return + } + // create known message + // should use + // cipher.xorBytes from crypto/cipher/xor.go for fast xor + sharedKnowledge = Xor(sharedSecret, sessionToken) + var signedMsg = Xor(sharedKnowledge, nonce) + + // generate random keypair to use for signing + var ecdsaRandomPrvKey *ecdsa.PrivateKey + if ecdsaRandomPrvKey, err = crypto.GenerateKey(); err != nil { + return + } + // var ecdsaRandomPubKey *ecdsa.PublicKey + // ecdsaRandomPubKey= &ecdsaRandomPrvKey.PublicKey + + // message known to both parties ecdh-shared-secret^nonce^token + var signature []byte + // signature = sign(ecdhe-random, ecdh-shared-secret^nonce^token) + // uses secp256k1.Sign + if signature, err = crypto.Sign(signedMsg, ecdsaRandomPrvKey); err != nil { + return + } + // msg = signature || 0x80 || pubk || nonce + copy(msg, signature) + msg[sigLen] = 0x80 + self.pubKeyR.ReadAt(msg[sigLen+1:], int64(pubKeyLen)) // gives pubKeyLen, io.EOF (since we dont read onto the nonce) + + // auth = eciesEncrypt(remote-pubk, msg) + if auth, err = crypto.Encrypt(remotePubKey, msg); err != nil { + return + } + return +} + +func (self *cryptoId) verifyAuth(auth, nonce, sharedKnowledge []byte) (sessionToken []byte, rw *secretRW, err error) { + var msg []byte + // they prove that msg is meant for me, + // I prove I possess private key if i can read it + if msg, err = crypto.Decrypt(self.prvKey, auth); err != nil { + return + } + + var remoteNonce []byte = msg[msgLen-skLen:] + // I prove that i possess prv key (to derive shared secret, and read nonce off encrypted msg) and that I posessed the earlier one , our shared history + // they prove they possess their private key to derive the same shared secret, plus the same shared history (previous session token) + var signedMsg = Xor(sharedKnowledge, remoteNonce) + var remoteRandomPubKeyDER []byte + if remoteRandomPubKeyDER, err = secp256k1.RecoverPubkey(signedMsg, msg[:32]); err != nil { + return + } + var remoteRandomPubKey = crypto.ToECDSAPub(remoteRandomPubKeyDER) + if remoteRandomPubKey == nil { + err = fmt.Errorf("invalid remote public key") + return + } + // 3) Now we can trust ecdhe-random-pubk to derive keys + //ecdhe-shared-secret = ecdh.agree(ecdhe-random, remote-ecdhe-random-pubk) + var dhSharedSecret []byte + dhSharedSecret, err = ecies.ImportECDSA(self.prvKey).GenerateShared(ecies.ImportECDSAPublic(remoteRandomPubKey), skLen, skLen) + if err != nil { + return + } + // shared-secret = crypto.Sha3(ecdhe-shared-secret || crypto.Sha3(nonce || initiator-nonce)) + var sharedSecret []byte = crypto.Sha3(append(dhSharedSecret, crypto.Sha3(append(nonce, remoteNonce...))...)) + // token = crypto.Sha3(shared-secret) + sessionToken = crypto.Sha3(sharedSecret) + // aes-secret = crypto.Sha3(ecdhe-shared-secret || shared-secret) + var aesSecret = crypto.Sha3(append(dhSharedSecret, sharedSecret...)) + // # destroy shared-secret + // mac-secret = crypto.Sha3(ecdhe-shared-secret || aes-secret) + var macSecret = crypto.Sha3(append(dhSharedSecret, aesSecret...)) + // # destroy ecdhe-shared-secret + // egress-mac = crypto.Sha3(mac-secret^nonce || auth) + var egressMac = crypto.Sha3(append(Xor(macSecret, nonce), auth...)) + // # destroy nonce + // ingress-mac = crypto.Sha3(mac-secret^initiator-nonce || auth), + var ingressMac = crypto.Sha3(append(Xor(macSecret, remoteNonce), auth...)) + // # destroy remote-nonce + rw = &secretRW{ + aesSecret: aesSecret, + macSecret: macSecret, + egressMac: egressMac, + ingressMac: ingressMac, + } + return +} + +func Xor(one, other []byte) (xor []byte) { + for i := 0; i < len(one); i++ { + xor[i] = one[i] ^ other[i] + } + return +} From e2e0849cacee60e2911409c9cd51ab41ff8d8ac2 Mon Sep 17 00:00:00 2001 From: zelig Date: Sun, 18 Jan 2015 23:53:45 +0000 Subject: [PATCH 05/31] rewrite to comply with latest spec - correct sizes for the blocks : sec signature 65, ecies sklen 16, keylength 32 - added allocation to Xor (should be optimized later) - no pubkey reader needed, just do with copy - restructuring now into INITIATE, RESPOND, COMPLETE -> newSession initialises the encryption/authentication layer - crypto identity can be part of client identity, some initialisation when server created --- p2p/crypto.go | 201 +++++++++++++++++++++++++++++++++++--------------- 1 file changed, 143 insertions(+), 58 deletions(-) diff --git a/p2p/crypto.go b/p2p/crypto.go index 9204fa9d0f..6e3f360d92 100644 --- a/p2p/crypto.go +++ b/p2p/crypto.go @@ -1,11 +1,11 @@ package p2p import ( - "bytes" + // "bytes" "crypto/ecdsa" "crypto/rand" "fmt" - "io" + // "io" "github.com/ethereum/go-ethereum/crypto" "github.com/obscuren/ecies" @@ -13,21 +13,22 @@ import ( ) var ( - skLen int = 32 // ecies.MaxSharedKeyLength(pubKey) / 2 - sigLen int = 32 // elliptic S256 - pubKeyLen int = 32 // ECDSA - msgLen int = sigLen + 1 + pubKeyLen + skLen // 97 + sskLen int = 16 // ecies.MaxSharedKeyLength(pubKey) / 2 + sigLen int = 65 // elliptic S256 + keyLen int = 32 // ECDSA + msgLen int = sigLen + 3*keyLen + 1 // 162 + resLen int = 65 ) -//, aesSecret, macSecret, egressMac, ingress +// aesSecret, macSecret, egressMac, ingress type secretRW struct { aesSecret, macSecret, egressMac, ingressMac []byte } type cryptoId struct { - prvKey *ecdsa.PrivateKey - pubKey *ecdsa.PublicKey - pubKeyR io.ReaderAt + prvKey *ecdsa.PrivateKey + pubKey *ecdsa.PublicKey + pubKeyDER []byte } func newCryptoId(id ClientIdentity) (self *cryptoId, err error) { @@ -50,70 +51,151 @@ func newCryptoId(id ClientIdentity) (self *cryptoId, err error) { // to be created at server init shared between peers and sessions // for reuse, call wth ReadAt, no reset seek needed } - self.pubKeyR = bytes.NewReader(id.Pubkey()) + self.pubKeyDER = id.Pubkey() return } -// -func (self *cryptoId) setupAuth(remotePubKeyDER, sessionToken []byte) (auth []byte, nonce []byte, sharedKnowledge []byte, err error) { +// initAuth is called by peer if it initiated the connection +func (self *cryptoId) initAuth(remotePubKeyDER, sessionToken []byte) (auth []byte, initNonce []byte, remotePubKey *ecdsa.PublicKey, err error) { // session init, common to both parties - var remotePubKey = crypto.ToECDSAPub(remotePubKeyDER) + remotePubKey = crypto.ToECDSAPub(remotePubKeyDER) if remotePubKey == nil { err = fmt.Errorf("invalid remote public key") return } - var sharedSecret []byte - // generate shared key from prv and remote pubkey - sharedSecret, err = ecies.ImportECDSA(self.prvKey).GenerateShared(ecies.ImportECDSAPublic(remotePubKey), skLen, skLen) - if err != nil { - return - } - // check previous session token + + var tokenFlag byte if sessionToken == nil { - err = fmt.Errorf("no session token for peer") - return + // 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 + if sessionToken, err = ecies.ImportECDSA(self.prvKey).GenerateShared(ecies.ImportECDSAPublic(remotePubKey), sskLen, sskLen); err != nil { + return + } + fmt.Printf("secret generated: %v %x", len(sessionToken), sessionToken) + // tokenFlag = 0x00 // redundant + } else { + // for known peers, we use stored token from the previous session + tokenFlag = 0x01 } - // allocate msgLen long message + + //E(remote-pubk, S(ecdhe-random, ecdh-shared-secret^nonce) || H(ecdhe-random-pubk) || pubk || nonce || 0x0) + // E(remote-pubk, S(ecdhe-random, token^nonce) || H(ecdhe-random-pubk) || pubk || nonce || 0x1) + // allocate msgLen long message, var msg []byte = make([]byte, msgLen) - // generate skLen long nonce at the end - nonce = msg[msgLen-skLen:] - if _, err = rand.Read(nonce); err != nil { + // generate sskLen long nonce + initNonce = msg[msgLen-keyLen-1 : msgLen-1] + // nonce = msg[msgLen-sskLen-1 : msgLen-1] + if _, err = rand.Read(initNonce); err != nil { return } // create known message - // should use - // cipher.xorBytes from crypto/cipher/xor.go for fast xor - sharedKnowledge = Xor(sharedSecret, sessionToken) - var signedMsg = Xor(sharedKnowledge, nonce) + // ecdh-shared-secret^nonce for new peers + // token^nonce for old peers + var sharedSecret = Xor(sessionToken, initNonce) // generate random keypair to use for signing var ecdsaRandomPrvKey *ecdsa.PrivateKey if ecdsaRandomPrvKey, err = crypto.GenerateKey(); err != nil { return } + // sign shared secret (message known to both parties): shared-secret + var signature []byte + // signature = sign(ecdhe-random, shared-secret) + // uses secp256k1.Sign + if signature, err = crypto.Sign(sharedSecret, ecdsaRandomPrvKey); err != nil { + return + } + fmt.Printf("signature generated: %v %x", len(signature), signature) + + // message + // signed-shared-secret || H(ecdhe-random-pubk) || pubk || nonce || 0x0 + copy(msg, signature) // copy signed-shared-secret + // H(ecdhe-random-pubk) + copy(msg[sigLen:sigLen+keyLen], crypto.Sha3(crypto.FromECDSAPub(&ecdsaRandomPrvKey.PublicKey))) + // pubkey copied to the correct segment. + copy(msg[sigLen+keyLen:sigLen+2*keyLen], self.pubKeyDER) + // nonce is already in the slice + // stick tokenFlag byte to the end + msg[msgLen-1] = tokenFlag + + fmt.Printf("plaintext message generated: %v %x", len(msg), msg) + + // encrypt using remote-pubk + // auth = eciesEncrypt(remote-pubk, msg) + + if auth, err = crypto.Encrypt(remotePubKey, msg); err != nil { + return + } + fmt.Printf("encrypted message generated: %v %x\n used pubkey: %x\n", len(auth), auth, crypto.FromECDSAPub(remotePubKey)) + + return +} + +// verifyAuth is called by peer if it accepted (but not initiated) the connection +func (self *cryptoId) verifyAuth(auth, sharedSecret []byte, remotePubKey *ecdsa.PublicKey) (authResp []byte, respNonce []byte, initNonce []byte, remoteRandomPubKey *ecdsa.PublicKey, err error) { + var msg []byte + fmt.Printf("encrypted message received: %v %x\n used pubkey: %x\n", len(auth), auth, crypto.FromECDSAPub(self.pubKey)) + // they prove that msg is meant for me, + // I prove I possess private key if i can read it + if msg, err = crypto.Decrypt(self.prvKey, auth); err != nil { + return + } + + // var remoteNonce []byte = msg[msgLen-skLen-1 : msgLen-1] + initNonce = msg[msgLen-keyLen-1 : msgLen-1] + // I prove that i own prv key (to derive shared secret, and read nonce off encrypted msg) and that I own shared secret + // they prove they own the private key belonging to ecdhe-random-pubk + var signedMsg = Xor(sharedSecret, initNonce) + var remoteRandomPubKeyDER []byte + if remoteRandomPubKeyDER, err = secp256k1.RecoverPubkey(signedMsg, msg[:sigLen]); err != nil { + return + } + remoteRandomPubKey = crypto.ToECDSAPub(remoteRandomPubKeyDER) + if remoteRandomPubKey == nil { + err = fmt.Errorf("invalid remote public key") + return + } + + var resp = make([]byte, 2*keyLen+1) + // generate sskLen long nonce + respNonce = msg[msgLen-keyLen-1 : msgLen-1] + if _, err = rand.Read(respNonce); err != nil { + return + } + // generate random keypair + var ecdsaRandomPrvKey *ecdsa.PrivateKey + if ecdsaRandomPrvKey, err = crypto.GenerateKey(); err != nil { + return + } // var ecdsaRandomPubKey *ecdsa.PublicKey // ecdsaRandomPubKey= &ecdsaRandomPrvKey.PublicKey - // message known to both parties ecdh-shared-secret^nonce^token - var signature []byte - // signature = sign(ecdhe-random, ecdh-shared-secret^nonce^token) - // uses secp256k1.Sign - if signature, err = crypto.Sign(signedMsg, ecdsaRandomPrvKey); err != nil { - return + // message + // E(remote-pubk, ecdhe-random-pubk || nonce || 0x0) + copy(resp[:keyLen], crypto.FromECDSAPub(&ecdsaRandomPrvKey.PublicKey)) + // pubkey copied to the correct segment. + copy(resp[keyLen:2*keyLen], self.pubKeyDER) + // nonce is already in the slice + // stick tokenFlag byte to the end + var tokenFlag byte + if sharedSecret == nil { + } else { + // for known peers, we use stored token from the previous session + tokenFlag = 0x01 } - // msg = signature || 0x80 || pubk || nonce - copy(msg, signature) - msg[sigLen] = 0x80 - self.pubKeyR.ReadAt(msg[sigLen+1:], int64(pubKeyLen)) // gives pubKeyLen, io.EOF (since we dont read onto the nonce) + resp[resLen] = tokenFlag + // encrypt using remote-pubk // auth = eciesEncrypt(remote-pubk, msg) - if auth, err = crypto.Encrypt(remotePubKey, msg); err != nil { + // why not encrypt with ecdhe-random-remote + if authResp, err = crypto.Encrypt(remotePubKey, resp); err != nil { return } return } -func (self *cryptoId) verifyAuth(auth, nonce, sharedKnowledge []byte) (sessionToken []byte, rw *secretRW, err error) { +func (self *cryptoId) verifyAuthResp(auth []byte) (respNonce []byte, remoteRandomPubKey *ecdsa.PublicKey, tokenFlag bool, err error) { var msg []byte // they prove that msg is meant for me, // I prove I possess private key if i can read it @@ -121,28 +203,29 @@ func (self *cryptoId) verifyAuth(auth, nonce, sharedKnowledge []byte) (sessionTo return } - var remoteNonce []byte = msg[msgLen-skLen:] - // I prove that i possess prv key (to derive shared secret, and read nonce off encrypted msg) and that I posessed the earlier one , our shared history - // they prove they possess their private key to derive the same shared secret, plus the same shared history (previous session token) - var signedMsg = Xor(sharedKnowledge, remoteNonce) - var remoteRandomPubKeyDER []byte - if remoteRandomPubKeyDER, err = secp256k1.RecoverPubkey(signedMsg, msg[:32]); err != nil { - return - } - var remoteRandomPubKey = crypto.ToECDSAPub(remoteRandomPubKeyDER) + respNonce = msg[resLen-keyLen-1 : resLen-1] + var remoteRandomPubKeyDER = msg[:keyLen] + remoteRandomPubKey = crypto.ToECDSAPub(remoteRandomPubKeyDER) if remoteRandomPubKey == nil { - err = fmt.Errorf("invalid remote public key") + err = fmt.Errorf("invalid ecdh random remote public key") return } - // 3) Now we can trust ecdhe-random-pubk to derive keys + if msg[resLen-1] == 0x01 { + tokenFlag = true + } + return +} + +func (self *cryptoId) newSession(initNonce, respNonce, auth []byte, remoteRandomPubKey *ecdsa.PublicKey) (sessionToken []byte, rw *secretRW, err error) { + // 3) Now we can trust ecdhe-random-pubk to derive new keys //ecdhe-shared-secret = ecdh.agree(ecdhe-random, remote-ecdhe-random-pubk) var dhSharedSecret []byte - dhSharedSecret, err = ecies.ImportECDSA(self.prvKey).GenerateShared(ecies.ImportECDSAPublic(remoteRandomPubKey), skLen, skLen) + dhSharedSecret, err = ecies.ImportECDSA(self.prvKey).GenerateShared(ecies.ImportECDSAPublic(remoteRandomPubKey), sskLen, sskLen) if err != nil { return } // shared-secret = crypto.Sha3(ecdhe-shared-secret || crypto.Sha3(nonce || initiator-nonce)) - var sharedSecret []byte = crypto.Sha3(append(dhSharedSecret, crypto.Sha3(append(nonce, remoteNonce...))...)) + var sharedSecret = crypto.Sha3(append(dhSharedSecret, crypto.Sha3(append(respNonce, initNonce...))...)) // token = crypto.Sha3(shared-secret) sessionToken = crypto.Sha3(sharedSecret) // aes-secret = crypto.Sha3(ecdhe-shared-secret || shared-secret) @@ -152,10 +235,10 @@ func (self *cryptoId) verifyAuth(auth, nonce, sharedKnowledge []byte) (sessionTo var macSecret = crypto.Sha3(append(dhSharedSecret, aesSecret...)) // # destroy ecdhe-shared-secret // egress-mac = crypto.Sha3(mac-secret^nonce || auth) - var egressMac = crypto.Sha3(append(Xor(macSecret, nonce), auth...)) + var egressMac = crypto.Sha3(append(Xor(macSecret, respNonce), auth...)) // # destroy nonce // ingress-mac = crypto.Sha3(mac-secret^initiator-nonce || auth), - var ingressMac = crypto.Sha3(append(Xor(macSecret, remoteNonce), auth...)) + var ingressMac = crypto.Sha3(append(Xor(macSecret, initNonce), auth...)) // # destroy remote-nonce rw = &secretRW{ aesSecret: aesSecret, @@ -166,7 +249,9 @@ func (self *cryptoId) verifyAuth(auth, nonce, sharedKnowledge []byte) (sessionTo return } +// should use cipher.xorBytes from crypto/cipher/xor.go for fast xor func Xor(one, other []byte) (xor []byte) { + xor = make([]byte, len(one)) for i := 0; i < len(one); i++ { xor[i] = one[i] ^ other[i] } From 248ab7185a05bceaadc3343f16cc052a320d359e Mon Sep 17 00:00:00 2001 From: zelig Date: Mon, 19 Jan 2015 00:55:24 +0000 Subject: [PATCH 06/31] fix crash - add session token check and fallback to shared secret in responder call too - use explicit length for the types of new messages - fix typo resp[resLen-1] = tokenFlag --- p2p/crypto.go | 51 +++++++++++++++++++++++++++++---------------------- 1 file changed, 29 insertions(+), 22 deletions(-) diff --git a/p2p/crypto.go b/p2p/crypto.go index 6e3f360d92..643bd431e4 100644 --- a/p2p/crypto.go +++ b/p2p/crypto.go @@ -17,7 +17,7 @@ var ( sigLen int = 65 // elliptic S256 keyLen int = 32 // ECDSA msgLen int = sigLen + 3*keyLen + 1 // 162 - resLen int = 65 + resLen int = 65 // ) // aesSecret, macSecret, egressMac, ingress @@ -133,7 +133,7 @@ func (self *cryptoId) initAuth(remotePubKeyDER, sessionToken []byte) (auth []byt } // verifyAuth is called by peer if it accepted (but not initiated) the connection -func (self *cryptoId) verifyAuth(auth, sharedSecret []byte, remotePubKey *ecdsa.PublicKey) (authResp []byte, respNonce []byte, initNonce []byte, remoteRandomPubKey *ecdsa.PublicKey, err error) { +func (self *cryptoId) verifyAuth(auth, sessionToken []byte, remotePubKey *ecdsa.PublicKey) (authResp []byte, respNonce []byte, initNonce []byte, remoteRandomPubKey *ecdsa.PublicKey, err error) { var msg []byte fmt.Printf("encrypted message received: %v %x\n used pubkey: %x\n", len(auth), auth, crypto.FromECDSAPub(self.pubKey)) // they prove that msg is meant for me, @@ -141,50 +141,57 @@ func (self *cryptoId) verifyAuth(auth, sharedSecret []byte, remotePubKey *ecdsa. if msg, err = crypto.Decrypt(self.prvKey, auth); err != nil { return } + fmt.Printf("\nplaintext message retrieved: %v %x\n", len(msg), msg) - // var remoteNonce []byte = msg[msgLen-skLen-1 : msgLen-1] + var tokenFlag byte + if sessionToken == 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 + if sessionToken, err = ecies.ImportECDSA(self.prvKey).GenerateShared(ecies.ImportECDSAPublic(remotePubKey), sskLen, sskLen); err != nil { + return + } + fmt.Printf("secret generated: %v %x", len(sessionToken), sessionToken) + // tokenFlag = 0x00 // redundant + } else { + // for known peers, we use stored token from the previous session + tokenFlag = 0x01 + } + + // the initiator nonce is read off the end of the message initNonce = msg[msgLen-keyLen-1 : msgLen-1] // I prove that i own prv key (to derive shared secret, and read nonce off encrypted msg) and that I own shared secret // they prove they own the private key belonging to ecdhe-random-pubk - var signedMsg = Xor(sharedSecret, initNonce) + // we can now reconstruct the signed message and recover the peers pubkey + var signedMsg = Xor(sessionToken, initNonce) var remoteRandomPubKeyDER []byte if remoteRandomPubKeyDER, err = secp256k1.RecoverPubkey(signedMsg, msg[:sigLen]); err != nil { return } + // convert to ECDSA standard remoteRandomPubKey = crypto.ToECDSAPub(remoteRandomPubKeyDER) if remoteRandomPubKey == nil { err = fmt.Errorf("invalid remote public key") return } - var resp = make([]byte, 2*keyLen+1) - // generate sskLen long nonce - respNonce = msg[msgLen-keyLen-1 : msgLen-1] + // now we find ourselves a long task too, fill it random + var resp = make([]byte, resLen) + // generate keyLen long nonce + respNonce = msg[resLen-keyLen-1 : msgLen-1] if _, err = rand.Read(respNonce); err != nil { return } - // generate random keypair + // generate random keypair for session var ecdsaRandomPrvKey *ecdsa.PrivateKey if ecdsaRandomPrvKey, err = crypto.GenerateKey(); err != nil { return } - // var ecdsaRandomPubKey *ecdsa.PublicKey - // ecdsaRandomPubKey= &ecdsaRandomPrvKey.PublicKey - - // message + // responder auth message // E(remote-pubk, ecdhe-random-pubk || nonce || 0x0) copy(resp[:keyLen], crypto.FromECDSAPub(&ecdsaRandomPrvKey.PublicKey)) - // pubkey copied to the correct segment. - copy(resp[keyLen:2*keyLen], self.pubKeyDER) // nonce is already in the slice - // stick tokenFlag byte to the end - var tokenFlag byte - if sharedSecret == nil { - } else { - // for known peers, we use stored token from the previous session - tokenFlag = 0x01 - } - resp[resLen] = tokenFlag + resp[resLen-1] = tokenFlag // encrypt using remote-pubk // auth = eciesEncrypt(remote-pubk, msg) From 605ee4d2e8194e0471ed6d1cf400789c116ceae4 Mon Sep 17 00:00:00 2001 From: zelig Date: Mon, 19 Jan 2015 01:24:09 +0000 Subject: [PATCH 07/31] handshake test to crypto --- p2p/crypto.go | 2 -- p2p/crypto_test.go | 54 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 p2p/crypto_test.go diff --git a/p2p/crypto.go b/p2p/crypto.go index 643bd431e4..10c82d3a11 100644 --- a/p2p/crypto.go +++ b/p2p/crypto.go @@ -1,11 +1,9 @@ package p2p import ( - // "bytes" "crypto/ecdsa" "crypto/rand" "fmt" - // "io" "github.com/ethereum/go-ethereum/crypto" "github.com/obscuren/ecies" diff --git a/p2p/crypto_test.go b/p2p/crypto_test.go new file mode 100644 index 0000000000..6b4afb16a4 --- /dev/null +++ b/p2p/crypto_test.go @@ -0,0 +1,54 @@ +package p2p + +import ( + // "bytes" + "fmt" + "testing" + + "github.com/ethereum/go-ethereum/crypto" +) + +func TestCryptoHandshake(t *testing.T) { + var err error + var sessionToken []byte + prvInit, _ := crypto.GenerateKey() + pubInit := &prvInit.PublicKey + prvResp, _ := crypto.GenerateKey() + pubResp := &prvResp.PublicKey + + var initiator, responder *cryptoId + if initiator, err = newCryptoId(&peerId{crypto.FromECDSA(prvInit), crypto.FromECDSAPub(pubInit)}); err != nil { + return + } + if responder, err = newCryptoId(&peerId{crypto.FromECDSA(prvResp), crypto.FromECDSAPub(pubResp)}); err != nil { + return + } + + auth, initNonce, _, _ := initiator.initAuth(responder.pubKeyDER, sessionToken) + + response, remoteRespNonce, remoteInitNonce, remoteRandomPubKey, _ := responder.verifyAuth(auth, sessionToken, pubInit) + + respNonce, randomPubKey, _, _ := initiator.verifyAuthResp(response) + + fmt.Printf("%x\n%x\n%x\n%x\n%x\n%x\n%x\n%x\n", auth, initNonce, response, remoteRespNonce, remoteInitNonce, remoteRandomPubKey, respNonce, randomPubKey) + // initSessionToken, initSecretRW, _ := initiator.newSession(initNonce, respNonce, auth, randomPubKey) + // respSessionToken, respSecretRW, _ := responder.newSession(remoteInitNonce, remoteRespNonce, auth, remoteRandomPubKey) + + // if !bytes.Equal(initSessionToken, respSessionToken) { + // t.Errorf("session tokens do not match") + // } + // // aesSecret, macSecret, egressMac, ingressMac + // if !bytes.Equal(initSecretRW.aesSecret, respSecretRW.aesSecret) { + // t.Errorf("AES secrets do not match") + // } + // if !bytes.Equal(initSecretRW.macSecret, respSecretRW.macSecret) { + // t.Errorf("macSecrets do not match") + // } + // if !bytes.Equal(initSecretRW.egressMac, respSecretRW.egressMac) { + // t.Errorf("egressMacs do not match") + // } + // if !bytes.Equal(initSecretRW.ingressMac, respSecretRW.ingressMac) { + // t.Errorf("ingressMacs do not match") + // } + +} From a314bbffaba05862ed7f6e60a9010ab75481b9b0 Mon Sep 17 00:00:00 2001 From: zelig Date: Mon, 19 Jan 2015 01:24:28 +0000 Subject: [PATCH 08/31] handshake test to crypto --- p2p/crypto_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/p2p/crypto_test.go b/p2p/crypto_test.go index 6b4afb16a4..1785b5c458 100644 --- a/p2p/crypto_test.go +++ b/p2p/crypto_test.go @@ -31,7 +31,7 @@ func TestCryptoHandshake(t *testing.T) { respNonce, randomPubKey, _, _ := initiator.verifyAuthResp(response) fmt.Printf("%x\n%x\n%x\n%x\n%x\n%x\n%x\n%x\n", auth, initNonce, response, remoteRespNonce, remoteInitNonce, remoteRandomPubKey, respNonce, randomPubKey) - // initSessionToken, initSecretRW, _ := initiator.newSession(initNonce, respNonce, auth, randomPubKey) + initSessionToken, initSecretRW, _ := initiator.newSession(initNonce, respNonce, auth, randomPubKey) // respSessionToken, respSecretRW, _ := responder.newSession(remoteInitNonce, remoteRespNonce, auth, remoteRandomPubKey) // if !bytes.Equal(initSessionToken, respSessionToken) { From b5734f3cce8779edbb410578868b4cd44b333d4e Mon Sep 17 00:00:00 2001 From: zelig Date: Mon, 19 Jan 2015 04:53:48 +0000 Subject: [PATCH 09/31] completed the test. FAIL now. it crashes at diffie-hellman. ECIES -> secp256k1-go panics --- p2p/crypto.go | 45 ++++++++++++++++++++++++++++---------------- p2p/crypto_test.go | 47 +++++++++++++++++++++++----------------------- 2 files changed, 53 insertions(+), 39 deletions(-) diff --git a/p2p/crypto.go b/p2p/crypto.go index 10c82d3a11..37c6e1fc98 100644 --- a/p2p/crypto.go +++ b/p2p/crypto.go @@ -53,10 +53,24 @@ func newCryptoId(id ClientIdentity) (self *cryptoId, err error) { return } -// initAuth is called by peer if it initiated the connection -func (self *cryptoId) initAuth(remotePubKeyDER, sessionToken []byte) (auth []byte, initNonce []byte, remotePubKey *ecdsa.PublicKey, err error) { +/* startHandshake is called by peer if it initiated the connection. + By protocol spec, the party who initiates the connection (initiator) will send an 'auth' packet +New: authInitiator -> E(remote-pubk, S(ecdhe-random, ecdh-shared-secret^nonce) || H(ecdhe-random-pubk) || pubk || nonce || 0x0) + authRecipient -> E(remote-pubk, ecdhe-random-pubk || nonce || 0x0) + +Known: authInitiator = E(remote-pubk, S(ecdhe-random, token^nonce) || H(ecdhe-random-pubk) || pubk || nonce || 0x1) + authRecipient = E(remote-pubk, ecdhe-random-pubk || nonce || 0x1) // token found + authRecipient = E(remote-pubk, ecdhe-random-pubk || nonce || 0x0) // token not found + +The caller provides the public key of the peer as conjuctured from lookup based on IP:port, given as user input or proven by signatures. The caller must have access to persistant information about the peers, and pass the previous session token as an argument to cryptoId. + +The handshake is the process by which the peers establish their connection for a session. + +*/ + +func (self *cryptoId) startHandshake(remotePubKeyDER, sessionToken []byte) (auth []byte, initNonce []byte, randomPrvKey *ecdsa.PrivateKey, randomPubKey *ecdsa.PublicKey, err error) { // session init, common to both parties - remotePubKey = crypto.ToECDSAPub(remotePubKeyDER) + remotePubKey := crypto.ToECDSAPub(remotePubKeyDER) if remotePubKey == nil { err = fmt.Errorf("invalid remote public key") return @@ -70,6 +84,7 @@ func (self *cryptoId) initAuth(remotePubKeyDER, sessionToken []byte) (auth []byt if sessionToken, err = ecies.ImportECDSA(self.prvKey).GenerateShared(ecies.ImportECDSAPublic(remotePubKey), sskLen, sskLen); err != nil { return } + // this will not stay here ;) fmt.Printf("secret generated: %v %x", len(sessionToken), sessionToken) // tokenFlag = 0x00 // redundant } else { @@ -93,15 +108,14 @@ func (self *cryptoId) initAuth(remotePubKeyDER, sessionToken []byte) (auth []byt var sharedSecret = Xor(sessionToken, initNonce) // generate random keypair to use for signing - var ecdsaRandomPrvKey *ecdsa.PrivateKey - if ecdsaRandomPrvKey, err = crypto.GenerateKey(); err != nil { + if randomPrvKey, err = crypto.GenerateKey(); err != nil { return } // sign shared secret (message known to both parties): shared-secret var signature []byte // signature = sign(ecdhe-random, shared-secret) // uses secp256k1.Sign - if signature, err = crypto.Sign(sharedSecret, ecdsaRandomPrvKey); err != nil { + if signature, err = crypto.Sign(sharedSecret, randomPrvKey); err != nil { return } fmt.Printf("signature generated: %v %x", len(signature), signature) @@ -110,7 +124,7 @@ func (self *cryptoId) initAuth(remotePubKeyDER, sessionToken []byte) (auth []byt // signed-shared-secret || H(ecdhe-random-pubk) || pubk || nonce || 0x0 copy(msg, signature) // copy signed-shared-secret // H(ecdhe-random-pubk) - copy(msg[sigLen:sigLen+keyLen], crypto.Sha3(crypto.FromECDSAPub(&ecdsaRandomPrvKey.PublicKey))) + copy(msg[sigLen:sigLen+keyLen], crypto.Sha3(crypto.FromECDSAPub(&randomPrvKey.PublicKey))) // pubkey copied to the correct segment. copy(msg[sigLen+keyLen:sigLen+2*keyLen], self.pubKeyDER) // nonce is already in the slice @@ -131,7 +145,7 @@ func (self *cryptoId) initAuth(remotePubKeyDER, sessionToken []byte) (auth []byt } // verifyAuth is called by peer if it accepted (but not initiated) the connection -func (self *cryptoId) verifyAuth(auth, sessionToken []byte, remotePubKey *ecdsa.PublicKey) (authResp []byte, respNonce []byte, initNonce []byte, remoteRandomPubKey *ecdsa.PublicKey, err error) { +func (self *cryptoId) respondToHandshake(auth, sessionToken []byte, remotePubKey *ecdsa.PublicKey) (authResp []byte, respNonce []byte, initNonce []byte, randomPrvKey *ecdsa.PrivateKey, err error) { var msg []byte fmt.Printf("encrypted message received: %v %x\n used pubkey: %x\n", len(auth), auth, crypto.FromECDSAPub(self.pubKey)) // they prove that msg is meant for me, @@ -167,7 +181,7 @@ func (self *cryptoId) verifyAuth(auth, sessionToken []byte, remotePubKey *ecdsa. return } // convert to ECDSA standard - remoteRandomPubKey = crypto.ToECDSAPub(remoteRandomPubKeyDER) + remoteRandomPubKey := crypto.ToECDSAPub(remoteRandomPubKeyDER) if remoteRandomPubKey == nil { err = fmt.Errorf("invalid remote public key") return @@ -181,13 +195,12 @@ func (self *cryptoId) verifyAuth(auth, sessionToken []byte, remotePubKey *ecdsa. return } // generate random keypair for session - var ecdsaRandomPrvKey *ecdsa.PrivateKey - if ecdsaRandomPrvKey, err = crypto.GenerateKey(); err != nil { + if randomPrvKey, err = crypto.GenerateKey(); err != nil { return } // responder auth message // E(remote-pubk, ecdhe-random-pubk || nonce || 0x0) - copy(resp[:keyLen], crypto.FromECDSAPub(&ecdsaRandomPrvKey.PublicKey)) + copy(resp[:keyLen], crypto.FromECDSAPub(&randomPrvKey.PublicKey)) // nonce is already in the slice resp[resLen-1] = tokenFlag @@ -200,7 +213,7 @@ func (self *cryptoId) verifyAuth(auth, sessionToken []byte, remotePubKey *ecdsa. return } -func (self *cryptoId) verifyAuthResp(auth []byte) (respNonce []byte, remoteRandomPubKey *ecdsa.PublicKey, tokenFlag bool, err error) { +func (self *cryptoId) completeHandshake(auth []byte) (respNonce []byte, remoteRandomPubKey *ecdsa.PublicKey, tokenFlag bool, err error) { var msg []byte // they prove that msg is meant for me, // I prove I possess private key if i can read it @@ -221,12 +234,12 @@ func (self *cryptoId) verifyAuthResp(auth []byte) (respNonce []byte, remoteRando return } -func (self *cryptoId) newSession(initNonce, respNonce, auth []byte, remoteRandomPubKey *ecdsa.PublicKey) (sessionToken []byte, rw *secretRW, err error) { +func (self *cryptoId) newSession(initNonce, respNonce, auth []byte, privKey *ecdsa.PrivateKey, remoteRandomPubKey *ecdsa.PublicKey) (sessionToken []byte, rw *secretRW, err error) { // 3) Now we can trust ecdhe-random-pubk to derive new keys //ecdhe-shared-secret = ecdh.agree(ecdhe-random, remote-ecdhe-random-pubk) var dhSharedSecret []byte - dhSharedSecret, err = ecies.ImportECDSA(self.prvKey).GenerateShared(ecies.ImportECDSAPublic(remoteRandomPubKey), sskLen, sskLen) - if err != nil { + pubKey := ecies.ImportECDSAPublic(remoteRandomPubKey) + if dhSharedSecret, err = ecies.ImportECDSA(privKey).GenerateShared(pubKey, sskLen, sskLen); err != nil { return } // shared-secret = crypto.Sha3(ecdhe-shared-secret || crypto.Sha3(nonce || initiator-nonce)) diff --git a/p2p/crypto_test.go b/p2p/crypto_test.go index 1785b5c458..cfb2d19d1a 100644 --- a/p2p/crypto_test.go +++ b/p2p/crypto_test.go @@ -1,7 +1,7 @@ package p2p import ( - // "bytes" + "bytes" "fmt" "testing" @@ -24,31 +24,32 @@ func TestCryptoHandshake(t *testing.T) { return } - auth, initNonce, _, _ := initiator.initAuth(responder.pubKeyDER, sessionToken) + auth, initNonce, randomPrvKey, randomPubKey, _ := initiator.initAuth(responder.pubKeyDER, sessionToken) - response, remoteRespNonce, remoteInitNonce, remoteRandomPubKey, _ := responder.verifyAuth(auth, sessionToken, pubInit) + response, remoteRespNonce, remoteInitNonce, remoteRandomPrivKey, _ := responder.verifyAuth(auth, sessionToken, pubInit) - respNonce, randomPubKey, _, _ := initiator.verifyAuthResp(response) + respNonce, remoteRandomPubKey, _, _ := initiator.verifyAuthResp(response) - fmt.Printf("%x\n%x\n%x\n%x\n%x\n%x\n%x\n%x\n", auth, initNonce, response, remoteRespNonce, remoteInitNonce, remoteRandomPubKey, respNonce, randomPubKey) - initSessionToken, initSecretRW, _ := initiator.newSession(initNonce, respNonce, auth, randomPubKey) - // respSessionToken, respSecretRW, _ := responder.newSession(remoteInitNonce, remoteRespNonce, auth, remoteRandomPubKey) + initSessionToken, initSecretRW, _ := initiator.newSession(initNonce, respNonce, auth, randomPrvKey, remoteRandomPubKey) + respSessionToken, respSecretRW, _ := responder.newSession(remoteInitNonce, remoteRespNonce, auth, remoteRandomPrivKey, randomPubKey) - // if !bytes.Equal(initSessionToken, respSessionToken) { - // t.Errorf("session tokens do not match") - // } - // // aesSecret, macSecret, egressMac, ingressMac - // if !bytes.Equal(initSecretRW.aesSecret, respSecretRW.aesSecret) { - // t.Errorf("AES secrets do not match") - // } - // if !bytes.Equal(initSecretRW.macSecret, respSecretRW.macSecret) { - // t.Errorf("macSecrets do not match") - // } - // if !bytes.Equal(initSecretRW.egressMac, respSecretRW.egressMac) { - // t.Errorf("egressMacs do not match") - // } - // if !bytes.Equal(initSecretRW.ingressMac, respSecretRW.ingressMac) { - // t.Errorf("ingressMacs do not match") - // } + fmt.Printf("%x\n%x\n%x\n%x\n%x\n%x\n%x\n%x\n%x\n%x\n", auth, initNonce, response, remoteRespNonce, remoteInitNonce, remoteRandomPubKey, respNonce, randomPubKey, initSessionToken, initSecretRW) + + if !bytes.Equal(initSessionToken, respSessionToken) { + t.Errorf("session tokens do not match") + } + // aesSecret, macSecret, egressMac, ingressMac + if !bytes.Equal(initSecretRW.aesSecret, respSecretRW.aesSecret) { + t.Errorf("AES secrets do not match") + } + if !bytes.Equal(initSecretRW.macSecret, respSecretRW.macSecret) { + t.Errorf("macSecrets do not match") + } + if !bytes.Equal(initSecretRW.egressMac, respSecretRW.egressMac) { + t.Errorf("egressMacs do not match") + } + if !bytes.Equal(initSecretRW.ingressMac, respSecretRW.ingressMac) { + t.Errorf("ingressMacs do not match") + } } From 44779d854a97ec58d2c68491eff7b64ffbfd9abb Mon Sep 17 00:00:00 2001 From: zelig Date: Mon, 19 Jan 2015 11:21:13 +0000 Subject: [PATCH 10/31] integrate cryptoId into peer and connection lifecycle --- p2p/crypto.go | 15 +++++++++++++++ p2p/peer.go | 21 ++++++++++++++++++--- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/p2p/crypto.go b/p2p/crypto.go index 37c6e1fc98..728b8e884e 100644 --- a/p2p/crypto.go +++ b/p2p/crypto.go @@ -53,6 +53,21 @@ func newCryptoId(id ClientIdentity) (self *cryptoId, err error) { return } +func (self *cryptoId) Run(remotePubKeyDER []byte) (rw *secretRW) { + if self.initiator { + auth, initNonce, randomPrvKey, randomPubKey, err := initiator.initAuth(remotePubKeyDER, sessionToken) + + respNonce, remoteRandomPubKey, _, _ := initiator.verifyAuthResp(response) + } else { + // we are listening connection. we are responders in the haandshake. + // Extract info from the authentication. The initiator starts by sending us a handshake that we need to respond to. + response, remoteRespNonce, remoteInitNonce, remoteRandomPrivKey, _ := responder.verifyAuth(auth, sessionToken, pubInit) + + } + initSessionToken, initSecretRW, _ := initiator.newSession(initNonce, respNonce, auth, randomPrvKey, remoteRandomPubKey) + respSessionToken, respSecretRW, _ := responder.newSession(remoteInitNonce, remoteRespNonce, auth, remoteRandomPrivKey, randomPubKey) +} + /* startHandshake is called by peer if it initiated the connection. By protocol spec, the party who initiates the connection (initiator) will send an 'auth' packet New: authInitiator -> E(remote-pubk, S(ecdhe-random, ecdh-shared-secret^nonce) || H(ecdhe-random-pubk) || pubk || nonce || 0x0) diff --git a/p2p/peer.go b/p2p/peer.go index 886b95a80b..e98c3d5608 100644 --- a/p2p/peer.go +++ b/p2p/peer.go @@ -222,10 +222,14 @@ func (p *Peer) loop() (reason DiscReason, err error) { defer close(p.closed) defer p.conn.Close() + var readLoop func(chan Msg, chan error, chan bool) if p.cryptoHandshake { - if err := p.handleCryptoHandshake(); err != nil { + if readLoop, err := p.handleCryptoHandshake(); err != nil { + // from here on everything can be encrypted, authenticated return DiscProtocolError, err // no graceful disconnect } + } else { + readLoop = p.readLoop } // read loop @@ -233,7 +237,7 @@ func (p *Peer) loop() (reason DiscReason, err error) { readErr := make(chan error) readNext := make(chan bool, 1) protoDone := make(chan struct{}, 1) - go p.readLoop(readMsg, readErr, readNext) + go readLoop(readMsg, readErr, readNext) readNext <- true if p.runBaseProtocol { @@ -329,8 +333,19 @@ func (p *Peer) dispatch(msg Msg, protoDone chan struct{}) (wait bool, err error) } func (p *Peer) handleCryptoHandshake() (err error) { + // cryptoId is just created for the lifecycle of the handshake + // it is survived by an encrypted readwriter + if p.dialAddr != 0 { // this should have its own method Outgoing() bool + initiator = true + } + // create crypto layer + cryptoId := newCryptoId(p.identity, initiator, sessionToken) + // run on peer + if rw, err := cryptoId.Run(p.Pubkey()); err != nil { + return err + } + p.conn = rw.Run(p.conn) - return nil } func (p *Peer) startBaseProtocol() { From a8b3c03fbd51a0c1456e48dc11a0c3989f80cef6 Mon Sep 17 00:00:00 2001 From: zelig Date: Mon, 19 Jan 2015 23:42:13 +0000 Subject: [PATCH 11/31] first stab at integrating crypto in our p2p - abstract the entire handshake logic in cryptoId.Run() taking session-relevant parameters - changes in peer to accomodate how the encryption layer would be switched on - modify arguments of handshake components - fixed test getting the wrong pubkey but it till crashes on DH in newSession() --- p2p/crypto.go | 53 +++++++++++++++++++++++++++++++++------------- p2p/crypto_test.go | 39 +++++++++++++++++----------------- p2p/peer.go | 33 ++++++++++++++++++++--------- 3 files changed, 80 insertions(+), 45 deletions(-) diff --git a/p2p/crypto.go b/p2p/crypto.go index 728b8e884e..b6d600826f 100644 --- a/p2p/crypto.go +++ b/p2p/crypto.go @@ -4,6 +4,7 @@ import ( "crypto/ecdsa" "crypto/rand" "fmt" + "io" "github.com/ethereum/go-ethereum/crypto" "github.com/obscuren/ecies" @@ -53,19 +54,35 @@ func newCryptoId(id ClientIdentity) (self *cryptoId, err error) { return } -func (self *cryptoId) Run(remotePubKeyDER []byte) (rw *secretRW) { - if self.initiator { - auth, initNonce, randomPrvKey, randomPubKey, err := initiator.initAuth(remotePubKeyDER, sessionToken) - - respNonce, remoteRandomPubKey, _, _ := initiator.verifyAuthResp(response) +func (self *cryptoId) Run(conn io.ReadWriter, remotePubKeyDER []byte, sessionToken []byte, initiator bool) (token []byte, rw *secretRW, err error) { + var auth, initNonce, recNonce []byte + var randomPrivKey *ecdsa.PrivateKey + var remoteRandomPubKey *ecdsa.PublicKey + if initiator { + if auth, initNonce, randomPrivKey, _, err = self.startHandshake(remotePubKeyDER, sessionToken); err != nil { + return + } + conn.Write(auth) + var response []byte + conn.Read(response) + // write out auth message + // wait for response, then call complete + if recNonce, remoteRandomPubKey, _, err = self.completeHandshake(response); err != nil { + return + } } else { - // we are listening connection. we are responders in the haandshake. + conn.Read(auth) + // we are listening connection. we are responders in the handshake. // Extract info from the authentication. The initiator starts by sending us a handshake that we need to respond to. - response, remoteRespNonce, remoteInitNonce, remoteRandomPrivKey, _ := responder.verifyAuth(auth, sessionToken, pubInit) - + // so we read auth message first, then respond + var response []byte + if response, recNonce, initNonce, randomPrivKey, err = self.respondToHandshake(auth, remotePubKeyDER, sessionToken); err != nil { + return + } + remoteRandomPubKey = &randomPrivKey.PublicKey + conn.Write(response) } - initSessionToken, initSecretRW, _ := initiator.newSession(initNonce, respNonce, auth, randomPrvKey, remoteRandomPubKey) - respSessionToken, respSecretRW, _ := responder.newSession(remoteInitNonce, remoteRespNonce, auth, remoteRandomPrivKey, randomPubKey) + return self.newSession(initNonce, recNonce, auth, randomPrivKey, remoteRandomPubKey) } /* startHandshake is called by peer if it initiated the connection. @@ -83,9 +100,9 @@ The handshake is the process by which the peers establish their connection for a */ -func (self *cryptoId) startHandshake(remotePubKeyDER, sessionToken []byte) (auth []byte, initNonce []byte, randomPrvKey *ecdsa.PrivateKey, randomPubKey *ecdsa.PublicKey, err error) { +func (self *cryptoId) startHandshake(remotePubKeyDER, sessionToken []byte) (auth []byte, initNonce []byte, randomPrvKey *ecdsa.PrivateKey, remotePubKey *ecdsa.PublicKey, err error) { // session init, common to both parties - remotePubKey := crypto.ToECDSAPub(remotePubKeyDER) + remotePubKey = crypto.ToECDSAPub(remotePubKeyDER) if remotePubKey == nil { err = fmt.Errorf("invalid remote public key") return @@ -160,8 +177,14 @@ func (self *cryptoId) startHandshake(remotePubKeyDER, sessionToken []byte) (auth } // verifyAuth is called by peer if it accepted (but not initiated) the connection -func (self *cryptoId) respondToHandshake(auth, sessionToken []byte, remotePubKey *ecdsa.PublicKey) (authResp []byte, respNonce []byte, initNonce []byte, randomPrvKey *ecdsa.PrivateKey, err error) { +func (self *cryptoId) respondToHandshake(auth, remotePubKeyDER, sessionToken []byte) (authResp []byte, respNonce []byte, initNonce []byte, randomPrivKey *ecdsa.PrivateKey, err error) { var msg []byte + remotePubKey := crypto.ToECDSAPub(remotePubKeyDER) + if remotePubKey == nil { + err = fmt.Errorf("invalid public key") + return + } + fmt.Printf("encrypted message received: %v %x\n used pubkey: %x\n", len(auth), auth, crypto.FromECDSAPub(self.pubKey)) // they prove that msg is meant for me, // I prove I possess private key if i can read it @@ -210,12 +233,12 @@ func (self *cryptoId) respondToHandshake(auth, sessionToken []byte, remotePubKey return } // generate random keypair for session - if randomPrvKey, err = crypto.GenerateKey(); err != nil { + if randomPrivKey, err = crypto.GenerateKey(); err != nil { return } // responder auth message // E(remote-pubk, ecdhe-random-pubk || nonce || 0x0) - copy(resp[:keyLen], crypto.FromECDSAPub(&randomPrvKey.PublicKey)) + copy(resp[:keyLen], crypto.FromECDSAPub(&randomPrivKey.PublicKey)) // nonce is already in the slice resp[resLen-1] = tokenFlag diff --git a/p2p/crypto_test.go b/p2p/crypto_test.go index cfb2d19d1a..fb7df6b50e 100644 --- a/p2p/crypto_test.go +++ b/p2p/crypto_test.go @@ -11,44 +11,43 @@ import ( func TestCryptoHandshake(t *testing.T) { var err error var sessionToken []byte - prvInit, _ := crypto.GenerateKey() - pubInit := &prvInit.PublicKey - prvResp, _ := crypto.GenerateKey() - pubResp := &prvResp.PublicKey + prv0, _ := crypto.GenerateKey() + pub0 := &prv0.PublicKey + prv1, _ := crypto.GenerateKey() + pub1 := &prv1.PublicKey - var initiator, responder *cryptoId - if initiator, err = newCryptoId(&peerId{crypto.FromECDSA(prvInit), crypto.FromECDSAPub(pubInit)}); err != nil { + var initiator, receiver *cryptoId + if initiator, err = newCryptoId(&peerId{crypto.FromECDSA(prv0), crypto.FromECDSAPub(pub0)}); err != nil { return } - if responder, err = newCryptoId(&peerId{crypto.FromECDSA(prvResp), crypto.FromECDSAPub(pubResp)}); err != nil { + if receiver, err = newCryptoId(&peerId{crypto.FromECDSA(prv1), crypto.FromECDSAPub(pub1)}); err != nil { return } - auth, initNonce, randomPrvKey, randomPubKey, _ := initiator.initAuth(responder.pubKeyDER, sessionToken) + // simulate handshake by feeding output to input + auth, initNonce, randomPrivKey, _, _ := initiator.startHandshake(receiver.pubKeyDER, sessionToken) + response, remoteRecNonce, remoteInitNonce, remoteRandomPrivKey, _ := receiver.respondToHandshake(auth, crypto.FromECDSAPub(pub0), sessionToken) + recNonce, remoteRandomPubKey, _, _ := initiator.completeHandshake(response) - response, remoteRespNonce, remoteInitNonce, remoteRandomPrivKey, _ := responder.verifyAuth(auth, sessionToken, pubInit) + initSessionToken, initSecretRW, _ := initiator.newSession(initNonce, recNonce, auth, randomPrivKey, remoteRandomPubKey) + recSessionToken, recSecretRW, _ := receiver.newSession(remoteInitNonce, remoteRecNonce, auth, remoteRandomPrivKey, &randomPrivKey.PublicKey) - respNonce, remoteRandomPubKey, _, _ := initiator.verifyAuthResp(response) + fmt.Printf("%x\n%x\n%x\n%x\n%x\n%x\n%x\n%x\n%x\n%x\n", auth, initNonce, response, remoteRecNonce, remoteInitNonce, remoteRandomPubKey, recNonce, &randomPrivKey.PublicKey, initSessionToken, initSecretRW) - initSessionToken, initSecretRW, _ := initiator.newSession(initNonce, respNonce, auth, randomPrvKey, remoteRandomPubKey) - respSessionToken, respSecretRW, _ := responder.newSession(remoteInitNonce, remoteRespNonce, auth, remoteRandomPrivKey, randomPubKey) - - fmt.Printf("%x\n%x\n%x\n%x\n%x\n%x\n%x\n%x\n%x\n%x\n", auth, initNonce, response, remoteRespNonce, remoteInitNonce, remoteRandomPubKey, respNonce, randomPubKey, initSessionToken, initSecretRW) - - if !bytes.Equal(initSessionToken, respSessionToken) { + if !bytes.Equal(initSessionToken, recSessionToken) { t.Errorf("session tokens do not match") } // aesSecret, macSecret, egressMac, ingressMac - if !bytes.Equal(initSecretRW.aesSecret, respSecretRW.aesSecret) { + if !bytes.Equal(initSecretRW.aesSecret, recSecretRW.aesSecret) { t.Errorf("AES secrets do not match") } - if !bytes.Equal(initSecretRW.macSecret, respSecretRW.macSecret) { + if !bytes.Equal(initSecretRW.macSecret, recSecretRW.macSecret) { t.Errorf("macSecrets do not match") } - if !bytes.Equal(initSecretRW.egressMac, respSecretRW.egressMac) { + if !bytes.Equal(initSecretRW.egressMac, recSecretRW.egressMac) { t.Errorf("egressMacs do not match") } - if !bytes.Equal(initSecretRW.ingressMac, respSecretRW.ingressMac) { + if !bytes.Equal(initSecretRW.ingressMac, recSecretRW.ingressMac) { t.Errorf("ingressMacs do not match") } diff --git a/p2p/peer.go b/p2p/peer.go index e98c3d5608..e3e04ee652 100644 --- a/p2p/peer.go +++ b/p2p/peer.go @@ -222,9 +222,9 @@ func (p *Peer) loop() (reason DiscReason, err error) { defer close(p.closed) defer p.conn.Close() - var readLoop func(chan Msg, chan error, chan bool) + var readLoop func(chan<- Msg, chan<- error, <-chan bool) if p.cryptoHandshake { - if readLoop, err := p.handleCryptoHandshake(); err != nil { + if readLoop, err = p.handleCryptoHandshake(); err != nil { // from here on everything can be encrypted, authenticated return DiscProtocolError, err // no graceful disconnect } @@ -332,20 +332,33 @@ func (p *Peer) dispatch(msg Msg, protoDone chan struct{}) (wait bool, err error) return wait, nil } -func (p *Peer) handleCryptoHandshake() (err error) { +type readLoop func(chan<- Msg, chan<- error, <-chan bool) + +func (p *Peer) handleCryptoHandshake() (loop readLoop, err error) { // cryptoId is just created for the lifecycle of the handshake // it is survived by an encrypted readwriter - if p.dialAddr != 0 { // this should have its own method Outgoing() bool + var initiator bool + var sessionToken []byte + if p.dialAddr != nil { // this should have its own method Outgoing() bool initiator = true } // create crypto layer - cryptoId := newCryptoId(p.identity, initiator, sessionToken) - // run on peer - if rw, err := cryptoId.Run(p.Pubkey()); err != nil { - return err + // this could in principle run only once but maybe we want to allow + // identity switching + var crypto *cryptoId + if crypto, err = newCryptoId(p.ourID); err != nil { + return } - p.conn = rw.Run(p.conn) - + // run on peer + // this bit handles the handshake and creates a secure communications channel with + // var rw *secretRW + if sessionToken, _, err = crypto.Run(p.conn, p.Pubkey(), sessionToken, initiator); err != nil { + return + } + loop = func(msg chan<- Msg, err chan<- error, next <-chan bool) { + // this is the readloop :) + } + return } func (p *Peer) startBaseProtocol() { From 6bb203d52348a9c7a31f6444662f12126c763abe Mon Sep 17 00:00:00 2001 From: zelig Date: Tue, 20 Jan 2015 00:23:10 +0000 Subject: [PATCH 12/31] add minor comments to the test --- p2p/crypto_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/p2p/crypto_test.go b/p2p/crypto_test.go index fb7df6b50e..33cdb3f83f 100644 --- a/p2p/crypto_test.go +++ b/p2p/crypto_test.go @@ -25,10 +25,14 @@ func TestCryptoHandshake(t *testing.T) { } // simulate handshake by feeding output to input + // initiator sends handshake 'auth' auth, initNonce, randomPrivKey, _, _ := initiator.startHandshake(receiver.pubKeyDER, sessionToken) + // receiver reads auth and responds with response response, remoteRecNonce, remoteInitNonce, remoteRandomPrivKey, _ := receiver.respondToHandshake(auth, crypto.FromECDSAPub(pub0), sessionToken) + // initiator reads receiver's response and the key exchange completes recNonce, remoteRandomPubKey, _, _ := initiator.completeHandshake(response) + // now both parties should have the same session parameters initSessionToken, initSecretRW, _ := initiator.newSession(initNonce, recNonce, auth, randomPrivKey, remoteRandomPubKey) recSessionToken, recSecretRW, _ := receiver.newSession(remoteInitNonce, remoteRecNonce, auth, remoteRandomPrivKey, &randomPrivKey.PublicKey) From 20ac18751efce1365d90f81098c499134ab79c87 Mon Sep 17 00:00:00 2001 From: zelig Date: Tue, 20 Jan 2015 00:41:45 +0000 Subject: [PATCH 13/31] add equality check for nonce and remote nonce --- p2p/crypto_test.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/p2p/crypto_test.go b/p2p/crypto_test.go index 33cdb3f83f..89baca0844 100644 --- a/p2p/crypto_test.go +++ b/p2p/crypto_test.go @@ -38,6 +38,12 @@ func TestCryptoHandshake(t *testing.T) { fmt.Printf("%x\n%x\n%x\n%x\n%x\n%x\n%x\n%x\n%x\n%x\n", auth, initNonce, response, remoteRecNonce, remoteInitNonce, remoteRandomPubKey, recNonce, &randomPrivKey.PublicKey, initSessionToken, initSecretRW) + 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") } From 2d4f5985c00324073e666d57f7fbd4d60b9592e4 Mon Sep 17 00:00:00 2001 From: zelig Date: Tue, 20 Jan 2015 15:20:18 +0000 Subject: [PATCH 14/31] important fix for peer pubkey. when taken from identity, chop first format byte! --- p2p/peer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/p2p/peer.go b/p2p/peer.go index e3e04ee652..e44eaab343 100644 --- a/p2p/peer.go +++ b/p2p/peer.go @@ -147,7 +147,7 @@ func (self *Peer) Pubkey() (pubkey []byte) { defer self.infolock.Unlock() switch { case self.identity != nil: - pubkey = self.identity.Pubkey() + pubkey = self.identity.Pubkey()[1:] case self.dialAddr != nil: pubkey = self.dialAddr.Pubkey case self.listenAddr != nil: From 0433cae5a632b5065d0aef1c6281a960c06a42e2 Mon Sep 17 00:00:00 2001 From: zelig Date: Tue, 20 Jan 2015 16:47:46 +0000 Subject: [PATCH 15/31] changes that fix it all: - set proper public key serialisation length in pubLen = 64 - reset all sizes and offsets - rename from DER to S (we are not using DER encoding) - add remoteInitRandomPubKey as return value to respondToHandshake - add ImportPublicKey with error return to read both EC golang.elliptic style 65 byte encoding and 64 byte one - add ExportPublicKey falling back to go-ethereum/crypto.FromECDSAPub() chopping off the first byte - add Import - Export tests - all tests pass --- p2p/crypto.go | 111 +++++++++++++++++++++++++-------------------- p2p/crypto_test.go | 92 ++++++++++++++++++++++++++++++++++--- 2 files changed, 146 insertions(+), 57 deletions(-) diff --git a/p2p/crypto.go b/p2p/crypto.go index b6d600826f..dbef022ccd 100644 --- a/p2p/crypto.go +++ b/p2p/crypto.go @@ -12,11 +12,12 @@ import ( ) var ( - sskLen int = 16 // ecies.MaxSharedKeyLength(pubKey) / 2 - sigLen int = 65 // elliptic S256 - keyLen int = 32 // ECDSA - msgLen int = sigLen + 3*keyLen + 1 // 162 - resLen int = 65 // + sskLen int = 16 // ecies.MaxSharedKeyLength(pubKey) / 2 + sigLen int = 65 // elliptic S256 + pubLen int = 64 // 512 bit pubkey in uncompressed representation without format byte + keyLen int = 32 // ECDSA + msgLen int = 194 // sigLen + keyLen + pubLen + keyLen + 1 = 194 + resLen int = 97 // pubLen + keyLen + 1 ) // aesSecret, macSecret, egressMac, ingress @@ -25,20 +26,21 @@ type secretRW struct { } type cryptoId struct { - prvKey *ecdsa.PrivateKey - pubKey *ecdsa.PublicKey - pubKeyDER []byte + prvKey *ecdsa.PrivateKey + pubKey *ecdsa.PublicKey + pubKeyS []byte } func newCryptoId(id ClientIdentity) (self *cryptoId, err error) { // will be at server init - var prvKeyDER []byte = id.PrivKey() - if prvKeyDER == nil { + var prvKeyS []byte = id.PrivKey() + if prvKeyS == nil { err = fmt.Errorf("no private key for client") return } - // initialise ecies private key via importing DER encoded keys (known via our own clientIdentity) - var prvKey = crypto.ToECDSA(prvKeyDER) + // initialise ecies private key via importing keys (known via our own clientIdentity) + // the key format is what elliptic package is using: elliptic.Marshal(Curve, X, Y) + var prvKey = crypto.ToECDSA(prvKeyS) if prvKey == nil { err = fmt.Errorf("invalid private key for client") return @@ -50,16 +52,16 @@ func newCryptoId(id ClientIdentity) (self *cryptoId, err error) { // to be created at server init shared between peers and sessions // for reuse, call wth ReadAt, no reset seek needed } - self.pubKeyDER = id.Pubkey() + self.pubKeyS = id.Pubkey() return } -func (self *cryptoId) Run(conn io.ReadWriter, remotePubKeyDER []byte, sessionToken []byte, initiator bool) (token []byte, rw *secretRW, err error) { +func (self *cryptoId) Run(conn io.ReadWriter, remotePubKeyS []byte, sessionToken []byte, initiator bool) (token []byte, rw *secretRW, err error) { var auth, initNonce, recNonce []byte var randomPrivKey *ecdsa.PrivateKey var remoteRandomPubKey *ecdsa.PublicKey if initiator { - if auth, initNonce, randomPrivKey, _, err = self.startHandshake(remotePubKeyDER, sessionToken); err != nil { + if auth, initNonce, randomPrivKey, _, err = self.startHandshake(remotePubKeyS, sessionToken); err != nil { return } conn.Write(auth) @@ -76,10 +78,9 @@ func (self *cryptoId) Run(conn io.ReadWriter, remotePubKeyDER []byte, sessionTok // Extract info from the authentication. The initiator starts by sending us a handshake that we need to respond to. // so we read auth message first, then respond var response []byte - if response, recNonce, initNonce, randomPrivKey, err = self.respondToHandshake(auth, remotePubKeyDER, sessionToken); err != nil { + if response, recNonce, initNonce, randomPrivKey, remoteRandomPubKey, err = self.respondToHandshake(auth, remotePubKeyS, sessionToken); err != nil { return } - remoteRandomPubKey = &randomPrivKey.PublicKey conn.Write(response) } return self.newSession(initNonce, recNonce, auth, randomPrivKey, remoteRandomPubKey) @@ -100,11 +101,29 @@ The handshake is the process by which the peers establish their connection for a */ -func (self *cryptoId) startHandshake(remotePubKeyDER, sessionToken []byte) (auth []byte, initNonce []byte, randomPrvKey *ecdsa.PrivateKey, remotePubKey *ecdsa.PublicKey, err error) { +func ImportPublicKey(pubKey []byte) (pubKeyEC *ecdsa.PublicKey, err error) { + var pubKey65 []byte + switch len(pubKey) { + case 64: + pubKey65 = append([]byte{0x04}, pubKey...) + case 65: + pubKey65 = pubKey + default: + return nil, fmt.Errorf("invalid public key length %v (expect 64/65)", len(pubKey)) + } + return crypto.ToECDSAPub(pubKey65), nil +} + +func ExportPublicKey(pubKeyEC *ecdsa.PublicKey) (pubKey []byte, err error) { + if pubKeyEC == nil { + return nil, fmt.Errorf("no ECDSA public key given") + } + return crypto.FromECDSAPub(pubKeyEC)[1:], nil +} + +func (self *cryptoId) startHandshake(remotePubKeyS, sessionToken []byte) (auth []byte, initNonce []byte, randomPrvKey *ecdsa.PrivateKey, remotePubKey *ecdsa.PublicKey, err error) { // session init, common to both parties - remotePubKey = crypto.ToECDSAPub(remotePubKeyDER) - if remotePubKey == nil { - err = fmt.Errorf("invalid remote public key") + if remotePubKey, err = ImportPublicKey(remotePubKeyS); err != nil { return } @@ -116,8 +135,6 @@ func (self *cryptoId) startHandshake(remotePubKeyDER, sessionToken []byte) (auth if sessionToken, err = ecies.ImportECDSA(self.prvKey).GenerateShared(ecies.ImportECDSAPublic(remotePubKey), sskLen, sskLen); err != nil { return } - // this will not stay here ;) - fmt.Printf("secret generated: %v %x", len(sessionToken), sessionToken) // tokenFlag = 0x00 // redundant } else { // for known peers, we use stored token from the previous session @@ -128,9 +145,7 @@ func (self *cryptoId) startHandshake(remotePubKeyDER, sessionToken []byte) (auth // E(remote-pubk, S(ecdhe-random, token^nonce) || H(ecdhe-random-pubk) || pubk || nonce || 0x1) // allocate msgLen long message, var msg []byte = make([]byte, msgLen) - // generate sskLen long nonce initNonce = msg[msgLen-keyLen-1 : msgLen-1] - // nonce = msg[msgLen-sskLen-1 : msgLen-1] if _, err = rand.Read(initNonce); err != nil { return } @@ -150,48 +165,45 @@ func (self *cryptoId) startHandshake(remotePubKeyDER, sessionToken []byte) (auth if signature, err = crypto.Sign(sharedSecret, randomPrvKey); err != nil { return } - fmt.Printf("signature generated: %v %x", len(signature), signature) // message // signed-shared-secret || H(ecdhe-random-pubk) || pubk || nonce || 0x0 copy(msg, signature) // copy signed-shared-secret // H(ecdhe-random-pubk) - copy(msg[sigLen:sigLen+keyLen], crypto.Sha3(crypto.FromECDSAPub(&randomPrvKey.PublicKey))) + var randomPubKey64 []byte + if randomPubKey64, err = ExportPublicKey(&randomPrvKey.PublicKey); err != nil { + return + } + copy(msg[sigLen:sigLen+keyLen], crypto.Sha3(randomPubKey64)) // pubkey copied to the correct segment. - copy(msg[sigLen+keyLen:sigLen+2*keyLen], self.pubKeyDER) + copy(msg[sigLen+keyLen:sigLen+keyLen+pubLen], self.pubKeyS) // nonce is already in the slice // stick tokenFlag byte to the end msg[msgLen-1] = tokenFlag - fmt.Printf("plaintext message generated: %v %x", len(msg), msg) - // encrypt using remote-pubk // auth = eciesEncrypt(remote-pubk, msg) if auth, err = crypto.Encrypt(remotePubKey, msg); err != nil { return } - fmt.Printf("encrypted message generated: %v %x\n used pubkey: %x\n", len(auth), auth, crypto.FromECDSAPub(remotePubKey)) return } // verifyAuth is called by peer if it accepted (but not initiated) the connection -func (self *cryptoId) respondToHandshake(auth, remotePubKeyDER, sessionToken []byte) (authResp []byte, respNonce []byte, initNonce []byte, randomPrivKey *ecdsa.PrivateKey, err error) { +func (self *cryptoId) respondToHandshake(auth, remotePubKeyS, sessionToken []byte) (authResp []byte, respNonce []byte, initNonce []byte, randomPrivKey *ecdsa.PrivateKey, remoteRandomPubKey *ecdsa.PublicKey, err error) { var msg []byte - remotePubKey := crypto.ToECDSAPub(remotePubKeyDER) - if remotePubKey == nil { - err = fmt.Errorf("invalid public key") + var remotePubKey *ecdsa.PublicKey + if remotePubKey, err = ImportPublicKey(remotePubKeyS); err != nil { return } - fmt.Printf("encrypted message received: %v %x\n used pubkey: %x\n", len(auth), auth, crypto.FromECDSAPub(self.pubKey)) // they prove that msg is meant for me, // I prove I possess private key if i can read it if msg, err = crypto.Decrypt(self.prvKey, auth); err != nil { return } - fmt.Printf("\nplaintext message retrieved: %v %x\n", len(msg), msg) var tokenFlag byte if sessionToken == nil { @@ -201,7 +213,6 @@ func (self *cryptoId) respondToHandshake(auth, remotePubKeyDER, sessionToken []b if sessionToken, err = ecies.ImportECDSA(self.prvKey).GenerateShared(ecies.ImportECDSAPublic(remotePubKey), sskLen, sskLen); err != nil { return } - fmt.Printf("secret generated: %v %x", len(sessionToken), sessionToken) // tokenFlag = 0x00 // redundant } else { // for known peers, we use stored token from the previous session @@ -214,21 +225,19 @@ func (self *cryptoId) respondToHandshake(auth, remotePubKeyDER, sessionToken []b // they prove they own the private key belonging to ecdhe-random-pubk // we can now reconstruct the signed message and recover the peers pubkey var signedMsg = Xor(sessionToken, initNonce) - var remoteRandomPubKeyDER []byte - if remoteRandomPubKeyDER, err = secp256k1.RecoverPubkey(signedMsg, msg[:sigLen]); err != nil { + var remoteRandomPubKeyS []byte + if remoteRandomPubKeyS, err = secp256k1.RecoverPubkey(signedMsg, msg[:sigLen]); err != nil { return } // convert to ECDSA standard - remoteRandomPubKey := crypto.ToECDSAPub(remoteRandomPubKeyDER) - if remoteRandomPubKey == nil { - err = fmt.Errorf("invalid remote public key") + if remoteRandomPubKey, err = ImportPublicKey(remoteRandomPubKeyS); err != nil { return } // now we find ourselves a long task too, fill it random var resp = make([]byte, resLen) // generate keyLen long nonce - respNonce = msg[resLen-keyLen-1 : msgLen-1] + respNonce = resp[pubLen : pubLen+keyLen] if _, err = rand.Read(respNonce); err != nil { return } @@ -238,7 +247,11 @@ func (self *cryptoId) respondToHandshake(auth, remotePubKeyDER, sessionToken []b } // responder auth message // E(remote-pubk, ecdhe-random-pubk || nonce || 0x0) - copy(resp[:keyLen], crypto.FromECDSAPub(&randomPrivKey.PublicKey)) + var randomPubKeyS []byte + if randomPubKeyS, err = ExportPublicKey(&randomPrivKey.PublicKey); err != nil { + return + } + copy(resp[:pubLen], randomPubKeyS) // nonce is already in the slice resp[resLen-1] = tokenFlag @@ -259,11 +272,9 @@ func (self *cryptoId) completeHandshake(auth []byte) (respNonce []byte, remoteRa return } - respNonce = msg[resLen-keyLen-1 : resLen-1] - var remoteRandomPubKeyDER = msg[:keyLen] - remoteRandomPubKey = crypto.ToECDSAPub(remoteRandomPubKeyDER) - if remoteRandomPubKey == nil { - err = fmt.Errorf("invalid ecdh random remote public key") + respNonce = msg[pubLen : pubLen+keyLen] + var remoteRandomPubKeyS = msg[:pubLen] + if remoteRandomPubKey, err = ImportPublicKey(remoteRandomPubKeyS); err != nil { return } if msg[resLen-1] == 0x01 { diff --git a/p2p/crypto_test.go b/p2p/crypto_test.go index 89baca0844..8000efaf19 100644 --- a/p2p/crypto_test.go +++ b/p2p/crypto_test.go @@ -2,16 +2,76 @@ package p2p import ( "bytes" + // "crypto/ecdsa" + // "crypto/elliptic" + // "crypto/rand" "fmt" "testing" "github.com/ethereum/go-ethereum/crypto" + "github.com/obscuren/ecies" ) +func TestPublicKeyEncoding(t *testing.T) { + prv0, _ := crypto.GenerateKey() // = ecdsa.GenerateKey(crypto.S256(), rand.Reader) + pub0 := &prv0.PublicKey + pub0s := crypto.FromECDSAPub(pub0) + pub1, err := ImportPublicKey(pub0s) + if err != nil { + t.Errorf("%v", err) + } + eciesPub1 := ecies.ImportECDSAPublic(pub1) + if eciesPub1 == nil { + t.Errorf("invalid ecdsa public key") + } + pub1s, err := ExportPublicKey(pub1) + if err != nil { + t.Errorf("%v", err) + } + if len(pub1s) != 64 { + t.Errorf("wrong length expect 64, got", len(pub1s)) + } + pub2, err := ImportPublicKey(pub1s) + if err != nil { + t.Errorf("%v", err) + } + pub2s, err := ExportPublicKey(pub2) + if err != nil { + t.Errorf("%v", err) + } + if !bytes.Equal(pub1s, pub2s) { + t.Errorf("exports dont match") + } + pub2sEC := crypto.FromECDSAPub(pub2) + if !bytes.Equal(pub0s, pub2sEC) { + t.Errorf("exports dont match") + } +} + +func TestSharedSecret(t *testing.T) { + prv0, _ := crypto.GenerateKey() // = ecdsa.GenerateKey(crypto.S256(), rand.Reader) + pub0 := &prv0.PublicKey + prv1, _ := crypto.GenerateKey() + pub1 := &prv1.PublicKey + + ss0, err := ecies.ImportECDSA(prv0).GenerateShared(ecies.ImportECDSAPublic(pub1), sskLen, sskLen) + if err != nil { + return + } + ss1, err := ecies.ImportECDSA(prv1).GenerateShared(ecies.ImportECDSAPublic(pub0), sskLen, sskLen) + if err != nil { + return + } + t.Logf("Secret:\n%v %x\n%v %x", len(ss0), ss0, len(ss0), ss1) + if !bytes.Equal(ss0, ss1) { + t.Errorf("dont match :(") + } +} + func TestCryptoHandshake(t *testing.T) { var err error var sessionToken []byte - prv0, _ := crypto.GenerateKey() + prv0, _ := crypto.GenerateKey() // = ecdsa.GenerateKey(crypto.S256(), rand.Reader) pub0 := &prv0.PublicKey prv1, _ := crypto.GenerateKey() pub1 := &prv1.PublicKey @@ -26,17 +86,35 @@ func TestCryptoHandshake(t *testing.T) { // simulate handshake by feeding output to input // initiator sends handshake 'auth' - auth, initNonce, randomPrivKey, _, _ := initiator.startHandshake(receiver.pubKeyDER, sessionToken) + auth, initNonce, randomPrivKey, _, err := initiator.startHandshake(receiver.pubKeyS, sessionToken) + if err != nil { + t.Errorf("%v", err) + } + // receiver reads auth and responds with response - response, remoteRecNonce, remoteInitNonce, remoteRandomPrivKey, _ := receiver.respondToHandshake(auth, crypto.FromECDSAPub(pub0), sessionToken) + response, remoteRecNonce, remoteInitNonce, remoteRandomPrivKey, remoteInitRandomPubKey, err := receiver.respondToHandshake(auth, crypto.FromECDSAPub(pub0), sessionToken) + if err != nil { + t.Errorf("%v", err) + } + // initiator reads receiver's response and the key exchange completes - recNonce, remoteRandomPubKey, _, _ := initiator.completeHandshake(response) + recNonce, remoteRandomPubKey, _, err := initiator.completeHandshake(response) + if err != nil { + t.Errorf("%v", err) + } // now both parties should have the same session parameters - initSessionToken, initSecretRW, _ := initiator.newSession(initNonce, recNonce, auth, randomPrivKey, remoteRandomPubKey) - recSessionToken, recSecretRW, _ := receiver.newSession(remoteInitNonce, remoteRecNonce, auth, remoteRandomPrivKey, &randomPrivKey.PublicKey) + initSessionToken, initSecretRW, err := initiator.newSession(initNonce, recNonce, auth, randomPrivKey, remoteRandomPubKey) + if err != nil { + t.Errorf("%v", err) + } - fmt.Printf("%x\n%x\n%x\n%x\n%x\n%x\n%x\n%x\n%x\n%x\n", auth, initNonce, response, remoteRecNonce, remoteInitNonce, remoteRandomPubKey, recNonce, &randomPrivKey.PublicKey, initSessionToken, initSecretRW) + recSessionToken, recSecretRW, err := receiver.newSession(remoteInitNonce, remoteRecNonce, auth, remoteRandomPrivKey, remoteInitRandomPubKey) + if err != nil { + t.Errorf("%v", err) + } + + 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") From 8f938c7aa43977f83338fe953252d6d8c297ab8d Mon Sep 17 00:00:00 2001 From: zelig Date: Wed, 21 Jan 2015 10:22:07 +0000 Subject: [PATCH 16/31] add code documentation --- p2p/crypto.go | 62 ++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 47 insertions(+), 15 deletions(-) diff --git a/p2p/crypto.go b/p2p/crypto.go index dbef022ccd..8551e317c7 100644 --- a/p2p/crypto.go +++ b/p2p/crypto.go @@ -20,17 +20,27 @@ var ( resLen int = 97 // pubLen + keyLen + 1 ) +// secretRW implements a message read writer with encryption and authentication +// it is initialised by cryptoId.Run() after a successful crypto handshake // aesSecret, macSecret, egressMac, ingress type secretRW struct { aesSecret, macSecret, egressMac, ingressMac []byte } +/* +cryptoId implements the crypto layer for the p2p networking +It is initialised on the node's own identity (which has access to the node's private key) and run separately on a peer connection to set up a secure session after a crypto handshake +After it performs a crypto handshake it returns +*/ type cryptoId struct { prvKey *ecdsa.PrivateKey pubKey *ecdsa.PublicKey pubKeyS []byte } +/* +newCryptoId(id ClientIdentity) initialises a crypto layer manager. This object has a short lifecycle when the peer connection starts. It is survived by a secretRW (an message read writer with encryption and authentication) if the crypto handshake is successful. +*/ func newCryptoId(id ClientIdentity) (self *cryptoId, err error) { // will be at server init var prvKeyS []byte = id.PrivKey() @@ -56,6 +66,20 @@ func newCryptoId(id ClientIdentity) (self *cryptoId, err error) { return } +/* +Run(connection, remotePublicKey, sessionToken) is called when the peer connection starts to set up a secure session by performing a crypto handshake. + + connection is (a buffered) network connection. + + remotePublicKey is the remote peer's node Id. + + sessionToken is the token from the previous session with this same peer. Nil if no token is found. + + initiator is a boolean flag. True if the node represented by cryptoId is the initiator of the connection (ie., remote is an outbound peer reached by dialing out). False if the connection was established by accepting a call from the remote peer via a listener. + + It returns a secretRW which implements the MsgReadWriter interface. +*/ + func (self *cryptoId) Run(conn io.ReadWriter, remotePubKeyS []byte, sessionToken []byte, initiator bool) (token []byte, rw *secretRW, err error) { var auth, initNonce, recNonce []byte var randomPrivKey *ecdsa.PrivateKey @@ -86,21 +110,9 @@ func (self *cryptoId) Run(conn io.ReadWriter, remotePubKeyS []byte, sessionToken return self.newSession(initNonce, recNonce, auth, randomPrivKey, remoteRandomPubKey) } -/* startHandshake is called by peer if it initiated the connection. - By protocol spec, the party who initiates the connection (initiator) will send an 'auth' packet -New: authInitiator -> E(remote-pubk, S(ecdhe-random, ecdh-shared-secret^nonce) || H(ecdhe-random-pubk) || pubk || nonce || 0x0) - authRecipient -> E(remote-pubk, ecdhe-random-pubk || nonce || 0x0) - -Known: authInitiator = E(remote-pubk, S(ecdhe-random, token^nonce) || H(ecdhe-random-pubk) || pubk || nonce || 0x1) - authRecipient = E(remote-pubk, ecdhe-random-pubk || nonce || 0x1) // token found - authRecipient = E(remote-pubk, ecdhe-random-pubk || nonce || 0x0) // token not found - -The caller provides the public key of the peer as conjuctured from lookup based on IP:port, given as user input or proven by signatures. The caller must have access to persistant information about the peers, and pass the previous session token as an argument to cryptoId. - -The handshake is the process by which the peers establish their connection for a session. - +/* +ImportPublicKey creates a 512 bit *ecsda.PublicKey from a byte slice. It accepts the simple 64 byte uncompressed format or the 65 byte format given by calling elliptic.Marshal on the EC point represented by the key. Any other length will result in an invalid public key error. */ - func ImportPublicKey(pubKey []byte) (pubKeyEC *ecdsa.PublicKey, err error) { var pubKey65 []byte switch len(pubKey) { @@ -114,6 +126,9 @@ func ImportPublicKey(pubKey []byte) (pubKeyEC *ecdsa.PublicKey, err error) { return crypto.ToECDSAPub(pubKey65), nil } +/* +ExportPublicKey exports a *ecdsa.PublicKey into a byte slice using a simple 64-byte format. and is used for simple serialisation in network communication +*/ func ExportPublicKey(pubKeyEC *ecdsa.PublicKey) (pubKey []byte, err error) { if pubKeyEC == nil { return nil, fmt.Errorf("no ECDSA public key given") @@ -121,6 +136,12 @@ func ExportPublicKey(pubKeyEC *ecdsa.PublicKey) (pubKey []byte, err error) { return crypto.FromECDSAPub(pubKeyEC)[1:], nil } +/* startHandshake is called by if the node is the initiator of the connection. + +The caller provides the public key of the peer as conjuctured from lookup based on IP:port, given as user input or proven by signatures. The caller must have access to persistant information about the peers, and pass the previous session token as an argument to cryptoId. + +The first return value is the auth message that is to be sent out to the remote receiver. +*/ func (self *cryptoId) startHandshake(remotePubKeyS, sessionToken []byte) (auth []byte, initNonce []byte, randomPrvKey *ecdsa.PrivateKey, remotePubKey *ecdsa.PublicKey, err error) { // session init, common to both parties if remotePubKey, err = ImportPublicKey(remotePubKeyS); err != nil { @@ -191,7 +212,11 @@ func (self *cryptoId) startHandshake(remotePubKeyS, sessionToken []byte) (auth [ return } -// verifyAuth is called by peer if it accepted (but not initiated) the connection +/* +respondToHandshake is called by peer if it accepted (but not initiated) the connection from the remote. It is passed the initiator handshake received, the public key and session token belonging to the remote initiator. + +The first return value is the authentication response (aka receiver handshake) that is to be sent to the remote initiator. +*/ func (self *cryptoId) respondToHandshake(auth, remotePubKeyS, sessionToken []byte) (authResp []byte, respNonce []byte, initNonce []byte, randomPrivKey *ecdsa.PrivateKey, remoteRandomPubKey *ecdsa.PublicKey, err error) { var msg []byte var remotePubKey *ecdsa.PublicKey @@ -264,6 +289,9 @@ func (self *cryptoId) respondToHandshake(auth, remotePubKeyS, sessionToken []byt return } +/* +completeHandshake is called when the initiator receives an authentication response (aka receiver handshake). It completes the handshake by reading off parameters the remote peer provides needed to set up the secure session +*/ func (self *cryptoId) completeHandshake(auth []byte) (respNonce []byte, remoteRandomPubKey *ecdsa.PublicKey, tokenFlag bool, err error) { var msg []byte // they prove that msg is meant for me, @@ -283,6 +311,9 @@ func (self *cryptoId) completeHandshake(auth []byte) (respNonce []byte, remoteRa return } +/* +newSession is called after the handshake is completed. The arguments are values negotiated in the handshake and the return value is a new session : a new session Token to be remembered for the next time we connect with this peer. And a MsgReadWriter that implements an encrypted and authenticated connection with key material obtained from the crypto handshake key exchange +*/ func (self *cryptoId) newSession(initNonce, respNonce, auth []byte, privKey *ecdsa.PrivateKey, remoteRandomPubKey *ecdsa.PublicKey) (sessionToken []byte, rw *secretRW, err error) { // 3) Now we can trust ecdhe-random-pubk to derive new keys //ecdhe-shared-secret = ecdh.agree(ecdhe-random, remote-ecdhe-random-pubk) @@ -316,6 +347,7 @@ func (self *cryptoId) newSession(initNonce, respNonce, auth []byte, privKey *ecd return } +// TODO: optimisation // should use cipher.xorBytes from crypto/cipher/xor.go for fast xor func Xor(one, other []byte) (xor []byte) { xor = make([]byte, len(one)) From 012d9e1e827ca2fb502b64d9ca9038497172f2cf Mon Sep 17 00:00:00 2001 From: zelig Date: Wed, 21 Jan 2015 14:42:12 +0000 Subject: [PATCH 17/31] add initial peer level test (failing) --- p2p/crypto_test.go | 53 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/p2p/crypto_test.go b/p2p/crypto_test.go index 8000efaf19..5fbdc61e3e 100644 --- a/p2p/crypto_test.go +++ b/p2p/crypto_test.go @@ -6,6 +6,7 @@ import ( // "crypto/elliptic" // "crypto/rand" "fmt" + "net" "testing" "github.com/ethereum/go-ethereum/crypto" @@ -114,7 +115,9 @@ func TestCryptoHandshake(t *testing.T) { t.Errorf("%v", err) } - 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) + 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") @@ -140,3 +143,51 @@ func TestCryptoHandshake(t *testing.T) { } } + +func TestPeersHandshake(t *testing.T) { + defer testlog(t).detach() + var err error + // var sessionToken []byte + prv0, _ := crypto.GenerateKey() // = ecdsa.GenerateKey(crypto.S256(), rand.Reader) + pub0 := &prv0.PublicKey + prv1, _ := crypto.GenerateKey() + pub1 := &prv1.PublicKey + + prv0s := crypto.FromECDSA(prv0) + pub0s := crypto.FromECDSAPub(pub0) + prv1s := crypto.FromECDSA(prv1) + pub1s := crypto.FromECDSAPub(pub1) + + conn1, conn2 := net.Pipe() + initiator := newPeer(conn1, []Protocol{}, nil) + receiver := newPeer(conn2, []Protocol{}, nil) + initiator.dialAddr = &peerAddr{IP: net.ParseIP("1.2.3.4"), Port: 2222, Pubkey: pub1s[1:]} + initiator.ourID = &peerId{prv0s, pub0s} + + // this is cheating. identity of initiator/dialler not available to listener/receiver + // its public key should be looked up based on IP address + receiver.identity = initiator.ourID + receiver.ourID = &peerId{prv1s, pub1s} + + initiator.pubkeyHook = func(*peerAddr) error { return nil } + receiver.pubkeyHook = func(*peerAddr) error { return nil } + + initiator.cryptoHandshake = true + receiver.cryptoHandshake = true + errc0 := make(chan error, 1) + errc1 := make(chan error, 1) + go func() { + _, err := initiator.loop() + errc0 <- err + }() + go func() { + _, err := receiver.loop() + errc1 <- err + }() + select { + case err = <-errc0: + t.Errorf("peer 0 quit with error: %v", err) + case err = <-errc1: + t.Errorf("peer 1 quit with error: %v", err) + } +} From 1dda555230bc97b03cc4dd6f565d8ee88a430208 Mon Sep 17 00:00:00 2001 From: zelig Date: Wed, 21 Jan 2015 14:45:53 +0000 Subject: [PATCH 18/31] chop first byte when cryptoid.PubKeyS is set from identity.Pubkey() since this is directly copied in the auth message --- p2p/crypto.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/p2p/crypto.go b/p2p/crypto.go index 8551e317c7..91d60aa7e3 100644 --- a/p2p/crypto.go +++ b/p2p/crypto.go @@ -7,10 +7,13 @@ import ( "io" "github.com/ethereum/go-ethereum/crypto" + ethlogger "github.com/ethereum/go-ethereum/logger" "github.com/obscuren/ecies" "github.com/obscuren/secp256k1-go" ) +var clogger = ethlogger.NewLogger("CRYPTOID") + var ( sskLen int = 16 // ecies.MaxSharedKeyLength(pubKey) / 2 sigLen int = 65 // elliptic S256 @@ -62,10 +65,17 @@ func newCryptoId(id ClientIdentity) (self *cryptoId, err error) { // to be created at server init shared between peers and sessions // for reuse, call wth ReadAt, no reset seek needed } - self.pubKeyS = id.Pubkey() + self.pubKeyS = id.Pubkey()[1:] + clogger.Debugf("crytoid starting for %v", hexkey(self.pubKeyS)) return } +type hexkey []byte + +func (self hexkey) String() string { + return fmt.Sprintf("(%d) %x", len(self), []byte(self)) +} + /* Run(connection, remotePublicKey, sessionToken) is called when the peer connection starts to set up a secure session by performing a crypto handshake. From 92bd2532c98e7f03d8a549ad4c70816ee51c040d Mon Sep 17 00:00:00 2001 From: zelig Date: Wed, 21 Jan 2015 16:22:49 +0000 Subject: [PATCH 19/31] peer-level integration test for crypto handshake - add const length params for handshake messages - add length check to fail early - add debug logs to help interop testing (!ABSOLUTELY SHOULD BE DELETED LATER) - wrap connection read/writes in error check - add cryptoReady channel in peer to signal when secure session setup is finished - wait for cryptoReady or timeout in TestPeersHandshake --- p2p/crypto.go | 53 ++++++++++++++++++++++++++++++++++++++++------ p2p/crypto_test.go | 11 ++++++++++ p2p/peer.go | 22 +++++++++++-------- 3 files changed, 71 insertions(+), 15 deletions(-) diff --git a/p2p/crypto.go b/p2p/crypto.go index 91d60aa7e3..e8f4d551bd 100644 --- a/p2p/crypto.go +++ b/p2p/crypto.go @@ -21,6 +21,8 @@ var ( keyLen int = 32 // ECDSA msgLen int = 194 // sigLen + keyLen + pubLen + keyLen + 1 = 194 resLen int = 97 // pubLen + keyLen + 1 + iHSLen int = 307 // size of the final ECIES payload sent as initiator's handshake + rHSLen int = 210 // size of the final ECIES payload sent as receiver's handshake ) // secretRW implements a message read writer with encryption and authentication @@ -66,7 +68,8 @@ func newCryptoId(id ClientIdentity) (self *cryptoId, err error) { // for reuse, call wth ReadAt, no reset seek needed } self.pubKeyS = id.Pubkey()[1:] - clogger.Debugf("crytoid starting for %v", hexkey(self.pubKeyS)) + clogger.Debugf("initialise crypto for NodeId %v", hexkey(self.pubKeyS)) + clogger.Debugf("private-key %v\npublic key %v", hexkey(prvKeyS), hexkey(self.pubKeyS)) return } @@ -92,22 +95,51 @@ Run(connection, remotePublicKey, sessionToken) is called when the peer connectio func (self *cryptoId) Run(conn io.ReadWriter, remotePubKeyS []byte, sessionToken []byte, initiator bool) (token []byte, rw *secretRW, err error) { var auth, initNonce, recNonce []byte + var read int var randomPrivKey *ecdsa.PrivateKey var remoteRandomPubKey *ecdsa.PublicKey + clogger.Debugf("attempting session with %v", hexkey(remotePubKeyS)) if initiator { if auth, initNonce, randomPrivKey, _, err = self.startHandshake(remotePubKeyS, sessionToken); err != nil { return } - conn.Write(auth) - var response []byte - conn.Read(response) + clogger.Debugf("initiator-nonce: %v", hexkey(initNonce)) + clogger.Debugf("initiator-random-private-key: %v", hexkey(crypto.FromECDSA(randomPrivKey))) + randomPublicKeyS, _ := ExportPublicKey(&randomPrivKey.PublicKey) + clogger.Debugf("initiator-random-public-key: %v", hexkey(randomPublicKeyS)) + + if _, err = conn.Write(auth); err != nil { + return + } + clogger.Debugf("initiator handshake (sent to %v):\n%v", hexkey(remotePubKeyS), hexkey(auth)) + var response []byte = make([]byte, rHSLen) + if read, err = conn.Read(response); err != nil || read == 0 { + return + } + if read != rHSLen { + err = fmt.Errorf("remote receiver's handshake has invalid length. expect %v, got %v", rHSLen, read) + return + } // write out auth message // wait for response, then call complete if recNonce, remoteRandomPubKey, _, err = self.completeHandshake(response); err != nil { return } + clogger.Debugf("receiver-nonce: %v", hexkey(recNonce)) + remoteRandomPubKeyS, _ := ExportPublicKey(remoteRandomPubKey) + clogger.Debugf("receiver-random-public-key: %v", hexkey(remoteRandomPubKeyS)) + } else { - conn.Read(auth) + auth = make([]byte, iHSLen) + clogger.Debugf("waiting for initiator handshake (from %v)", hexkey(remotePubKeyS)) + if read, err = conn.Read(auth); err != nil { + return + } + if read != iHSLen { + err = fmt.Errorf("remote initiator's handshake has invalid length. expect %v, got %v", iHSLen, read) + return + } + clogger.Debugf("received initiator handshake (from %v):\n%v", hexkey(remotePubKeyS), hexkey(auth)) // we are listening connection. we are responders in the handshake. // Extract info from the authentication. The initiator starts by sending us a handshake that we need to respond to. // so we read auth message first, then respond @@ -115,7 +147,12 @@ func (self *cryptoId) Run(conn io.ReadWriter, remotePubKeyS []byte, sessionToken if response, recNonce, initNonce, randomPrivKey, remoteRandomPubKey, err = self.respondToHandshake(auth, remotePubKeyS, sessionToken); err != nil { return } - conn.Write(response) + clogger.Debugf("receiver-nonce: %v", hexkey(recNonce)) + clogger.Debugf("receiver-random-priv-key: %v", hexkey(crypto.FromECDSA(randomPrivKey))) + if _, err = conn.Write(response); err != nil { + return + } + clogger.Debugf("receiver handshake (sent to %v):\n%v", hexkey(remotePubKeyS), hexkey(response)) } return self.newSession(initNonce, recNonce, auth, randomPrivKey, remoteRandomPubKey) } @@ -354,6 +391,10 @@ func (self *cryptoId) newSession(initNonce, respNonce, auth []byte, privKey *ecd egressMac: egressMac, ingressMac: ingressMac, } + clogger.Debugf("aes-secret: %v", hexkey(aesSecret)) + clogger.Debugf("mac-secret: %v", hexkey(macSecret)) + clogger.Debugf("egress-mac: %v", hexkey(egressMac)) + clogger.Debugf("ingress-mac: %v", hexkey(ingressMac)) return } diff --git a/p2p/crypto_test.go b/p2p/crypto_test.go index 5fbdc61e3e..47b16040ae 100644 --- a/p2p/crypto_test.go +++ b/p2p/crypto_test.go @@ -8,6 +8,7 @@ import ( "fmt" "net" "testing" + "time" "github.com/ethereum/go-ethereum/crypto" "github.com/obscuren/ecies" @@ -184,7 +185,17 @@ func TestPeersHandshake(t *testing.T) { _, err := receiver.loop() errc1 <- err }() + ready := make(chan bool) + go func() { + <-initiator.cryptoReady + <-receiver.cryptoReady + close(ready) + }() + timeout := time.After(1 * time.Second) select { + case <-ready: + case <-timeout: + t.Errorf("crypto handshake hanging for too long") case err = <-errc0: t.Errorf("peer 0 quit with error: %v", err) case err = <-errc1: diff --git a/p2p/peer.go b/p2p/peer.go index e44eaab343..818f805802 100644 --- a/p2p/peer.go +++ b/p2p/peer.go @@ -71,6 +71,7 @@ type Peer struct { protocols []Protocol runBaseProtocol bool // for testing cryptoHandshake bool // for testing + cryptoReady chan struct{} runlock sync.RWMutex // protects running running map[string]*proto @@ -120,15 +121,16 @@ func newServerPeer(server *Server, conn net.Conn, dialAddr *peerAddr) *Peer { func newPeer(conn net.Conn, protocols []Protocol, dialAddr *peerAddr) *Peer { p := &Peer{ - Logger: logger.NewLogger("P2P " + conn.RemoteAddr().String()), - conn: conn, - dialAddr: dialAddr, - bufconn: bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn)), - protocols: protocols, - running: make(map[string]*proto), - disc: make(chan DiscReason), - protoErr: make(chan error), - closed: make(chan struct{}), + Logger: logger.NewLogger("P2P " + conn.RemoteAddr().String()), + conn: conn, + dialAddr: dialAddr, + bufconn: bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn)), + protocols: protocols, + running: make(map[string]*proto), + disc: make(chan DiscReason), + protoErr: make(chan error), + closed: make(chan struct{}), + cryptoReady: make(chan struct{}), } return p } @@ -240,6 +242,7 @@ func (p *Peer) loop() (reason DiscReason, err error) { go readLoop(readMsg, readErr, readNext) readNext <- true + close(p.cryptoReady) if p.runBaseProtocol { p.startBaseProtocol() } @@ -353,6 +356,7 @@ func (p *Peer) handleCryptoHandshake() (loop readLoop, err error) { // this bit handles the handshake and creates a secure communications channel with // var rw *secretRW if sessionToken, _, err = crypto.Run(p.conn, p.Pubkey(), sessionToken, initiator); err != nil { + p.Debugf("unable to setup secure session: %v", err) return } loop = func(msg chan<- Msg, err chan<- error, next <-chan bool) { From c5001a4722ab5e1dbc9710a0fc44da8bb1314aae Mon Sep 17 00:00:00 2001 From: zelig Date: Wed, 21 Jan 2015 16:53:13 +0000 Subject: [PATCH 20/31] add temporary forced session token generation --- p2p/crypto.go | 3 +++ p2p/peer.go | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/p2p/crypto.go b/p2p/crypto.go index e8f4d551bd..f5307cd5a7 100644 --- a/p2p/crypto.go +++ b/p2p/crypto.go @@ -103,6 +103,9 @@ func (self *cryptoId) Run(conn io.ReadWriter, remotePubKeyS []byte, sessionToken if auth, initNonce, randomPrivKey, _, err = self.startHandshake(remotePubKeyS, sessionToken); err != nil { return } + if sessionToken != nil { + clogger.Debugf("session-token: %v", hexkey(sessionToken)) + } clogger.Debugf("initiator-nonce: %v", hexkey(initNonce)) clogger.Debugf("initiator-random-private-key: %v", hexkey(crypto.FromECDSA(randomPrivKey))) randomPublicKeyS, _ := ExportPublicKey(&randomPrivKey.PublicKey) diff --git a/p2p/peer.go b/p2p/peer.go index 818f805802..99f1a61d38 100644 --- a/p2p/peer.go +++ b/p2p/peer.go @@ -3,6 +3,7 @@ package p2p import ( "bufio" "bytes" + "crypto/rand" "fmt" "io" "io/ioutil" @@ -342,6 +343,10 @@ func (p *Peer) handleCryptoHandshake() (loop readLoop, err error) { // it is survived by an encrypted readwriter var initiator bool var sessionToken []byte + sessionToken = make([]byte, keyLen) + if _, err = rand.Read(sessionToken); err != nil { + return + } if p.dialAddr != nil { // this should have its own method Outgoing() bool initiator = true } From 1853903fc037ed07c4d6a2cd8036efb35f6b9edb Mon Sep 17 00:00:00 2001 From: zelig Date: Wed, 21 Jan 2015 19:43:29 +0000 Subject: [PATCH 21/31] reorg and comment to prepare for refactor of read/write --- p2p/message.go | 135 ++++++++++++----------------------- p2p/messenger.go | 178 +++++++++++++++++++++++++++++++++++++++++++++++ p2p/peer.go | 89 ------------------------ 3 files changed, 222 insertions(+), 180 deletions(-) create mode 100644 p2p/messenger.go diff --git a/p2p/message.go b/p2p/message.go index daf2bf05c1..09eefb7658 100644 --- a/p2p/message.go +++ b/p2p/message.go @@ -2,12 +2,10 @@ package p2p import ( "bytes" - "encoding/binary" "errors" "fmt" "io" "io/ioutil" - "math/big" "sync/atomic" "github.com/ethereum/go-ethereum/ethutil" @@ -23,7 +21,7 @@ import ( // separate Msg with a bytes.Reader as Payload for each send. type Msg struct { Code uint64 - Size uint32 // size of the paylod + Size uint32 // size of the payload Payload io.Reader } @@ -36,6 +34,7 @@ func NewMsg(code uint64, params ...interface{}) Msg { return Msg{Code: code, Size: uint32(buf.Len()), Payload: buf} } +// this should probably use the new rlp encoding func encodePayload(params ...interface{}) []byte { buf := new(bytes.Buffer) for _, p := range params { @@ -66,6 +65,11 @@ func (msg Msg) Discard() error { return err } +/* +MsgReadWriter is an interface for reading and writing messages +It is aware of message structure and knows how to encode/decode +*/ + type MsgReader interface { ReadMsg() (Msg, error) } @@ -84,89 +88,15 @@ type MsgReadWriter interface { MsgWriter } +// EncodeMsg should be explicit in the interface and should have the MsgWriter +// as receiver + // EncodeMsg writes an RLP-encoded message with the given code and // data elements. func EncodeMsg(w MsgWriter, code uint64, data ...interface{}) error { return w.WriteMsg(NewMsg(code, data...)) } -var magicToken = []byte{34, 64, 8, 145} - -func writeMsg(w io.Writer, msg Msg) error { - // TODO: handle case when Size + len(code) + len(listhdr) overflows uint32 - code := ethutil.Encode(uint32(msg.Code)) - listhdr := makeListHeader(msg.Size + uint32(len(code))) - payloadLen := uint32(len(listhdr)) + uint32(len(code)) + msg.Size - - start := make([]byte, 8) - copy(start, magicToken) - binary.BigEndian.PutUint32(start[4:], payloadLen) - - for _, b := range [][]byte{start, listhdr, code} { - if _, err := w.Write(b); err != nil { - return err - } - } - _, err := io.CopyN(w, msg.Payload, int64(msg.Size)) - return err -} - -func makeListHeader(length uint32) []byte { - if length < 56 { - return []byte{byte(length + 0xc0)} - } - enc := big.NewInt(int64(length)).Bytes() - lenb := byte(len(enc)) + 0xf7 - return append([]byte{lenb}, enc...) -} - -// readMsg reads a message header from r. -// It takes an rlp.ByteReader to ensure that the decoding doesn't buffer. -func readMsg(r rlp.ByteReader) (msg Msg, err error) { - // read magic and payload size - start := make([]byte, 8) - if _, err = io.ReadFull(r, start); err != nil { - return msg, newPeerError(errRead, "%v", err) - } - if !bytes.HasPrefix(start, magicToken) { - return msg, newPeerError(errMagicTokenMismatch, "got %x, want %x", start[:4], magicToken) - } - size := binary.BigEndian.Uint32(start[4:]) - - // decode start of RLP message to get the message code - posr := &postrack{r, 0} - s := rlp.NewStream(posr) - if _, err := s.List(); err != nil { - return msg, err - } - code, err := s.Uint() - if err != nil { - return msg, err - } - payloadsize := size - posr.p - return Msg{code, payloadsize, io.LimitReader(r, int64(payloadsize))}, nil -} - -// postrack wraps an rlp.ByteReader with a position counter. -type postrack struct { - r rlp.ByteReader - p uint32 -} - -func (r *postrack) Read(buf []byte) (int, error) { - n, err := r.r.Read(buf) - r.p += uint32(n) - return n, err -} - -func (r *postrack) ReadByte() (byte, error) { - b, err := r.r.ReadByte() - if err == nil { - r.p++ - } - return b, err -} - // MsgPipe creates a message pipe. Reads on one end are matched // with writes on the other. The pipe is full-duplex, both ends // implement MsgReadWriter. @@ -185,6 +115,19 @@ func MsgPipe() (*MsgPipeRW, *MsgPipeRW) { // pipe has been closed. var ErrPipeClosed = errors.New("p2p: read or write on closed message pipe") +// Close unblocks any pending ReadMsg and WriteMsg calls on both ends +// of the pipe. They will return ErrPipeClosed. Note that Close does +// not interrupt any reads from a message payload. +func (p *MsgPipeRW) Close() error { + if atomic.AddInt32(p.closed, 1) != 1 { + // someone else is already closing + atomic.StoreInt32(p.closed, 1) // avoid overflow + return nil + } + close(p.closing) + return nil +} + // MsgPipeRW is an endpoint of a MsgReadWriter pipe. type MsgPipeRW struct { w chan<- Msg @@ -224,15 +167,25 @@ func (p *MsgPipeRW) ReadMsg() (Msg, error) { return Msg{}, ErrPipeClosed } -// Close unblocks any pending ReadMsg and WriteMsg calls on both ends -// of the pipe. They will return ErrPipeClosed. Note that Close does -// not interrupt any reads from a message payload. -func (p *MsgPipeRW) Close() error { - if atomic.AddInt32(p.closed, 1) != 1 { - // someone else is already closing - atomic.StoreInt32(p.closed, 1) // avoid overflow - return nil - } - close(p.closing) - return nil +// eofSignal wraps a reader with eof signaling. the eof channel is +// closed when the wrapped reader returns an error or when count bytes +// have been read. +// +type eofSignal struct { + wrapped io.Reader + count int64 + eof chan<- struct{} +} + +// note: when using eofSignal to detect whether a message payload +// has been read, Read might not be called for zero sized messages. + +func (r *eofSignal) Read(buf []byte) (int, error) { + n, err := r.wrapped.Read(buf) + r.count -= int64(n) + if (err != nil || r.count <= 0) && r.eof != nil { + r.eof <- struct{}{} // tell Peer that msg has been consumed + r.eof = nil + } + return n, err } diff --git a/p2p/messenger.go b/p2p/messenger.go new file mode 100644 index 0000000000..57268a6542 --- /dev/null +++ b/p2p/messenger.go @@ -0,0 +1,178 @@ +package p2p + +import ( + "bytes" + "encoding/binary" + "fmt" + "io" + "math/big" + "time" + + "github.com/ethereum/go-ethereum/ethutil" + "github.com/ethereum/go-ethereum/rlp" +) + +var magicToken = []byte{34, 64, 8, 145} + +/* + writeMsg, readMsg (makeListHeader) should all be internal to the default legacy + MsgReadWriter implementation + + A MsgReadWriter implementation will typically sit on a multiplexed peer connection and runs a single read and a write loop without need to use locking. + + It passes on incoming messages to its channel picked up by the interface methods Read + the peer runs a dispatch loop that figures out which protocol to forward the message to. + + Because framing and header structure will change there will be hardly any overlap with the new code so I do not abstract readers any further. +*/ +func (p *Peer) readLoop(msgc chan<- Msg, errc chan<- error, unblock <-chan bool) { + for _ = range unblock { + p.conn.SetReadDeadline(time.Now().Add(msgReadTimeout)) + if msg, err := readMsg(p.bufconn); err != nil { + errc <- err + } else { + msgc <- msg + } + } + close(errc) +} + +func writeMsg(w io.Writer, msg Msg) error { + // TODO: handle case when Size + len(code) + len(listhdr) overflows uint32 + code := ethutil.Encode(uint32(msg.Code)) + listhdr := makeListHeader(msg.Size + uint32(len(code))) + payloadLen := uint32(len(listhdr)) + uint32(len(code)) + msg.Size + + start := make([]byte, 8) + copy(start, magicToken) + binary.BigEndian.PutUint32(start[4:], payloadLen) + + for _, b := range [][]byte{start, listhdr, code} { + if _, err := w.Write(b); err != nil { + return err + } + } + _, err := io.CopyN(w, msg.Payload, int64(msg.Size)) + return err +} + +func makeListHeader(length uint32) []byte { + if length < 56 { + return []byte{byte(length + 0xc0)} + } + enc := big.NewInt(int64(length)).Bytes() + lenb := byte(len(enc)) + 0xf7 + return append([]byte{lenb}, enc...) +} + +// readMsg reads a message header from r. +// It takes an rlp.ByteReader to ensure that the decoding doesn't buffer. +func readMsg(r rlp.ByteReader) (msg Msg, err error) { + // read magic and payload size + start := make([]byte, 8) + if _, err = io.ReadFull(r, start); err != nil { + return msg, newPeerError(errRead, "%v", err) + } + if !bytes.HasPrefix(start, magicToken) { + return msg, newPeerError(errMagicTokenMismatch, "got %x, want %x", start[:4], magicToken) + } + size := binary.BigEndian.Uint32(start[4:]) + + // decode start of RLP message to get the message code + posr := &postrack{r, 0} + s := rlp.NewStream(posr) + if _, err := s.List(); err != nil { + return msg, err + } + code, err := s.Uint() + if err != nil { + return msg, err + } + payloadsize := size - posr.p + return Msg{code, payloadsize, io.LimitReader(r, int64(payloadsize))}, nil +} + +// postrack wraps an rlp.ByteReader with a position counter. +type postrack struct { + r rlp.ByteReader + p uint32 +} + +func (r *postrack) Read(buf []byte) (int, error) { + n, err := r.r.Read(buf) + r.p += uint32(n) + return n, err +} + +func (r *postrack) ReadByte() (byte, error) { + b, err := r.r.ReadByte() + if err == nil { + r.p++ + } + return b, err +} + +// this duplicates functionality of proto.WriteMsg +// if we need this for broadcasting via a server interface then +// simply call the appropriate Write function of the protocol RW +// writeProtoMsg sends the given message on behalf of the given named protocol. +func (p *Peer) writeProtoMsg(protoName string, msg Msg) error { + p.runlock.RLock() + proto, ok := p.running[protoName] + p.runlock.RUnlock() + if !ok { + return fmt.Errorf("protocol %s not handled by peer", protoName) + } + if msg.Code >= proto.maxcode { + return newPeerError(errInvalidMsgCode, "code %x is out of range for protocol %q", msg.Code, protoName) + } + msg.Code += proto.offset + return p.writeMsg(msg, msgWriteTimeout) +} + +/* +this function is not needed write will be done directly by the msgReadWriter +with the connection attached +if the interface is channel, then no write locking is needed for synchronous write +*/ +// writeMsg writes a message to the connection. +func (p *Peer) writeMsg(msg Msg, timeout time.Duration) error { + p.writeMu.Lock() + defer p.writeMu.Unlock() + p.conn.SetWriteDeadline(time.Now().Add(timeout)) + if err := writeMsg(p.bufconn, msg); err != nil { + return newPeerError(errWrite, "%v", err) + } + return p.bufconn.Flush() +} + +// proto will embed the same writer channel as given to the readwriter +// in the legacy code it knows about the code offset +// no need to go through peer for writing , so do not need to embed peer as field +type proto struct { + name string + in chan Msg + maxcode, offset uint64 + peer *Peer +} + +func (rw *proto) WriteMsg(msg Msg) error { + if msg.Code >= rw.maxcode { + return newPeerError(errInvalidMsgCode, "not handled") + } + msg.Code += rw.offset + return rw.peer.writeMsg(msg, msgWriteTimeout) +} + +func (rw *proto) EncodeMsg(code uint64, data ...interface{}) error { + return rw.WriteMsg(NewMsg(code, data...)) +} + +func (rw *proto) ReadMsg() (Msg, error) { + msg, ok := <-rw.in + if !ok { + return msg, io.EOF + } + msg.Code -= rw.offset + return msg, nil +} diff --git a/p2p/peer.go b/p2p/peer.go index 99f1a61d38..c0cf1dee0c 100644 --- a/p2p/peer.go +++ b/p2p/peer.go @@ -301,18 +301,6 @@ loop: return reason, err } -func (p *Peer) readLoop(msgc chan<- Msg, errc chan<- error, unblock <-chan bool) { - for _ = range unblock { - p.conn.SetReadDeadline(time.Now().Add(msgReadTimeout)) - if msg, err := readMsg(p.bufconn); err != nil { - errc <- err - } else { - msgc <- msg - } - } - close(errc) -} - func (p *Peer) dispatch(msg Msg, protoDone chan struct{}) (wait bool, err error) { proto, err := p.getProto(msg.Code) if err != nil { @@ -446,80 +434,3 @@ func (p *Peer) closeProtocols() { p.runlock.RUnlock() p.protoWG.Wait() } - -// writeProtoMsg sends the given message on behalf of the given named protocol. -func (p *Peer) writeProtoMsg(protoName string, msg Msg) error { - p.runlock.RLock() - proto, ok := p.running[protoName] - p.runlock.RUnlock() - if !ok { - return fmt.Errorf("protocol %s not handled by peer", protoName) - } - if msg.Code >= proto.maxcode { - return newPeerError(errInvalidMsgCode, "code %x is out of range for protocol %q", msg.Code, protoName) - } - msg.Code += proto.offset - return p.writeMsg(msg, msgWriteTimeout) -} - -// writeMsg writes a message to the connection. -func (p *Peer) writeMsg(msg Msg, timeout time.Duration) error { - p.writeMu.Lock() - defer p.writeMu.Unlock() - p.conn.SetWriteDeadline(time.Now().Add(timeout)) - if err := writeMsg(p.bufconn, msg); err != nil { - return newPeerError(errWrite, "%v", err) - } - return p.bufconn.Flush() -} - -type proto struct { - name string - in chan Msg - maxcode, offset uint64 - peer *Peer -} - -func (rw *proto) WriteMsg(msg Msg) error { - if msg.Code >= rw.maxcode { - return newPeerError(errInvalidMsgCode, "not handled") - } - msg.Code += rw.offset - return rw.peer.writeMsg(msg, msgWriteTimeout) -} - -func (rw *proto) EncodeMsg(code uint64, data ...interface{}) error { - return rw.WriteMsg(NewMsg(code, data...)) -} - -func (rw *proto) ReadMsg() (Msg, error) { - msg, ok := <-rw.in - if !ok { - return msg, io.EOF - } - msg.Code -= rw.offset - return msg, nil -} - -// eofSignal wraps a reader with eof signaling. the eof channel is -// closed when the wrapped reader returns an error or when count bytes -// have been read. -// -type eofSignal struct { - wrapped io.Reader - count int64 - eof chan<- struct{} -} - -// note: when using eofSignal to detect whether a message payload -// has been read, Read might not be called for zero sized messages. - -func (r *eofSignal) Read(buf []byte) (int, error) { - n, err := r.wrapped.Read(buf) - r.count -= int64(n) - if (err != nil || r.count <= 0) && r.eof != nil { - r.eof <- struct{}{} // tell Peer that msg has been consumed - r.eof = nil - } - return n, err -} From 7a33109e855f825c9cee92b02eb462a3e5872b2c Mon Sep 17 00:00:00 2001 From: zelig Date: Wed, 21 Jan 2015 21:46:40 +0000 Subject: [PATCH 22/31] write via proto.out channel - read/write timeout constants moved to messenger - add writeLoop (temporarily a method on Peer) that feeds on a channel and writes to bufconn - add outgoing message channel to proto readwriter - outgoing message channel is a field of Peer and shared between protocols (no need to lock) - setting write timeout per message is no longer possible! - read and write loops share error channel. both watched by the dispatch loop in peer --- p2p/messenger.go | 30 +++++++++++++++++++++++++--- p2p/peer.go | 52 +++++++++++++++++++++--------------------------- p2p/peer_test.go | 1 + 3 files changed, 51 insertions(+), 32 deletions(-) diff --git a/p2p/messenger.go b/p2p/messenger.go index 57268a6542..56cd0c9da0 100644 --- a/p2p/messenger.go +++ b/p2p/messenger.go @@ -12,6 +12,16 @@ import ( "github.com/ethereum/go-ethereum/rlp" ) +const ( + // maximum amount of time allowed for reading a message + msgReadTimeout = 5 * time.Second + // maximum amount of time allowed for writing a message + msgWriteTimeout = 5 * time.Second + // messages smaller than this many bytes will be read at + // once before passing them to a protocol. + wholePayloadSize = 64 * 1024 +) + var magicToken = []byte{34, 64, 8, 145} /* @@ -37,6 +47,16 @@ func (p *Peer) readLoop(msgc chan<- Msg, errc chan<- error, unblock <-chan bool) close(errc) } +func (p *Peer) writeLoop(msgc <-chan Msg, errc chan<- error) { + p.conn.SetWriteDeadline(time.Now().Add(msgWriteTimeout)) + for msg := range msgc { + if err := writeMsg(p.bufconn, msg); err != nil { + errc <- newPeerError(errWrite, "%v", err) + } + p.bufconn.Flush() + } +} + func writeMsg(w io.Writer, msg Msg) error { // TODO: handle case when Size + len(code) + len(listhdr) overflows uint32 code := ethutil.Encode(uint32(msg.Code)) @@ -151,9 +171,8 @@ func (p *Peer) writeMsg(msg Msg, timeout time.Duration) error { // no need to go through peer for writing , so do not need to embed peer as field type proto struct { name string - in chan Msg + in, out chan Msg maxcode, offset uint64 - peer *Peer } func (rw *proto) WriteMsg(msg Msg) error { @@ -161,7 +180,12 @@ func (rw *proto) WriteMsg(msg Msg) error { return newPeerError(errInvalidMsgCode, "not handled") } msg.Code += rw.offset - return rw.peer.writeMsg(msg, msgWriteTimeout) + select { + case rw.out <- msg: + return nil + case <-time.After(msgWriteTimeout): + return newPeerError(errWrite, "messenger timeout") + } } func (rw *proto) EncodeMsg(code uint64, data ...interface{}) error { diff --git a/p2p/peer.go b/p2p/peer.go index c0cf1dee0c..a9a4cea591 100644 --- a/p2p/peer.go +++ b/p2p/peer.go @@ -64,9 +64,10 @@ type Peer struct { // The mutex protects the connection // so only one protocol can write at a time. - writeMu sync.Mutex - conn net.Conn - bufconn *bufio.ReadWriter + writeMu sync.Mutex + conn net.Conn + bufconn *bufio.ReadWriter + outgoingMsgC chan Msg // These fields maintain the running protocols. protocols []Protocol @@ -122,16 +123,17 @@ func newServerPeer(server *Server, conn net.Conn, dialAddr *peerAddr) *Peer { func newPeer(conn net.Conn, protocols []Protocol, dialAddr *peerAddr) *Peer { p := &Peer{ - Logger: logger.NewLogger("P2P " + conn.RemoteAddr().String()), - conn: conn, - dialAddr: dialAddr, - bufconn: bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn)), - protocols: protocols, - running: make(map[string]*proto), - disc: make(chan DiscReason), - protoErr: make(chan error), - closed: make(chan struct{}), - cryptoReady: make(chan struct{}), + Logger: logger.NewLogger("P2P " + conn.RemoteAddr().String()), + conn: conn, + dialAddr: dialAddr, + bufconn: bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn)), + protocols: protocols, + running: make(map[string]*proto), + disc: make(chan DiscReason), + protoErr: make(chan error), + closed: make(chan struct{}), + cryptoReady: make(chan struct{}), + outgoingMsgC: make(chan Msg), } return p } @@ -204,16 +206,6 @@ func (p *Peer) String() string { return fmt.Sprintf("Peer(%p %v %s)", p, p.conn.RemoteAddr(), kind) } -const ( - // maximum amount of time allowed for reading a message - msgReadTimeout = 5 * time.Second - // maximum amount of time allowed for writing a message - msgWriteTimeout = 5 * time.Second - // messages smaller than this many bytes will be read at - // once before passing them to a protocol. - wholePayloadSize = 64 * 1024 -) - var ( inactivityTimeout = 2 * time.Second disconnectGracePeriod = 2 * time.Second @@ -237,10 +229,11 @@ func (p *Peer) loop() (reason DiscReason, err error) { // read loop readMsg := make(chan Msg) - readErr := make(chan error) + rwErr := make(chan error) readNext := make(chan bool, 1) protoDone := make(chan struct{}, 1) - go readLoop(readMsg, readErr, readNext) + go readLoop(readMsg, rwErr, readNext) + go p.writeLoop(p.outgoingMsgC, rwErr) readNext <- true close(p.cryptoReady) @@ -269,8 +262,8 @@ loop: // we can continue reading from the socket. readNext <- true - case err := <-readErr: - // read failed. there is no need to run the + case err := <-rwErr: + // read or write failed. there is no need to run the // polite disconnect sequence because the connection // is probably dead anyway. // TODO: handle write errors as well @@ -284,8 +277,9 @@ loop: } // wait for read loop to return. + close(p.outgoingMsgC) close(readNext) - <-readErr + <-rwErr // tell the remote end to disconnect done := make(chan struct{}) go func() { @@ -391,9 +385,9 @@ outer: func (p *Peer) startProto(offset uint64, impl Protocol) *proto { rw := &proto{ in: make(chan Msg), + out: p.outgoingMsgC, offset: offset, maxcode: impl.Length, - peer: p, } p.protoWG.Add(1) go func() { diff --git a/p2p/peer_test.go b/p2p/peer_test.go index 4ee88f112b..814421cb82 100644 --- a/p2p/peer_test.go +++ b/p2p/peer_test.go @@ -33,6 +33,7 @@ func testPeer(protos []Protocol) (net.Conn, *Peer, <-chan error) { peer := newPeer(conn1, protos, nil) peer.ourID = &peerId{} peer.pubkeyHook = func(*peerAddr) error { return nil } + peer.outgoingMsgC = make(chan Msg) errc := make(chan error, 1) go func() { _, err := peer.loop() From b29b7fedd0ea27012a41db17c524a83800075026 Mon Sep 17 00:00:00 2001 From: zelig Date: Wed, 21 Jan 2015 22:03:28 +0000 Subject: [PATCH 23/31] remove peer.writeMsg - remove it, its redundant - remove duplicate code in peer.writeProtoMsg, now falling back to proto.WriteMsg - remove peer.writeMu, no need for mutex - no timer set in p.WriteMsg, - write will be async for the protocol anyway, so no write timeout argument anywhere, only constant set for conn writeDeadline --- p2p/messenger.go | 30 +++--------------------------- p2p/peer.go | 5 +---- 2 files changed, 4 insertions(+), 31 deletions(-) diff --git a/p2p/messenger.go b/p2p/messenger.go index 56cd0c9da0..cc56233037 100644 --- a/p2p/messenger.go +++ b/p2p/messenger.go @@ -143,27 +143,7 @@ func (p *Peer) writeProtoMsg(protoName string, msg Msg) error { if !ok { return fmt.Errorf("protocol %s not handled by peer", protoName) } - if msg.Code >= proto.maxcode { - return newPeerError(errInvalidMsgCode, "code %x is out of range for protocol %q", msg.Code, protoName) - } - msg.Code += proto.offset - return p.writeMsg(msg, msgWriteTimeout) -} - -/* -this function is not needed write will be done directly by the msgReadWriter -with the connection attached -if the interface is channel, then no write locking is needed for synchronous write -*/ -// writeMsg writes a message to the connection. -func (p *Peer) writeMsg(msg Msg, timeout time.Duration) error { - p.writeMu.Lock() - defer p.writeMu.Unlock() - p.conn.SetWriteDeadline(time.Now().Add(timeout)) - if err := writeMsg(p.bufconn, msg); err != nil { - return newPeerError(errWrite, "%v", err) - } - return p.bufconn.Flush() + return proto.WriteMsg(msg) } // proto will embed the same writer channel as given to the readwriter @@ -180,12 +160,8 @@ func (rw *proto) WriteMsg(msg Msg) error { return newPeerError(errInvalidMsgCode, "not handled") } msg.Code += rw.offset - select { - case rw.out <- msg: - return nil - case <-time.After(msgWriteTimeout): - return newPeerError(errWrite, "messenger timeout") - } + rw.out <- msg + return nil } func (rw *proto) EncodeMsg(code uint64, data ...interface{}) error { diff --git a/p2p/peer.go b/p2p/peer.go index a9a4cea591..f63bba1ba6 100644 --- a/p2p/peer.go +++ b/p2p/peer.go @@ -62,9 +62,6 @@ type Peer struct { listenAddr *peerAddr // what remote peer is listening on dialAddr *peerAddr // non-nil if dialing - // The mutex protects the connection - // so only one protocol can write at a time. - writeMu sync.Mutex conn net.Conn bufconn *bufio.ReadWriter outgoingMsgC chan Msg @@ -284,7 +281,7 @@ loop: done := make(chan struct{}) go func() { p.conn.SetDeadline(time.Now().Add(disconnectGracePeriod)) - p.writeMsg(NewMsg(discMsg, reason), disconnectGracePeriod) + p.writeProtoMsg("", NewMsg(discMsg, reason)) io.Copy(ioutil.Discard, p.conn) close(done) }() From 4bfe53eb6ed2d3decb615b31c6da91ac8aab1d89 Mon Sep 17 00:00:00 2001 From: zelig Date: Thu, 22 Jan 2015 00:21:38 +0000 Subject: [PATCH 24/31] Messenger and SecureMessenger implement MsgChanReadWriter - implement peer readLoop and writeLoop as internals to Messenger - cryptoid.Run -> NewSession - returns a func(net.Conn) MsgChanReadWriter - MsgChanReadWriter channel read writer interface - SecureMessenger embeds Messenger - Peer fields bufconn moved to Messenger - no more need for short-lived outgoingMsgC - modify peer.loop to use a MsgChanReadWriter --- p2p/crypto.go | 30 +++++++++++---- p2p/crypto_test.go | 17 +++++++-- p2p/messenger.go | 91 ++++++++++++++++++++++++++++++++++++++-------- p2p/peer.go | 85 +++++++++++++++++-------------------------- p2p/peer_test.go | 2 +- p2p/server_test.go | 1 + 6 files changed, 146 insertions(+), 80 deletions(-) diff --git a/p2p/crypto.go b/p2p/crypto.go index f5307cd5a7..622e2608c7 100644 --- a/p2p/crypto.go +++ b/p2p/crypto.go @@ -5,6 +5,7 @@ import ( "crypto/rand" "fmt" "io" + "net" "github.com/ethereum/go-ethereum/crypto" ethlogger "github.com/ethereum/go-ethereum/logger" @@ -93,7 +94,7 @@ Run(connection, remotePublicKey, sessionToken) is called when the peer connectio It returns a secretRW which implements the MsgReadWriter interface. */ -func (self *cryptoId) Run(conn io.ReadWriter, remotePubKeyS []byte, sessionToken []byte, initiator bool) (token []byte, rw *secretRW, err error) { +func (self *cryptoId) NewSession(conn io.ReadWriter, remotePubKeyS []byte, sessionToken []byte, initiator bool) (token []byte, rwF func(net.Conn) MsgChanReadWriter, err error) { var auth, initNonce, recNonce []byte var read int var randomPrivKey *ecdsa.PrivateKey @@ -364,7 +365,7 @@ func (self *cryptoId) completeHandshake(auth []byte) (respNonce []byte, remoteRa /* newSession is called after the handshake is completed. The arguments are values negotiated in the handshake and the return value is a new session : a new session Token to be remembered for the next time we connect with this peer. And a MsgReadWriter that implements an encrypted and authenticated connection with key material obtained from the crypto handshake key exchange */ -func (self *cryptoId) newSession(initNonce, respNonce, auth []byte, privKey *ecdsa.PrivateKey, remoteRandomPubKey *ecdsa.PublicKey) (sessionToken []byte, rw *secretRW, err error) { +func (self *cryptoId) newSession(initNonce, respNonce, auth []byte, privKey *ecdsa.PrivateKey, remoteRandomPubKey *ecdsa.PublicKey) (sessionToken []byte, rwF func(net.Conn) MsgChanReadWriter, err error) { // 3) Now we can trust ecdhe-random-pubk to derive new keys //ecdhe-shared-secret = ecdh.agree(ecdhe-random, remote-ecdhe-random-pubk) var dhSharedSecret []byte @@ -388,16 +389,15 @@ func (self *cryptoId) newSession(initNonce, respNonce, auth []byte, privKey *ecd // ingress-mac = crypto.Sha3(mac-secret^initiator-nonce || auth), var ingressMac = crypto.Sha3(append(Xor(macSecret, initNonce), auth...)) // # destroy remote-nonce - rw = &secretRW{ - aesSecret: aesSecret, - macSecret: macSecret, - egressMac: egressMac, - ingressMac: ingressMac, - } + clogger.Debugf("aes-secret: %v", hexkey(aesSecret)) clogger.Debugf("mac-secret: %v", hexkey(macSecret)) clogger.Debugf("egress-mac: %v", hexkey(egressMac)) clogger.Debugf("ingress-mac: %v", hexkey(ingressMac)) + + rwF = func(conn net.Conn) MsgChanReadWriter { + return NewSecureMessenger(conn, aesSecret, macSecret, egressMac, ingressMac) + } return } @@ -410,3 +410,17 @@ func Xor(one, other []byte) (xor []byte) { } return } + +type SecureMessenger struct { + Messenger + aesSecret, macSecret, egressMac, ingressMac []byte +} + +func NewSecureMessenger(conn net.Conn, aesSecret, macSecret, egressMac, ingressMac []byte) *SecureMessenger { + return &SecureMessenger{ + aesSecret: aesSecret, + macSecret: macSecret, + egressMac: egressMac, + ingressMac: ingressMac, + } +} diff --git a/p2p/crypto_test.go b/p2p/crypto_test.go index 47b16040ae..582eabe14b 100644 --- a/p2p/crypto_test.go +++ b/p2p/crypto_test.go @@ -106,12 +106,12 @@ func TestCryptoHandshake(t *testing.T) { } // now both parties should have the same session parameters - initSessionToken, initSecretRW, err := initiator.newSession(initNonce, recNonce, auth, randomPrivKey, remoteRandomPubKey) + initSessionToken, initSecretRWF, err := initiator.newSession(initNonce, recNonce, auth, randomPrivKey, remoteRandomPubKey) if err != nil { t.Errorf("%v", err) } - recSessionToken, recSecretRW, err := receiver.newSession(remoteInitNonce, remoteRecNonce, auth, remoteRandomPrivKey, remoteInitRandomPubKey) + recSessionToken, recSecretRWF, err := receiver.newSession(remoteInitNonce, remoteRecNonce, auth, remoteRandomPrivKey, remoteInitRandomPubKey) if err != nil { t.Errorf("%v", err) } @@ -129,6 +129,17 @@ func TestCryptoHandshake(t *testing.T) { if !bytes.Equal(initSessionToken, recSessionToken) { t.Errorf("session tokens do not match") } + + conn0, conn1 := net.Pipe() + initSecretRW, ok := initSecretRWF(conn0).(*SecureMessenger) + if !ok { + t.Errorf("incorrect return type. expected *SecureMessenger, got %T", initSecretRW) + } + recSecretRW, ok := recSecretRWF(conn1).(*SecureMessenger) + if !ok { + t.Errorf("incorrect return type. expected *SecureMessenger, got %T", initSecretRW) + } + // aesSecret, macSecret, egressMac, ingressMac if !bytes.Equal(initSecretRW.aesSecret, recSecretRW.aesSecret) { t.Errorf("AES secrets do not match") @@ -191,7 +202,7 @@ func TestPeersHandshake(t *testing.T) { <-receiver.cryptoReady close(ready) }() - timeout := time.After(1 * time.Second) + timeout := time.After(20 * time.Second) select { case <-ready: case <-timeout: diff --git a/p2p/messenger.go b/p2p/messenger.go index cc56233037..2999ebda9b 100644 --- a/p2p/messenger.go +++ b/p2p/messenger.go @@ -1,11 +1,13 @@ package p2p import ( + "bufio" "bytes" "encoding/binary" "fmt" "io" "math/big" + "net" "time" "github.com/ethereum/go-ethereum/ethutil" @@ -35,25 +37,82 @@ var magicToken = []byte{34, 64, 8, 145} Because framing and header structure will change there will be hardly any overlap with the new code so I do not abstract readers any further. */ -func (p *Peer) readLoop(msgc chan<- Msg, errc chan<- error, unblock <-chan bool) { - for _ = range unblock { - p.conn.SetReadDeadline(time.Now().Add(msgReadTimeout)) - if msg, err := readMsg(p.bufconn); err != nil { - errc <- err - } else { - msgc <- msg - } - } - close(errc) + +type MsgChanReadWriter interface { + ReadC() chan Msg + WriteC() chan Msg + ErrorC() chan error + ReadNextC() chan bool + Close() } -func (p *Peer) writeLoop(msgc <-chan Msg, errc chan<- error) { - p.conn.SetWriteDeadline(time.Now().Add(msgWriteTimeout)) - for msg := range msgc { - if err := writeMsg(p.bufconn, msg); err != nil { - errc <- newPeerError(errWrite, "%v", err) +type Messenger struct { + msgReadTimeout time.Duration + msgWriteTimeout time.Duration + in chan Msg + out chan Msg + errc chan error + unblock chan bool + conn net.Conn + bufconn *bufio.ReadWriter +} + +func NewMessenger(conn net.Conn) *Messenger { + self := &Messenger{ + msgReadTimeout: msgReadTimeout, + msgWriteTimeout: msgWriteTimeout, + in: make(chan Msg), + out: make(chan Msg), + errc: make(chan error), + unblock: make(chan bool, 1), + conn: conn, + bufconn: bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn)), + } + go self.readLoop() + go self.writeLoop() + return self +} + +func (self *Messenger) Close() { + close(self.unblock) + close(self.out) +} + +func (self *Messenger) ReadC() chan Msg { + return self.in +} + +func (self *Messenger) WriteC() chan Msg { + return self.out +} + +func (self *Messenger) ErrorC() chan error { + return self.errc +} + +func (self *Messenger) ReadNextC() chan bool { + return self.unblock +} + +func (self *Messenger) readLoop() { + for _ = range self.unblock { + self.conn.SetReadDeadline(time.Now().Add(self.msgReadTimeout)) + if msg, err := readMsg(self.bufconn); err != nil { + self.errc <- err + } else { + self.in <- msg } - p.bufconn.Flush() + } + close(self.errc) +} + +func (self *Messenger) writeLoop() { + for msg := range self.out { + self.conn.SetWriteDeadline(time.Now().Add(self.msgWriteTimeout)) + if err := writeMsg(self.bufconn, msg); err != nil { + self.errc <- newPeerError(errWrite, "%v", err) + } + self.bufconn.Flush() } } diff --git a/p2p/peer.go b/p2p/peer.go index f63bba1ba6..bc2f38c2cc 100644 --- a/p2p/peer.go +++ b/p2p/peer.go @@ -1,11 +1,9 @@ package p2p import ( - "bufio" "bytes" "crypto/rand" "fmt" - "io" "io/ioutil" "net" "sort" @@ -62,9 +60,8 @@ type Peer struct { listenAddr *peerAddr // what remote peer is listening on dialAddr *peerAddr // non-nil if dialing - conn net.Conn - bufconn *bufio.ReadWriter - outgoingMsgC chan Msg + conn net.Conn + crw MsgChanReadWriter // These fields maintain the running protocols. protocols []Protocol @@ -120,17 +117,15 @@ func newServerPeer(server *Server, conn net.Conn, dialAddr *peerAddr) *Peer { func newPeer(conn net.Conn, protocols []Protocol, dialAddr *peerAddr) *Peer { p := &Peer{ - Logger: logger.NewLogger("P2P " + conn.RemoteAddr().String()), - conn: conn, - dialAddr: dialAddr, - bufconn: bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn)), - protocols: protocols, - running: make(map[string]*proto), - disc: make(chan DiscReason), - protoErr: make(chan error), - closed: make(chan struct{}), - cryptoReady: make(chan struct{}), - outgoingMsgC: make(chan Msg), + Logger: logger.NewLogger("P2P " + conn.RemoteAddr().String()), + conn: conn, + dialAddr: dialAddr, + protocols: protocols, + running: make(map[string]*proto), + disc: make(chan DiscReason), + protoErr: make(chan error), + closed: make(chan struct{}), + cryptoReady: make(chan struct{}), } return p } @@ -214,26 +209,27 @@ func (p *Peer) loop() (reason DiscReason, err error) { defer close(p.closed) defer p.conn.Close() - var readLoop func(chan<- Msg, chan<- error, <-chan bool) if p.cryptoHandshake { - if readLoop, err = p.handleCryptoHandshake(); err != nil { + if crwF, err := p.handleCryptoHandshake(); err != nil { // from here on everything can be encrypted, authenticated return DiscProtocolError, err // no graceful disconnect + } else { + p.crw = crwF(p.conn) } } else { - readLoop = p.readLoop + p.crw = NewMessenger(p.conn) } + close(p.cryptoReady) // read loop - readMsg := make(chan Msg) - rwErr := make(chan error) - readNext := make(chan bool, 1) protoDone := make(chan struct{}, 1) - go readLoop(readMsg, rwErr, readNext) - go p.writeLoop(p.outgoingMsgC, rwErr) - readNext <- true - close(p.cryptoReady) + in := p.crw.ReadC() + errc := p.crw.ErrorC() + unblock := p.crw.ReadNextC() + + unblock <- true + if p.runBaseProtocol { p.startBaseProtocol() } @@ -241,7 +237,7 @@ func (p *Peer) loop() (reason DiscReason, err error) { loop: for { select { - case msg := <-readMsg: + case msg := <-in: // a new message has arrived. var wait bool if wait, err = p.dispatch(msg, protoDone); err != nil { @@ -251,15 +247,15 @@ loop: } if !wait { // Msg has already been read completely, continue with next message. - readNext <- true + unblock <- true } p.activity.Post(time.Now()) case <-protoDone: // protocol has consumed the message payload, // we can continue reading from the socket. - readNext <- true + unblock <- true - case err := <-rwErr: + case err := <-errc: // read or write failed. there is no need to run the // polite disconnect sequence because the connection // is probably dead anyway. @@ -272,23 +268,11 @@ loop: break loop } } - - // wait for read loop to return. - close(p.outgoingMsgC) - close(readNext) - <-rwErr // tell the remote end to disconnect - done := make(chan struct{}) - go func() { - p.conn.SetDeadline(time.Now().Add(disconnectGracePeriod)) - p.writeProtoMsg("", NewMsg(discMsg, reason)) - io.Copy(ioutil.Discard, p.conn) - close(done) - }() - select { - case <-done: - case <-time.After(disconnectGracePeriod): - } + p.writeProtoMsg("", NewMsg(discMsg, reason)) + // io.Copy(ioutil.Discard, p.conn)//?? + p.crw.Close() + <-time.After(disconnectGracePeriod) return reason, err } @@ -317,7 +301,7 @@ func (p *Peer) dispatch(msg Msg, protoDone chan struct{}) (wait bool, err error) type readLoop func(chan<- Msg, chan<- error, <-chan bool) -func (p *Peer) handleCryptoHandshake() (loop readLoop, err error) { +func (p *Peer) handleCryptoHandshake() (crw func(net.Conn) MsgChanReadWriter, err error) { // cryptoId is just created for the lifecycle of the handshake // it is survived by an encrypted readwriter var initiator bool @@ -339,13 +323,10 @@ func (p *Peer) handleCryptoHandshake() (loop readLoop, err error) { // run on peer // this bit handles the handshake and creates a secure communications channel with // var rw *secretRW - if sessionToken, _, err = crypto.Run(p.conn, p.Pubkey(), sessionToken, initiator); err != nil { + if sessionToken, crw, err = crypto.NewSession(p.conn, p.Pubkey(), sessionToken, initiator); err != nil { p.Debugf("unable to setup secure session: %v", err) return } - loop = func(msg chan<- Msg, err chan<- error, next <-chan bool) { - // this is the readloop :) - } return } @@ -382,7 +363,7 @@ outer: func (p *Peer) startProto(offset uint64, impl Protocol) *proto { rw := &proto{ in: make(chan Msg), - out: p.outgoingMsgC, + out: p.crw.WriteC(), offset: offset, maxcode: impl.Length, } diff --git a/p2p/peer_test.go b/p2p/peer_test.go index 814421cb82..affba1d7c7 100644 --- a/p2p/peer_test.go +++ b/p2p/peer_test.go @@ -33,7 +33,7 @@ func testPeer(protos []Protocol) (net.Conn, *Peer, <-chan error) { peer := newPeer(conn1, protos, nil) peer.ourID = &peerId{} peer.pubkeyHook = func(*peerAddr) error { return nil } - peer.outgoingMsgC = make(chan Msg) + peer.crw = NewMessenger(conn1) errc := make(chan error, 1) go func() { _, err := peer.loop() diff --git a/p2p/server_test.go b/p2p/server_test.go index ceb89e3f7f..b7cc40ea22 100644 --- a/p2p/server_test.go +++ b/p2p/server_test.go @@ -121,6 +121,7 @@ func TestServerBroadcast(t *testing.T) { var connected sync.WaitGroup srv := startTestServer(t, func(srv *Server, c net.Conn, dialAddr *peerAddr) *Peer { peer := newPeer(c, []Protocol{discard}, dialAddr) + peer.crw = NewMessenger(peer.conn) peer.startSubprotocols([]Cap{discard.cap()}) connected.Done() return peer From ba8318db6a3bc475ea1a9a541db6e6b73d2b55cb Mon Sep 17 00:00:00 2001 From: zelig Date: Thu, 22 Jan 2015 00:53:22 +0000 Subject: [PATCH 25/31] private and public key method names consistent --- p2p/client_identity.go | 10 +++++----- p2p/client_identity_test.go | 4 ++-- p2p/crypto.go | 6 +++--- p2p/peer.go | 8 ++++---- p2p/protocol.go | 6 +++--- p2p/protocol_test.go | 16 ++++++++-------- p2p/server.go | 4 ++-- 7 files changed, 27 insertions(+), 27 deletions(-) diff --git a/p2p/client_identity.go b/p2p/client_identity.go index fca2756bd6..db8cf9dec9 100644 --- a/p2p/client_identity.go +++ b/p2p/client_identity.go @@ -7,9 +7,9 @@ import ( // ClientIdentity represents the identity of a peer. type ClientIdentity interface { - String() string // human readable identity - Pubkey() []byte // 512-bit public key - PrivKey() []byte // 512-bit private key + String() string // human readable identity + PublicKey() []byte // 512-bit public key represented in 65 byte format as per golang/elliptic.Marshal, first byte encodes curve + PrivateKey() []byte // 256-bit private key } type SimpleClientIdentity struct { @@ -53,11 +53,11 @@ func (c *SimpleClientIdentity) String() string { c.implementation) } -func (c *SimpleClientIdentity) Privkey() []byte { +func (c *SimpleClientIdentity) PrivateKey() []byte { return c.privkey } -func (c *SimpleClientIdentity) Pubkey() []byte { +func (c *SimpleClientIdentity) PublicKey() []byte { return c.pubkey } diff --git a/p2p/client_identity_test.go b/p2p/client_identity_test.go index 61c34fbf11..9c15b8895c 100644 --- a/p2p/client_identity_test.go +++ b/p2p/client_identity_test.go @@ -9,11 +9,11 @@ import ( func TestClientIdentity(t *testing.T) { clientIdentity := NewSimpleClientIdentity("Ethereum(G)", "0.5.16", "test", []byte("privkey"), []byte("pubkey")) - key := clientIdentity.Privkey() + key := clientIdentity.PrivateKey() if !bytes.Equal(key, []byte("privkey")) { t.Errorf("Expected Privkey to be %x, got %x", key, []byte("privkey")) } - key = clientIdentity.Pubkey() + key = clientIdentity.PublicKey() if !bytes.Equal(key, []byte("pubkey")) { t.Errorf("Expected Pubkey to be %x, got %x", key, []byte("pubkey")) } diff --git a/p2p/crypto.go b/p2p/crypto.go index 622e2608c7..242bad7977 100644 --- a/p2p/crypto.go +++ b/p2p/crypto.go @@ -8,9 +8,9 @@ import ( "net" "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/crypto/secp256k1" ethlogger "github.com/ethereum/go-ethereum/logger" "github.com/obscuren/ecies" - "github.com/obscuren/secp256k1-go" ) var clogger = ethlogger.NewLogger("CRYPTOID") @@ -49,7 +49,7 @@ newCryptoId(id ClientIdentity) initialises a crypto layer manager. This object h */ func newCryptoId(id ClientIdentity) (self *cryptoId, err error) { // will be at server init - var prvKeyS []byte = id.PrivKey() + var prvKeyS []byte = id.PrivateKey() if prvKeyS == nil { err = fmt.Errorf("no private key for client") return @@ -68,7 +68,7 @@ func newCryptoId(id ClientIdentity) (self *cryptoId, err error) { // to be created at server init shared between peers and sessions // for reuse, call wth ReadAt, no reset seek needed } - self.pubKeyS = id.Pubkey()[1:] + self.pubKeyS = id.PublicKey()[1:] clogger.Debugf("initialise crypto for NodeId %v", hexkey(self.pubKeyS)) clogger.Debugf("private-key %v\npublic key %v", hexkey(prvKeyS), hexkey(self.pubKeyS)) return diff --git a/p2p/peer.go b/p2p/peer.go index bc2f38c2cc..119e98e128 100644 --- a/p2p/peer.go +++ b/p2p/peer.go @@ -110,7 +110,7 @@ func newServerPeer(server *Server, conn net.Conn, dialAddr *peerAddr) *Peer { // laddr can be updated concurrently by NAT traversal. // newServerPeer must be called with the server lock held. if server.laddr != nil { - p.ourListenAddr = newPeerAddr(server.laddr, server.Identity.Pubkey()) + p.ourListenAddr = newPeerAddr(server.laddr, server.Identity.PublicKey()) } return p } @@ -139,12 +139,12 @@ func (p *Peer) Identity() ClientIdentity { return p.identity } -func (self *Peer) Pubkey() (pubkey []byte) { +func (self *Peer) PublicKey() (pubkey []byte) { self.infolock.Lock() defer self.infolock.Unlock() switch { case self.identity != nil: - pubkey = self.identity.Pubkey()[1:] + pubkey = self.identity.PublicKey()[1:] case self.dialAddr != nil: pubkey = self.dialAddr.Pubkey case self.listenAddr != nil: @@ -323,7 +323,7 @@ func (p *Peer) handleCryptoHandshake() (crw func(net.Conn) MsgChanReadWriter, er // run on peer // this bit handles the handshake and creates a secure communications channel with // var rw *secretRW - if sessionToken, crw, err = crypto.NewSession(p.conn, p.Pubkey(), sessionToken, initiator); err != nil { + if sessionToken, crw, err = crypto.NewSession(p.conn, p.PublicKey(), sessionToken, initiator); err != nil { p.Debugf("unable to setup secure session: %v", err) return } diff --git a/p2p/protocol.go b/p2p/protocol.go index 62ada929d7..3aa39ce9c8 100644 --- a/p2p/protocol.go +++ b/p2p/protocol.go @@ -60,11 +60,11 @@ type handshake struct { func (h *handshake) String() string { return h.ID } -func (h *handshake) Pubkey() []byte { +func (h *handshake) PublicKey() []byte { return h.NodeID } -func (h *handshake) PrivKey() []byte { +func (h *handshake) PrivateKey() []byte { return nil } @@ -265,7 +265,7 @@ func (bp *baseProtocol) handshakeMsg() Msg { bp.peer.ourID.String(), caps, port, - bp.peer.ourID.Pubkey()[1:], + bp.peer.ourID.PublicKey()[1:], ) } diff --git a/p2p/protocol_test.go b/p2p/protocol_test.go index 6804a9d40b..1cd72626d6 100644 --- a/p2p/protocol_test.go +++ b/p2p/protocol_test.go @@ -11,14 +11,14 @@ import ( ) type peerId struct { - privKey, pubkey []byte + privkey, pubkey []byte } func (self *peerId) String() string { - return fmt.Sprintf("test peer %x", self.Pubkey()[:4]) + return fmt.Sprintf("test peer %x", self.PublicKey()[:4]) } -func (self *peerId) Pubkey() (pubkey []byte) { +func (self *peerId) PublicKey() (pubkey []byte) { pubkey = self.pubkey if len(pubkey) == 0 { pubkey = crypto.GenerateNewKeyPair().PublicKey @@ -27,11 +27,11 @@ func (self *peerId) Pubkey() (pubkey []byte) { return } -func (self *peerId) PrivKey() (privKey []byte) { - privKey = self.privKey - if len(privKey) == 0 { - privKey = crypto.GenerateNewKeyPair().PublicKey - self.privKey = privKey +func (self *peerId) PrivateKey() (privkey []byte) { + privkey = self.privkey + if len(privkey) == 0 { + privkey = crypto.GenerateNewKeyPair().PublicKey + self.privkey = privkey } return } diff --git a/p2p/server.go b/p2p/server.go index 4fd1f7d034..9a05a7619d 100644 --- a/p2p/server.go +++ b/p2p/server.go @@ -399,7 +399,7 @@ func (srv *Server) verifyPeer(addr *peerAddr) error { if srv.Blacklist.Exists(addr.Pubkey) { return errors.New("blacklisted") } - if bytes.Equal(srv.Identity.Pubkey()[1:], addr.Pubkey) { + if bytes.Equal(srv.Identity.PublicKey()[1:], addr.Pubkey) { return newPeerError(errPubkeyForbidden, "not allowed to connect to srv") } srv.lock.RLock() @@ -407,7 +407,7 @@ func (srv *Server) verifyPeer(addr *peerAddr) error { for _, peer := range srv.peers { if peer != nil { id := peer.Identity() - if id != nil && bytes.Equal(id.Pubkey(), addr.Pubkey) { + if id != nil && bytes.Equal(id.PublicKey(), addr.Pubkey) { return errors.New("already connected") } } From cd235b24c5378df21404feea2fea469a14f6ccf0 Mon Sep 17 00:00:00 2001 From: zelig Date: Fri, 23 Jan 2015 11:44:45 +0000 Subject: [PATCH 26/31] Refactor message read/write - take readwriter out of crypto - cryptoId.NewSession now gets a reader and writer simply and wraps them in a encrypted MsgReadWriter which it returns if handshake is successful - refactor reader/writers MsgReadWriter and Messenger - message got NewMsgFromRLP method - remove package-wide readMsg and writeMsg (they need to be scoped under the encryption scheme) - writeProtoMsg back to peer - peer has new field CryptoType - add errAuthentication and errEncryption to peer Errors - testPeer has to wait till handshake done (rw is set up) <-peer.cryptoReady --- p2p/crypto.go | 42 ++++------------ p2p/crypto_test.go | 20 ++++---- p2p/message.go | 15 ++++++ p2p/message_test.go | 9 ++-- p2p/messenger.go | 115 ++++++++++++++++++++------------------------ p2p/peer.go | 108 +++++++++++++++++++++++++++-------------- p2p/peer_error.go | 4 ++ p2p/peer_test.go | 23 ++++++--- p2p/server_test.go | 11 +++-- 9 files changed, 190 insertions(+), 157 deletions(-) diff --git a/p2p/crypto.go b/p2p/crypto.go index 242bad7977..e7ad29ec39 100644 --- a/p2p/crypto.go +++ b/p2p/crypto.go @@ -5,7 +5,6 @@ import ( "crypto/rand" "fmt" "io" - "net" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto/secp256k1" @@ -26,13 +25,6 @@ var ( rHSLen int = 210 // size of the final ECIES payload sent as receiver's handshake ) -// secretRW implements a message read writer with encryption and authentication -// it is initialised by cryptoId.Run() after a successful crypto handshake -// aesSecret, macSecret, egressMac, ingress -type secretRW struct { - aesSecret, macSecret, egressMac, ingressMac []byte -} - /* cryptoId implements the crypto layer for the p2p networking It is initialised on the node's own identity (which has access to the node's private key) and run separately on a peer connection to set up a secure session after a crypto handshake @@ -81,7 +73,7 @@ func (self hexkey) String() string { } /* -Run(connection, remotePublicKey, sessionToken) is called when the peer connection starts to set up a secure session by performing a crypto handshake. +NewSession is called when the peer connection starts to set up a secure session by performing a crypto handshake. connection is (a buffered) network connection. @@ -94,7 +86,7 @@ Run(connection, remotePublicKey, sessionToken) is called when the peer connectio It returns a secretRW which implements the MsgReadWriter interface. */ -func (self *cryptoId) NewSession(conn io.ReadWriter, remotePubKeyS []byte, sessionToken []byte, initiator bool) (token []byte, rwF func(net.Conn) MsgChanReadWriter, err error) { +func (self *cryptoId) NewSession(r io.Reader, w io.Writer, remotePubKeyS []byte, sessionToken []byte, initiator bool) (token []byte, rw MsgReadWriter, err error) { var auth, initNonce, recNonce []byte var read int var randomPrivKey *ecdsa.PrivateKey @@ -112,12 +104,12 @@ func (self *cryptoId) NewSession(conn io.ReadWriter, remotePubKeyS []byte, sessi randomPublicKeyS, _ := ExportPublicKey(&randomPrivKey.PublicKey) clogger.Debugf("initiator-random-public-key: %v", hexkey(randomPublicKeyS)) - if _, err = conn.Write(auth); err != nil { + if _, err = w.Write(auth); err != nil { return } clogger.Debugf("initiator handshake (sent to %v):\n%v", hexkey(remotePubKeyS), hexkey(auth)) var response []byte = make([]byte, rHSLen) - if read, err = conn.Read(response); err != nil || read == 0 { + if read, err = r.Read(response); err != nil || read == 0 { return } if read != rHSLen { @@ -136,7 +128,7 @@ func (self *cryptoId) NewSession(conn io.ReadWriter, remotePubKeyS []byte, sessi } else { auth = make([]byte, iHSLen) clogger.Debugf("waiting for initiator handshake (from %v)", hexkey(remotePubKeyS)) - if read, err = conn.Read(auth); err != nil { + if read, err = r.Read(auth); err != nil { return } if read != iHSLen { @@ -153,12 +145,12 @@ func (self *cryptoId) NewSession(conn io.ReadWriter, remotePubKeyS []byte, sessi } clogger.Debugf("receiver-nonce: %v", hexkey(recNonce)) clogger.Debugf("receiver-random-priv-key: %v", hexkey(crypto.FromECDSA(randomPrivKey))) - if _, err = conn.Write(response); err != nil { + if _, err = w.Write(response); err != nil { return } clogger.Debugf("receiver handshake (sent to %v):\n%v", hexkey(remotePubKeyS), hexkey(response)) } - return self.newSession(initNonce, recNonce, auth, randomPrivKey, remoteRandomPubKey) + return self.newSession(r, w, initNonce, recNonce, auth, randomPrivKey, remoteRandomPubKey) } /* @@ -365,7 +357,7 @@ func (self *cryptoId) completeHandshake(auth []byte) (respNonce []byte, remoteRa /* newSession is called after the handshake is completed. The arguments are values negotiated in the handshake and the return value is a new session : a new session Token to be remembered for the next time we connect with this peer. And a MsgReadWriter that implements an encrypted and authenticated connection with key material obtained from the crypto handshake key exchange */ -func (self *cryptoId) newSession(initNonce, respNonce, auth []byte, privKey *ecdsa.PrivateKey, remoteRandomPubKey *ecdsa.PublicKey) (sessionToken []byte, rwF func(net.Conn) MsgChanReadWriter, err error) { +func (self *cryptoId) newSession(r io.Reader, w io.Writer, initNonce, respNonce, auth []byte, privKey *ecdsa.PrivateKey, remoteRandomPubKey *ecdsa.PublicKey) (sessionToken []byte, rw MsgReadWriter, err error) { // 3) Now we can trust ecdhe-random-pubk to derive new keys //ecdhe-shared-secret = ecdh.agree(ecdhe-random, remote-ecdhe-random-pubk) var dhSharedSecret []byte @@ -395,9 +387,7 @@ func (self *cryptoId) newSession(initNonce, respNonce, auth []byte, privKey *ecd clogger.Debugf("egress-mac: %v", hexkey(egressMac)) clogger.Debugf("ingress-mac: %v", hexkey(ingressMac)) - rwF = func(conn net.Conn) MsgChanReadWriter { - return NewSecureMessenger(conn, aesSecret, macSecret, egressMac, ingressMac) - } + rw, err = NewCryptoMsgRW(r, w, aesSecret, macSecret, egressMac, ingressMac) return } @@ -410,17 +400,3 @@ func Xor(one, other []byte) (xor []byte) { } return } - -type SecureMessenger struct { - Messenger - aesSecret, macSecret, egressMac, ingressMac []byte -} - -func NewSecureMessenger(conn net.Conn, aesSecret, macSecret, egressMac, ingressMac []byte) *SecureMessenger { - return &SecureMessenger{ - aesSecret: aesSecret, - macSecret: macSecret, - egressMac: egressMac, - ingressMac: ingressMac, - } -} diff --git a/p2p/crypto_test.go b/p2p/crypto_test.go index 582eabe14b..2cb9c9bbe4 100644 --- a/p2p/crypto_test.go +++ b/p2p/crypto_test.go @@ -5,6 +5,7 @@ import ( // "crypto/ecdsa" // "crypto/elliptic" // "crypto/rand" + "bufio" "fmt" "net" "testing" @@ -105,13 +106,15 @@ func TestCryptoHandshake(t *testing.T) { t.Errorf("%v", err) } + conn0, conn1 := net.Pipe() + // now both parties should have the same session parameters - initSessionToken, initSecretRWF, err := initiator.newSession(initNonce, recNonce, auth, randomPrivKey, remoteRandomPubKey) + initSessionToken, initRW, err := initiator.newSession(bufio.NewReader(conn0), conn0, initNonce, recNonce, auth, randomPrivKey, remoteRandomPubKey) if err != nil { t.Errorf("%v", err) } - recSessionToken, recSecretRWF, err := receiver.newSession(remoteInitNonce, remoteRecNonce, auth, remoteRandomPrivKey, remoteInitRandomPubKey) + recSessionToken, recRW, err := receiver.newSession(bufio.NewReader(conn1), conn1, remoteInitNonce, remoteRecNonce, auth, remoteRandomPrivKey, remoteInitRandomPubKey) if err != nil { t.Errorf("%v", err) } @@ -130,14 +133,13 @@ func TestCryptoHandshake(t *testing.T) { t.Errorf("session tokens do not match") } - conn0, conn1 := net.Pipe() - initSecretRW, ok := initSecretRWF(conn0).(*SecureMessenger) + initSecretRW, ok := initRW.(*CryptoMsgRW) if !ok { - t.Errorf("incorrect return type. expected *SecureMessenger, got %T", initSecretRW) + t.Errorf("incorrect return type. expected *CryptoMsgRW, got %T", initRW) } - recSecretRW, ok := recSecretRWF(conn1).(*SecureMessenger) + recSecretRW, ok := recRW.(*CryptoMsgRW) if !ok { - t.Errorf("incorrect return type. expected *SecureMessenger, got %T", initSecretRW) + t.Errorf("incorrect return type. expected *CryptoMsgRW, got %T", recRW) } // aesSecret, macSecret, egressMac, ingressMac @@ -184,8 +186,8 @@ func TestPeersHandshake(t *testing.T) { initiator.pubkeyHook = func(*peerAddr) error { return nil } receiver.pubkeyHook = func(*peerAddr) error { return nil } - initiator.cryptoHandshake = true - receiver.cryptoHandshake = true + initiator.CryptoType = EthCrypto + receiver.CryptoType = EthCrypto errc0 := make(chan error, 1) errc1 := make(chan error, 1) go func() { diff --git a/p2p/message.go b/p2p/message.go index 09eefb7658..e4406adc39 100644 --- a/p2p/message.go +++ b/p2p/message.go @@ -65,6 +65,21 @@ func (msg Msg) Discard() error { return err } +func NewMsgFromRLP(size uint32, r rlp.ByteReader) (msg Msg, err error) { + // decode start of RLP message to get the message code + posr := &postrack{r, 0} + s := rlp.NewStream(posr) + if _, err := s.List(); err != nil { + return msg, err + } + code, err := s.Uint() + if err != nil { + return msg, err + } + payloadsize := size - posr.p + return Msg{code, payloadsize, io.LimitReader(r, int64(payloadsize))}, nil +} + /* MsgReadWriter is an interface for reading and writing messages It is aware of message structure and knows how to encode/decode diff --git a/p2p/message_test.go b/p2p/message_test.go index 5cde9abf5b..34b30065f1 100644 --- a/p2p/message_test.go +++ b/p2p/message_test.go @@ -29,12 +29,13 @@ func TestNewMsg(t *testing.T) { func TestEncodeDecodeMsg(t *testing.T) { msg := NewMsg(3, 1, "000") buf := new(bytes.Buffer) - if err := writeMsg(buf, msg); err != nil { + rw, _ := NewMsgRW(buf, buf) + if err := rw.WriteMsg(msg); err != nil { t.Fatalf("encodeMsg error: %v", err) } // t.Logf("encoded: %x", buf.Bytes()) - decmsg, err := readMsg(buf) + decmsg, err := rw.ReadMsg() if err != nil { t.Fatalf("readMsg error: %v", err) } @@ -62,7 +63,9 @@ func TestEncodeDecodeMsg(t *testing.T) { func TestDecodeRealMsg(t *testing.T) { data := ethutil.Hex2Bytes("2240089100000080f87e8002b5457468657265756d282b2b292f5065657220536572766572204f6e652f76302e372e382f52656c656173652f4c696e75782f672b2bc082765fb84086dd80b7aefd6a6d2e3b93f4f300a86bfb6ef7bdc97cb03f793db6bb") - msg, err := readMsg(bytes.NewReader(data)) + buf := bytes.NewReader(data) + rw, _ := NewMsgRW(buf, nil) + msg, err := rw.ReadMsg() if err != nil { t.Fatalf("unexpected error: %v", err) } diff --git a/p2p/messenger.go b/p2p/messenger.go index 2999ebda9b..2bed6228b3 100644 --- a/p2p/messenger.go +++ b/p2p/messenger.go @@ -1,13 +1,10 @@ package p2p import ( - "bufio" "bytes" "encoding/binary" - "fmt" "io" "math/big" - "net" "time" "github.com/ethereum/go-ethereum/ethutil" @@ -27,15 +24,12 @@ const ( var magicToken = []byte{34, 64, 8, 145} /* - writeMsg, readMsg (makeListHeader) should all be internal to the default legacy - MsgReadWriter implementation + A MsgChanReadWriter implementation will typically sit on a multiplexed peer connection and runs a single read and a write loop without need to use locking. - A MsgReadWriter implementation will typically sit on a multiplexed peer connection and runs a single read and a write loop without need to use locking. - - It passes on incoming messages to its channel picked up by the interface methods Read + It passes on incoming messages to its channel ReadC() the peer runs a dispatch loop that figures out which protocol to forward the message to. - Because framing and header structure will change there will be hardly any overlap with the new code so I do not abstract readers any further. + The channel for outcgoing messages (WriteC) is simply shared between the individual MsgReadWriter instances for each protocol */ type MsgChanReadWriter interface { @@ -47,26 +41,24 @@ type MsgChanReadWriter interface { } type Messenger struct { - msgReadTimeout time.Duration - msgWriteTimeout time.Duration - in chan Msg - out chan Msg - errc chan error - unblock chan bool - conn net.Conn - bufconn *bufio.ReadWriter + in chan Msg + out chan Msg + errc chan error + unblock chan bool + rw MsgReadWriter } -func NewMessenger(conn net.Conn) *Messenger { +/* +Messenger is a simple implementation of a read and write loop using a MsgReadWriter to encode/decode individual messages +This MsgReadWriter can implement parsing from/to any kind of packet structure and employ encryption and authentication +*/ +func NewMessenger(rw MsgReadWriter) *Messenger { self := &Messenger{ - msgReadTimeout: msgReadTimeout, - msgWriteTimeout: msgWriteTimeout, - in: make(chan Msg), - out: make(chan Msg), - errc: make(chan error), - unblock: make(chan bool, 1), - conn: conn, - bufconn: bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn)), + in: make(chan Msg), + out: make(chan Msg), + errc: make(chan error), + unblock: make(chan bool, 1), + rw: rw, } go self.readLoop() go self.writeLoop() @@ -90,14 +82,14 @@ func (self *Messenger) ErrorC() chan error { return self.errc } +// ReadNextC <- true must be called before the next read is attempted func (self *Messenger) ReadNextC() chan bool { return self.unblock } func (self *Messenger) readLoop() { for _ = range self.unblock { - self.conn.SetReadDeadline(time.Now().Add(self.msgReadTimeout)) - if msg, err := readMsg(self.bufconn); err != nil { + if msg, err := self.rw.ReadMsg(); err != nil { self.errc <- err } else { self.in <- msg @@ -108,15 +100,35 @@ func (self *Messenger) readLoop() { func (self *Messenger) writeLoop() { for msg := range self.out { - self.conn.SetWriteDeadline(time.Now().Add(self.msgWriteTimeout)) - if err := writeMsg(self.bufconn, msg); err != nil { + if err := self.rw.WriteMsg(msg); err != nil { self.errc <- newPeerError(errWrite, "%v", err) } - self.bufconn.Flush() + } } -func writeMsg(w io.Writer, msg Msg) error { +/* +MsgReadWriter is an interface for reading and writing messages +It is aware of message structure and knows how to encode/decode + +MsgRW is a simple encoder implementing MsgReadWriter +It complies with the legacy devp2p packet structure and no encryption or authentication +*/ +type MsgRW struct { + r rlp.ByteReader // this is implemented by bufio.ReadWriter + // r io.Reader + w io.Writer +} + +func NewMsgRW(r rlp.ByteReader, w io.Writer) (*MsgRW, error) { + return &MsgRW{ + r: r, + w: w, + }, nil +} + +func (self *MsgRW) WriteMsg(msg Msg) error { + // TODO: handle case when Size + len(code) + len(listhdr) overflows uint32 code := ethutil.Encode(uint32(msg.Code)) listhdr := makeListHeader(msg.Size + uint32(len(code))) @@ -127,11 +139,11 @@ func writeMsg(w io.Writer, msg Msg) error { binary.BigEndian.PutUint32(start[4:], payloadLen) for _, b := range [][]byte{start, listhdr, code} { - if _, err := w.Write(b); err != nil { + if _, err := self.w.Write(b); err != nil { return err } } - _, err := io.CopyN(w, msg.Payload, int64(msg.Size)) + _, err := io.CopyN(self.w, msg.Payload, int64(msg.Size)) return err } @@ -146,29 +158,19 @@ func makeListHeader(length uint32) []byte { // readMsg reads a message header from r. // It takes an rlp.ByteReader to ensure that the decoding doesn't buffer. -func readMsg(r rlp.ByteReader) (msg Msg, err error) { +func (self *MsgRW) ReadMsg() (msg Msg, err error) { + // read magic and payload size start := make([]byte, 8) - if _, err = io.ReadFull(r, start); err != nil { + if _, err = io.ReadFull(self.r, start); err != nil { return msg, newPeerError(errRead, "%v", err) } + if !bytes.HasPrefix(start, magicToken) { return msg, newPeerError(errMagicTokenMismatch, "got %x, want %x", start[:4], magicToken) } size := binary.BigEndian.Uint32(start[4:]) - - // decode start of RLP message to get the message code - posr := &postrack{r, 0} - s := rlp.NewStream(posr) - if _, err := s.List(); err != nil { - return msg, err - } - code, err := s.Uint() - if err != nil { - return msg, err - } - payloadsize := size - posr.p - return Msg{code, payloadsize, io.LimitReader(r, int64(payloadsize))}, nil + return NewMsgFromRLP(size, self.r) } // postrack wraps an rlp.ByteReader with a position counter. @@ -191,20 +193,6 @@ func (r *postrack) ReadByte() (byte, error) { return b, err } -// this duplicates functionality of proto.WriteMsg -// if we need this for broadcasting via a server interface then -// simply call the appropriate Write function of the protocol RW -// writeProtoMsg sends the given message on behalf of the given named protocol. -func (p *Peer) writeProtoMsg(protoName string, msg Msg) error { - p.runlock.RLock() - proto, ok := p.running[protoName] - p.runlock.RUnlock() - if !ok { - return fmt.Errorf("protocol %s not handled by peer", protoName) - } - return proto.WriteMsg(msg) -} - // proto will embed the same writer channel as given to the readwriter // in the legacy code it knows about the code offset // no need to go through peer for writing , so do not need to embed peer as field @@ -214,6 +202,7 @@ type proto struct { maxcode, offset uint64 } +// WriteMsg proto implements MsgWriter interface func (rw *proto) WriteMsg(msg Msg) error { if msg.Code >= rw.maxcode { return newPeerError(errInvalidMsgCode, "not handled") diff --git a/p2p/peer.go b/p2p/peer.go index 119e98e128..7704c764b8 100644 --- a/p2p/peer.go +++ b/p2p/peer.go @@ -1,6 +1,7 @@ package p2p import ( + "bufio" "bytes" "crypto/rand" "fmt" @@ -65,8 +66,8 @@ type Peer struct { // These fields maintain the running protocols. protocols []Protocol - runBaseProtocol bool // for testing - cryptoHandshake bool // for testing + runBaseProtocol bool // for testing + CryptoType CryptoType // cryptoReady chan struct{} runlock sync.RWMutex // protects running @@ -209,17 +210,12 @@ func (p *Peer) loop() (reason DiscReason, err error) { defer close(p.closed) defer p.conn.Close() - if p.cryptoHandshake { - if crwF, err := p.handleCryptoHandshake(); err != nil { - // from here on everything can be encrypted, authenticated - return DiscProtocolError, err // no graceful disconnect - } else { - p.crw = crwF(p.conn) - } - } else { - p.crw = NewMessenger(p.conn) + if err = p.handleCryptoHandshake(); err != nil { + // from here on everything can be encrypted, authenticated + return DiscProtocolError, err // no graceful disconnect } close(p.cryptoReady) + defer p.crw.Close() // read loop protoDone := make(chan struct{}, 1) @@ -271,7 +267,6 @@ loop: // tell the remote end to disconnect p.writeProtoMsg("", NewMsg(discMsg, reason)) // io.Copy(ioutil.Discard, p.conn)//?? - p.crw.Close() <-time.After(disconnectGracePeriod) return reason, err } @@ -299,34 +294,62 @@ func (p *Peer) dispatch(msg Msg, protoDone chan struct{}) (wait bool, err error) return wait, nil } -type readLoop func(chan<- Msg, chan<- error, <-chan bool) +type CryptoType byte -func (p *Peer) handleCryptoHandshake() (crw func(net.Conn) MsgChanReadWriter, err error) { - // cryptoId is just created for the lifecycle of the handshake - // it is survived by an encrypted readwriter - var initiator bool - var sessionToken []byte - sessionToken = make([]byte, keyLen) - if _, err = rand.Read(sessionToken); err != nil { - return +const ( + NoCrypto CryptoType = iota + EthCrypto +) + +var cryptoType = map[CryptoType]string{ + NoCrypto: "no encryption", + EthCrypto: "AES256 CTR HMAC SHA256", +} + +func (self CryptoType) String() (s string) { + s = cryptoType[self] + if len(s) == 0 { + s = string([]byte{byte(self)}) } - if p.dialAddr != nil { // this should have its own method Outgoing() bool - initiator = true - } - // create crypto layer - // this could in principle run only once but maybe we want to allow - // identity switching - var crypto *cryptoId - if crypto, err = newCryptoId(p.ourID); err != nil { - return - } - // run on peer - // this bit handles the handshake and creates a secure communications channel with - // var rw *secretRW - if sessionToken, crw, err = crypto.NewSession(p.conn, p.PublicKey(), sessionToken, initiator); err != nil { - p.Debugf("unable to setup secure session: %v", err) - return + return +} + +func (p *Peer) handleCryptoHandshake() (err error) { + var crw MsgReadWriter + switch p.CryptoType { + case NoCrypto: + if crw, err = NewMsgRW(bufio.NewReader(p.conn), p.conn); err != nil { + return + } + case EthCrypto: + // cryptoId is just created for the lifecycle of the handshake + // it is survived by an encrypted readwriter + var initiator bool + var sessionToken []byte + sessionToken = make([]byte, keyLen) + if _, err = rand.Read(sessionToken); err != nil { + return + } + if p.dialAddr != nil { // this should have its own method Outgoing() bool + initiator = true + } + // create crypto layer + // this could in principle run only once but maybe we want to allow + // identity switching + var crypto *cryptoId + if crypto, err = newCryptoId(p.ourID); err != nil { + return + } + // run on peer + // this bit handles the handshake and creates a secure communications channel with + if sessionToken, crw, err = crypto.NewSession(bufio.NewReader(p.conn), p.conn, p.PublicKey(), sessionToken, initiator); err != nil { + p.Errorf("unable to setup secure session: %v", err) + } + default: + err = fmt.Errorf("unrecognised crypto type %v", p.CryptoType) + p.Errorf("%v", err) } + p.crw = NewMessenger(crw) return } @@ -406,3 +429,14 @@ func (p *Peer) closeProtocols() { p.runlock.RUnlock() p.protoWG.Wait() } + +// writeProtoMsg sends the given message on behalf of the given named protocol. +func (p *Peer) writeProtoMsg(protoName string, msg Msg) error { + p.runlock.RLock() + proto, ok := p.running[protoName] + p.runlock.RUnlock() + if !ok { + return fmt.Errorf("protocol %s not handled by peer", protoName) + } + return proto.WriteMsg(msg) +} diff --git a/p2p/peer_error.go b/p2p/peer_error.go index 0eb7ec838d..d67c42a36d 100644 --- a/p2p/peer_error.go +++ b/p2p/peer_error.go @@ -19,6 +19,8 @@ const ( errPingTimeout errInvalidNetworkId errInvalidProtocolVersion + errAuthentication + errEncryption ) var errorToString = map[int]string{ @@ -36,6 +38,8 @@ var errorToString = map[int]string{ errPingTimeout: "Ping timeout", errInvalidNetworkId: "Invalid network id", errInvalidProtocolVersion: "Invalid protocol version", + errAuthentication: "Authentication error", + errEncryption: "Encryption error", } type peerError struct { diff --git a/p2p/peer_test.go b/p2p/peer_test.go index affba1d7c7..7419488acc 100644 --- a/p2p/peer_test.go +++ b/p2p/peer_test.go @@ -33,12 +33,12 @@ func testPeer(protos []Protocol) (net.Conn, *Peer, <-chan error) { peer := newPeer(conn1, protos, nil) peer.ourID = &peerId{} peer.pubkeyHook = func(*peerAddr) error { return nil } - peer.crw = NewMessenger(conn1) errc := make(chan error, 1) go func() { _, err := peer.loop() errc <- err }() + <-peer.cryptoReady return conn2, peer, errc } @@ -72,9 +72,11 @@ func TestPeerProtoReadMsg(t *testing.T) { net, peer, errc := testPeer([]Protocol{proto}) defer net.Close() + peer.startSubprotocols([]Cap{proto.cap()}) - writeMsg(net, NewMsg(18, 1, "000")) + rw, _ := NewMsgRW(nil, net) + rw.WriteMsg(NewMsg(18, 1, "000")) select { case <-done: case err := <-errc: @@ -108,9 +110,11 @@ func TestPeerProtoReadLargeMsg(t *testing.T) { net, peer, errc := testPeer([]Protocol{proto}) defer net.Close() - peer.startSubprotocols([]Cap{proto.cap()}) - writeMsg(net, NewMsg(18, make([]byte, msgsize))) + rw, _ := NewMsgRW(nil, net) + + peer.startSubprotocols([]Cap{proto.cap()}) + rw.WriteMsg(NewMsg(18, make([]byte, msgsize))) select { case <-done: case err := <-errc: @@ -138,10 +142,11 @@ func TestPeerProtoEncodeMsg(t *testing.T) { } net, peer, _ := testPeer([]Protocol{proto}) defer net.Close() - peer.startSubprotocols([]Cap{proto.cap()}) bufr := bufio.NewReader(net) - msg, err := readMsg(bufr) + rw, _ := NewMsgRW(bufr, nil) + peer.startSubprotocols([]Cap{proto.cap()}) + msg, err := rw.ReadMsg() if err != nil { t.Errorf("read error: %v", err) } @@ -178,7 +183,8 @@ func TestPeerWrite(t *testing.T) { read := make(chan struct{}) go func() { bufr := bufio.NewReader(net) - msg, err := readMsg(bufr) + rw, _ := NewMsgRW(bufr, nil) + msg, err := rw.ReadMsg() if err != nil { t.Errorf("read error: %v", err) } else if msg.Code != 16 { @@ -212,8 +218,9 @@ func TestPeerActivity(t *testing.T) { sub := peer.activity.Subscribe(time.Time{}) defer sub.Unsubscribe() + rw, _ := NewMsgRW(nil, net) for i := 0; i < 6; i++ { - writeMsg(net, NewMsg(16)) + rw.WriteMsg(NewMsg(16)) select { case <-sub.Chan(): case <-time.After(inactivityTimeout / 2): diff --git a/p2p/server_test.go b/p2p/server_test.go index b7cc40ea22..1375344244 100644 --- a/p2p/server_test.go +++ b/p2p/server_test.go @@ -1,6 +1,7 @@ package p2p import ( + "bufio" "bytes" "io" "net" @@ -121,7 +122,8 @@ func TestServerBroadcast(t *testing.T) { var connected sync.WaitGroup srv := startTestServer(t, func(srv *Server, c net.Conn, dialAddr *peerAddr) *Peer { peer := newPeer(c, []Protocol{discard}, dialAddr) - peer.crw = NewMessenger(peer.conn) + rw, _ := NewMsgRW(bufio.NewReader(c), c) + peer.crw = NewMessenger(rw) peer.startSubprotocols([]Cap{discard.cap()}) connected.Done() return peer @@ -146,9 +148,10 @@ func TestServerBroadcast(t *testing.T) { // broadcast one message srv.Broadcast("discard", 0, "foo") - goldbuf := new(bytes.Buffer) - writeMsg(goldbuf, NewMsg(16, "foo")) - golden := goldbuf.Bytes() + buf := new(bytes.Buffer) + rw, _ := NewMsgRW(buf, buf) + rw.WriteMsg(NewMsg(16, "foo")) + golden := buf.Bytes() // check that the message has been written everywhere for i, conn := range conns { From a4b4f680a12a7e1e58ae6c3d4699c9c3d69477c8 Mon Sep 17 00:00:00 2001 From: zelig Date: Fri, 23 Jan 2015 11:45:26 +0000 Subject: [PATCH 27/31] add encryption/authentication with test --- p2p/encryption.go | 147 +++++++++++++++++++++++++++++++++++++++++ p2p/encryption_test.go | 82 +++++++++++++++++++++++ 2 files changed, 229 insertions(+) create mode 100644 p2p/encryption.go create mode 100644 p2p/encryption_test.go diff --git a/p2p/encryption.go b/p2p/encryption.go new file mode 100644 index 0000000000..b0e6cfd501 --- /dev/null +++ b/p2p/encryption.go @@ -0,0 +1,147 @@ +package p2p + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/hmac" + "crypto/sha256" + "encoding/binary" + "fmt" + "hash" + "io" + + // "github.com/ethereum/go-ethereum/crypto/sha256" + "github.com/ethereum/go-ethereum/ethutil" + "github.com/ethereum/go-ethereum/rlp" +) + +/* +CryptoMsgRW implements MsgReadWriter a message read writer with encryption and authentication +it is initialised by cryptoId.NewSession() after a successful crypto handshake on the same IO +It uses the legacy devp2p packet structure (temporary) +*/ + +type CryptoMsgRW struct { + r io.Reader + w io.Writer + aesSecret, macSecret []byte + egressMac, ingressMac []byte + ingress, egress hash.Hash + stream cipher.Stream +} + +func NewCryptoMsgRW(r io.Reader, w io.Writer, aesSecret, macSecret, egressMac, ingressMac []byte) (*CryptoMsgRW, error) { + self := &CryptoMsgRW{ + r: r, + w: w, + aesSecret: aesSecret, + macSecret: macSecret, + egressMac: egressMac, + ingressMac: ingressMac, + } + block, err := aes.NewCipher(aesSecret) + if err != nil { + return nil, err + } + self.stream = cipher.NewCTR(block, macSecret[:aes.BlockSize]) + self.egress = hmac.New(sha256.New, egressMac) + self.ingress = hmac.New(sha256.New, ingressMac) + return self, nil +} + +func (self *CryptoMsgRW) Decrypt(plaintext, ciphertext []byte) (err error) { + self.stream.XORKeyStream(plaintext, ciphertext) + self.ingress.Write(plaintext) + return +} + +func (self *CryptoMsgRW) Encrypt(ciphertext, plaintext []byte) (err error) { + self.stream.XORKeyStream(ciphertext, plaintext) + self.egress.Write(plaintext) + return +} + +func (self *CryptoMsgRW) WriteMsg(msg Msg) (err error) { + // TODO: handle case when Size + len(code) + len(listhdr) overflows uint32 + code := ethutil.Encode(uint32(msg.Code)) + listhdr := makeListHeader(msg.Size + uint32(len(code))) + payloadLen := uint32(len(listhdr)) + uint32(len(code)) + msg.Size + + start := make([]byte, 8) + copy(start, magicToken) + binary.BigEndian.PutUint32(start[4:], payloadLen) + if _, err = self.w.Write(start); err != nil { + return + } + listhdrLen := uint32(len(listhdr)) + codeLen := uint32(len(code)) + ciphertext := make([]byte, listhdrLen+codeLen+msg.Size) + plaintext := make([]byte, listhdrLen+codeLen+msg.Size) + copy(plaintext, listhdr) + copy(plaintext[listhdrLen:], code) + msg.Payload.Read(plaintext[listhdrLen+codeLen:]) + self.Encrypt(ciphertext, plaintext) + fmt.Printf("ENCRYPT:\npt: %v\nct: %v\n", hexkey(plaintext), hexkey(ciphertext)) + if _, err = self.w.Write(ciphertext); err != nil { + return + } + if _, err = self.w.Write(self.egress.Sum(nil)); err != nil { + return + } + + return +} + +func (self *CryptoMsgRW) ReadMsg() (msg Msg, err error) { + var size uint32 + if size, err = self.readHeader(); err != nil { + err = newPeerError(errRead, "%v", err) + return + } + // authenticate size + var payload rlp.ByteReader + if payload, err = self.readPayload(size); err != nil { + err = newPeerError(errRead, "%v", err) + return + } + return NewMsgFromRLP(size, payload) +} + +func (self *CryptoMsgRW) readHeader() (size uint32, err error) { + // read magic and payload size + start := make([]byte, 8) + if _, err = io.ReadFull(self.r, start); err != nil { + err = newPeerError(errRead, "%v", err) + return + } + if !bytes.HasPrefix(start, magicToken) { + err = newPeerError(errMagicTokenMismatch, "got %x, want %x", start[:4], magicToken) + return + } + // here we could deobfuscate and auth the header... + size = binary.BigEndian.Uint32(start[4:]) + // here more header type metainfo... + return +} + +func (self *CryptoMsgRW) readPayload(size uint32) (r rlp.ByteReader, err error) { + plaintext := make([]byte, size) + ciphertext := make([]byte, size) + self.r.Read(ciphertext) + self.Decrypt(plaintext, ciphertext) + fmt.Printf("DECRYPT:\npt: %v\nct: %v\n", hexkey(plaintext), hexkey(ciphertext)) + mac := make([]byte, 32) + if _, err = self.r.Read(mac); err != nil { + err = newPeerError(errRead, "%v", err) + return + } + // var expectedMac = self.ingress.Sum(nil) + // if !hmac.Equal(expectedMac, mac) { + // err = newPeerError(errAuthentication, "ingress incorrect") + // return + // } + r = bytes.NewReader(plaintext) + // r = io.LimitReader(bytes.NewReader(plaintext), int64(size)) + return +} diff --git a/p2p/encryption_test.go b/p2p/encryption_test.go new file mode 100644 index 0000000000..5128c95eda --- /dev/null +++ b/p2p/encryption_test.go @@ -0,0 +1,82 @@ +package p2p + +import ( + "bufio" + "bytes" + "crypto/rand" + // "fmt" + "io" + "net" + "testing" +) + +func randomKey(i int) (key []byte) { + key = make([]byte, i) + if _, err := io.ReadFull(rand.Reader, key); err != nil { + panic(err) + } + return +} + +func TestEncryption(t *testing.T) { + var args [][]byte = make([][]byte, 4) + for i, _ := range args { + args[i] = randomKey(32) + } + var pubkey = randomKey(64) + + var caps []interface{} + for _, p := range []Cap{Cap{"bzz", 0}, Cap{"shh", 1}, Cap{"eth", 2}} { + caps = append(caps, p) + } + + var msg0 = NewMsg(handshakeMsg, + baseProtocolVersion, + "ethersphere", + caps, + 3301, + pubkey, + ) + + var hs handshake + + conn0, conn1 := net.Pipe() + rw0, err := NewCryptoMsgRW(bufio.NewReader(conn0), conn0, args[0], args[1], args[2], args[3]) + if err != nil { + return + } + messenger0 := NewMessenger(rw0) + + rw1, err := NewCryptoMsgRW(bufio.NewReader(conn1), conn1, args[0], args[1], args[2], args[3]) + if err != nil { + return + } + messenger1 := NewMessenger(rw1) + + messenger0.WriteC() <- msg0 + + messenger1.ReadNextC() <- true + + var msg1 Msg + select { + case msg1 = <-messenger1.ReadC(): + + case err = <-messenger0.ErrorC(): + t.Errorf("unexpected error on initiator%v", err) + + case err = <-messenger1.ErrorC(): + t.Errorf("unexpected error on receiver %v", err) + } + + if err = msg1.Decode(&hs); err != nil { + t.Errorf("rlp decoding error: %v", err) + } + + if //!bytes.Equal(hs.ListenPort, 3301) || + hs.ID != "ethersphere" || + len(hs.Caps) != 3 || + !bytes.Equal(hs.PublicKey(), pubkey) { + t.Errorf("mismatch") + } + +} From 2f00e5cad120ae333ed88f90dee5f0fcad90f4ed Mon Sep 17 00:00:00 2001 From: zelig Date: Fri, 23 Jan 2015 12:44:57 +0000 Subject: [PATCH 28/31] add correct authentication with HMAC --- p2p/crypto.go | 19 ++++++++++--------- p2p/crypto_test.go | 12 ++++++------ p2p/encryption.go | 18 +++++++----------- p2p/encryption_test.go | 4 ++-- p2p/peer.go | 3 +++ 5 files changed, 28 insertions(+), 28 deletions(-) diff --git a/p2p/crypto.go b/p2p/crypto.go index e7ad29ec39..e6cd82d80e 100644 --- a/p2p/crypto.go +++ b/p2p/crypto.go @@ -150,7 +150,7 @@ func (self *cryptoId) NewSession(r io.Reader, w io.Writer, remotePubKeyS []byte, } clogger.Debugf("receiver handshake (sent to %v):\n%v", hexkey(remotePubKeyS), hexkey(response)) } - return self.newSession(r, w, initNonce, recNonce, auth, randomPrivKey, remoteRandomPubKey) + return self.newSession(r, w, initiator, initNonce, recNonce, auth, randomPrivKey, remoteRandomPubKey) } /* @@ -357,7 +357,7 @@ func (self *cryptoId) completeHandshake(auth []byte) (respNonce []byte, remoteRa /* newSession is called after the handshake is completed. The arguments are values negotiated in the handshake and the return value is a new session : a new session Token to be remembered for the next time we connect with this peer. And a MsgReadWriter that implements an encrypted and authenticated connection with key material obtained from the crypto handshake key exchange */ -func (self *cryptoId) newSession(r io.Reader, w io.Writer, initNonce, respNonce, auth []byte, privKey *ecdsa.PrivateKey, remoteRandomPubKey *ecdsa.PublicKey) (sessionToken []byte, rw MsgReadWriter, err error) { +func (self *cryptoId) newSession(r io.Reader, w io.Writer, initiator bool, initNonce, respNonce, auth []byte, privKey *ecdsa.PrivateKey, remoteRandomPubKey *ecdsa.PublicKey) (sessionToken []byte, rw MsgReadWriter, err error) { // 3) Now we can trust ecdhe-random-pubk to derive new keys //ecdhe-shared-secret = ecdh.agree(ecdhe-random, remote-ecdhe-random-pubk) var dhSharedSecret []byte @@ -375,13 +375,14 @@ func (self *cryptoId) newSession(r io.Reader, w io.Writer, initNonce, respNonce, // mac-secret = crypto.Sha3(ecdhe-shared-secret || aes-secret) var macSecret = crypto.Sha3(append(dhSharedSecret, aesSecret...)) // # destroy ecdhe-shared-secret - // egress-mac = crypto.Sha3(mac-secret^nonce || auth) - var egressMac = crypto.Sha3(append(Xor(macSecret, respNonce), auth...)) - // # destroy nonce - // ingress-mac = crypto.Sha3(mac-secret^initiator-nonce || auth), - var ingressMac = crypto.Sha3(append(Xor(macSecret, initNonce), auth...)) - // # destroy remote-nonce - + var egressMac, ingressMac []byte + if initiator { + egressMac = Xor(macSecret, respNonce) + ingressMac = Xor(macSecret, initNonce) + } else { + egressMac = Xor(macSecret, initNonce) + ingressMac = Xor(macSecret, respNonce) + } clogger.Debugf("aes-secret: %v", hexkey(aesSecret)) clogger.Debugf("mac-secret: %v", hexkey(macSecret)) clogger.Debugf("egress-mac: %v", hexkey(egressMac)) diff --git a/p2p/crypto_test.go b/p2p/crypto_test.go index 2cb9c9bbe4..fd6b8aa290 100644 --- a/p2p/crypto_test.go +++ b/p2p/crypto_test.go @@ -109,12 +109,12 @@ func TestCryptoHandshake(t *testing.T) { conn0, conn1 := net.Pipe() // now both parties should have the same session parameters - initSessionToken, initRW, err := initiator.newSession(bufio.NewReader(conn0), conn0, initNonce, recNonce, auth, randomPrivKey, remoteRandomPubKey) + initSessionToken, initRW, err := initiator.newSession(bufio.NewReader(conn0), conn0, true, initNonce, recNonce, auth, randomPrivKey, remoteRandomPubKey) if err != nil { t.Errorf("%v", err) } - recSessionToken, recRW, err := receiver.newSession(bufio.NewReader(conn1), conn1, remoteInitNonce, remoteRecNonce, auth, remoteRandomPrivKey, remoteInitRandomPubKey) + recSessionToken, recRW, err := receiver.newSession(bufio.NewReader(conn1), conn1, false, remoteInitNonce, remoteRecNonce, auth, remoteRandomPrivKey, remoteInitRandomPubKey) if err != nil { t.Errorf("%v", err) } @@ -149,11 +149,11 @@ func TestCryptoHandshake(t *testing.T) { if !bytes.Equal(initSecretRW.macSecret, recSecretRW.macSecret) { t.Errorf("macSecrets do not match") } - if !bytes.Equal(initSecretRW.egressMac, recSecretRW.egressMac) { - t.Errorf("egressMacs do not match") + if !bytes.Equal(initSecretRW.egressMac, recSecretRW.ingressMac) { + t.Errorf("initiator's egressMac do not match receiver's ingressMac") } - if !bytes.Equal(initSecretRW.ingressMac, recSecretRW.ingressMac) { - t.Errorf("ingressMacs do not match") + if !bytes.Equal(initSecretRW.ingressMac, recSecretRW.egressMac) { + t.Errorf("initiator's inressMac do not match receiver's egressMac") } } diff --git a/p2p/encryption.go b/p2p/encryption.go index b0e6cfd501..66fc3c4277 100644 --- a/p2p/encryption.go +++ b/p2p/encryption.go @@ -7,11 +7,9 @@ import ( "crypto/hmac" "crypto/sha256" "encoding/binary" - "fmt" "hash" "io" - // "github.com/ethereum/go-ethereum/crypto/sha256" "github.com/ethereum/go-ethereum/ethutil" "github.com/ethereum/go-ethereum/rlp" ) @@ -82,11 +80,11 @@ func (self *CryptoMsgRW) WriteMsg(msg Msg) (err error) { copy(plaintext[listhdrLen:], code) msg.Payload.Read(plaintext[listhdrLen+codeLen:]) self.Encrypt(ciphertext, plaintext) - fmt.Printf("ENCRYPT:\npt: %v\nct: %v\n", hexkey(plaintext), hexkey(ciphertext)) if _, err = self.w.Write(ciphertext); err != nil { return } - if _, err = self.w.Write(self.egress.Sum(nil)); err != nil { + mac := self.egress.Sum(nil) + if _, err = self.w.Write(mac); err != nil { return } @@ -130,18 +128,16 @@ func (self *CryptoMsgRW) readPayload(size uint32) (r rlp.ByteReader, err error) ciphertext := make([]byte, size) self.r.Read(ciphertext) self.Decrypt(plaintext, ciphertext) - fmt.Printf("DECRYPT:\npt: %v\nct: %v\n", hexkey(plaintext), hexkey(ciphertext)) mac := make([]byte, 32) if _, err = self.r.Read(mac); err != nil { err = newPeerError(errRead, "%v", err) return } - // var expectedMac = self.ingress.Sum(nil) - // if !hmac.Equal(expectedMac, mac) { - // err = newPeerError(errAuthentication, "ingress incorrect") - // return - // } + var expectedMac = self.ingress.Sum(nil) + if !hmac.Equal(expectedMac, mac) { + err = newPeerError(errAuthentication, "ingress incorrect:\nexp %v\ngot %v\n", hexkey(expectedMac), hexkey(mac)) + return + } r = bytes.NewReader(plaintext) - // r = io.LimitReader(bytes.NewReader(plaintext), int64(size)) return } diff --git a/p2p/encryption_test.go b/p2p/encryption_test.go index 5128c95eda..20de23a23c 100644 --- a/p2p/encryption_test.go +++ b/p2p/encryption_test.go @@ -4,7 +4,6 @@ import ( "bufio" "bytes" "crypto/rand" - // "fmt" "io" "net" "testing" @@ -47,7 +46,8 @@ func TestEncryption(t *testing.T) { } messenger0 := NewMessenger(rw0) - rw1, err := NewCryptoMsgRW(bufio.NewReader(conn1), conn1, args[0], args[1], args[2], args[3]) + // note that args 3/2 swapped! ingress <-> egress MAC should reverse + rw1, err := NewCryptoMsgRW(bufio.NewReader(conn1), conn1, args[0], args[1], args[3], args[2]) if err != nil { return } diff --git a/p2p/peer.go b/p2p/peer.go index 7704c764b8..a9afe4322a 100644 --- a/p2p/peer.go +++ b/p2p/peer.go @@ -321,6 +321,8 @@ func (p *Peer) handleCryptoHandshake() (err error) { if crw, err = NewMsgRW(bufio.NewReader(p.conn), p.conn); err != nil { return } + p.Infof("insecure connection using no encryption/authentication") + case EthCrypto: // cryptoId is just created for the lifecycle of the handshake // it is survived by an encrypted readwriter @@ -350,6 +352,7 @@ func (p *Peer) handleCryptoHandshake() (err error) { p.Errorf("%v", err) } p.crw = NewMessenger(crw) + p.Infof("secure connection using %v", p.CryptoType) return } From 7f12ac702f67f07c1f315f642bdfee67f4041fed Mon Sep 17 00:00:00 2001 From: zelig Date: Fri, 23 Jan 2015 13:00:02 +0000 Subject: [PATCH 29/31] add -crypto option to switch encryption on by a flag --- cmd/ethereum/flags.go | 2 ++ cmd/ethereum/main.go | 1 + eth/backend.go | 20 +++++++++++--------- eth/protocol.go | 2 +- p2p/peer.go | 3 +++ p2p/server.go | 3 +++ 6 files changed, 21 insertions(+), 10 deletions(-) diff --git a/cmd/ethereum/flags.go b/cmd/ethereum/flags.go index f829744dc9..94aa765576 100644 --- a/cmd/ethereum/flags.go +++ b/cmd/ethereum/flags.go @@ -65,6 +65,7 @@ var ( SHH bool Dial bool PrintVersion bool + Encryption bool ) // flags specific to cli client @@ -115,6 +116,7 @@ func Init() { flag.BoolVar(&ShowGenesis, "genesis", false, "Dump the genesis block") flag.StringVar(&ImportChain, "chain", "", "Imports given chain") + flag.BoolVar(&Encryption, "crypto", false, "whether to use encryption (experimental, temporary)") flag.BoolVar(&Dump, "dump", false, "output the ethereum state in JSON format. Sub args [number, hash]") flag.StringVar(&DumpHash, "hash", "", "specify arg in hex") flag.IntVar(&DumpNumber, "number", -1, "specify arg in number") diff --git a/cmd/ethereum/main.go b/cmd/ethereum/main.go index b816c678e7..2eec4695ff 100644 --- a/cmd/ethereum/main.go +++ b/cmd/ethereum/main.go @@ -75,6 +75,7 @@ func main() { KeyRing: KeyRing, Shh: SHH, Dial: Dial, + Encryption: Encryption, }) if err != nil { diff --git a/eth/backend.go b/eth/backend.go index c3c7d12878..4cbf30bbe3 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -36,8 +36,9 @@ type Config struct { NATType string PMPGateway string - Shh bool - Dial bool + Shh bool + Dial bool + Encryption bool KeyManager *crypto.KeyManager } @@ -107,7 +108,7 @@ func New(config *Config) (*Ethereum, error) { keyManager.Init(config.KeyRing, 0, false) // Create a new client id for this instance. This will help identifying the node on the network - clientId := p2p.NewSimpleClientIdentity(config.Name, config.Version, config.Identifier, keyManager.PublicKey()) + clientId := p2p.NewSimpleClientIdentity(config.Name, config.Version, config.Identifier, keyManager.PrivateKey(), keyManager.PublicKey()) saveProtocolVersion(db) //ethutil.Config.Db = db @@ -143,12 +144,13 @@ func New(config *Config) (*Ethereum, error) { fmt.Println(nat) eth.net = &p2p.Server{ - Identity: clientId, - MaxPeers: config.MaxPeers, - Protocols: protocols, - Blacklist: eth.blacklist, - NAT: p2p.UPNP(), - NoDial: !config.Dial, + Identity: clientId, + MaxPeers: config.MaxPeers, + Protocols: protocols, + Blacklist: eth.blacklist, + NAT: p2p.UPNP(), + NoDial: !config.Dial, + Encryption: config.Encryption, } if len(config.Port) > 0 { diff --git a/eth/protocol.go b/eth/protocol.go index 24a0f0a8e8..3ade8a2ec8 100644 --- a/eth/protocol.go +++ b/eth/protocol.go @@ -97,7 +97,7 @@ func runEthProtocol(txPool txPool, chainManager chainManager, blockPool blockPoo blockPool: blockPool, rw: rw, peer: peer, - id: fmt.Sprintf("%x", peer.Identity().Pubkey()[:8]), + id: fmt.Sprintf("%x", peer.PublicKey()[:8]), } err = self.handleStatus() if err == nil { diff --git a/p2p/peer.go b/p2p/peer.go index a9afe4322a..94da74b503 100644 --- a/p2p/peer.go +++ b/p2p/peer.go @@ -107,6 +107,9 @@ func newServerPeer(server *Server, conn net.Conn, dialAddr *peerAddr) *Peer { p.otherPeers = server.Peers p.pubkeyHook = server.verifyPeer p.runBaseProtocol = true + if server.Encryption { + p.CryptoType = EthCrypto + } // laddr can be updated concurrently by NAT traversal. // newServerPeer must be called with the server lock held. diff --git a/p2p/server.go b/p2p/server.go index 9a05a7619d..a553862363 100644 --- a/p2p/server.go +++ b/p2p/server.go @@ -62,6 +62,9 @@ type Server struct { // If NoDial is true, the server will not dial any peers. NoDial bool + // whether or not use encryption + Encryption bool + // Hook for testing. This is useful because we can inhibit // the whole protocol stack. newPeerFunc peerFunc From 876fd8bd176fdd48aa5fa17ac3d35a5884f987fd Mon Sep 17 00:00:00 2001 From: zelig Date: Fri, 23 Jan 2015 13:04:28 +0000 Subject: [PATCH 30/31] return from peer loop if crypto fails, oops --- p2p/peer.go | 1 + 1 file changed, 1 insertion(+) diff --git a/p2p/peer.go b/p2p/peer.go index 94da74b503..1dc3a4b2a6 100644 --- a/p2p/peer.go +++ b/p2p/peer.go @@ -349,6 +349,7 @@ func (p *Peer) handleCryptoHandshake() (err error) { // this bit handles the handshake and creates a secure communications channel with if sessionToken, crw, err = crypto.NewSession(bufio.NewReader(p.conn), p.conn, p.PublicKey(), sessionToken, initiator); err != nil { p.Errorf("unable to setup secure session: %v", err) + return } default: err = fmt.Errorf("unrecognised crypto type %v", p.CryptoType) From 94af107fbc0175494ef04683c114c0de5b241cf4 Mon Sep 17 00:00:00 2001 From: zelig Date: Mon, 26 Jan 2015 14:08:30 +0000 Subject: [PATCH 31/31] a few minor changes from review --- p2p/crypto.go | 25 ++++++++++++------------- p2p/encryption.go | 1 - p2p/peer.go | 4 ++-- 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/p2p/crypto.go b/p2p/crypto.go index e6cd82d80e..c4bf4bcea5 100644 --- a/p2p/crypto.go +++ b/p2p/crypto.go @@ -14,13 +14,13 @@ import ( var clogger = ethlogger.NewLogger("CRYPTOID") -var ( +const ( sskLen int = 16 // ecies.MaxSharedKeyLength(pubKey) / 2 sigLen int = 65 // elliptic S256 pubLen int = 64 // 512 bit pubkey in uncompressed representation without format byte - keyLen int = 32 // ECDSA - msgLen int = 194 // sigLen + keyLen + pubLen + keyLen + 1 = 194 - resLen int = 97 // pubLen + keyLen + 1 + shaLen int = 32 // hash length (for nonce etc) + msgLen int = 194 // sigLen + shaLen + pubLen + shaLen + 1 = 194 + resLen int = 97 // pubLen + shaLen + 1 iHSLen int = 307 // size of the final ECIES payload sent as initiator's handshake rHSLen int = 210 // size of the final ECIES payload sent as receiver's handshake ) @@ -191,7 +191,7 @@ func (self *cryptoId) startHandshake(remotePubKeyS, sessionToken []byte) (auth [ return } - var tokenFlag byte + var tokenFlag byte // = 0x00 if sessionToken == nil { // no session token found means we need to generate shared secret. // ecies shared secret is used as initial session token for new peers @@ -199,7 +199,6 @@ func (self *cryptoId) startHandshake(remotePubKeyS, sessionToken []byte) (auth [ if sessionToken, err = ecies.ImportECDSA(self.prvKey).GenerateShared(ecies.ImportECDSAPublic(remotePubKey), sskLen, sskLen); err != nil { return } - // tokenFlag = 0x00 // redundant } else { // for known peers, we use stored token from the previous session tokenFlag = 0x01 @@ -209,7 +208,7 @@ func (self *cryptoId) startHandshake(remotePubKeyS, sessionToken []byte) (auth [ // E(remote-pubk, S(ecdhe-random, token^nonce) || H(ecdhe-random-pubk) || pubk || nonce || 0x1) // allocate msgLen long message, var msg []byte = make([]byte, msgLen) - initNonce = msg[msgLen-keyLen-1 : msgLen-1] + initNonce = msg[msgLen-shaLen-1 : msgLen-1] if _, err = rand.Read(initNonce); err != nil { return } @@ -238,9 +237,9 @@ func (self *cryptoId) startHandshake(remotePubKeyS, sessionToken []byte) (auth [ if randomPubKey64, err = ExportPublicKey(&randomPrvKey.PublicKey); err != nil { return } - copy(msg[sigLen:sigLen+keyLen], crypto.Sha3(randomPubKey64)) + copy(msg[sigLen:sigLen+shaLen], crypto.Sha3(randomPubKey64)) // pubkey copied to the correct segment. - copy(msg[sigLen+keyLen:sigLen+keyLen+pubLen], self.pubKeyS) + copy(msg[sigLen+shaLen:sigLen+shaLen+pubLen], self.pubKeyS) // nonce is already in the slice // stick tokenFlag byte to the end msg[msgLen-1] = tokenFlag @@ -288,7 +287,7 @@ func (self *cryptoId) respondToHandshake(auth, remotePubKeyS, sessionToken []byt } // the initiator nonce is read off the end of the message - initNonce = msg[msgLen-keyLen-1 : msgLen-1] + initNonce = msg[msgLen-shaLen-1 : msgLen-1] // I prove that i own prv key (to derive shared secret, and read nonce off encrypted msg) and that I own shared secret // they prove they own the private key belonging to ecdhe-random-pubk // we can now reconstruct the signed message and recover the peers pubkey @@ -304,8 +303,8 @@ func (self *cryptoId) respondToHandshake(auth, remotePubKeyS, sessionToken []byt // now we find ourselves a long task too, fill it random var resp = make([]byte, resLen) - // generate keyLen long nonce - respNonce = resp[pubLen : pubLen+keyLen] + // generate shaLen long nonce + respNonce = resp[pubLen : pubLen+shaLen] if _, err = rand.Read(respNonce); err != nil { return } @@ -343,7 +342,7 @@ func (self *cryptoId) completeHandshake(auth []byte) (respNonce []byte, remoteRa return } - respNonce = msg[pubLen : pubLen+keyLen] + respNonce = msg[pubLen : pubLen+shaLen] var remoteRandomPubKeyS = msg[:pubLen] if remoteRandomPubKey, err = ImportPublicKey(remoteRandomPubKeyS); err != nil { return diff --git a/p2p/encryption.go b/p2p/encryption.go index 66fc3c4277..cfe69907fd 100644 --- a/p2p/encryption.go +++ b/p2p/encryption.go @@ -19,7 +19,6 @@ CryptoMsgRW implements MsgReadWriter a message read writer with encryption and a it is initialised by cryptoId.NewSession() after a successful crypto handshake on the same IO It uses the legacy devp2p packet structure (temporary) */ - type CryptoMsgRW struct { r io.Reader w io.Writer diff --git a/p2p/peer.go b/p2p/peer.go index 1dc3a4b2a6..473baf5135 100644 --- a/p2p/peer.go +++ b/p2p/peer.go @@ -217,8 +217,8 @@ func (p *Peer) loop() (reason DiscReason, err error) { // from here on everything can be encrypted, authenticated return DiscProtocolError, err // no graceful disconnect } - close(p.cryptoReady) defer p.crw.Close() + close(p.cryptoReady) // read loop protoDone := make(chan struct{}, 1) @@ -258,7 +258,6 @@ loop: // read or write failed. there is no need to run the // polite disconnect sequence because the connection // is probably dead anyway. - // TODO: handle write errors as well return DiscNetworkError, err case err = <-p.protoErr: reason = discReasonForError(err) @@ -330,6 +329,7 @@ func (p *Peer) handleCryptoHandshake() (err error) { // cryptoId is just created for the lifecycle of the handshake // it is survived by an encrypted readwriter var initiator bool + // TODO: this is clearly a placeholder until we figure how we store session Token var sessionToken []byte sessionToken = make([]byte, keyLen) if _, err = rand.Read(sessionToken); err != nil {