From b194b046a75d879f2fb796b089f555a649eeaba5 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Wed, 20 May 2020 14:57:17 +0200 Subject: [PATCH 01/70] p2p/discover: implement new packet format proposal --- p2p/discover/node.go | 5 +- p2p/discover/v4_lookup_test.go | 2 +- p2p/discover/v5_encoding.go | 690 ++++++++++++++++++------------- p2p/discover/v5_encoding_test.go | 19 +- p2p/discover/v5_session.go | 5 +- 5 files changed, 425 insertions(+), 296 deletions(-) diff --git a/p2p/discover/node.go b/p2p/discover/node.go index e635c64ac90..9ffe101ccff 100644 --- a/p2p/discover/node.go +++ b/p2p/discover/node.go @@ -46,7 +46,10 @@ func encodePubkey(key *ecdsa.PublicKey) encPubkey { return e } -func decodePubkey(curve elliptic.Curve, e encPubkey) (*ecdsa.PublicKey, error) { +func decodePubkey(curve elliptic.Curve, e []byte) (*ecdsa.PublicKey, error) { + if len(e) != len(encPubkey{}) { + return nil, errors.New("wrong size public key data") + } p := &ecdsa.PublicKey{Curve: curve, X: new(big.Int), Y: new(big.Int)} half := len(e) / 2 p.X.SetBytes(e[:half]) diff --git a/p2p/discover/v4_lookup_test.go b/p2p/discover/v4_lookup_test.go index 20093852622..52d7323d3ac 100644 --- a/p2p/discover/v4_lookup_test.go +++ b/p2p/discover/v4_lookup_test.go @@ -34,7 +34,7 @@ func TestUDPv4_Lookup(t *testing.T) { test := newUDPTest(t) // Lookup on empty table returns no nodes. - targetKey, _ := decodePubkey(crypto.S256(), lookupTestnet.target) + targetKey, _ := decodePubkey(crypto.S256(), lookupTestnet.target[:]) if results := test.udp.LookupPubkey(targetKey); len(results) > 0 { t.Fatalf("lookup on empty table returned %d results: %#v", len(results), results) } diff --git a/p2p/discover/v5_encoding.go b/p2p/discover/v5_encoding.go index c1a790dd2c4..34497f4e964 100644 --- a/p2p/discover/v5_encoding.go +++ b/p2p/discover/v5_encoding.go @@ -21,12 +21,13 @@ import ( "crypto/aes" "crypto/cipher" "crypto/ecdsa" - "crypto/elliptic" crand "crypto/rand" "crypto/sha256" + "encoding/binary" "errors" "fmt" "hash" + "hash/crc64" "net" "time" @@ -60,6 +61,51 @@ const ( ) // Discovery v5 packet structures. +type ( + packetHeaderV5 struct { + Checksum uint64 + SrcID enode.ID + Flags byte + AuthSize uint16 + } + + whoareyouAuthDataV5 struct { + Nonce [gcmNonceSize]byte // nonce of request packet + IDNonce [32]byte // ID proof data + RecordSeq uint64 // highest known ENR sequence of requester + } + + handshakeAuthDataV5 struct { + h struct { + Version uint8 // protocol version + Nonce [gcmNonceSize]byte // AES-GCM nonce of message + SigSize byte // ignature data + PubkeySize byte // offset of + } + // Trailing variable-size data. + signature, pubkey, record []byte + } + + messageAuthDataV5 struct { + Nonce [gcmNonceSize]byte // AES-GCM nonce of message + } +) + +// Packet header flag values. +const ( + flagMessage = 0 + flagWhoareyou = 1 + flagHandshake = 2 +) + +var ( + sizeofPacketHeaderV5 = binary.Size(packetHeaderV5{}) + sizeofWhoareyouAuthDataV5 = binary.Size(whoareyouAuthDataV5{}) + sizeofHandshakeAuthDataV5 = binary.Size(handshakeAuthDataV5{}.h) + sizeofMessageAuthDataV5 = binary.Size(messageAuthDataV5{}) +) + +// Discovery v5 messages. type ( // unknownV5 represents any packet that can't be decrypted. unknownV5 struct { @@ -137,76 +183,45 @@ type ( const ( // Encryption/authentication parameters. - authSchemeName = "gcm" aesKeySize = 16 gcmNonceSize = 12 idNoncePrefix = "discovery-id-nonce" handshakeTimeout = time.Second + + // Protocol constants. + handshakeVersion = 1 + minVersion = 1 ) var ( errTooShort = errors.New("packet too short") + errInvalidHeader = errors.New("invalid packet header") errUnexpectedHandshake = errors.New("unexpected auth response, not in handshake") errHandshakeNonceMismatch = errors.New("wrong nonce in auth response") errInvalidAuthKey = errors.New("invalid ephemeral pubkey") errUnknownAuthScheme = errors.New("unknown auth scheme in handshake") errNoRecord = errors.New("expected ENR in handshake but none sent") errInvalidNonceSig = errors.New("invalid ID nonce signature") - zeroNonce = make([]byte, gcmNonceSize) + errMessageTooShort = errors.New("message contains no data") + errMessageDecrypt = errors.New("cannot decrypt message") ) // wireCodec encodes and decodes discovery v5 packets. type wireCodec struct { - sha256 hash.Hash - localnode *enode.LocalNode - privkey *ecdsa.PrivateKey - myChtagHash enode.ID - myWhoareyouMagic []byte - - sc *sessionCache + sha256 hash.Hash + localnode *enode.LocalNode + privkey *ecdsa.PrivateKey + checksum hash.Hash64 + myChecksum uint64 // protocol checksum of local node + + buf bytes.Buffer // used for encoding of packets + msgbuf bytes.Buffer // used for encoding message content + reader bytes.Reader // used for decoding + sc *sessionCache } type handshakeSecrets struct { - writeKey, readKey, authRespKey []byte -} - -type authHeader struct { - authHeaderList - isHandshake bool -} - -type authHeaderList struct { - Auth []byte // authentication info of packet - IDNonce [32]byte // IDNonce of WHOAREYOU - Scheme string // name of encryption/authentication scheme - EphemeralKey []byte // ephemeral public key - Response []byte // encrypted authResponse -} - -type authResponse struct { - Version uint - Signature []byte - Record *enr.Record `rlp:"nil"` // sender's record -} - -func (h *authHeader) DecodeRLP(r *rlp.Stream) error { - k, _, err := r.Kind() - if err != nil { - return err - } - if k == rlp.Byte || k == rlp.String { - return r.Decode(&h.Auth) - } - h.isHandshake = true - return r.Decode(&h.authHeaderList) -} - -// ephemeralKey decodes the ephemeral public key in the header. -func (h *authHeaderList) ephemeralKey(curve elliptic.Curve) *ecdsa.PublicKey { - var key encPubkey - copy(key[:], h.EphemeralKey) - pubkey, _ := decodePubkey(curve, key) - return pubkey + writeKey, readKey []byte } // newWireCodec creates a wire codec. @@ -215,12 +230,10 @@ func newWireCodec(ln *enode.LocalNode, key *ecdsa.PrivateKey, clock mclock.Clock sha256: sha256.New(), localnode: ln, privkey: key, + checksum: crc64.New(crc64.MakeTable(crc64.ISO)), sc: newSessionCache(1024, clock), } - // Create magic strings for packet matching. - self := ln.ID() - c.myWhoareyouMagic = c.sha256sum(self[:], []byte("WHOAREYOU")) - copy(c.myChtagHash[:], c.sha256sum(self[:])) + c.myChecksum = c.makeChecksum(ln.ID()) return c } @@ -236,98 +249,121 @@ func (c *wireCodec) encode(id enode.ID, addr string, packet packetV5, challenge } return enc, nil, err } - // Ensure calling code sets node if needed. - if challenge != nil && challenge.node == nil { - panic("BUG: missing challenge.node in encode") + + if challenge != nil { + return c.encodeHandshakeMessage(id, addr, packet, challenge) } - writeKey := c.sc.writeKey(id, addr) - if writeKey != nil || challenge != nil { - return c.encodeEncrypted(id, addr, packet, writeKey, challenge) + if key := c.sc.writeKey(id, addr); key != nil { + return c.encodeMessage(id, addr, packet, key) } + // No keys, no handshake: send random data to kick off the handshake. return c.encodeRandom(id) } -// encodeRandom encodes a random packet. -func (c *wireCodec) encodeRandom(toID enode.ID) ([]byte, []byte, error) { - tag := xorTag(c.sha256sum(toID[:]), c.localnode.ID()) - r := make([]byte, 44) // TODO randomize size - if _, err := crand.Read(r); err != nil { - return nil, nil, err +// makeHeader creates a packet header. +func (c *wireCodec) makeHeader(toID enode.ID, flags byte, authsizeExtra int) *packetHeaderV5 { + var authsize int + switch flags { + case flagMessage: + authsize = sizeofMessageAuthDataV5 + case flagWhoareyou: + authsize = sizeofWhoareyouAuthDataV5 + case flagHandshake: + authsize = sizeofHandshakeAuthDataV5 + default: + panic(fmt.Errorf("BUG: invalid packet header flags %x", flags)) + } + authsize += authsizeExtra + if authsize > int(^uint16(0)) { + panic(fmt.Errorf("BUG: auth size %d overflows uint16", authsize)) } - nonce := make([]byte, gcmNonceSize) - if _, err := crand.Read(nonce); err != nil { + return &packetHeaderV5{ + Checksum: c.makeChecksum(toID), + SrcID: c.localnode.ID(), + Flags: flags, + AuthSize: uint16(authsize), + } +} + +// encodeRandom encodes a packet with random content. +func (c *wireCodec) encodeRandom(toID enode.ID) ([]byte, []byte, error) { + var auth messageAuthDataV5 + if _, err := crand.Read(auth.Nonce[:]); err != nil { return nil, nil, fmt.Errorf("can't get random data: %v", err) } - b := new(bytes.Buffer) - b.Write(tag[:]) - rlp.Encode(b, nonce) - b.Write(r) - return b.Bytes(), nonce, nil + + c.buf.Reset() + binary.Write(&c.buf, binary.BigEndian, c.makeHeader(toID, flagMessage, 0)) + binary.Write(&c.buf, binary.BigEndian, &auth) + return c.buf.Bytes(), auth.Nonce[:], nil } -// encodeWhoareyou encodes WHOAREYOU. +// encodeWhoareyou encodes a WHOAREYOU packet. func (c *wireCodec) encodeWhoareyou(toID enode.ID, packet *whoareyouV5) ([]byte, error) { // Sanity check node field to catch misbehaving callers. if packet.RecordSeq > 0 && packet.node == nil { panic("BUG: missing node in whoareyouV5 with non-zero seq") } - b := new(bytes.Buffer) - b.Write(c.sha256sum(toID[:], []byte("WHOAREYOU"))) - err := rlp.Encode(b, packet) - return b.Bytes(), err + auth := &whoareyouAuthDataV5{ + IDNonce: packet.IDNonce, + RecordSeq: packet.RecordSeq, + } + copy(auth.Nonce[:], packet.AuthTag) + head := c.makeHeader(toID, flagWhoareyou, 0) + + c.buf.Reset() + binary.Write(&c.buf, binary.BigEndian, head) + binary.Write(&c.buf, binary.BigEndian, auth) + return c.buf.Bytes(), nil } -// encodeEncrypted encodes an encrypted packet. -func (c *wireCodec) encodeEncrypted(toID enode.ID, toAddr string, packet packetV5, writeKey []byte, challenge *whoareyouV5) (enc []byte, authTag []byte, err error) { - nonce := make([]byte, gcmNonceSize) - if _, err := crand.Read(nonce); err != nil { - return nil, nil, fmt.Errorf("can't get random data: %v", err) +// encodeHandshakeMessage encodes an encrypted message with a handshake +// response header. +func (c *wireCodec) encodeHandshakeMessage(toID enode.ID, addr string, packet packetV5, challenge *whoareyouV5) ([]byte, []byte, error) { + // Ensure calling code sets challenge.node. + if challenge.node == nil { + panic("BUG: missing challenge.node in encode") } - var headEnc []byte - if challenge == nil { - // Regular packet, use existing key and simply encode nonce. - headEnc, _ = rlp.EncodeToBytes(nonce) - } else { - // We're answering WHOAREYOU, generate new keys and encrypt with those. - header, sec, err := c.makeAuthHeader(nonce, challenge) - if err != nil { - return nil, nil, err - } - if headEnc, err = rlp.EncodeToBytes(header); err != nil { - return nil, nil, err - } - c.sc.storeNewSession(toID, toAddr, sec.readKey, sec.writeKey) - writeKey = sec.writeKey + // Generate new secrets. + auth, sec, err := c.makeHandshakeHeader(toID, addr, challenge) + if err != nil { + return nil, nil, err } - // Encode the packet. - body := new(bytes.Buffer) - body.WriteByte(packet.kind()) - if err := rlp.Encode(body, packet); err != nil { + // TODO: this should happen when the first authenticated message is received + c.sc.storeNewSession(toID, addr, sec.readKey, sec.writeKey) + + // Encode header and auth header. + var ( + authsizeExtra = len(auth.pubkey) + len(auth.signature) + len(auth.record) + head = c.makeHeader(toID, flagHandshake, authsizeExtra) + ) + c.buf.Reset() + binary.Write(&c.buf, binary.BigEndian, head) + binary.Write(&c.buf, binary.BigEndian, &auth.h) + c.buf.Write(auth.signature) + c.buf.Write(auth.pubkey) + c.buf.Write(auth.record) + output := c.buf.Bytes() + + // Encrypt packet body. + c.msgbuf.Reset() + c.msgbuf.WriteByte(packet.kind()) + if err := rlp.Encode(&c.msgbuf, packet); err != nil { return nil, nil, err } - tag := xorTag(c.sha256sum(toID[:]), c.localnode.ID()) - headsize := len(tag) + len(headEnc) - headbuf := make([]byte, headsize) - copy(headbuf[:], tag[:]) - copy(headbuf[len(tag):], headEnc) - - // Encrypt the body. - enc, err = encryptGCM(headbuf, writeKey, nonce, body.Bytes(), tag[:]) - return enc, nonce, err + messagePT := c.msgbuf.Bytes() + headerData := output + output, err = encryptGCM(output, sec.writeKey, auth.h.Nonce[:], messagePT, headerData) + return output, auth.h.Nonce[:], err } // encodeAuthHeader creates the auth header on a call packet following WHOAREYOU. -func (c *wireCodec) makeAuthHeader(nonce []byte, challenge *whoareyouV5) (*authHeaderList, *handshakeSecrets, error) { - resp := &authResponse{Version: 5} - - // Add our record to response if it's newer than what remote - // side has. - ln := c.localnode.Node() - if challenge.RecordSeq < ln.Seq() { - resp.Record = ln.Record() - } +func (c *wireCodec) makeHandshakeHeader(toID enode.ID, addr string, challenge *whoareyouV5) (*handshakeAuthDataV5, *handshakeSecrets, error) { + auth := new(handshakeAuthDataV5) + auth.h.Version = handshakeVersion + auth.h.Nonce = c.sc.nextNonce(toID, addr) // Create the ephemeral key. This needs to be first because the // key is part of the ID nonce signature. @@ -340,226 +376,255 @@ func (c *wireCodec) makeAuthHeader(nonce []byte, challenge *whoareyouV5) (*authH return nil, nil, fmt.Errorf("can't generate ephemeral key") } ephpubkey := encodePubkey(&ephkey.PublicKey) + auth.pubkey = ephpubkey[:] + auth.h.PubkeySize = byte(len(auth.pubkey)) // Add ID nonce signature to response. idsig, err := c.signIDNonce(challenge.IDNonce[:], ephpubkey[:]) if err != nil { return nil, nil, fmt.Errorf("can't sign: %v", err) } - resp.Signature = idsig + auth.signature = idsig + auth.h.SigSize = byte(len(auth.signature)) + + // Add our record to response if it's newer than what remote + // side has. + ln := c.localnode.Node() + if challenge.RecordSeq < ln.Seq() { + auth.record, _ = rlp.EncodeToBytes(ln.Record()) + } // Create session keys. sec := c.deriveKeys(c.localnode.ID(), challenge.node.ID(), ephkey, remotePubkey, challenge) if sec == nil { return nil, nil, fmt.Errorf("key derivation failed") } - - // Encrypt the authentication response and assemble the auth header. - respRLP, err := rlp.EncodeToBytes(resp) - if err != nil { - return nil, nil, fmt.Errorf("can't encode auth response: %v", err) - } - respEnc, err := encryptGCM(nil, sec.authRespKey, zeroNonce, respRLP, nil) - if err != nil { - return nil, nil, fmt.Errorf("can't encrypt auth response: %v", err) - } - head := &authHeaderList{ - Auth: nonce, - Scheme: authSchemeName, - IDNonce: challenge.IDNonce, - EphemeralKey: ephpubkey[:], - Response: respEnc, - } - return head, sec, err -} - -// deriveKeys generates session keys using elliptic-curve Diffie-Hellman key agreement. -func (c *wireCodec) deriveKeys(n1, n2 enode.ID, priv *ecdsa.PrivateKey, pub *ecdsa.PublicKey, challenge *whoareyouV5) *handshakeSecrets { - eph := ecdh(priv, pub) - if eph == nil { - return nil - } - - info := []byte("discovery v5 key agreement") - info = append(info, n1[:]...) - info = append(info, n2[:]...) - kdf := hkdf.New(sha256.New, eph, challenge.IDNonce[:], info) - sec := handshakeSecrets{ - writeKey: make([]byte, aesKeySize), - readKey: make([]byte, aesKeySize), - authRespKey: make([]byte, aesKeySize), - } - kdf.Read(sec.writeKey) - kdf.Read(sec.readKey) - kdf.Read(sec.authRespKey) - for i := range eph { - eph[i] = 0 - } - return &sec + return auth, sec, err } -// signIDNonce creates the ID nonce signature. -func (c *wireCodec) signIDNonce(nonce, ephkey []byte) ([]byte, error) { - idsig, err := crypto.Sign(c.idNonceHash(nonce, ephkey), c.privkey) - if err != nil { - return nil, fmt.Errorf("can't sign: %v", err) +// encodeMessage encodes an encrypted message packet. +func (c *wireCodec) encodeMessage(toID enode.ID, addr string, packet packetV5, writeKey []byte) (enc []byte, authTag []byte, err error) { + var ( + auth messageAuthDataV5 + head = c.makeHeader(toID, flagMessage, 0) + ) + auth.Nonce = c.sc.nextNonce(toID, addr) + c.buf.Reset() + binary.Write(&c.buf, binary.BigEndian, head) + binary.Write(&c.buf, binary.BigEndian, &auth) + output := c.buf.Bytes() + + // Encode the message plaintext. + c.msgbuf.Reset() + c.msgbuf.WriteByte(packet.kind()) + if err := rlp.Encode(&c.msgbuf, packet); err != nil { + return nil, nil, err } - return idsig[:len(idsig)-1], nil // remove recovery ID -} + messagePT := c.msgbuf.Bytes() -// idNonceHash computes the hash of id nonce with prefix. -func (c *wireCodec) idNonceHash(nonce, ephkey []byte) []byte { - h := c.sha256reset() - h.Write([]byte(idNoncePrefix)) - h.Write(nonce) - h.Write(ephkey) - return h.Sum(nil) + // Encrypt message data. + headerData := output + output, err = encryptGCM(output, writeKey, auth.Nonce[:], messagePT, headerData) + return output, auth.Nonce[:], err } // decode decodes a discovery packet. -func (c *wireCodec) decode(input []byte, addr string) (enode.ID, *enode.Node, packetV5, error) { +func (c *wireCodec) decode(input []byte, addr string) (src enode.ID, n *enode.Node, p packetV5, err error) { // Delete timed-out handshakes. This must happen before decoding to avoid // processing the same handshake twice. c.sc.handshakeGC() - if len(input) < 32 { + // Decode and verify the header. + var head packetHeaderV5 + c.reader.Reset(input) + if c.reader.Len() < sizeofPacketHeaderV5 { return enode.ID{}, nil, nil, errTooShort } - if bytes.HasPrefix(input, c.myWhoareyouMagic) { - p, err := c.decodeWhoareyou(input) - return enode.ID{}, nil, p, err + binary.Read(&c.reader, binary.BigEndian, &head) + if head.Checksum != c.myChecksum { + return enode.ID{}, nil, nil, errBadHash } - sender := xorTag(input[:32], c.myChtagHash) - p, n, err := c.decodeEncrypted(sender, addr, input) - return sender, n, p, err -} + if int(head.AuthSize) > c.reader.Len() { + return enode.ID{}, nil, nil, errInvalidHeader + } + src = head.SrcID -// decodeWhoareyou decode a WHOAREYOU packet. -func (c *wireCodec) decodeWhoareyou(input []byte) (packetV5, error) { - packet := new(whoareyouV5) - err := rlp.DecodeBytes(input[32:], packet) - return packet, err + // Decode auth part and message. + switch { + case head.Flags&flagWhoareyou != 0: + p, err = c.decodeWhoareyou(&head) + case head.Flags&flagHandshake != 0: + n, p, err = c.decodeHandshakeMessage(addr, &head, input) + default: + p, err = c.decodeMessage(addr, &head, input) + } + return src, n, p, err } -// decodeEncrypted decodes an encrypted discovery packet. -func (c *wireCodec) decodeEncrypted(fromID enode.ID, fromAddr string, input []byte) (packetV5, *enode.Node, error) { - // Decode packet header. - var head authHeader - r := bytes.NewReader(input[32:]) - err := rlp.Decode(r, &head) - if err != nil { - return nil, nil, err +// decodeWhoareyou reads packet data after the header as a WHOAREYOU packet. +func (c *wireCodec) decodeWhoareyou(head *packetHeaderV5) (packetV5, error) { + if c.reader.Len() < sizeofWhoareyouAuthDataV5 { + return nil, errTooShort } + if int(head.AuthSize) != sizeofWhoareyouAuthDataV5 { + return nil, fmt.Errorf("invalid auth size for whoareyou") + } + auth := new(whoareyouAuthDataV5) + binary.Read(&c.reader, binary.BigEndian, auth) + p := &whoareyouV5{ + AuthTag: auth.Nonce[:], + IDNonce: auth.IDNonce, + RecordSeq: auth.RecordSeq, + } + return p, nil +} - // Decrypt and process auth response. - readKey, node, err := c.decodeAuth(fromID, fromAddr, &head) +func (c *wireCodec) decodeHandshakeMessage(fromAddr string, head *packetHeaderV5, input []byte) (n *enode.Node, p packetV5, err error) { + node, nonce, sec, err := c.decodeHandshake(fromAddr, head) if err != nil { return nil, nil, err } - // Decrypt and decode the packet body. - headsize := len(input) - r.Len() - bodyEnc := input[headsize:] - body, err := decryptGCM(readKey, head.Auth, bodyEnc, input[:32]) - if err != nil { - if !head.isHandshake { - // Can't decrypt, start handshake. - return &unknownV5{AuthTag: head.Auth}, nil, nil - } - return nil, nil, fmt.Errorf("handshake failed: %v", err) - } - if len(body) == 0 { - return nil, nil, errTooShort - } - p, err := decodePacketBodyV5(body[0], body[1:]) - return p, node, err + // Handshake OK, drop the challenge and store the new session keys. + sec.readKey, sec.writeKey = sec.writeKey, sec.readKey + c.sc.storeNewSession(head.SrcID, fromAddr, sec.readKey, sec.writeKey) + c.sc.deleteHandshake(head.SrcID, fromAddr) + + // Decrypt the message using the new session keys. + msg, err := c.decryptMessage(input, nonce, sec.readKey) + return node, msg, err } -// decodeAuth processes an auth header. -func (c *wireCodec) decodeAuth(fromID enode.ID, fromAddr string, head *authHeader) ([]byte, *enode.Node, error) { - if !head.isHandshake { - return c.sc.readKey(fromID, fromAddr), nil, nil +func (c *wireCodec) decodeHandshake(fromAddr string, head *packetHeaderV5) (*enode.Node, []byte, *handshakeSecrets, error) { + auth, err := c.decodeHandshakeAuthData(head) + if err != nil { + return nil, nil, nil, err } - // Remote is attempting handshake. Verify against our last WHOAREYOU. - challenge := c.sc.getHandshake(fromID, fromAddr) + // Verify against our last WHOAREYOU. + challenge := c.sc.getHandshake(head.SrcID, fromAddr) if challenge == nil { - return nil, nil, errUnexpectedHandshake + return nil, nil, nil, errUnexpectedHandshake } - if head.IDNonce != challenge.IDNonce { - return nil, nil, errHandshakeNonceMismatch + // Get node record. + node, err := c.decodeHandshakeRecord(challenge.node, head.SrcID, auth.record) + if err != nil { + return nil, nil, nil, err } - sec, n, err := c.decodeAuthResp(fromID, fromAddr, &head.authHeaderList, challenge) + // Verify ephemeral key is on curve. + ephkey, err := decodePubkey(c.privkey.Curve, auth.pubkey) if err != nil { - return nil, n, err + return nil, nil, nil, errInvalidAuthKey } - // Swap keys to match remote. - sec.readKey, sec.writeKey = sec.writeKey, sec.readKey - c.sc.storeNewSession(fromID, fromAddr, sec.readKey, sec.writeKey) - c.sc.deleteHandshake(fromID, fromAddr) - return sec.readKey, n, err + // Verify ID nonce signature. + err = c.verifyIDSignature(challenge.IDNonce[:], auth.pubkey, auth.signature, node) + if err != nil { + return nil, nil, nil, err + } + // Derive sesssion keys. + sec := c.deriveKeys(head.SrcID, c.localnode.ID(), c.privkey, ephkey, challenge) + return node, auth.h.Nonce[:], sec, nil } -// decodeAuthResp decodes and verifies an authentication response. -func (c *wireCodec) decodeAuthResp(fromID enode.ID, fromAddr string, head *authHeaderList, challenge *whoareyouV5) (*handshakeSecrets, *enode.Node, error) { - // Decrypt / decode the response. - if head.Scheme != authSchemeName { - return nil, nil, errUnknownAuthScheme +// decodeHandshakeAuthData reads the authdata section of a handshake packet. +func (c *wireCodec) decodeHandshakeAuthData(head *packetHeaderV5) (*handshakeAuthDataV5, error) { + if int(head.AuthSize) < sizeofHandshakeAuthDataV5 { + return nil, fmt.Errorf("header authsize %d too low for handshake", head.AuthSize) } - ephkey := head.ephemeralKey(c.privkey.Curve) - if ephkey == nil { - return nil, nil, errInvalidAuthKey + if c.reader.Len() < int(head.AuthSize) { + return nil, errTooShort } - sec := c.deriveKeys(fromID, c.localnode.ID(), c.privkey, ephkey, challenge) - respPT, err := decryptGCM(sec.authRespKey, zeroNonce, head.Response, nil) - if err != nil { - return nil, nil, fmt.Errorf("can't decrypt auth response header: %v", err) + + // Decode fixed size part. + var auth handshakeAuthDataV5 + binary.Read(&c.reader, binary.BigEndian, &auth.h) + if auth.h.Version > handshakeVersion || auth.h.Version < minVersion { + return nil, fmt.Errorf("invalid handshake version %d", auth.h.Version) } - var resp authResponse - if err := rlp.DecodeBytes(respPT, &resp); err != nil { - return nil, nil, fmt.Errorf("invalid auth response: %v", err) + // Decode variable-size part. + varspace := int(head.AuthSize) - sizeofHandshakeAuthDataV5 + if int(auth.h.SigSize)+int(auth.h.PubkeySize) > varspace { + return nil, fmt.Errorf("invalid handshake data sizes (%d+%d > %d)", auth.h.SigSize, auth.h.PubkeySize, varspace) + } + if !readNew(&auth.signature, int(auth.h.SigSize), &c.reader) { + return nil, fmt.Errorf("can't read auth signature") + } + if !readNew(&auth.pubkey, int(auth.h.PubkeySize), &c.reader) { + return nil, fmt.Errorf("can't read auth pubkey") + } + recordsize := varspace - int(auth.h.SigSize) - int(auth.h.PubkeySize) + if !readNew(&auth.record, recordsize, &c.reader) { + return nil, fmt.Errorf("can't read auth node record") + } + return &auth, nil +} + +// readNew reads 'length' bytes from 'r' and stores them into 'data'. +func readNew(data *[]byte, length int, r *bytes.Reader) bool { + if length == 0 { + return true } + *data = make([]byte, length) + n, _ := r.Read(*data) + return n == length +} - // Verify response node record. The remote node should include the record - // if we don't have one or if ours is older than the latest version. - node := challenge.node - if resp.Record != nil { - if node == nil || node.Seq() < resp.Record.Seq() { - n, err := enode.New(enode.ValidSchemes, resp.Record) +// decodeHandshakeRecord verifies the node record contained in a handshake packet. The +// remote node should include the record if we don't have one or if ours is older than the +// latest sequence number. +func (c *wireCodec) decodeHandshakeRecord(local *enode.Node, wantID enode.ID, remote []byte) (node *enode.Node, err error) { + node = local + if len(remote) > 0 { + var record enr.Record + if err := rlp.DecodeBytes(remote, &record); err != nil { + return nil, err + } + if local == nil || local.Seq() < record.Seq() { + n, err := enode.New(enode.ValidSchemes, &record) if err != nil { - return nil, nil, fmt.Errorf("invalid node record: %v", err) + return nil, fmt.Errorf("invalid node record: %v", err) } - if n.ID() != fromID { - return nil, nil, fmt.Errorf("record in auth respose has wrong ID: %v", n.ID()) + if n.ID() != wantID { + return nil, fmt.Errorf("record in handshake has wrong ID: %v", n.ID()) } node = n } } if node == nil { - return nil, nil, errNoRecord + err = errNoRecord } + return node, err +} - // Verify ID nonce signature. - err = c.verifyIDSignature(challenge.IDNonce[:], head.EphemeralKey, resp.Signature, node) - if err != nil { - return nil, nil, err +// decodeMessage reads packet data following the header as an ordinary message packet. +func (c *wireCodec) decodeMessage(fromAddr string, head *packetHeaderV5, input []byte) (packetV5, error) { + if c.reader.Len() < sizeofMessageAuthDataV5 { + return nil, errTooShort + } + key := c.sc.readKey(head.SrcID, fromAddr) + auth := new(messageAuthDataV5) + binary.Read(&c.reader, binary.BigEndian, auth) + + // Try decrypting the message. + msg, err := c.decryptMessage(input, auth.Nonce[:], key) + if err == errMessageDecrypt { + // It didn't work. Start the handshake since this is an ordinary message packet. + return &unknownV5{AuthTag: auth.Nonce[:]}, nil } - return sec, node, nil + return msg, err } -// verifyIDSignature checks that signature over idnonce was made by the node with given record. -func (c *wireCodec) verifyIDSignature(nonce, ephkey, sig []byte, n *enode.Node) error { - switch idscheme := n.Record().IdentityScheme(); idscheme { - case "v4": - var pk ecdsa.PublicKey - n.Load((*enode.Secp256k1)(&pk)) // cannot fail because record is valid - if !crypto.VerifySignature(crypto.FromECDSAPub(&pk), c.idNonceHash(nonce, ephkey), sig) { - return errInvalidNonceSig - } - return nil - default: - return fmt.Errorf("can't verify ID nonce signature against scheme %q", idscheme) +func (c *wireCodec) decryptMessage(input, nonce, key []byte) (packetV5, error) { + headerData := input[:len(input)-c.reader.Len()] + messageCT := input[len(headerData):] + message, err := decryptGCM(key, nonce, messageCT, headerData) + if err != nil { + return nil, errMessageDecrypt } + if len(message) == 0 { + return nil, errMessageTooShort + } + return decodePacketBodyV5(message[0], message[1:]) } // decodePacketBody decodes the body of an encrypted discovery packet. @@ -593,6 +658,71 @@ func decodePacketBodyV5(ptype byte, body []byte) (packetV5, error) { return dec, nil } +// makeChecksum computes the header checksum field for a node +// with the given ID. +func (c *wireCodec) makeChecksum(id enode.ID) uint64 { + c.checksum.Reset() + c.checksum.Write([]byte("discv5")) + c.checksum.Write(id[:]) + return c.checksum.Sum64() +} + +// signIDNonce creates the ID nonce signature. +func (c *wireCodec) signIDNonce(nonce, ephkey []byte) ([]byte, error) { + idsig, err := crypto.Sign(c.idNonceHash(nonce, ephkey), c.privkey) + if err != nil { + return nil, fmt.Errorf("can't sign: %v", err) + } + return idsig[:len(idsig)-1], nil // remove recovery ID +} + +// idNonceHash computes the hash of id nonce with prefix. +func (c *wireCodec) idNonceHash(nonce, ephkey []byte) []byte { + h := c.sha256reset() + h.Write([]byte(idNoncePrefix)) + h.Write(nonce) + h.Write(ephkey) + return h.Sum(nil) +} + +// verifyIDSignature checks that signature over idnonce was made by the node with given record. +func (c *wireCodec) verifyIDSignature(nonce, ephkey, sig []byte, n *enode.Node) error { + switch idscheme := n.Record().IdentityScheme(); idscheme { + case "v4": + var pk ecdsa.PublicKey + n.Load((*enode.Secp256k1)(&pk)) // cannot fail because record is valid + if !crypto.VerifySignature(crypto.FromECDSAPub(&pk), c.idNonceHash(nonce, ephkey), sig) { + return errInvalidNonceSig + } + return nil + default: + return fmt.Errorf("can't verify ID nonce signature against scheme %q", idscheme) + } +} + +// deriveKeys generates session keys using elliptic-curve Diffie-Hellman key agreement. +func (c *wireCodec) deriveKeys(n1, n2 enode.ID, priv *ecdsa.PrivateKey, pub *ecdsa.PublicKey, challenge *whoareyouV5) *handshakeSecrets { + eph := ecdh(priv, pub) + if eph == nil { + return nil + } + + info := []byte("discovery v5 key agreement") + info = append(info, n1[:]...) + info = append(info, n2[:]...) + kdf := hkdf.New(c.sha256reset, eph, challenge.IDNonce[:], info) + sec := handshakeSecrets{ + writeKey: make([]byte, aesKeySize), + readKey: make([]byte, aesKeySize), + } + kdf.Read(sec.writeKey) + kdf.Read(sec.readKey) + for i := range eph { + eph[i] = 0 + } + return &sec +} + // sha256reset returns the shared hash instance. func (c *wireCodec) sha256reset() hash.Hash { c.sha256.Reset() @@ -608,14 +738,6 @@ func (c *wireCodec) sha256sum(inputs ...[]byte) []byte { return c.sha256.Sum(nil) } -func xorTag(a []byte, b enode.ID) enode.ID { - var r enode.ID - for i := range r { - r[i] = a[i] ^ b[i] - } - return r -} - // ecdh creates a shared secret. func ecdh(privkey *ecdsa.PrivateKey, pubkey *ecdsa.PublicKey) []byte { secX, secY := pubkey.ScalarMult(pubkey.X, pubkey.Y, privkey.D.Bytes()) diff --git a/p2p/discover/v5_encoding_test.go b/p2p/discover/v5_encoding_test.go index 77e6bae6aef..4328115f7c2 100644 --- a/p2p/discover/v5_encoding_test.go +++ b/p2p/discover/v5_encoding_test.go @@ -231,25 +231,30 @@ func TestDecodeErrorsV5(t *testing.T) { net.nodeA.expectDecodeErr(t, errTooShort, []byte{}) // TODO some more tests would be nice :) + // - check invalid authdata sizes + // - check invalid handshake data sizes } -// This benchmark checks performance of authHeader decoding, verification and key derivation. -func BenchmarkV5_DecodeAuthSecp256k1(b *testing.B) { +// This benchmark checks performance of handshake packet decoding. +func BenchmarkV5_DecodeHandshakePingSecp256k1(b *testing.B) { net := newHandshakeTest() defer net.close() var ( idA = net.nodeA.id() - addrA = net.nodeA.addr() - challenge = &whoareyouV5{AuthTag: []byte("authresp"), RecordSeq: 0, node: net.nodeB.n()} - nonce = make([]byte, gcmNonceSize) + challenge = &whoareyouV5{AuthTag: []byte("authresp"), node: net.nodeB.n()} + message = &pingV5{ReqID: []byte("reqid")} ) - header, _, _ := net.nodeA.c.makeAuthHeader(nonce, challenge) + packet, _, err := net.nodeA.c.encode(net.nodeB.id(), "", message, challenge) + if err != nil { + b.Fatal("can't encode handshake packet") + } challenge.node = nil // force ENR signature verification in decoder b.ResetTimer() for i := 0; i < b.N; i++ { - _, _, err := net.nodeB.c.decodeAuthResp(idA, addrA, header, challenge) + net.nodeB.c.sc.storeSentHandshake(idA, "", challenge) + _, _, _, err := net.nodeB.c.decode(packet, "") if err != nil { b.Fatal(err) } diff --git a/p2p/discover/v5_session.go b/p2p/discover/v5_session.go index 8a0eeb6977c..a2a87251131 100644 --- a/p2p/discover/v5_session.go +++ b/p2p/discover/v5_session.go @@ -58,9 +58,8 @@ func newSessionCache(maxItems int, clock mclock.Clock) *sessionCache { } // nextNonce creates a nonce for encrypting a message to the given session. -func (sc *sessionCache) nextNonce(id enode.ID, addr string) []byte { - n := make([]byte, gcmNonceSize) - crand.Read(n) +func (sc *sessionCache) nextNonce(id enode.ID, addr string) (n [gcmNonceSize]byte) { + crand.Read(n[:]) return n } From e54f43c4c2bdf07060a531bc78a3d7ef43a2cc1d Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Wed, 27 May 2020 00:46:00 +0200 Subject: [PATCH 02/70] p2p/discover: reset input slice for each decoder benchmark round --- p2p/discover/v5_encoding_test.go | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/p2p/discover/v5_encoding_test.go b/p2p/discover/v5_encoding_test.go index 4328115f7c2..39d94fcf293 100644 --- a/p2p/discover/v5_encoding_test.go +++ b/p2p/discover/v5_encoding_test.go @@ -245,16 +245,18 @@ func BenchmarkV5_DecodeHandshakePingSecp256k1(b *testing.B) { challenge = &whoareyouV5{AuthTag: []byte("authresp"), node: net.nodeB.n()} message = &pingV5{ReqID: []byte("reqid")} ) - packet, _, err := net.nodeA.c.encode(net.nodeB.id(), "", message, challenge) + enc, _, err := net.nodeA.c.encode(net.nodeB.id(), "", message, challenge) if err != nil { b.Fatal("can't encode handshake packet") } challenge.node = nil // force ENR signature verification in decoder b.ResetTimer() + input := make([]byte, len(enc)) for i := 0; i < b.N; i++ { + copy(input, enc) net.nodeB.c.sc.storeSentHandshake(idA, "", challenge) - _, _, _, err := net.nodeB.c.decode(packet, "") + _, _, _, err := net.nodeB.c.decode(input, "") if err != nil { b.Fatal(err) } @@ -278,10 +280,12 @@ func BenchmarkV5_DecodePing(b *testing.B) { } b.ResetTimer() + input := make([]byte, len(enc)) for i := 0; i < b.N; i++ { - _, _, p, _ := net.nodeB.c.decode(enc, addrB) - if _, ok := p.(*pingV5); !ok { - b.Fatalf("wrong packet type %T", p) + copy(input, enc) + _, _, packet, _ := net.nodeB.c.decode(input, addrB) + if _, ok := packet.(*pingV5); !ok { + b.Fatalf("wrong packet type %T", packet) } } } From d1a736b78a450880b37ae898d12ff60cd6f93941 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Wed, 27 May 2020 00:18:26 +0200 Subject: [PATCH 03/70] p2p/discover: add header masking code for testing --- p2p/discover/v5_encoding.go | 49 +++++++++++++++++++++++++++++++++---- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/p2p/discover/v5_encoding.go b/p2p/discover/v5_encoding.go index 34497f4e964..6f3e3a289c5 100644 --- a/p2p/discover/v5_encoding.go +++ b/p2p/discover/v5_encoding.go @@ -295,7 +295,8 @@ func (c *wireCodec) encodeRandom(toID enode.ID) ([]byte, []byte, error) { c.buf.Reset() binary.Write(&c.buf, binary.BigEndian, c.makeHeader(toID, flagMessage, 0)) binary.Write(&c.buf, binary.BigEndian, &auth) - return c.buf.Bytes(), auth.Nonce[:], nil + output := maskOutputPacket(toID, c.buf.Bytes(), c.buf.Len()) + return output, auth.Nonce[:], nil } // encodeWhoareyou encodes a WHOAREYOU packet. @@ -314,7 +315,8 @@ func (c *wireCodec) encodeWhoareyou(toID enode.ID, packet *whoareyouV5) ([]byte, c.buf.Reset() binary.Write(&c.buf, binary.BigEndian, head) binary.Write(&c.buf, binary.BigEndian, auth) - return c.buf.Bytes(), nil + output := maskOutputPacket(toID, c.buf.Bytes(), c.buf.Len()) + return output, nil } // encodeHandshakeMessage encodes an encrypted message with a handshake @@ -356,6 +358,9 @@ func (c *wireCodec) encodeHandshakeMessage(toID enode.ID, addr string, packet pa messagePT := c.msgbuf.Bytes() headerData := output output, err = encryptGCM(output, sec.writeKey, auth.h.Nonce[:], messagePT, headerData) + if err == nil { + output = maskOutputPacket(toID, output, len(headerData)) + } return output, auth.h.Nonce[:], err } @@ -425,6 +430,9 @@ func (c *wireCodec) encodeMessage(toID enode.ID, addr string, packet packetV5, w // Encrypt message data. headerData := output output, err = encryptGCM(output, writeKey, auth.Nonce[:], messagePT, headerData) + if err == nil { + output = maskOutputPacket(toID, output, len(headerData)) + } return output, auth.Nonce[:], err } @@ -434,12 +442,18 @@ func (c *wireCodec) decode(input []byte, addr string) (src enode.ID, n *enode.No // processing the same handshake twice. c.sc.handshakeGC() + // Unmask the header. + if len(input) < sizeofPacketHeaderV5+maskIVSize { + return enode.ID{}, nil, nil, errTooShort + } + mask := headerMask(c.localnode.ID(), input) + input = input[maskIVSize:] + headerData := input[:sizeofPacketHeaderV5] + mask.XORKeyStream(headerData, headerData) + // Decode and verify the header. var head packetHeaderV5 c.reader.Reset(input) - if c.reader.Len() < sizeofPacketHeaderV5 { - return enode.ID{}, nil, nil, errTooShort - } binary.Read(&c.reader, binary.BigEndian, &head) if head.Checksum != c.myChecksum { return enode.ID{}, nil, nil, errBadHash @@ -449,6 +463,10 @@ func (c *wireCodec) decode(input []byte, addr string) (src enode.ID, n *enode.No } src = head.SrcID + // Unmask auth data. + authData := input[sizeofPacketHeaderV5 : sizeofPacketHeaderV5+int(head.AuthSize)] + mask.XORKeyStream(authData, authData) + // Decode auth part and message. switch { case head.Flags&flagWhoareyou != 0: @@ -779,3 +797,24 @@ func decryptGCM(key, nonce, ct, authData []byte) ([]byte, error) { pt := make([]byte, 0, len(ct)) return aesgcm.Open(pt, nonce, ct, authData) } + +// header masking + +const maskIVSize = 16 + +func headerMask(destID enode.ID, input []byte) cipher.Stream { + block, err := aes.NewCipher(destID[:16]) + if err != nil { + panic("can't create cipher") + } + return cipher.NewCTR(block, input[:maskIVSize]) +} + +func maskOutputPacket(destID enode.ID, output []byte, headerDataLen int) []byte { + masked := make([]byte, maskIVSize+len(output)) + crand.Read(masked[:maskIVSize]) + mask := headerMask(destID, masked) + copy(masked[maskIVSize:], output) + mask.XORKeyStream(masked[maskIVSize:], output[:headerDataLen]) + return masked +} From 6f88e7505c6ce88730204919bcfaed4d9e8e4ec2 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Tue, 18 Aug 2020 12:53:40 +0200 Subject: [PATCH 04/70] p2p/discover: replace checksum with static protocol identifier --- p2p/discover/v5_encoding.go | 50 +++++++++++++------------------------ 1 file changed, 18 insertions(+), 32 deletions(-) diff --git a/p2p/discover/v5_encoding.go b/p2p/discover/v5_encoding.go index 6f3e3a289c5..4cdddfaff39 100644 --- a/p2p/discover/v5_encoding.go +++ b/p2p/discover/v5_encoding.go @@ -27,7 +27,6 @@ import ( "errors" "fmt" "hash" - "hash/crc64" "net" "time" @@ -63,10 +62,10 @@ const ( // Discovery v5 packet structures. type ( packetHeaderV5 struct { - Checksum uint64 - SrcID enode.ID - Flags byte - AuthSize uint16 + ProtocolID [8]byte + SrcID enode.ID + Flags byte + AuthSize uint16 } whoareyouAuthDataV5 struct { @@ -103,6 +102,7 @@ var ( sizeofWhoareyouAuthDataV5 = binary.Size(whoareyouAuthDataV5{}) sizeofHandshakeAuthDataV5 = binary.Size(handshakeAuthDataV5{}.h) sizeofMessageAuthDataV5 = binary.Size(messageAuthDataV5{}) + protocolIDV5 = [8]byte{'d', 'i', 's', 'c', 'v', '5', ' ', ' '} ) // Discovery v5 messages. @@ -208,16 +208,13 @@ var ( // wireCodec encodes and decodes discovery v5 packets. type wireCodec struct { - sha256 hash.Hash - localnode *enode.LocalNode - privkey *ecdsa.PrivateKey - checksum hash.Hash64 - myChecksum uint64 // protocol checksum of local node - - buf bytes.Buffer // used for encoding of packets - msgbuf bytes.Buffer // used for encoding message content - reader bytes.Reader // used for decoding - sc *sessionCache + sha256 hash.Hash + localnode *enode.LocalNode + privkey *ecdsa.PrivateKey + buf bytes.Buffer // used for encoding of packets + msgbuf bytes.Buffer // used for encoding of message content + reader bytes.Reader // used for decoding + sc *sessionCache } type handshakeSecrets struct { @@ -230,10 +227,8 @@ func newWireCodec(ln *enode.LocalNode, key *ecdsa.PrivateKey, clock mclock.Clock sha256: sha256.New(), localnode: ln, privkey: key, - checksum: crc64.New(crc64.MakeTable(crc64.ISO)), sc: newSessionCache(1024, clock), } - c.myChecksum = c.makeChecksum(ln.ID()) return c } @@ -278,10 +273,10 @@ func (c *wireCodec) makeHeader(toID enode.ID, flags byte, authsizeExtra int) *pa panic(fmt.Errorf("BUG: auth size %d overflows uint16", authsize)) } return &packetHeaderV5{ - Checksum: c.makeChecksum(toID), - SrcID: c.localnode.ID(), - Flags: flags, - AuthSize: uint16(authsize), + ProtocolID: protocolIDV5, + SrcID: c.localnode.ID(), + Flags: flags, + AuthSize: uint16(authsize), } } @@ -455,8 +450,8 @@ func (c *wireCodec) decode(input []byte, addr string) (src enode.ID, n *enode.No var head packetHeaderV5 c.reader.Reset(input) binary.Read(&c.reader, binary.BigEndian, &head) - if head.Checksum != c.myChecksum { - return enode.ID{}, nil, nil, errBadHash + if head.ProtocolID != protocolIDV5 { + return enode.ID{}, nil, nil, errInvalidHeader } if int(head.AuthSize) > c.reader.Len() { return enode.ID{}, nil, nil, errInvalidHeader @@ -676,15 +671,6 @@ func decodePacketBodyV5(ptype byte, body []byte) (packetV5, error) { return dec, nil } -// makeChecksum computes the header checksum field for a node -// with the given ID. -func (c *wireCodec) makeChecksum(id enode.ID) uint64 { - c.checksum.Reset() - c.checksum.Write([]byte("discv5")) - c.checksum.Write(id[:]) - return c.checksum.Sum64() -} - // signIDNonce creates the ID nonce signature. func (c *wireCodec) signIDNonce(nonce, ephkey []byte) ([]byte, error) { idsig, err := crypto.Sign(c.idNonceHash(nonce, ephkey), c.privkey) From 23a1bb17e41b5f7d0a11a957e0945f4e0bc7c918 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Tue, 18 Aug 2020 14:26:09 +0200 Subject: [PATCH 05/70] p2p/discover: implement talk requests --- p2p/discover/v5_encoding.go | 19 ++++++++ p2p/discover/v5_udp.go | 57 +++++++++++++++++++++++ p2p/discover/v5_udp_test.go | 91 +++++++++++++++++++++++++++++++++++++ 3 files changed, 167 insertions(+) diff --git a/p2p/discover/v5_encoding.go b/p2p/discover/v5_encoding.go index 4cdddfaff39..3ce7b2df287 100644 --- a/p2p/discover/v5_encoding.go +++ b/p2p/discover/v5_encoding.go @@ -50,6 +50,8 @@ const ( p_pongV5 p_findnodeV5 p_nodesV5 + p_talkreqV5 + p_talkrespV5 p_requestTicketV5 p_ticketV5 p_regtopicV5 @@ -149,6 +151,19 @@ type ( Nodes []*enr.Record } + // TALKREQ is an application-level request. + talkreqV5 struct { + ReqID []byte + Protocol string + Message []byte + } + + // TALKRESP is the reply to TALKREQ. + talkrespV5 struct { + ReqID []byte + Message []byte + } + // REQUESTTICKET requests a ticket for a topic queue. requestTicketV5 struct { ReqID []byte @@ -652,6 +667,10 @@ func decodePacketBodyV5(ptype byte, body []byte) (packetV5, error) { dec = new(findnodeV5) case p_nodesV5: dec = new(nodesV5) + case p_talkreqV5: + dec = new(talkreqV5) + case p_talkrespV5: + dec = new(talkrespV5) case p_requestTicketV5: dec = new(requestTicketV5) case p_ticketV5: diff --git a/p2p/discover/v5_udp.go b/p2p/discover/v5_udp.go index d53375b48b6..c287581befd 100644 --- a/p2p/discover/v5_udp.go +++ b/p2p/discover/v5_udp.go @@ -83,6 +83,10 @@ type UDPv5 struct { clock mclock.Clock validSchemes enr.IdentityScheme + // talkreq handler registry + trlock sync.Mutex + trhandlers map[string]func([]byte) []byte + // channels into dispatch packetInCh chan ReadPacket readNextCh chan struct{} @@ -152,6 +156,7 @@ func newUDPv5(conn UDPConn, ln *enode.LocalNode, cfg Config) (*UDPv5, error) { log: cfg.Log, validSchemes: cfg.ValidSchemes, clock: cfg.Clock, + trhandlers: make(map[string]func([]byte) []byte), // channels into dispatch packetInCh: make(chan ReadPacket, 1), readNextCh: make(chan struct{}, 1), @@ -236,6 +241,28 @@ func (t *UDPv5) LocalNode() *enode.LocalNode { return t.localNode } +// RegisterTalkHandler adds a handler for 'talk requests'. The handler function is called +// whenever a request for the given protocol is received and should return the response +// data or nil. +func (t *UDPv5) RegisterTalkHandler(protocol string, handler func([]byte) []byte) { + t.trlock.Lock() + defer t.trlock.Unlock() + t.trhandlers[protocol] = handler +} + +// TalkRequest sends a talk request to n and waits for a response. +func (t *UDPv5) TalkRequest(n *enode.Node, protocol string, request []byte) ([]byte, error) { + resp := t.call(n, p_talkrespV5, &talkreqV5{Protocol: protocol, Message: request}) + defer t.callDone(resp) + select { + case respMsg := <-resp.ch: + return respMsg.(*talkrespV5).Message, nil + case err := <-resp.err: + return nil, err + } +} + +// RandomNodes returns an iterator that finds random nodes in the DHT. func (t *UDPv5) RandomNodes() enode.Iterator { if t.tab.len() == 0 { // All nodes were dropped, refresh. The very first query will hit this @@ -802,6 +829,36 @@ func (p *nodesV5) handle(t *UDPv5, fromID enode.ID, fromAddr *net.UDPAddr) { t.handleCallResponse(fromID, fromAddr, p.ReqID, p) } +// TALKREQ + +func (p *talkreqV5) name() string { return "TALKREQ/v5" } +func (p *talkreqV5) kind() byte { return p_talkreqV5 } +func (p *talkreqV5) setreqid(id []byte) { p.ReqID = id } + +func (p *talkreqV5) handle(t *UDPv5, fromID enode.ID, fromAddr *net.UDPAddr) { + t.trlock.Lock() + handler := t.trhandlers[p.Protocol] + t.trlock.Unlock() + + var response []byte + if handler != nil { + response = handler(p.Message) + } + if len(p.ReqID) > 0 { + t.sendResponse(fromID, fromAddr, &talkrespV5{ReqID: p.ReqID, Message: response}) + } +} + +// TALKRESP + +func (p *talkrespV5) name() string { return "TALKRESP/v5" } +func (p *talkrespV5) kind() byte { return p_talkrespV5 } +func (p *talkrespV5) setreqid(id []byte) { p.ReqID = id } + +func (p *talkrespV5) handle(t *UDPv5, fromID enode.ID, fromAddr *net.UDPAddr) { + t.handleCallResponse(fromID, fromAddr, p.ReqID, p) +} + // REQUESTTICKET func (p *requestTicketV5) name() string { return "REQUESTTICKET/v5" } diff --git a/p2p/discover/v5_udp_test.go b/p2p/discover/v5_udp_test.go index 7d3915e2dce..a20a4e5d8a6 100644 --- a/p2p/discover/v5_udp_test.go +++ b/p2p/discover/v5_udp_test.go @@ -398,6 +398,97 @@ func TestUDPv5_callTimeoutReset(t *testing.T) { } } +// This test checks that TALKREQ calls the registered handler function. +func TestUDPv5_talkHandling(t *testing.T) { + t.Parallel() + test := newUDPV5Test(t) + defer test.close() + + var recvMessage []byte + test.udp.RegisterTalkHandler("test", func(message []byte) []byte { + recvMessage = message + return []byte("test response") + }) + + // Successful case: + test.packetIn(&talkreqV5{ + ReqID: []byte("foo"), + Protocol: "test", + Message: []byte("test request"), + }) + test.waitPacketOut(func(p *talkrespV5, addr *net.UDPAddr, authTag []byte) { + if !bytes.Equal(p.ReqID, []byte("foo")) { + t.Error("wrong request ID in response:", p.ReqID) + } + if string(p.Message) != "test response" { + t.Errorf("wrong talk response message: %q", p.Message) + } + if string(recvMessage) != "test request" { + t.Errorf("wrong message received in handler: %q", recvMessage) + } + }) + + // Check that empty response is returned for unregistered protocols. + recvMessage = nil + test.packetIn(&talkreqV5{ + ReqID: []byte("2"), + Protocol: "wrong", + Message: []byte("test request"), + }) + test.waitPacketOut(func(p *talkrespV5, addr *net.UDPAddr, authTag []byte) { + if !bytes.Equal(p.ReqID, []byte("2")) { + t.Error("wrong request ID in response:", p.ReqID) + } + if string(p.Message) != "" { + t.Errorf("wrong talk response message: %q", p.Message) + } + if recvMessage != nil { + t.Errorf("handler was called for wrong protocol: %q", recvMessage) + } + }) +} + +// This test checks that outgoing TALKREQ calls work. +func TestUDPv5_talkRequest(t *testing.T) { + t.Parallel() + test := newUDPV5Test(t) + defer test.close() + + remote := test.getNode(test.remotekey, test.remoteaddr).Node() + done := make(chan error, 1) + + // This request times out. + go func() { + _, err := test.udp.TalkRequest(remote, "test", []byte("test request")) + done <- err + }() + test.waitPacketOut(func(p *talkreqV5, addr *net.UDPAddr, authTag []byte) {}) + if err := <-done; err != errTimeout { + t.Fatalf("want errTimeout, got %q", err) + } + + // This request works. + go func() { + _, err := test.udp.TalkRequest(remote, "test", []byte("test request")) + done <- err + }() + test.waitPacketOut(func(p *talkreqV5, addr *net.UDPAddr, authTag []byte) { + if p.Protocol != "test" { + t.Errorf("wrong protocol ID in talk request: %q", p.Protocol) + } + if string(p.Message) != "test request" { + t.Errorf("wrong message talk request: %q", p.Message) + } + test.packetInFrom(test.remotekey, test.remoteaddr, &talkrespV5{ + ReqID: p.ReqID, + Message: []byte("test response"), + }) + }) + if err := <-done; err != nil { + t.Fatal(err) + } +} + // This test checks that lookup works. func TestUDPv5_lookup(t *testing.T) { t.Parallel() From 731fc1349fcaa626c0675a6966b41e18ec1b9fcf Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Wed, 19 Aug 2020 13:31:44 +0200 Subject: [PATCH 06/70] p2p/discover: implement multi-distance FINDNODE --- p2p/discover/v5_encoding.go | 4 +- p2p/discover/v5_encoding_test.go | 2 +- p2p/discover/v5_udp.go | 102 +++++++++++++++++++------------ p2p/discover/v5_udp_test.go | 34 +++++------ 4 files changed, 82 insertions(+), 60 deletions(-) diff --git a/p2p/discover/v5_encoding.go b/p2p/discover/v5_encoding.go index 3ce7b2df287..3678b2f17fc 100644 --- a/p2p/discover/v5_encoding.go +++ b/p2p/discover/v5_encoding.go @@ -140,8 +140,8 @@ type ( // FINDNODE is a query for nodes in the given bucket. findnodeV5 struct { - ReqID []byte - Distance uint + ReqID []byte + Distances []uint } // NODES is the reply to FINDNODE and TOPICQUERY. diff --git a/p2p/discover/v5_encoding_test.go b/p2p/discover/v5_encoding_test.go index 39d94fcf293..bfad193d027 100644 --- a/p2p/discover/v5_encoding_test.go +++ b/p2p/discover/v5_encoding_test.go @@ -206,7 +206,7 @@ func TestHandshakeV5_rekey2(t *testing.T) { net.nodeB.c.sc.storeNewSession(net.nodeA.id(), net.nodeA.addr(), initKeysB.readKey, initKeysA.writeKey) // A -> B FINDNODE encrypted with initKeysA - findnode, authTag := net.nodeA.encode(t, net.nodeB, &findnodeV5{Distance: 3}) + findnode, authTag := net.nodeA.encode(t, net.nodeB, &findnodeV5{Distances: []uint{3}}) net.nodeB.expectDecode(t, p_unknownV5, findnode) // A <- B WHOAREYOU diff --git a/p2p/discover/v5_udp.go b/p2p/discover/v5_udp.go index c287581befd..6ea877fc373 100644 --- a/p2p/discover/v5_udp.go +++ b/p2p/discover/v5_udp.go @@ -38,7 +38,7 @@ import ( const ( lookupRequestLimit = 3 // max requests against a single node during lookup - findnodeResultLimit = 15 // applies in FINDNODE handler + findnodeResultLimit = 16 // applies in FINDNODE handler totalNodesResponseLimit = 5 // applies in waitForNodes nodesResponseItemLimit = 3 // applies in sendNodes @@ -310,16 +310,14 @@ func (t *UDPv5) lookupWorker(destNode *node, target enode.ID) ([]*node, error) { nodes = nodesByDistance{target: target} err error ) - for i := 0; i < lookupRequestLimit && len(nodes.entries) < findnodeResultLimit; i++ { - var r []*enode.Node - r, err = t.findnode(unwrapNode(destNode), dists[i]) - if err == errClosed { - return nil, err - } - for _, n := range r { - if n.ID() != t.Self().ID() { - nodes.push(wrapNode(n), findnodeResultLimit) - } + var r []*enode.Node + r, err = t.findnode(unwrapNode(destNode), dists) + if err == errClosed { + return nil, err + } + for _, n := range r { + if n.ID() != t.Self().ID() { + nodes.push(wrapNode(n), findnodeResultLimit) } } return nodes.entries, err @@ -328,15 +326,15 @@ func (t *UDPv5) lookupWorker(destNode *node, target enode.ID) ([]*node, error) { // lookupDistances computes the distance parameter for FINDNODE calls to dest. // It chooses distances adjacent to logdist(target, dest), e.g. for a target // with logdist(target, dest) = 255 the result is [255, 256, 254]. -func lookupDistances(target, dest enode.ID) (dists []int) { +func lookupDistances(target, dest enode.ID) (dists []uint) { td := enode.LogDist(target, dest) - dists = append(dists, td) + dists = append(dists, uint(td)) for i := 1; len(dists) < lookupRequestLimit; i++ { if td+i < 256 { - dists = append(dists, td+i) + dists = append(dists, uint(td+i)) } if td-i > 0 { - dists = append(dists, td-i) + dists = append(dists, uint(td-i)) } } return dists @@ -356,7 +354,7 @@ func (t *UDPv5) ping(n *enode.Node) (uint64, error) { // requestENR requests n's record. func (t *UDPv5) RequestENR(n *enode.Node) (*enode.Node, error) { - nodes, err := t.findnode(n, 0) + nodes, err := t.findnode(n, []uint{0}) if err != nil { return nil, err } @@ -379,13 +377,13 @@ func (t *UDPv5) requestTicket(n *enode.Node) ([]byte, error) { } // findnode calls FINDNODE on a node and waits for responses. -func (t *UDPv5) findnode(n *enode.Node, distance int) ([]*enode.Node, error) { - resp := t.call(n, p_nodesV5, &findnodeV5{Distance: uint(distance)}) - return t.waitForNodes(resp, distance) +func (t *UDPv5) findnode(n *enode.Node, distances []uint) ([]*enode.Node, error) { + resp := t.call(n, p_nodesV5, &findnodeV5{Distances: distances}) + return t.waitForNodes(resp, distances) } // waitForNodes waits for NODES responses to the given call. -func (t *UDPv5) waitForNodes(c *callV5, distance int) ([]*enode.Node, error) { +func (t *UDPv5) waitForNodes(c *callV5, distances []uint) ([]*enode.Node, error) { defer t.callDone(c) var ( @@ -398,7 +396,7 @@ func (t *UDPv5) waitForNodes(c *callV5, distance int) ([]*enode.Node, error) { case responseP := <-c.ch: response := responseP.(*nodesV5) for _, record := range response.Nodes { - node, err := t.verifyResponseNode(c, record, distance, seen) + node, err := t.verifyResponseNode(c, record, distances, seen) if err != nil { t.log.Debug("Invalid record in "+response.name(), "id", c.node.ID(), "err", err) continue @@ -418,7 +416,7 @@ func (t *UDPv5) waitForNodes(c *callV5, distance int) ([]*enode.Node, error) { } // verifyResponseNode checks validity of a record in a NODES response. -func (t *UDPv5) verifyResponseNode(c *callV5, r *enr.Record, distance int, seen map[enode.ID]struct{}) (*enode.Node, error) { +func (t *UDPv5) verifyResponseNode(c *callV5, r *enr.Record, distances []uint, seen map[enode.ID]struct{}) (*enode.Node, error) { node, err := enode.New(t.validSchemes, r) if err != nil { return nil, err @@ -429,9 +427,10 @@ func (t *UDPv5) verifyResponseNode(c *callV5, r *enr.Record, distance int, seen if c.node.UDP() <= 1024 { return nil, errLowPort } - if distance != -1 { - if d := enode.LogDist(c.node.ID(), node.ID()); d != distance { - return nil, fmt.Errorf("wrong distance %d", d) + if distances != nil { + nd := enode.LogDist(c.node.ID(), node.ID()) + if !containsUint(uint(nd), distances) { + return nil, errors.New("does not match any requested distance") } } if _, ok := seen[node.ID()]; ok { @@ -441,6 +440,15 @@ func (t *UDPv5) verifyResponseNode(c *callV5, r *enr.Record, distance int, seen return node, nil } +func containsUint(x uint, xs []uint) bool { + for _, v := range xs { + if x == v { + return true + } + } + return false +} + // call sends the given call and sets up a handler for response packets (of type c.responseType). // Responses are dispatched to the call's response channel. func (t *UDPv5) call(node *enode.Node, responseType byte, packet packetV5) *callV5 { @@ -777,27 +785,41 @@ func (p *findnodeV5) kind() byte { return p_findnodeV5 } func (p *findnodeV5) setreqid(id []byte) { p.ReqID = id } func (p *findnodeV5) handle(t *UDPv5, fromID enode.ID, fromAddr *net.UDPAddr) { - if p.Distance == 0 { - t.sendNodes(fromID, fromAddr, p.ReqID, []*enode.Node{t.Self()}) - return - } - if p.Distance > 256 { - p.Distance = 256 - } - // Get bucket entries. + nodes := t.collectTableNodes(fromAddr.IP, p.Distances) + t.sendNodes(fromID, fromAddr, p.ReqID, nodes) +} + +func (t *UDPv5) collectTableNodes(rip net.IP, distances []uint) []*enode.Node { t.tab.mutex.Lock() - nodes := unwrapNodes(t.tab.bucketAtDistance(int(p.Distance)).entries) - t.tab.mutex.Unlock() - if len(nodes) > findnodeResultLimit { - nodes = nodes[:findnodeResultLimit] + defer t.tab.mutex.Unlock() + + var nodes []*enode.Node + for _, dist := range distances { + var bn []*enode.Node + if dist == 0 { + bn = []*enode.Node{t.Self()} + } else if dist <= 256 { + bn = unwrapNodes(t.tab.bucketAtDistance(int(dist)).entries) + } else { + continue // invalid distance + } + + for _, n := range bn { + // TODO livenessChecks > 1 + if netutil.CheckRelayIP(rip, n.IP()) != nil { + continue + } + nodes = append(nodes, n) + if len(nodes) >= nodesResponseItemLimit { + return nodes + } + } } - t.sendNodes(fromID, fromAddr, p.ReqID, nodes) + return nodes } // sendNodes sends the given records in one or more NODES packets. func (t *UDPv5) sendNodes(toID enode.ID, toAddr *net.UDPAddr, reqid []byte, nodes []*enode.Node) { - // TODO livenessChecks > 1 - // TODO CheckRelayIP total := uint8(math.Ceil(float64(len(nodes)) / 3)) resp := &nodesV5{ReqID: reqid, Total: total, Nodes: make([]*enr.Record, 3)} sent := false diff --git a/p2p/discover/v5_udp_test.go b/p2p/discover/v5_udp_test.go index a20a4e5d8a6..6cda8446518 100644 --- a/p2p/discover/v5_udp_test.go +++ b/p2p/discover/v5_udp_test.go @@ -151,19 +151,19 @@ func TestUDPv5_findnodeHandling(t *testing.T) { fillTable(test.table, wrapNodes(nodes)) // Requesting with distance zero should return the node's own record. - test.packetIn(&findnodeV5{ReqID: []byte{0}, Distance: 0}) + test.packetIn(&findnodeV5{ReqID: []byte{0}, Distances: []uint{0}}) test.expectNodes([]byte{0}, 1, []*enode.Node{test.udp.Self()}) - // Requesting with distance > 256 caps it at 256. - test.packetIn(&findnodeV5{ReqID: []byte{1}, Distance: 4234098}) + // Requesting with distance > 256 shouldn't crash. + test.packetIn(&findnodeV5{ReqID: []byte{1}, Distances: []uint{4234098}}) test.expectNodes([]byte{1}, 1, nil) // This request gets no nodes because the corresponding bucket is empty. - test.packetIn(&findnodeV5{ReqID: []byte{2}, Distance: 254}) + test.packetIn(&findnodeV5{ReqID: []byte{2}, Distances: []uint{254}}) test.expectNodes([]byte{2}, 1, nil) // This request gets all test nodes. - test.packetIn(&findnodeV5{ReqID: []byte{3}, Distance: 253}) + test.packetIn(&findnodeV5{ReqID: []byte{3}, Distances: []uint{253}}) test.expectNodes([]byte{3}, 4, nodes) } @@ -255,22 +255,22 @@ func TestUDPv5_findnodeCall(t *testing.T) { // Launch the request: var ( - distance = 230 - remote = test.getNode(test.remotekey, test.remoteaddr).Node() - nodes = nodesAtDistance(remote.ID(), distance, 8) - done = make(chan error, 1) - response []*enode.Node + distances = []uint{230} + remote = test.getNode(test.remotekey, test.remoteaddr).Node() + nodes = nodesAtDistance(remote.ID(), int(distances[0]), 8) + done = make(chan error, 1) + response []*enode.Node ) go func() { var err error - response, err = test.udp.findnode(remote, distance) + response, err = test.udp.findnode(remote, distances) done <- err }() // Serve the responses: test.waitPacketOut(func(p *findnodeV5, addr *net.UDPAddr, authTag []byte) { - if p.Distance != uint(distance) { - t.Fatalf("wrong bucket: %d", p.Distance) + if !reflect.DeepEqual(p.Distances, distances) { + t.Fatalf("wrong distances in request: %v", p.Distances) } test.packetIn(&nodesV5{ ReqID: p.ReqID, @@ -367,13 +367,13 @@ func TestUDPv5_callTimeoutReset(t *testing.T) { // Launch the request: var ( - distance = 230 + distance = uint(230) remote = test.getNode(test.remotekey, test.remoteaddr).Node() - nodes = nodesAtDistance(remote.ID(), distance, 8) + nodes = nodesAtDistance(remote.ID(), int(distance), 8) done = make(chan error, 1) ) go func() { - _, err := test.udp.findnode(remote, distance) + _, err := test.udp.findnode(remote, []uint{distance}) done <- err }() @@ -525,7 +525,7 @@ func TestUDPv5_lookup(t *testing.T) { case *pingV5: test.packetInFrom(key, to, &pongV5{ReqID: p.ReqID}) case *findnodeV5: - nodes := lookupTestnet.neighborsAtDistance(recipient, p.Distance, 3) + nodes := lookupTestnet.neighborsAtDistance(recipient, p.Distances[0], 3) response := &nodesV5{ReqID: p.ReqID, Total: 1, Nodes: nodesToRecords(nodes)} test.packetInFrom(key, to, response) } From a482a915c73ecfa28adb575798bdd2fa61848a55 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Wed, 19 Aug 2020 13:44:51 +0200 Subject: [PATCH 07/70] p2p/discover: improve collectTableNodes --- p2p/discover/v5_udp.go | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/p2p/discover/v5_udp.go b/p2p/discover/v5_udp.go index 6ea877fc373..f482e672949 100644 --- a/p2p/discover/v5_udp.go +++ b/p2p/discover/v5_udp.go @@ -789,21 +789,29 @@ func (p *findnodeV5) handle(t *UDPv5, fromID enode.ID, fromAddr *net.UDPAddr) { t.sendNodes(fromID, fromAddr, p.ReqID, nodes) } +// collectTableNodes creates a FINDNODE result set for the given distances. func (t *UDPv5) collectTableNodes(rip net.IP, distances []uint) []*enode.Node { - t.tab.mutex.Lock() - defer t.tab.mutex.Unlock() - var nodes []*enode.Node + var processed = make(map[uint]struct{}) for _, dist := range distances { + // Reject duplicate / invalid distances. + _, seen := processed[dist] + if seen || dist > 256 { + continue + } + + // Get the nodes. var bn []*enode.Node if dist == 0 { bn = []*enode.Node{t.Self()} } else if dist <= 256 { + t.tab.mutex.Lock() bn = unwrapNodes(t.tab.bucketAtDistance(int(dist)).entries) - } else { - continue // invalid distance + t.tab.mutex.Unlock() } + processed[dist] = struct{}{} + // Apply some pre-checks to avoid sending invalid nodes. for _, n := range bn { // TODO livenessChecks > 1 if netutil.CheckRelayIP(rip, n.IP()) != nil { From f608c233fcfd5c1e14fcbbf013a9830a647ad10e Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Thu, 27 Aug 2020 13:03:28 +0200 Subject: [PATCH 08/70] p2p/discover: fix result node count in collectTableNodes --- p2p/discover/v5_udp.go | 6 +++--- p2p/discover/v5_udp_test.go | 10 ++++++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/p2p/discover/v5_udp.go b/p2p/discover/v5_udp.go index f482e672949..37bf2254b8b 100644 --- a/p2p/discover/v5_udp.go +++ b/p2p/discover/v5_udp.go @@ -785,12 +785,12 @@ func (p *findnodeV5) kind() byte { return p_findnodeV5 } func (p *findnodeV5) setreqid(id []byte) { p.ReqID = id } func (p *findnodeV5) handle(t *UDPv5, fromID enode.ID, fromAddr *net.UDPAddr) { - nodes := t.collectTableNodes(fromAddr.IP, p.Distances) + nodes := t.collectTableNodes(fromAddr.IP, p.Distances, findnodeResultLimit) t.sendNodes(fromID, fromAddr, p.ReqID, nodes) } // collectTableNodes creates a FINDNODE result set for the given distances. -func (t *UDPv5) collectTableNodes(rip net.IP, distances []uint) []*enode.Node { +func (t *UDPv5) collectTableNodes(rip net.IP, distances []uint, limit int) []*enode.Node { var nodes []*enode.Node var processed = make(map[uint]struct{}) for _, dist := range distances { @@ -818,7 +818,7 @@ func (t *UDPv5) collectTableNodes(rip net.IP, distances []uint) []*enode.Node { continue } nodes = append(nodes, n) - if len(nodes) >= nodesResponseItemLimit { + if len(nodes) >= limit { return nodes } } diff --git a/p2p/discover/v5_udp_test.go b/p2p/discover/v5_udp_test.go index 6cda8446518..80ad754a7c0 100644 --- a/p2p/discover/v5_udp_test.go +++ b/p2p/discover/v5_udp_test.go @@ -172,16 +172,17 @@ func (test *udpV5Test) expectNodes(wantReqID []byte, wantTotal uint8, wantNodes for _, n := range wantNodes { nodeSet[n.ID()] = n.Record() } + for { test.waitPacketOut(func(p *nodesV5, addr *net.UDPAddr, authTag []byte) { + if !bytes.Equal(p.ReqID, wantReqID) { + test.t.Fatalf("wrong request ID %v in response, want %v", p.ReqID, wantReqID) + } if len(p.Nodes) > 3 { test.t.Fatalf("too many nodes in response") } if p.Total != wantTotal { - test.t.Fatalf("wrong total response count %d", p.Total) - } - if !bytes.Equal(p.ReqID, wantReqID) { - test.t.Fatalf("wrong request ID in response: %v", p.ReqID) + test.t.Fatalf("wrong total response count %d, want %d", p.Total, wantTotal) } for _, record := range p.Nodes { n, _ := enode.New(enode.ValidSchemesForTesting, record) @@ -689,6 +690,7 @@ func (test *udpV5Test) getNode(key *ecdsa.PrivateKey, addr *net.UDPAddr) *enode. func (test *udpV5Test) waitPacketOut(validate interface{}) (closed bool) { test.t.Helper() + fn := reflect.ValueOf(validate) exptype := fn.Type().In(0) From b6a9d1d69377646b894b3e232e6b210bd0c86f73 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Fri, 28 Aug 2020 10:07:50 +0200 Subject: [PATCH 09/70] p2p/discover: add one more test --- p2p/discover/v5_udp_test.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/p2p/discover/v5_udp_test.go b/p2p/discover/v5_udp_test.go index 80ad754a7c0..ebdfc103c04 100644 --- a/p2p/discover/v5_udp_test.go +++ b/p2p/discover/v5_udp_test.go @@ -158,13 +158,17 @@ func TestUDPv5_findnodeHandling(t *testing.T) { test.packetIn(&findnodeV5{ReqID: []byte{1}, Distances: []uint{4234098}}) test.expectNodes([]byte{1}, 1, nil) - // This request gets no nodes because the corresponding bucket is empty. - test.packetIn(&findnodeV5{ReqID: []byte{2}, Distances: []uint{254}}) + // Requesting with empty distance list shouldn't crash either. + test.packetIn(&findnodeV5{ReqID: []byte{2}, Distances: []uint{}}) test.expectNodes([]byte{2}, 1, nil) + // This request gets no nodes because the corresponding bucket is empty. + test.packetIn(&findnodeV5{ReqID: []byte{3}, Distances: []uint{254}}) + test.expectNodes([]byte{3}, 1, nil) + // This request gets all test nodes. - test.packetIn(&findnodeV5{ReqID: []byte{3}, Distances: []uint{253}}) - test.expectNodes([]byte{3}, 4, nodes) + test.packetIn(&findnodeV5{ReqID: []byte{4}, Distances: []uint{253}}) + test.expectNodes([]byte{4}, 4, nodes) } func (test *udpV5Test) expectNodes(wantReqID []byte, wantTotal uint8, wantNodes []*enode.Node) { From f276753a5220e01fbd600bda920e4e729f053ffa Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Wed, 2 Sep 2020 14:42:44 +0200 Subject: [PATCH 10/70] p2p/discover: add multi-distance findnode handler test --- p2p/discover/v5_udp_test.go | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/p2p/discover/v5_udp_test.go b/p2p/discover/v5_udp_test.go index ebdfc103c04..1a56ed5867a 100644 --- a/p2p/discover/v5_udp_test.go +++ b/p2p/discover/v5_udp_test.go @@ -147,8 +147,12 @@ func TestUDPv5_findnodeHandling(t *testing.T) { defer test.close() // Create test nodes and insert them into the table. - nodes := nodesAtDistance(test.table.self().ID(), 253, 10) - fillTable(test.table, wrapNodes(nodes)) + nodes253 := nodesAtDistance(test.table.self().ID(), 253, 10) + nodes249 := nodesAtDistance(test.table.self().ID(), 249, 4) + nodes248 := nodesAtDistance(test.table.self().ID(), 248, 10) + fillTable(test.table, wrapNodes(nodes253)) + fillTable(test.table, wrapNodes(nodes249)) + fillTable(test.table, wrapNodes(nodes248)) // Requesting with distance zero should return the node's own record. test.packetIn(&findnodeV5{ReqID: []byte{0}, Distances: []uint{0}}) @@ -166,9 +170,17 @@ func TestUDPv5_findnodeHandling(t *testing.T) { test.packetIn(&findnodeV5{ReqID: []byte{3}, Distances: []uint{254}}) test.expectNodes([]byte{3}, 1, nil) - // This request gets all test nodes. + // This request gets all the distance-253 nodes. test.packetIn(&findnodeV5{ReqID: []byte{4}, Distances: []uint{253}}) - test.expectNodes([]byte{4}, 4, nodes) + test.expectNodes([]byte{4}, 4, nodes253) + + // This request gets all the distance-249 nodes and some more at 248 because + // the bucket at 249 is not full. + test.packetIn(&findnodeV5{ReqID: []byte{5}, Distances: []uint{249, 248}}) + var nodes []*enode.Node + nodes = append(nodes, nodes249...) + nodes = append(nodes, nodes248[:10]...) + test.expectNodes([]byte{5}, 5, nodes) } func (test *udpV5Test) expectNodes(wantReqID []byte, wantTotal uint8, wantNodes []*enode.Node) { From 665dfdbbd5d00ed968e3c985e6878871e348d44e Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Wed, 2 Sep 2020 14:43:11 +0200 Subject: [PATCH 11/70] p2p/discover: move header masking constant to the top and add comments --- p2p/discover/v5_encoding.go | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/p2p/discover/v5_encoding.go b/p2p/discover/v5_encoding.go index 3678b2f17fc..6cf69c343b5 100644 --- a/p2p/discover/v5_encoding.go +++ b/p2p/discover/v5_encoding.go @@ -100,6 +100,7 @@ const ( ) var ( + sizeofMaskingIV = 16 sizeofPacketHeaderV5 = binary.Size(packetHeaderV5{}) sizeofWhoareyouAuthDataV5 = binary.Size(whoareyouAuthDataV5{}) sizeofHandshakeAuthDataV5 = binary.Size(handshakeAuthDataV5{}.h) @@ -453,11 +454,11 @@ func (c *wireCodec) decode(input []byte, addr string) (src enode.ID, n *enode.No c.sc.handshakeGC() // Unmask the header. - if len(input) < sizeofPacketHeaderV5+maskIVSize { + if len(input) < sizeofPacketHeaderV5+sizeofMaskingIV { return enode.ID{}, nil, nil, errTooShort } mask := headerMask(c.localnode.ID(), input) - input = input[maskIVSize:] + input = input[sizeofMaskingIV:] headerData := input[:sizeofPacketHeaderV5] mask.XORKeyStream(headerData, headerData) @@ -803,23 +804,21 @@ func decryptGCM(key, nonce, ct, authData []byte) ([]byte, error) { return aesgcm.Open(pt, nonce, ct, authData) } -// header masking - -const maskIVSize = 16 - +// headerMask returns a cipher for 'masking' / 'unmasking' packet headers. func headerMask(destID enode.ID, input []byte) cipher.Stream { block, err := aes.NewCipher(destID[:16]) if err != nil { panic("can't create cipher") } - return cipher.NewCTR(block, input[:maskIVSize]) + return cipher.NewCTR(block, input[:sizeofMaskingIV]) } +// maskOutputPacket applies protocol header masking to a packet sent to destID. func maskOutputPacket(destID enode.ID, output []byte, headerDataLen int) []byte { - masked := make([]byte, maskIVSize+len(output)) - crand.Read(masked[:maskIVSize]) + masked := make([]byte, sizeofMaskingIV+len(output)) + crand.Read(masked[:sizeofMaskingIV]) mask := headerMask(destID, masked) - copy(masked[maskIVSize:], output) - mask.XORKeyStream(masked[maskIVSize:], output[:headerDataLen]) + copy(masked[sizeofMaskingIV:], output) + mask.XORKeyStream(masked[sizeofMaskingIV:], output[:headerDataLen]) return masked } From a4454b46de173a94a86f306aa6d2d60b398507ed Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Thu, 17 Sep 2020 11:35:21 +0200 Subject: [PATCH 12/70] p2p/discover: add hexFile helper --- p2p/discover/table_util_test.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/p2p/discover/table_util_test.go b/p2p/discover/table_util_test.go index 44b62e751b4..33415a7e380 100644 --- a/p2p/discover/table_util_test.go +++ b/p2p/discover/table_util_test.go @@ -22,6 +22,7 @@ import ( "encoding/hex" "errors" "fmt" + "io/ioutil" "math/rand" "net" "sort" @@ -227,6 +228,7 @@ func sortedByDistanceTo(distbase enode.ID, slice []*node) bool { }) } +// hexEncPrivkey decodes h as a private key. func hexEncPrivkey(h string) *ecdsa.PrivateKey { b, err := hex.DecodeString(h) if err != nil { @@ -239,6 +241,7 @@ func hexEncPrivkey(h string) *ecdsa.PrivateKey { return key } +// hexEncPubkey decodes h as a public key. func hexEncPubkey(h string) (ret encPubkey) { b, err := hex.DecodeString(h) if err != nil { @@ -250,3 +253,20 @@ func hexEncPubkey(h string) (ret encPubkey) { copy(ret[:], b) return ret } + +// hexFile reads the given file and decodes the hex data contained in it. +func hexFile(file string) []byte { + text, err := ioutil.ReadFile(file) + if err != nil { + panic(err) + } + text = bytes.TrimSpace(text) + if bytes.HasPrefix(text, []byte("0x")) { + text = text[2:] + } + data := make([]byte, hex.DecodedLen(len(text))) + if _, err := hex.Decode(data, text); err != nil { + panic("invalid hex in " + file) + } + return data +} From eccc0e6b527bea7ca51c65a2c24920497100147e Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Thu, 17 Sep 2020 12:01:17 +0200 Subject: [PATCH 13/70] p2p/discover: add packetNonce type, implement random+counter nonce --- p2p/discover/v5_encoding.go | 120 ++++++++++++++++--------------- p2p/discover/v5_encoding_test.go | 41 ++++++----- p2p/discover/v5_session.go | 35 +++++---- p2p/discover/v5_udp.go | 18 ++--- p2p/discover/v5_udp_test.go | 60 ++++++++-------- 5 files changed, 145 insertions(+), 129 deletions(-) diff --git a/p2p/discover/v5_encoding.go b/p2p/discover/v5_encoding.go index 6cf69c343b5..458d8e14f53 100644 --- a/p2p/discover/v5_encoding.go +++ b/p2p/discover/v5_encoding.go @@ -71,25 +71,27 @@ type ( } whoareyouAuthDataV5 struct { - Nonce [gcmNonceSize]byte // nonce of request packet - IDNonce [32]byte // ID proof data - RecordSeq uint64 // highest known ENR sequence of requester + Nonce packetNonce // nonce of request packet + IDNonce [32]byte // ID proof data + RecordSeq uint64 // highest known ENR sequence of requester } handshakeAuthDataV5 struct { h struct { - Version uint8 // protocol version - Nonce [gcmNonceSize]byte // AES-GCM nonce of message - SigSize byte // ignature data - PubkeySize byte // offset of + Version uint8 // protocol version + Nonce packetNonce // AES-GCM nonce of message + SigSize byte // ignature data + PubkeySize byte // offset of } // Trailing variable-size data. signature, pubkey, record []byte } messageAuthDataV5 struct { - Nonce [gcmNonceSize]byte // AES-GCM nonce of message + Nonce packetNonce // AES-GCM nonce of message } + + packetNonce [gcmNonceSize]byte ) // Packet header flag values. @@ -112,12 +114,12 @@ var ( type ( // unknownV5 represents any packet that can't be decrypted. unknownV5 struct { - AuthTag []byte + AuthTag packetNonce } // WHOAREYOU contains the handshake challenge. whoareyouV5 struct { - AuthTag []byte + AuthTag packetNonce IDNonce [32]byte // To be signed by recipient. RecordSeq uint64 // ENR sequence number of recipient @@ -233,10 +235,6 @@ type wireCodec struct { sc *sessionCache } -type handshakeSecrets struct { - writeKey, readKey []byte -} - // newWireCodec creates a wire codec. func newWireCodec(ln *enode.LocalNode, key *ecdsa.PrivateKey, clock mclock.Clock) *wireCodec { c := &wireCodec{ @@ -251,21 +249,21 @@ func newWireCodec(ln *enode.LocalNode, key *ecdsa.PrivateKey, clock mclock.Clock // encode encodes a packet to a node. 'id' and 'addr' specify the destination node. The // 'challenge' parameter should be the most recently received WHOAREYOU packet from that // node. -func (c *wireCodec) encode(id enode.ID, addr string, packet packetV5, challenge *whoareyouV5) ([]byte, []byte, error) { +func (c *wireCodec) encode(id enode.ID, addr string, packet packetV5, challenge *whoareyouV5) ([]byte, packetNonce, error) { if packet.kind() == p_whoareyouV5 { p := packet.(*whoareyouV5) enc, err := c.encodeWhoareyou(id, p) if err == nil { c.sc.storeSentHandshake(id, addr, p) } - return enc, nil, err + return enc, packetNonce{}, err } if challenge != nil { return c.encodeHandshakeMessage(id, addr, packet, challenge) } - if key := c.sc.writeKey(id, addr); key != nil { - return c.encodeMessage(id, addr, packet, key) + if session := c.sc.session(id, addr); session != nil { + return c.encodeMessage(id, session, packet) } // No keys, no handshake: send random data to kick off the handshake. return c.encodeRandom(id) @@ -297,17 +295,17 @@ func (c *wireCodec) makeHeader(toID enode.ID, flags byte, authsizeExtra int) *pa } // encodeRandom encodes a packet with random content. -func (c *wireCodec) encodeRandom(toID enode.ID) ([]byte, []byte, error) { +func (c *wireCodec) encodeRandom(toID enode.ID) ([]byte, packetNonce, error) { var auth messageAuthDataV5 if _, err := crand.Read(auth.Nonce[:]); err != nil { - return nil, nil, fmt.Errorf("can't get random data: %v", err) + return nil, auth.Nonce, fmt.Errorf("can't get random data: %v", err) } c.buf.Reset() binary.Write(&c.buf, binary.BigEndian, c.makeHeader(toID, flagMessage, 0)) binary.Write(&c.buf, binary.BigEndian, &auth) output := maskOutputPacket(toID, c.buf.Bytes(), c.buf.Len()) - return output, auth.Nonce[:], nil + return output, auth.Nonce, nil } // encodeWhoareyou encodes a WHOAREYOU packet. @@ -317,10 +315,10 @@ func (c *wireCodec) encodeWhoareyou(toID enode.ID, packet *whoareyouV5) ([]byte, panic("BUG: missing node in whoareyouV5 with non-zero seq") } auth := &whoareyouAuthDataV5{ + Nonce: packet.AuthTag, IDNonce: packet.IDNonce, RecordSeq: packet.RecordSeq, } - copy(auth.Nonce[:], packet.AuthTag) head := c.makeHeader(toID, flagWhoareyou, 0) c.buf.Reset() @@ -332,20 +330,20 @@ func (c *wireCodec) encodeWhoareyou(toID enode.ID, packet *whoareyouV5) ([]byte, // encodeHandshakeMessage encodes an encrypted message with a handshake // response header. -func (c *wireCodec) encodeHandshakeMessage(toID enode.ID, addr string, packet packetV5, challenge *whoareyouV5) ([]byte, []byte, error) { +func (c *wireCodec) encodeHandshakeMessage(toID enode.ID, addr string, packet packetV5, challenge *whoareyouV5) ([]byte, packetNonce, error) { // Ensure calling code sets challenge.node. if challenge.node == nil { panic("BUG: missing challenge.node in encode") } // Generate new secrets. - auth, sec, err := c.makeHandshakeHeader(toID, addr, challenge) + auth, session, err := c.makeHandshakeHeader(toID, addr, challenge) if err != nil { - return nil, nil, err + return nil, packetNonce{}, err } // TODO: this should happen when the first authenticated message is received - c.sc.storeNewSession(toID, addr, sec.readKey, sec.writeKey) + c.sc.storeNewSession(toID, addr, session) // Encode header and auth header. var ( @@ -364,22 +362,23 @@ func (c *wireCodec) encodeHandshakeMessage(toID enode.ID, addr string, packet pa c.msgbuf.Reset() c.msgbuf.WriteByte(packet.kind()) if err := rlp.Encode(&c.msgbuf, packet); err != nil { - return nil, nil, err + return nil, auth.h.Nonce, err } messagePT := c.msgbuf.Bytes() headerData := output - output, err = encryptGCM(output, sec.writeKey, auth.h.Nonce[:], messagePT, headerData) + output, err = encryptGCM(output, session.writeKey, auth.h.Nonce[:], messagePT, headerData) if err == nil { output = maskOutputPacket(toID, output, len(headerData)) } - return output, auth.h.Nonce[:], err + return output, auth.h.Nonce, err } // encodeAuthHeader creates the auth header on a call packet following WHOAREYOU. -func (c *wireCodec) makeHandshakeHeader(toID enode.ID, addr string, challenge *whoareyouV5) (*handshakeAuthDataV5, *handshakeSecrets, error) { +func (c *wireCodec) makeHandshakeHeader(toID enode.ID, addr string, challenge *whoareyouV5) (*handshakeAuthDataV5, *session, error) { + session := new(session) auth := new(handshakeAuthDataV5) auth.h.Version = handshakeVersion - auth.h.Nonce = c.sc.nextNonce(toID, addr) + auth.h.Nonce = c.sc.nextNonce(session) // Create the ephemeral key. This needs to be first because the // key is part of the ID nonce signature. @@ -419,12 +418,12 @@ func (c *wireCodec) makeHandshakeHeader(toID enode.ID, addr string, challenge *w } // encodeMessage encodes an encrypted message packet. -func (c *wireCodec) encodeMessage(toID enode.ID, addr string, packet packetV5, writeKey []byte) (enc []byte, authTag []byte, err error) { +func (c *wireCodec) encodeMessage(toID enode.ID, s *session, packet packetV5) ([]byte, packetNonce, error) { var ( auth messageAuthDataV5 head = c.makeHeader(toID, flagMessage, 0) ) - auth.Nonce = c.sc.nextNonce(toID, addr) + auth.Nonce = c.sc.nextNonce(s) c.buf.Reset() binary.Write(&c.buf, binary.BigEndian, head) binary.Write(&c.buf, binary.BigEndian, &auth) @@ -434,17 +433,17 @@ func (c *wireCodec) encodeMessage(toID enode.ID, addr string, packet packetV5, w c.msgbuf.Reset() c.msgbuf.WriteByte(packet.kind()) if err := rlp.Encode(&c.msgbuf, packet); err != nil { - return nil, nil, err + return nil, auth.Nonce, err } messagePT := c.msgbuf.Bytes() // Encrypt message data. headerData := output - output, err = encryptGCM(output, writeKey, auth.Nonce[:], messagePT, headerData) + output, err := encryptGCM(output, s.writeKey, auth.Nonce[:], messagePT, headerData) if err == nil { output = maskOutputPacket(toID, output, len(headerData)) } - return output, auth.Nonce[:], err + return output, auth.Nonce, err } // decode decodes a discovery packet. @@ -501,7 +500,7 @@ func (c *wireCodec) decodeWhoareyou(head *packetHeaderV5) (packetV5, error) { auth := new(whoareyouAuthDataV5) binary.Read(&c.reader, binary.BigEndian, auth) p := &whoareyouV5{ - AuthTag: auth.Nonce[:], + AuthTag: auth.Nonce, IDNonce: auth.IDNonce, RecordSeq: auth.RecordSeq, } @@ -509,50 +508,53 @@ func (c *wireCodec) decodeWhoareyou(head *packetHeaderV5) (packetV5, error) { } func (c *wireCodec) decodeHandshakeMessage(fromAddr string, head *packetHeaderV5, input []byte) (n *enode.Node, p packetV5, err error) { - node, nonce, sec, err := c.decodeHandshake(fromAddr, head) + node, nonce, session, err := c.decodeHandshake(fromAddr, head) if err != nil { return nil, nil, err } + // Decrypt the message using the new session keys. + msg, err := c.decryptMessage(input, nonce, session.readKey) + if err != nil { + return node, msg, err + } + // Handshake OK, drop the challenge and store the new session keys. - sec.readKey, sec.writeKey = sec.writeKey, sec.readKey - c.sc.storeNewSession(head.SrcID, fromAddr, sec.readKey, sec.writeKey) + c.sc.storeNewSession(head.SrcID, fromAddr, session) c.sc.deleteHandshake(head.SrcID, fromAddr) - - // Decrypt the message using the new session keys. - msg, err := c.decryptMessage(input, nonce, sec.readKey) - return node, msg, err + return node, msg, nil } -func (c *wireCodec) decodeHandshake(fromAddr string, head *packetHeaderV5) (*enode.Node, []byte, *handshakeSecrets, error) { +func (c *wireCodec) decodeHandshake(fromAddr string, head *packetHeaderV5) (*enode.Node, packetNonce, *session, error) { auth, err := c.decodeHandshakeAuthData(head) if err != nil { - return nil, nil, nil, err + return nil, packetNonce{}, nil, err } // Verify against our last WHOAREYOU. challenge := c.sc.getHandshake(head.SrcID, fromAddr) if challenge == nil { - return nil, nil, nil, errUnexpectedHandshake + return nil, packetNonce{}, nil, errUnexpectedHandshake } // Get node record. node, err := c.decodeHandshakeRecord(challenge.node, head.SrcID, auth.record) if err != nil { - return nil, nil, nil, err + return nil, packetNonce{}, nil, err } // Verify ephemeral key is on curve. ephkey, err := decodePubkey(c.privkey.Curve, auth.pubkey) if err != nil { - return nil, nil, nil, errInvalidAuthKey + return nil, packetNonce{}, nil, errInvalidAuthKey } // Verify ID nonce signature. err = c.verifyIDSignature(challenge.IDNonce[:], auth.pubkey, auth.signature, node) if err != nil { - return nil, nil, nil, err + return nil, packetNonce{}, nil, err } // Derive sesssion keys. - sec := c.deriveKeys(head.SrcID, c.localnode.ID(), c.privkey, ephkey, challenge) - return node, auth.h.Nonce[:], sec, nil + session := c.deriveKeys(head.SrcID, c.localnode.ID(), c.privkey, ephkey, challenge) + session = session.keysFlipped() + return node, auth.h.Nonce, session, nil } // decodeHandshakeAuthData reads the authdata section of a handshake packet. @@ -630,23 +632,23 @@ func (c *wireCodec) decodeMessage(fromAddr string, head *packetHeaderV5, input [ if c.reader.Len() < sizeofMessageAuthDataV5 { return nil, errTooShort } - key := c.sc.readKey(head.SrcID, fromAddr) auth := new(messageAuthDataV5) binary.Read(&c.reader, binary.BigEndian, auth) // Try decrypting the message. - msg, err := c.decryptMessage(input, auth.Nonce[:], key) + key := c.sc.readKey(head.SrcID, fromAddr) + msg, err := c.decryptMessage(input, auth.Nonce, key) if err == errMessageDecrypt { // It didn't work. Start the handshake since this is an ordinary message packet. - return &unknownV5{AuthTag: auth.Nonce[:]}, nil + return &unknownV5{AuthTag: auth.Nonce}, nil } return msg, err } -func (c *wireCodec) decryptMessage(input, nonce, key []byte) (packetV5, error) { +func (c *wireCodec) decryptMessage(input []byte, nonce packetNonce, readKey []byte) (packetV5, error) { headerData := input[:len(input)-c.reader.Len()] messageCT := input[len(headerData):] - message, err := decryptGCM(key, nonce, messageCT, headerData) + message, err := decryptGCM(readKey, nonce[:], messageCT, headerData) if err != nil { return nil, errMessageDecrypt } @@ -725,7 +727,7 @@ func (c *wireCodec) verifyIDSignature(nonce, ephkey, sig []byte, n *enode.Node) } // deriveKeys generates session keys using elliptic-curve Diffie-Hellman key agreement. -func (c *wireCodec) deriveKeys(n1, n2 enode.ID, priv *ecdsa.PrivateKey, pub *ecdsa.PublicKey, challenge *whoareyouV5) *handshakeSecrets { +func (c *wireCodec) deriveKeys(n1, n2 enode.ID, priv *ecdsa.PrivateKey, pub *ecdsa.PublicKey, challenge *whoareyouV5) *session { eph := ecdh(priv, pub) if eph == nil { return nil @@ -735,7 +737,7 @@ func (c *wireCodec) deriveKeys(n1, n2 enode.ID, priv *ecdsa.PrivateKey, pub *ecd info = append(info, n1[:]...) info = append(info, n2[:]...) kdf := hkdf.New(c.sha256reset, eph, challenge.IDNonce[:], info) - sec := handshakeSecrets{ + sec := session{ writeKey: make([]byte, aesKeySize), readKey: make([]byte, aesKeySize), } diff --git a/p2p/discover/v5_encoding_test.go b/p2p/discover/v5_encoding_test.go index bfad193d027..1ff66e2996d 100644 --- a/p2p/discover/v5_encoding_test.go +++ b/p2p/discover/v5_encoding_test.go @@ -79,7 +79,7 @@ func TestHandshakeV5(t *testing.T) { whoareyou, _ := net.nodeB.encode(t, net.nodeA, challenge) net.nodeA.expectDecode(t, p_whoareyouV5, whoareyou) - // A -> B FINDNODE + // A -> B FINDNODE (handshake packet) findnode, _ := net.nodeA.encodeWithChallenge(t, net.nodeB, challenge, &findnodeV5{}) net.nodeB.expectDecode(t, p_findnodeV5, findnode) if len(net.nodeB.c.sc.handshakes) > 0 { @@ -110,7 +110,7 @@ func TestHandshakeV5_timeout(t *testing.T) { whoareyou, _ := net.nodeB.encode(t, net.nodeA, challenge) net.nodeA.expectDecode(t, p_whoareyouV5, whoareyou) - // A -> B FINDNODE after timeout + // A -> B FINDNODE (handshake packet) after timeout net.clock.Run(handshakeTimeout + 1) findnode, _ := net.nodeA.encodeWithChallenge(t, net.nodeB, challenge, &findnodeV5{}) net.nodeB.expectDecodeErr(t, errUnexpectedHandshake, findnode) @@ -156,11 +156,11 @@ func TestHandshakeV5_rekey(t *testing.T) { net := newHandshakeTest() defer net.close() - initKeys := &handshakeSecrets{ + session := &session{ readKey: []byte("BBBBBBBBBBBBBBBB"), writeKey: []byte("AAAAAAAAAAAAAAAA"), } - net.nodeA.c.sc.storeNewSession(net.nodeB.id(), net.nodeB.addr(), initKeys.readKey, initKeys.writeKey) + net.nodeA.c.sc.storeNewSession(net.nodeB.id(), net.nodeB.addr(), session) // A -> B FINDNODE (encrypted with zero keys) findnode, authTag := net.nodeA.encode(t, net.nodeB, &findnodeV5{}) @@ -172,7 +172,8 @@ func TestHandshakeV5_rekey(t *testing.T) { net.nodeA.expectDecode(t, p_whoareyouV5, whoareyou) // Check that new keys haven't been stored yet. - if s := net.nodeA.c.sc.session(net.nodeB.id(), net.nodeB.addr()); !bytes.Equal(s.writeKey, initKeys.writeKey) || !bytes.Equal(s.readKey, initKeys.readKey) { + sa := net.nodeA.c.sc.session(net.nodeB.id(), net.nodeB.addr()) + if !bytes.Equal(sa.writeKey, session.writeKey) || !bytes.Equal(sa.readKey, session.readKey) { t.Fatal("node A stored keys too early") } if s := net.nodeB.c.sc.session(net.nodeA.id(), net.nodeA.addr()); s != nil { @@ -194,16 +195,16 @@ func TestHandshakeV5_rekey2(t *testing.T) { net := newHandshakeTest() defer net.close() - initKeysA := &handshakeSecrets{ + initKeysA := &session{ readKey: []byte("BBBBBBBBBBBBBBBB"), writeKey: []byte("AAAAAAAAAAAAAAAA"), } - initKeysB := &handshakeSecrets{ + initKeysB := &session{ readKey: []byte("CCCCCCCCCCCCCCCC"), writeKey: []byte("DDDDDDDDDDDDDDDD"), } - net.nodeA.c.sc.storeNewSession(net.nodeB.id(), net.nodeB.addr(), initKeysA.readKey, initKeysA.writeKey) - net.nodeB.c.sc.storeNewSession(net.nodeA.id(), net.nodeA.addr(), initKeysB.readKey, initKeysA.writeKey) + net.nodeA.c.sc.storeNewSession(net.nodeB.id(), net.nodeB.addr(), initKeysA) + net.nodeB.c.sc.storeNewSession(net.nodeA.id(), net.nodeA.addr(), initKeysB) // A -> B FINDNODE encrypted with initKeysA findnode, authTag := net.nodeA.encode(t, net.nodeB, &findnodeV5{Distances: []uint{3}}) @@ -214,7 +215,7 @@ func TestHandshakeV5_rekey2(t *testing.T) { whoareyou, _ := net.nodeB.encode(t, net.nodeA, challenge) net.nodeA.expectDecode(t, p_whoareyouV5, whoareyou) - // A -> B FINDNODE encrypted with new keys + // A -> B FINDNODE (handshake packet) findnode, _ = net.nodeA.encodeWithChallenge(t, net.nodeB, challenge, &findnodeV5{}) net.nodeB.expectDecode(t, p_findnodeV5, findnode) @@ -242,7 +243,7 @@ func BenchmarkV5_DecodeHandshakePingSecp256k1(b *testing.B) { var ( idA = net.nodeA.id() - challenge = &whoareyouV5{AuthTag: []byte("authresp"), node: net.nodeB.n()} + challenge = &whoareyouV5{node: net.nodeB.n()} message = &pingV5{ReqID: []byte("reqid")} ) enc, _, err := net.nodeA.c.encode(net.nodeB.id(), "", message, challenge) @@ -268,10 +269,12 @@ func BenchmarkV5_DecodePing(b *testing.B) { net := newHandshakeTest() defer net.close() - r := []byte{233, 203, 93, 195, 86, 47, 177, 186, 227, 43, 2, 141, 244, 230, 120, 17} - w := []byte{79, 145, 252, 171, 167, 216, 252, 161, 208, 190, 176, 106, 214, 39, 178, 134} - net.nodeA.c.sc.storeNewSession(net.nodeB.id(), net.nodeB.addr(), r, w) - net.nodeB.c.sc.storeNewSession(net.nodeA.id(), net.nodeA.addr(), w, r) + session := &session{ + readKey: []byte{233, 203, 93, 195, 86, 47, 177, 186, 227, 43, 2, 141, 244, 230, 120, 17}, + writeKey: []byte{79, 145, 252, 171, 167, 216, 252, 161, 208, 190, 176, 106, 214, 39, 178, 134}, + } + net.nodeA.c.sc.storeNewSession(net.nodeB.id(), net.nodeB.addr(), session) + net.nodeB.c.sc.storeNewSession(net.nodeA.id(), net.nodeA.addr(), session.keysFlipped()) addrB := net.nodeA.addr() ping := &pingV5{ReqID: []byte("reqid"), ENRSeq: 5} enc, _, err := net.nodeA.c.encode(net.nodeB.id(), addrB, ping, nil) @@ -321,12 +324,12 @@ func (n *handshakeTestNode) init(key *ecdsa.PrivateKey, ip net.IP, clock mclock. n.c = newWireCodec(n.ln, key, clock) } -func (n *handshakeTestNode) encode(t testing.TB, to handshakeTestNode, p packetV5) ([]byte, []byte) { +func (n *handshakeTestNode) encode(t testing.TB, to handshakeTestNode, p packetV5) ([]byte, packetNonce) { t.Helper() return n.encodeWithChallenge(t, to, nil, p) } -func (n *handshakeTestNode) encodeWithChallenge(t testing.TB, to handshakeTestNode, c *whoareyouV5, p packetV5) ([]byte, []byte) { +func (n *handshakeTestNode) encodeWithChallenge(t testing.TB, to handshakeTestNode, c *whoareyouV5, p packetV5) ([]byte, packetNonce) { t.Helper() // Copy challenge and add destination node. This avoids sharing 'c' among the two codecs. var challenge *whoareyouV5 @@ -336,12 +339,12 @@ func (n *handshakeTestNode) encodeWithChallenge(t testing.TB, to handshakeTestNo challenge.node = to.n() } // Encode to destination. - enc, authTag, err := n.c.encode(to.id(), to.addr(), p, challenge) + enc, nonce, err := n.c.encode(to.id(), to.addr(), p, challenge) if err != nil { t.Fatal(fmt.Errorf("(%s) %v", n.ln.ID().TerminalString(), err)) } t.Logf("(%s) -> (%s) %s\n%s", n.ln.ID().TerminalString(), to.id().TerminalString(), p.name(), hex.Dump(enc)) - return enc, authTag + return enc, nonce } func (n *handshakeTestNode) expectDecode(t *testing.T, ptype byte, p []byte) packetV5 { diff --git a/p2p/discover/v5_session.go b/p2p/discover/v5_session.go index a2a87251131..f0691adcce0 100644 --- a/p2p/discover/v5_session.go +++ b/p2p/discover/v5_session.go @@ -18,6 +18,7 @@ package discover import ( crand "crypto/rand" + "encoding/binary" "github.com/ethereum/go-ethereum/common/mclock" "github.com/ethereum/go-ethereum/p2p/enode" @@ -30,6 +31,9 @@ type sessionCache struct { sessions *simplelru.LRU handshakes map[sessionID]*whoareyouV5 clock mclock.Clock + + // test hook for overriding the random nonce generator. + nonceFunc func(counter uint32) packetNonce } // sessionID identifies a session or handshake. @@ -45,6 +49,11 @@ type session struct { nonceCounter uint32 } +// keysFlipped returns a copy of s with the read and write keys flipped. +func (s *session) keysFlipped() *session { + return &session{s.readKey, s.writeKey, s.nonceCounter} +} + func newSessionCache(maxItems int, clock mclock.Clock) *sessionCache { cache, err := simplelru.NewLRU(maxItems, nil) if err != nil { @@ -54,12 +63,20 @@ func newSessionCache(maxItems int, clock mclock.Clock) *sessionCache { sessions: cache, handshakes: make(map[sessionID]*whoareyouV5), clock: clock, + nonceFunc: generateNonce, } } +func generateNonce(counter uint32) (n packetNonce) { + binary.BigEndian.PutUint32(n[:], counter) + crand.Read(n[4:]) + return n +} + // nextNonce creates a nonce for encrypting a message to the given session. -func (sc *sessionCache) nextNonce(id enode.ID, addr string) (n [gcmNonceSize]byte) { - crand.Read(n[:]) +func (sc *sessionCache) nextNonce(s *session) packetNonce { + n := sc.nonceFunc(s.nonceCounter) + s.nonceCounter++ return n } @@ -80,19 +97,9 @@ func (sc *sessionCache) readKey(id enode.ID, addr string) []byte { return nil } -// writeKey returns the current read key for the given node. -func (sc *sessionCache) writeKey(id enode.ID, addr string) []byte { - if s := sc.session(id, addr); s != nil { - return s.writeKey - } - return nil -} - // storeNewSession stores new encryption keys in the cache. -func (sc *sessionCache) storeNewSession(id enode.ID, addr string, r, w []byte) { - sc.sessions.Add(sessionID{id, addr}, &session{ - readKey: r, writeKey: w, - }) +func (sc *sessionCache) storeNewSession(id enode.ID, addr string, s *session) { + sc.sessions.Add(sessionID{id, addr}, s) } // getHandshake gets the handshake challenge we previously sent to the given remote node. diff --git a/p2p/discover/v5_udp.go b/p2p/discover/v5_udp.go index 37bf2254b8b..9a376daabe7 100644 --- a/p2p/discover/v5_udp.go +++ b/p2p/discover/v5_udp.go @@ -52,7 +52,7 @@ const ( type codecV5 interface { // encode encodes a packet. The 'challenge' parameter is non-nil for calls which got a // WHOAREYOU response. - encode(fromID enode.ID, fromAddr string, p packetV5, challenge *whoareyouV5) (enc []byte, authTag []byte, err error) + encode(fromID enode.ID, fromAddr string, p packetV5, challenge *whoareyouV5) ([]byte, packetNonce, error) // decode decodes a packet. It returns an *unknownV5 packet if decryption fails. // The fromNode return value is non-nil when the input contains a handshake response. @@ -117,7 +117,7 @@ type callV5 struct { err chan error // errors sent here // Valid for active calls only: - authTag []byte // authTag of request packet + authTag packetNonce // nonce of request packet handshakeCount int // # times we attempted handshake for this call challenge *whoareyouV5 // last sent handshake challenge timeout mclock.Timer @@ -517,7 +517,7 @@ func (t *UDPv5) dispatch() { panic("BUG: callDone for inactive call") } c.timeout.Stop() - delete(t.activeCallByAuth, string(c.authTag)) + delete(t.activeCallByAuth, string(c.authTag[:])) delete(t.activeCallByNode, id) t.sendNextCall(id) @@ -537,7 +537,7 @@ func (t *UDPv5) dispatch() { for id, c := range t.activeCallByNode { c.err <- errClosed delete(t.activeCallByNode, id) - delete(t.activeCallByAuth, string(c.authTag)) + delete(t.activeCallByAuth, string(c.authTag[:])) } return } @@ -587,13 +587,13 @@ func (t *UDPv5) sendCall(c *callV5) { // The call already has an authTag from a previous handshake attempt. Remove the // entry for the authTag because we're about to generate a new authTag for this // call. - delete(t.activeCallByAuth, string(c.authTag)) + delete(t.activeCallByAuth, string(c.authTag[:])) } addr := &net.UDPAddr{IP: c.node.IP(), Port: c.node.UDP()} newTag, _ := t.send(c.node.ID(), addr, c.packet, c.challenge) c.authTag = newTag - t.activeCallByAuth[string(c.authTag)] = c + t.activeCallByAuth[string(c.authTag[:])] = c t.startResponseTimeout(c) } @@ -605,7 +605,7 @@ func (t *UDPv5) sendResponse(toID enode.ID, toAddr *net.UDPAddr, packet packetV5 } // send sends a packet to the given node. -func (t *UDPv5) send(toID enode.ID, toAddr *net.UDPAddr, packet packetV5, c *whoareyouV5) ([]byte, error) { +func (t *UDPv5) send(toID enode.ID, toAddr *net.UDPAddr, packet packetV5, c *whoareyouV5) (packetNonce, error) { addr := toAddr.String() enc, authTag, err := t.codec.encode(toID, addr, packet, c) if err != nil { @@ -741,8 +741,8 @@ var ( ) // matchWithCall checks whether the handshake attempt matches the active call. -func (p *whoareyouV5) matchWithCall(t *UDPv5, authTag []byte) (*callV5, error) { - c := t.activeCallByAuth[string(authTag)] +func (p *whoareyouV5) matchWithCall(t *UDPv5, authTag packetNonce) (*callV5, error) { + c := t.activeCallByAuth[string(authTag[:])] if c == nil { return nil, errChallengeNoCall } diff --git a/p2p/discover/v5_udp_test.go b/p2p/discover/v5_udp_test.go index 1a56ed5867a..aad22fc72ec 100644 --- a/p2p/discover/v5_udp_test.go +++ b/p2p/discover/v5_udp_test.go @@ -94,7 +94,7 @@ func TestUDPv5_pingHandling(t *testing.T) { defer test.close() test.packetIn(&pingV5{ReqID: []byte("foo")}) - test.waitPacketOut(func(p *pongV5, addr *net.UDPAddr, authTag []byte) { + test.waitPacketOut(func(p *pongV5, addr *net.UDPAddr, _ packetNonce) { if !bytes.Equal(p.ReqID, []byte("foo")) { t.Error("wrong request ID in response:", p.ReqID) } @@ -110,11 +110,11 @@ func TestUDPv5_unknownPacket(t *testing.T) { test := newUDPV5Test(t) defer test.close() - authTag := [12]byte{1, 2, 3} + authTag := packetNonce{1, 2, 3} check := func(p *whoareyouV5, wantSeq uint64) { t.Helper() - if !bytes.Equal(p.AuthTag, authTag[:]) { - t.Error("wrong token in WHOAREYOU:", p.AuthTag, authTag[:]) + if p.AuthTag != authTag { + t.Error("wrong token in WHOAREYOU:", p.AuthTag, authTag) } if p.IDNonce == ([32]byte{}) { t.Error("all zero ID nonce") @@ -125,8 +125,8 @@ func TestUDPv5_unknownPacket(t *testing.T) { } // Unknown packet from unknown node. - test.packetIn(&unknownV5{AuthTag: authTag[:]}) - test.waitPacketOut(func(p *whoareyouV5, addr *net.UDPAddr, _ []byte) { + test.packetIn(&unknownV5{AuthTag: authTag}) + test.waitPacketOut(func(p *whoareyouV5, addr *net.UDPAddr, _ packetNonce) { check(p, 0) }) @@ -134,8 +134,8 @@ func TestUDPv5_unknownPacket(t *testing.T) { n := test.getNode(test.remotekey, test.remoteaddr).Node() test.table.addSeenNode(wrapNode(n)) - test.packetIn(&unknownV5{AuthTag: authTag[:]}) - test.waitPacketOut(func(p *whoareyouV5, addr *net.UDPAddr, _ []byte) { + test.packetIn(&unknownV5{AuthTag: authTag}) + test.waitPacketOut(func(p *whoareyouV5, addr *net.UDPAddr, _ packetNonce) { check(p, n.Seq()) }) } @@ -190,7 +190,7 @@ func (test *udpV5Test) expectNodes(wantReqID []byte, wantTotal uint8, wantNodes } for { - test.waitPacketOut(func(p *nodesV5, addr *net.UDPAddr, authTag []byte) { + test.waitPacketOut(func(p *nodesV5, addr *net.UDPAddr, _ packetNonce) { if !bytes.Equal(p.ReqID, wantReqID) { test.t.Fatalf("wrong request ID %v in response, want %v", p.ReqID, wantReqID) } @@ -232,7 +232,7 @@ func TestUDPv5_pingCall(t *testing.T) { _, err := test.udp.ping(remote) done <- err }() - test.waitPacketOut(func(p *pingV5, addr *net.UDPAddr, authTag []byte) {}) + test.waitPacketOut(func(p *pingV5, addr *net.UDPAddr, _ packetNonce) {}) if err := <-done; err != errTimeout { t.Fatalf("want errTimeout, got %q", err) } @@ -242,7 +242,7 @@ func TestUDPv5_pingCall(t *testing.T) { _, err := test.udp.ping(remote) done <- err }() - test.waitPacketOut(func(p *pingV5, addr *net.UDPAddr, authTag []byte) { + test.waitPacketOut(func(p *pingV5, addr *net.UDPAddr, _ packetNonce) { test.packetInFrom(test.remotekey, test.remoteaddr, &pongV5{ReqID: p.ReqID}) }) if err := <-done; err != nil { @@ -254,7 +254,7 @@ func TestUDPv5_pingCall(t *testing.T) { _, err := test.udp.ping(remote) done <- err }() - test.waitPacketOut(func(p *pingV5, addr *net.UDPAddr, authTag []byte) { + test.waitPacketOut(func(p *pingV5, addr *net.UDPAddr, _ packetNonce) { wrongAddr := &net.UDPAddr{IP: net.IP{33, 44, 55, 22}, Port: 10101} test.packetInFrom(test.remotekey, wrongAddr, &pongV5{ReqID: p.ReqID}) }) @@ -285,7 +285,7 @@ func TestUDPv5_findnodeCall(t *testing.T) { }() // Serve the responses: - test.waitPacketOut(func(p *findnodeV5, addr *net.UDPAddr, authTag []byte) { + test.waitPacketOut(func(p *findnodeV5, addr *net.UDPAddr, _ packetNonce) { if !reflect.DeepEqual(p.Distances, distances) { t.Fatalf("wrong distances in request: %v", p.Distances) } @@ -331,15 +331,15 @@ func TestUDPv5_callResend(t *testing.T) { }() // Ping answered by WHOAREYOU. - test.waitPacketOut(func(p *pingV5, addr *net.UDPAddr, authTag []byte) { + test.waitPacketOut(func(p *pingV5, addr *net.UDPAddr, authTag packetNonce) { test.packetIn(&whoareyouV5{AuthTag: authTag}) }) // Ping should be re-sent. - test.waitPacketOut(func(p *pingV5, addr *net.UDPAddr, authTag []byte) { + test.waitPacketOut(func(p *pingV5, addr *net.UDPAddr, _ packetNonce) { test.packetIn(&pongV5{ReqID: p.ReqID}) }) // Answer the other ping. - test.waitPacketOut(func(p *pingV5, addr *net.UDPAddr, authTag []byte) { + test.waitPacketOut(func(p *pingV5, addr *net.UDPAddr, _ packetNonce) { test.packetIn(&pongV5{ReqID: p.ReqID}) }) if err := <-done; err != nil { @@ -364,11 +364,11 @@ func TestUDPv5_multipleHandshakeRounds(t *testing.T) { }() // Ping answered by WHOAREYOU. - test.waitPacketOut(func(p *pingV5, addr *net.UDPAddr, authTag []byte) { + test.waitPacketOut(func(p *pingV5, addr *net.UDPAddr, authTag packetNonce) { test.packetIn(&whoareyouV5{AuthTag: authTag}) }) // Ping answered by WHOAREYOU again. - test.waitPacketOut(func(p *pingV5, addr *net.UDPAddr, authTag []byte) { + test.waitPacketOut(func(p *pingV5, addr *net.UDPAddr, authTag packetNonce) { test.packetIn(&whoareyouV5{AuthTag: authTag}) }) if err := <-done; err != errTimeout { @@ -395,7 +395,7 @@ func TestUDPv5_callTimeoutReset(t *testing.T) { }() // Serve two responses, slowly. - test.waitPacketOut(func(p *findnodeV5, addr *net.UDPAddr, authTag []byte) { + test.waitPacketOut(func(p *findnodeV5, addr *net.UDPAddr, _ packetNonce) { time.Sleep(respTimeout - 50*time.Millisecond) test.packetIn(&nodesV5{ ReqID: p.ReqID, @@ -433,7 +433,7 @@ func TestUDPv5_talkHandling(t *testing.T) { Protocol: "test", Message: []byte("test request"), }) - test.waitPacketOut(func(p *talkrespV5, addr *net.UDPAddr, authTag []byte) { + test.waitPacketOut(func(p *talkrespV5, addr *net.UDPAddr, _ packetNonce) { if !bytes.Equal(p.ReqID, []byte("foo")) { t.Error("wrong request ID in response:", p.ReqID) } @@ -452,7 +452,7 @@ func TestUDPv5_talkHandling(t *testing.T) { Protocol: "wrong", Message: []byte("test request"), }) - test.waitPacketOut(func(p *talkrespV5, addr *net.UDPAddr, authTag []byte) { + test.waitPacketOut(func(p *talkrespV5, addr *net.UDPAddr, _ packetNonce) { if !bytes.Equal(p.ReqID, []byte("2")) { t.Error("wrong request ID in response:", p.ReqID) } @@ -479,7 +479,7 @@ func TestUDPv5_talkRequest(t *testing.T) { _, err := test.udp.TalkRequest(remote, "test", []byte("test request")) done <- err }() - test.waitPacketOut(func(p *talkreqV5, addr *net.UDPAddr, authTag []byte) {}) + test.waitPacketOut(func(p *talkreqV5, addr *net.UDPAddr, _ packetNonce) {}) if err := <-done; err != errTimeout { t.Fatalf("want errTimeout, got %q", err) } @@ -489,7 +489,7 @@ func TestUDPv5_talkRequest(t *testing.T) { _, err := test.udp.TalkRequest(remote, "test", []byte("test request")) done <- err }() - test.waitPacketOut(func(p *talkreqV5, addr *net.UDPAddr, authTag []byte) { + test.waitPacketOut(func(p *talkreqV5, addr *net.UDPAddr, _ packetNonce) { if p.Protocol != "test" { t.Errorf("wrong protocol ID in talk request: %q", p.Protocol) } @@ -536,7 +536,7 @@ func TestUDPv5_lookup(t *testing.T) { // Answer lookup packets. for done := false; !done; { - done = test.waitPacketOut(func(p packetV5, to *net.UDPAddr, authTag []byte) { + done = test.waitPacketOut(func(p packetV5, to *net.UDPAddr, _ packetNonce) { recipient, key := lookupTestnet.nodeByAddr(to) switch p := p.(type) { case *pingV5: @@ -589,6 +589,7 @@ type udpV5Test struct { nodesByIP map[string]*enode.LocalNode } +// testCodec is the packet encoding used by protocol tests. This codec does not perform encryption. type testCodec struct { test *udpV5Test id enode.ID @@ -597,15 +598,15 @@ type testCodec struct { type testCodecFrame struct { NodeID enode.ID - AuthTag []byte + AuthTag packetNonce Ptype byte Packet rlp.RawValue } -func (c *testCodec) encode(toID enode.ID, addr string, p packetV5, _ *whoareyouV5) ([]byte, []byte, error) { +func (c *testCodec) encode(toID enode.ID, addr string, p packetV5, _ *whoareyouV5) ([]byte, packetNonce, error) { c.ctr++ - authTag := make([]byte, 8) - binary.BigEndian.PutUint64(authTag, c.ctr) + var authTag packetNonce + binary.BigEndian.PutUint64(authTag[:], c.ctr) penc, _ := rlp.EncodeToBytes(p) frame, err := rlp.EncodeToBytes(testCodecFrame{c.id, authTag, p.kind(), penc}) return frame, authTag, err @@ -704,6 +705,9 @@ func (test *udpV5Test) getNode(key *ecdsa.PrivateKey, addr *net.UDPAddr) *enode. return ln } +// waitPacketOut waits for the next output packet and handles it using the given 'validate' +// function. The function must be of type func (X, *net.UDPAddr, packetNonce) where X is +// assignable to packetV5. func (test *udpV5Test) waitPacketOut(validate interface{}) (closed bool) { test.t.Helper() From 691c3c779bc0db6eef15cfd5b883f1226dbba228 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Thu, 17 Sep 2020 13:27:23 +0200 Subject: [PATCH 14/70] p2p/discover: add test vector tests --- p2p/discover/table_util_test.go | 44 +++++- .../testdata/v5.1-ping-handshake-enr.txt | 21 +++ p2p/discover/testdata/v5.1-ping-handshake.txt | 17 ++ p2p/discover/testdata/v5.1-ping-message.txt | 10 ++ p2p/discover/testdata/v5.1-whoareyou.txt | 11 ++ p2p/discover/v5_encoding_test.go | 146 ++++++++++++++++++ 6 files changed, 247 insertions(+), 2 deletions(-) create mode 100644 p2p/discover/testdata/v5.1-ping-handshake-enr.txt create mode 100644 p2p/discover/testdata/v5.1-ping-handshake.txt create mode 100644 p2p/discover/testdata/v5.1-ping-message.txt create mode 100644 p2p/discover/testdata/v5.1-whoareyou.txt diff --git a/p2p/discover/table_util_test.go b/p2p/discover/table_util_test.go index 33415a7e380..535c65ce542 100644 --- a/p2p/discover/table_util_test.go +++ b/p2p/discover/table_util_test.go @@ -25,7 +25,9 @@ import ( "io/ioutil" "math/rand" "net" + "os" "sort" + "strings" "sync" "github.com/ethereum/go-ethereum/crypto" @@ -255,12 +257,24 @@ func hexEncPubkey(h string) (ret encPubkey) { } // hexFile reads the given file and decodes the hex data contained in it. +// Whitespace and any lines beginning with the # character are ignored. func hexFile(file string) []byte { - text, err := ioutil.ReadFile(file) + fileContent, err := ioutil.ReadFile(file) if err != nil { panic(err) } - text = bytes.TrimSpace(text) + + // Gather hex data, ignore comments. + var text []byte + for _, line := range bytes.Split(fileContent, []byte("\n")) { + line = bytes.TrimSpace(line) + if len(line) > 0 && line[0] == '#' { + continue + } + text = append(text, line...) + } + + // Parse the hex. if bytes.HasPrefix(text, []byte("0x")) { text = text[2:] } @@ -270,3 +284,29 @@ func hexFile(file string) []byte { } return data } + +// writeTestVector writes a test vector file with the given commentary and binary data. +func writeTestVector(file, comment string, data []byte) { + fd, err := os.OpenFile(file, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + panic(err) + } + defer fd.Close() + + if len(comment) > 0 { + for _, line := range strings.Split(comment, "\n") { + fmt.Fprintf(fd, "# %s\n", line) + } + fmt.Fprintln(fd) + } + for len(data) > 0 { + var chunk []byte + if len(data) < 32 { + chunk = data + } else { + chunk = data[:32] + } + data = data[len(chunk):] + fmt.Fprintf(fd, "%x\n", chunk) + } +} diff --git a/p2p/discover/testdata/v5.1-ping-handshake-enr.txt b/p2p/discover/testdata/v5.1-ping-handshake-enr.txt new file mode 100644 index 00000000000..c0ae04f47a8 --- /dev/null +++ b/p2p/discover/testdata/v5.1-ping-handshake-enr.txt @@ -0,0 +1,21 @@ +# src-node-id = 0xaaaa8419e9f49d0083561b48287df592939a8d19947d8c0ef88f2a4856a69fbb +# dest-node-id = 0xbbbb9d047f0488c0b5a93c1c3f2d8bafc7c8ff337024a55434a0d0555de64db9 +# whoareyou.auth-tag = 0x0102030405060708090a0b0c +# whoareyou.id-nonce = 0x0102030405060708090a0b0c0d0e0f1000000000000000000000000000000000 +# whoareyou.enr-seq = 0 +# auth-tag = 0xffffffffffffffffffffffff +# ping.req-id = 0x00000001 +# ping.enr-seq = 2 + +d85d60d07419d333acefdbc8e9c5fa1251fd4b32884a2290f764063260a68308 +5d89dd33a5893dc32a877ff47f4980d4367dfe062d3182891cd5392519846a06 +5052d3578d1756b199af4483993e6ada62f60c7c330dd905c1a55e6907a6ee8f +4c72667e4828d8c586dea5f2734ad5495945ce96c97e30f58dc7a91268435732 +7fb9bc68d156bd9888a21c13a91fbf446f348f1cfbba53d935c97a820818a099 +cab323c587a037031bf6cdf2c5a90ecff53b5092994f5aabb370a98a820631fa +e0c230913219b814e1198c702705d4363b9a67d70f7228a0bd2222d07d637187 +fe5efbafb6f11eb3726b3d3fb75261aec0ab1fdd88eddbd564a5a9d78e5b3fad +d975fe57adc6251c1a9530a3fed1c6215c5e09b8cca0fecb234bd6669200c999 +cd19f311595a69562e3f7d93da096247d9f42a4e50ec156362dcf80033531e4a +6c34d98f185c18ea25d26e2c47732b4cc2ec6317ea746e9a3491d66a30993067 +f1 diff --git a/p2p/discover/testdata/v5.1-ping-handshake.txt b/p2p/discover/testdata/v5.1-ping-handshake.txt new file mode 100644 index 00000000000..5caf9e58ae4 --- /dev/null +++ b/p2p/discover/testdata/v5.1-ping-handshake.txt @@ -0,0 +1,17 @@ +# src-node-id = 0xaaaa8419e9f49d0083561b48287df592939a8d19947d8c0ef88f2a4856a69fbb +# dest-node-id = 0xbbbb9d047f0488c0b5a93c1c3f2d8bafc7c8ff337024a55434a0d0555de64db9 +# whoareyou.auth-tag = 0x0102030405060708090a0b0c +# whoareyou.id-nonce = 0x0102030405060708090a0b0c0d0e0f1000000000000000000000000000000000 +# whoareyou.enr-seq = 1 +# auth-tag = 0xffffffffffffffffffffffff +# ping.req-id = 0x00000001 +# ping.enr-seq = 2 + +147ebae1f23aff4db44aecc4d653311c64ae2ad184fda5d5865760816c707256 +e779a21f56d9ff2f0b084d39a466cf9287bee46eb20a6e6b6426cfd6f3b14de5 +b776fd7341150d129586a3cb9ae59694986c7f513ccb821a17882634eec434da +c1ee0af96b60546e0f9f7a0e4ab7d89783370fe74fbd81870c0e15338772ec5d +423bb65dd50ecf810dffab6c22e1365c2d0d3323ed3a718e8e82ab7c857d7240 +c90dd92efcdbd11f5a1d8ce05961f9ea0d899a16ea973bd08b527dfdcb49be01 +2c84d2e40daca630076629f01d9e04a987312b748c74fb5da7a524fd8fbf32cb +e249 diff --git a/p2p/discover/testdata/v5.1-ping-message.txt b/p2p/discover/testdata/v5.1-ping-message.txt new file mode 100644 index 00000000000..5c356a88386 --- /dev/null +++ b/p2p/discover/testdata/v5.1-ping-message.txt @@ -0,0 +1,10 @@ +# src-node-id = 0xaaaa8419e9f49d0083561b48287df592939a8d19947d8c0ef88f2a4856a69fbb +# dest-node-id = 0xbbbb9d047f0488c0b5a93c1c3f2d8bafc7c8ff337024a55434a0d0555de64db9 +# read-key = 0x00000000000000000000000000000000 +# auth-tag = 0xffffffffffffffffffffffff +# ping.req-id = 0x00000001 +# ping.enr-seq = 2 + +4076162ea9513c747a2783efb1be1030ca1e91aaf871960fc9f477fd195bc2a4 +1a89c03f7c519fe0275cabbf9875874ca66e6a6908776d796a8ec9ffb6de03fc +995f10628cbdf0b84102ed931f66d180cbb4219f369a24f4e6b24d7bdc2a04 diff --git a/p2p/discover/testdata/v5.1-whoareyou.txt b/p2p/discover/testdata/v5.1-whoareyou.txt new file mode 100644 index 00000000000..1ff50e235c2 --- /dev/null +++ b/p2p/discover/testdata/v5.1-whoareyou.txt @@ -0,0 +1,11 @@ +# src-node-id = 0xaaaa8419e9f49d0083561b48287df592939a8d19947d8c0ef88f2a4856a69fbb +# dest-node-id = 0xbbbb9d047f0488c0b5a93c1c3f2d8bafc7c8ff337024a55434a0d0555de64db9 +# whoareyou.auth-tag = 0x0102030405060708090a0b0c +# whoareyou.id-nonce = 0x0000000000000000000000000000000000000000000000000000000000000000 +# whoareyou.enr-seq = 0 +# + +7899173fcecaf651504b79c3bb5b930dd99c619153f3c93f9afcecb214f45543 +b4e70b4fc8748887616eb793f2e90b19cc3a070fe957afdc3a639ca891144bfe +63dee066f5a78096bcf38d382b2f4a434d73986e0e4e018759a66b389a7d3f95 +9edba38ebb03851c3e31f3e11f32ae diff --git a/p2p/discover/v5_encoding_test.go b/p2p/discover/v5_encoding_test.go index 1ff66e2996d..646ca6096f9 100644 --- a/p2p/discover/v5_encoding_test.go +++ b/p2p/discover/v5_encoding_test.go @@ -20,17 +20,27 @@ import ( "bytes" "crypto/ecdsa" "encoding/hex" + "flag" "fmt" "net" + "path/filepath" "reflect" + "strings" "testing" "github.com/davecgh/go-spew/spew" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/mclock" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/p2p/enode" ) +// To regenerate discv5 test vectors, run +// +// go test -run TestVectors -write-test-vectors +// +var writeTestVectorsFlag = flag.Bool("write-test-vectors", false, "Overwrite discv5 test vectors in testdata/") + var ( testKeyA, _ = crypto.HexToECDSA("eef77acb6c6a6eebc5b363a475ac583ec7eccdb42b6481424c60f59aa326547f") testKeyB, _ = crypto.HexToECDSA("66fb62bfbd66b9177a138c1e5cddbe4f7c30c343e94e68df8769459cb1cde628") @@ -236,6 +246,139 @@ func TestDecodeErrorsV5(t *testing.T) { // - check invalid handshake data sizes } +// This test checks that all test vectors can be decoded. +func TestTestVectorsV5(t *testing.T) { + var ( + idA = enode.PubkeyToIDV4(&testKeyA.PublicKey) + idB = enode.PubkeyToIDV4(&testKeyB.PublicKey) + addr = "127.0.0.1" + session = &session{ + writeKey: hexutil.MustDecode("0x00000000000000000000000000000000"), + readKey: hexutil.MustDecode("0x01010101010101010101010101010101"), + } + challenge0 = &whoareyouV5{ + AuthTag: packetNonce{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}, + IDNonce: [32]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, + RecordSeq: 0, + } + challenge1 = &whoareyouV5{ + AuthTag: packetNonce{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}, + IDNonce: [32]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, + RecordSeq: 1, + } + ) + + type testVectorTest struct { + name string // test vector name + packet packetV5 // the packet to be encoded + challenge *whoareyouV5 // handshake challenge passed to encoder + prep func(*handshakeTest) // called before encode/decode + } + tests := []testVectorTest{ + { + name: "v5.1-whoareyou", + packet: &whoareyouV5{ + AuthTag: packetNonce{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}, + }, + }, + { + name: "v5.1-ping-message", + packet: &pingV5{ + ReqID: []byte{0, 0, 0, 1}, + ENRSeq: 2, + }, + prep: func(net *handshakeTest) { + net.nodeA.c.sc.storeNewSession(idB, addr, session) + net.nodeB.c.sc.storeNewSession(idA, addr, session.keysFlipped()) + }, + }, + { + name: "v5.1-ping-handshake", + packet: &pingV5{ + ReqID: []byte{0, 0, 0, 1}, + ENRSeq: 2, + }, + challenge: challenge1, + prep: func(net *handshakeTest) { + c := *challenge1 + c.node = net.nodeA.n() + net.nodeB.c.sc.storeSentHandshake(idA, addr, &c) + }, + }, + { + name: "v5.1-ping-handshake-enr", + packet: &pingV5{ + ReqID: []byte{0, 0, 0, 1}, + ENRSeq: 2, + }, + challenge: challenge0, + prep: func(net *handshakeTest) { + c := *challenge0 + c.node = net.nodeA.n() + net.nodeB.c.sc.storeSentHandshake(idA, addr, &c) + }, + }, + } + + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + net := newHandshakeTest() + net.nodeA.c.sc.nonceFunc = func(counter uint32) packetNonce { + return packetNonce{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF} + } + defer net.close() + + if test.prep != nil { + test.prep(net) + } + + file := filepath.Join("testdata", test.name+".txt") + if *writeTestVectorsFlag { + d, nonce := net.nodeA.encodeWithChallenge(t, net.nodeB, test.challenge, test.packet) + comment := testVectorComment(net, test.packet, test.challenge, nonce) + writeTestVector(file, comment, d) + } + enc := hexFile(file) + net.nodeB.expectDecode(t, test.packet.kind(), enc) + }) + } +} + +// testVectorComment creates the commentary for discv5 test vector files. +func testVectorComment(net *handshakeTest, p packetV5, challenge *whoareyouV5, nonce packetNonce) string { + o := new(strings.Builder) + fmt.Fprintf(o, "src-node-id = %#x\n", net.nodeA.id().Bytes()) + fmt.Fprintf(o, "dest-node-id = %#x\n", net.nodeB.id().Bytes()) + + printWhoareyou := func(p *whoareyouV5) { + fmt.Fprintf(o, "whoareyou.auth-tag = %#x\n", p.AuthTag[:]) + fmt.Fprintf(o, "whoareyou.id-nonce = %#x\n", p.IDNonce[:]) + fmt.Fprintf(o, "whoareyou.enr-seq = %d\n", p.RecordSeq) + } + + switch p := p.(type) { + case *whoareyouV5: + // WHOAREYOU packet. + printWhoareyou(p) + case *pingV5: + if challenge != nil { + // Handshake message packet. + printWhoareyou(challenge) + } else { + // Ordinary message packet. + fmt.Fprintf(o, "read-key = %#x\n", net.nodeB.c.sc.readKey(net.nodeA.id(), net.nodeA.addr())) + } + fmt.Fprintf(o, "auth-tag = %#x\n", nonce[:]) + fmt.Fprintf(o, "ping.req-id = %#x\n", p.ReqID) + fmt.Fprintf(o, "ping.enr-seq = %d", p.ENRSeq) + default: + panic(fmt.Errorf("unhandled packet type %T", p)) + } + + return o.String() +} + // This benchmark checks performance of handshake packet decoding. func BenchmarkV5_DecodeHandshakePingSecp256k1(b *testing.B) { net := newHandshakeTest() @@ -321,6 +464,9 @@ func (n *handshakeTestNode) init(key *ecdsa.PrivateKey, ip net.IP, clock mclock. db, _ := enode.OpenDB("") n.ln = enode.NewLocalNode(db, key) n.ln.SetStaticIP(ip) + if n.ln.Node().Seq() != 1 { + panic(fmt.Errorf("unexpected seq %d", n.ln.Node().Seq())) + } n.c = newWireCodec(n.ln, key, clock) } From e12144b2f9c6faa80c3958841885cfdcebd16964 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Thu, 17 Sep 2020 14:53:13 +0200 Subject: [PATCH 15/70] p2p/discover: make test vectors deterministic --- p2p/discover/table_util_test.go | 2 +- .../testdata/v5.1-ping-handshake-enr.txt | 37 +++++----- p2p/discover/testdata/v5.1-ping-handshake.txt | 29 ++++---- p2p/discover/testdata/v5.1-ping-message.txt | 8 +-- p2p/discover/testdata/v5.1-whoareyou.txt | 13 ++-- p2p/discover/v5_encoding.go | 71 +++++++++++-------- p2p/discover/v5_encoding_test.go | 51 +++++++------ p2p/discover/v5_session.go | 37 ++++++---- 8 files changed, 142 insertions(+), 106 deletions(-) diff --git a/p2p/discover/table_util_test.go b/p2p/discover/table_util_test.go index 535c65ce542..ed5eaae0f98 100644 --- a/p2p/discover/table_util_test.go +++ b/p2p/discover/table_util_test.go @@ -294,7 +294,7 @@ func writeTestVector(file, comment string, data []byte) { defer fd.Close() if len(comment) > 0 { - for _, line := range strings.Split(comment, "\n") { + for _, line := range strings.Split(strings.TrimSpace(comment), "\n") { fmt.Fprintf(fd, "# %s\n", line) } fmt.Fprintln(fd) diff --git a/p2p/discover/testdata/v5.1-ping-handshake-enr.txt b/p2p/discover/testdata/v5.1-ping-handshake-enr.txt index c0ae04f47a8..b8761e45100 100644 --- a/p2p/discover/testdata/v5.1-ping-handshake-enr.txt +++ b/p2p/discover/testdata/v5.1-ping-handshake-enr.txt @@ -1,21 +1,26 @@ # src-node-id = 0xaaaa8419e9f49d0083561b48287df592939a8d19947d8c0ef88f2a4856a69fbb # dest-node-id = 0xbbbb9d047f0488c0b5a93c1c3f2d8bafc7c8ff337024a55434a0d0555de64db9 -# whoareyou.auth-tag = 0x0102030405060708090a0b0c +# nonce = 0xffffffffffffffffffffffff +# read-key = 0x4917330b5aeb51650213f90d5f253c45 +# ping.req-id = 0x00000001 +# ping.enr-seq = 1 +# +# handshake inputs: +# +# whoareyou.request-nonce = 0x0102030405060708090a0b0c # whoareyou.id-nonce = 0x0102030405060708090a0b0c0d0e0f1000000000000000000000000000000000 # whoareyou.enr-seq = 0 -# auth-tag = 0xffffffffffffffffffffffff -# ping.req-id = 0x00000001 -# ping.enr-seq = 2 +# ephemeral-key = 0x0288ef00023598499cb6c940146d050d2b1fb914198c327f76aad590bead68b6 -d85d60d07419d333acefdbc8e9c5fa1251fd4b32884a2290f764063260a68308 -5d89dd33a5893dc32a877ff47f4980d4367dfe062d3182891cd5392519846a06 -5052d3578d1756b199af4483993e6ada62f60c7c330dd905c1a55e6907a6ee8f -4c72667e4828d8c586dea5f2734ad5495945ce96c97e30f58dc7a91268435732 -7fb9bc68d156bd9888a21c13a91fbf446f348f1cfbba53d935c97a820818a099 -cab323c587a037031bf6cdf2c5a90ecff53b5092994f5aabb370a98a820631fa -e0c230913219b814e1198c702705d4363b9a67d70f7228a0bd2222d07d637187 -fe5efbafb6f11eb3726b3d3fb75261aec0ab1fdd88eddbd564a5a9d78e5b3fad -d975fe57adc6251c1a9530a3fed1c6215c5e09b8cca0fecb234bd6669200c999 -cd19f311595a69562e3f7d93da096247d9f42a4e50ec156362dcf80033531e4a -6c34d98f185c18ea25d26e2c47732b4cc2ec6317ea746e9a3491d66a30993067 -f1 +00000000000000000000000000000000088b3d4342776668980a4adf72a8fcaa +963f24b27a2f6bb44c7ed5ca10e87de130f94d2390b9853c3dcaa0d51e9472d4 +3c9ae48d04689ef4d3d2602a5e89ac340f9e81e722b1d7dac2578d520dd5bc6d +c1e38ad3ab33012be1a5d259267a0947bf242219834c5702d1c694c0ceb4a6a2 +7b5d68bd2c2e32e6cb9696706adff216ab862a9186875f9494150c4ae06fa4d1 +f0396c93f215fa4ef52417d9c40a31564e8d5f31a7f08c38045ff5e30d966183 +8b1eabee9f1e561120bcc4d9f2f9c839152b4ab970e029b2395b97e8c3aa8d3b +497ee98a15e865bcd34effa8b83eb6396bca60ad8f0bff1e047e278454bc2b3d +6404c12106a9d0b6107fc2383976fc05fbda2c954d402c28c8fb53a2b3a4b111 +c286ba2ac4ff880168323c6e97b01dbcbeef4f234e5849f75ab007217c919820 +aaa1c8a7926d3625917fccc3d4569a69fd8aca026be87afab8e8e645d1ee8889 +92 diff --git a/p2p/discover/testdata/v5.1-ping-handshake.txt b/p2p/discover/testdata/v5.1-ping-handshake.txt index 5caf9e58ae4..737857b2b88 100644 --- a/p2p/discover/testdata/v5.1-ping-handshake.txt +++ b/p2p/discover/testdata/v5.1-ping-handshake.txt @@ -1,17 +1,22 @@ # src-node-id = 0xaaaa8419e9f49d0083561b48287df592939a8d19947d8c0ef88f2a4856a69fbb # dest-node-id = 0xbbbb9d047f0488c0b5a93c1c3f2d8bafc7c8ff337024a55434a0d0555de64db9 -# whoareyou.auth-tag = 0x0102030405060708090a0b0c +# nonce = 0xffffffffffffffffffffffff +# read-key = 0x4917330b5aeb51650213f90d5f253c45 +# ping.req-id = 0x00000001 +# ping.enr-seq = 1 +# +# handshake inputs: +# +# whoareyou.request-nonce = 0x0102030405060708090a0b0c # whoareyou.id-nonce = 0x0102030405060708090a0b0c0d0e0f1000000000000000000000000000000000 # whoareyou.enr-seq = 1 -# auth-tag = 0xffffffffffffffffffffffff -# ping.req-id = 0x00000001 -# ping.enr-seq = 2 +# ephemeral-key = 0x0288ef00023598499cb6c940146d050d2b1fb914198c327f76aad590bead68b6 -147ebae1f23aff4db44aecc4d653311c64ae2ad184fda5d5865760816c707256 -e779a21f56d9ff2f0b084d39a466cf9287bee46eb20a6e6b6426cfd6f3b14de5 -b776fd7341150d129586a3cb9ae59694986c7f513ccb821a17882634eec434da -c1ee0af96b60546e0f9f7a0e4ab7d89783370fe74fbd81870c0e15338772ec5d -423bb65dd50ecf810dffab6c22e1365c2d0d3323ed3a718e8e82ab7c857d7240 -c90dd92efcdbd11f5a1d8ce05961f9ea0d899a16ea973bd08b527dfdcb49be01 -2c84d2e40daca630076629f01d9e04a987312b748c74fb5da7a524fd8fbf32cb -e249 +00000000000000000000000000000000088b3d4342776668980a4adf72a8fcaa +963f24b27a2f6bb44c7ed5ca10e87de130f94d2390b9853c3dcb21d51e9472d4 +3c9ae48d04689ef4d3d2602a5e89ac340f9e81e722b1d7dac2578d520dd5bc6d +c1e38ad3ab33012be1a5d259267a0947bf242219834c5702d1c694c0ceb4a6a2 +7b5d68bd2c2e32e6cb9696706adff216ab862a9186875f9494150c4ae06fa4d1 +f0396c93f215fa4ef52417d9c40a31564e8d5f31a7f08c38045ff5e30d966183 +8b1eabee9f1e561120bc7fccc3d4569a69fdf04f31230ae4be20404467d9ea9a +b3cd diff --git a/p2p/discover/testdata/v5.1-ping-message.txt b/p2p/discover/testdata/v5.1-ping-message.txt index 5c356a88386..4a542173c42 100644 --- a/p2p/discover/testdata/v5.1-ping-message.txt +++ b/p2p/discover/testdata/v5.1-ping-message.txt @@ -1,10 +1,10 @@ # src-node-id = 0xaaaa8419e9f49d0083561b48287df592939a8d19947d8c0ef88f2a4856a69fbb # dest-node-id = 0xbbbb9d047f0488c0b5a93c1c3f2d8bafc7c8ff337024a55434a0d0555de64db9 +# nonce = 0xffffffffffffffffffffffff # read-key = 0x00000000000000000000000000000000 -# auth-tag = 0xffffffffffffffffffffffff # ping.req-id = 0x00000001 # ping.enr-seq = 2 -4076162ea9513c747a2783efb1be1030ca1e91aaf871960fc9f477fd195bc2a4 -1a89c03f7c519fe0275cabbf9875874ca66e6a6908776d796a8ec9ffb6de03fc -995f10628cbdf0b84102ed931f66d180cbb4219f369a24f4e6b24d7bdc2a04 +00000000000000000000000000000000088b3d4342776668980a4adf72a8fcaa +963f24b27a2f6bb44c7ed5ca10e87de130f94d2390b9853c3fcba22b1e9472d4 +3c9ae48d04689eb84102ed931f66d180cbb4219f369a24f4e6b24d7bdc2a04 diff --git a/p2p/discover/testdata/v5.1-whoareyou.txt b/p2p/discover/testdata/v5.1-whoareyou.txt index 1ff50e235c2..85cfdab4f0a 100644 --- a/p2p/discover/testdata/v5.1-whoareyou.txt +++ b/p2p/discover/testdata/v5.1-whoareyou.txt @@ -1,11 +1,10 @@ # src-node-id = 0xaaaa8419e9f49d0083561b48287df592939a8d19947d8c0ef88f2a4856a69fbb # dest-node-id = 0xbbbb9d047f0488c0b5a93c1c3f2d8bafc7c8ff337024a55434a0d0555de64db9 -# whoareyou.auth-tag = 0x0102030405060708090a0b0c -# whoareyou.id-nonce = 0x0000000000000000000000000000000000000000000000000000000000000000 +# whoareyou.request-nonce = 0x0102030405060708090a0b0c +# whoareyou.id-nonce = 0x0102030405060708090a0b0c0d0e0f1000000000000000000000000000000000 # whoareyou.enr-seq = 0 -# -7899173fcecaf651504b79c3bb5b930dd99c619153f3c93f9afcecb214f45543 -b4e70b4fc8748887616eb793f2e90b19cc3a070fe957afdc3a639ca891144bfe -63dee066f5a78096bcf38d382b2f4a434d73986e0e4e018759a66b389a7d3f95 -9edba38ebb03851c3e31f3e11f32ae +00000000000000000000000000000000088b3d4342776668980a4adf72a8fcaa +963f24b27a2f6bb44c7ed5ca10e87de130f94d2390b9853c3ecb9ad5e368892e +c562137bf19c6d0a9191a5651c4f415117bdfa0c7ab86af62b7a9784eceb2800 +8d03ede83bd1369631f9f3d8da0b45 diff --git a/p2p/discover/v5_encoding.go b/p2p/discover/v5_encoding.go index 458d8e14f53..b7e8e23c5ae 100644 --- a/p2p/discover/v5_encoding.go +++ b/p2p/discover/v5_encoding.go @@ -304,7 +304,7 @@ func (c *wireCodec) encodeRandom(toID enode.ID) ([]byte, packetNonce, error) { c.buf.Reset() binary.Write(&c.buf, binary.BigEndian, c.makeHeader(toID, flagMessage, 0)) binary.Write(&c.buf, binary.BigEndian, &auth) - output := maskOutputPacket(toID, c.buf.Bytes(), c.buf.Len()) + output := c.maskOutputPacket(toID, c.buf.Bytes(), c.buf.Len()) return output, auth.Nonce, nil } @@ -324,7 +324,7 @@ func (c *wireCodec) encodeWhoareyou(toID enode.ID, packet *whoareyouV5) ([]byte, c.buf.Reset() binary.Write(&c.buf, binary.BigEndian, head) binary.Write(&c.buf, binary.BigEndian, auth) - output := maskOutputPacket(toID, c.buf.Bytes(), c.buf.Len()) + output := c.maskOutputPacket(toID, c.buf.Bytes(), c.buf.Len()) return output, nil } @@ -368,7 +368,7 @@ func (c *wireCodec) encodeHandshakeMessage(toID enode.ID, addr string, packet pa headerData := output output, err = encryptGCM(output, session.writeKey, auth.h.Nonce[:], messagePT, headerData) if err == nil { - output = maskOutputPacket(toID, output, len(headerData)) + output = c.maskOutputPacket(toID, output, len(headerData)) } return output, auth.h.Nonce, err } @@ -376,9 +376,14 @@ func (c *wireCodec) encodeHandshakeMessage(toID enode.ID, addr string, packet pa // encodeAuthHeader creates the auth header on a call packet following WHOAREYOU. func (c *wireCodec) makeHandshakeHeader(toID enode.ID, addr string, challenge *whoareyouV5) (*handshakeAuthDataV5, *session, error) { session := new(session) + nonce, err := c.sc.nextNonce(session) + if err != nil { + return nil, nil, fmt.Errorf("can't generate nonce: %v", err) + } + auth := new(handshakeAuthDataV5) auth.h.Version = handshakeVersion - auth.h.Nonce = c.sc.nextNonce(session) + auth.h.Nonce = nonce // Create the ephemeral key. This needs to be first because the // key is part of the ID nonce signature. @@ -386,7 +391,7 @@ func (c *wireCodec) makeHandshakeHeader(toID enode.ID, addr string, challenge *w if err := challenge.node.Load((*enode.Secp256k1)(remotePubkey)); err != nil { return nil, nil, fmt.Errorf("can't find secp256k1 key for recipient") } - ephkey, err := crypto.GenerateKey() + ephkey, err := c.sc.ephemeralKeyGen() if err != nil { return nil, nil, fmt.Errorf("can't generate ephemeral key") } @@ -420,10 +425,18 @@ func (c *wireCodec) makeHandshakeHeader(toID enode.ID, addr string, challenge *w // encodeMessage encodes an encrypted message packet. func (c *wireCodec) encodeMessage(toID enode.ID, s *session, packet packetV5) ([]byte, packetNonce, error) { var ( - auth messageAuthDataV5 head = c.makeHeader(toID, flagMessage, 0) + auth messageAuthDataV5 ) - auth.Nonce = c.sc.nextNonce(s) + + // Create the nonce. + nonce, err := c.sc.nextNonce(s) + if err != nil { + return nil, auth.Nonce, fmt.Errorf("can't generate nonce: %v", err) + } + auth.Nonce = nonce + + // Encode the header. c.buf.Reset() binary.Write(&c.buf, binary.BigEndian, head) binary.Write(&c.buf, binary.BigEndian, &auth) @@ -437,11 +450,11 @@ func (c *wireCodec) encodeMessage(toID enode.ID, s *session, packet packetV5) ([ } messagePT := c.msgbuf.Bytes() - // Encrypt message data. + // Encrypt the message. headerData := output - output, err := encryptGCM(output, s.writeKey, auth.Nonce[:], messagePT, headerData) + output, err = encryptGCM(output, s.writeKey, nonce[:], messagePT, headerData) if err == nil { - output = maskOutputPacket(toID, output, len(headerData)) + output = c.maskOutputPacket(toID, output, len(headerData)) } return output, auth.Nonce, err } @@ -764,6 +777,25 @@ func (c *wireCodec) sha256sum(inputs ...[]byte) []byte { return c.sha256.Sum(nil) } +// maskOutputPacket applies protocol header masking to a packet sent to destID. +func (c *wireCodec) maskOutputPacket(destID enode.ID, output []byte, headerDataLen int) []byte { + masked := make([]byte, sizeofMaskingIV+len(output)) + c.sc.maskingIVGen(masked[:sizeofMaskingIV]) + mask := headerMask(destID, masked) + copy(masked[sizeofMaskingIV:], output) + mask.XORKeyStream(masked[sizeofMaskingIV:], output[:headerDataLen]) + return masked +} + +// headerMask returns a cipher for 'masking' / 'unmasking' packet headers. +func headerMask(destID enode.ID, input []byte) cipher.Stream { + block, err := aes.NewCipher(destID[:16]) + if err != nil { + panic("can't create cipher") + } + return cipher.NewCTR(block, input[:sizeofMaskingIV]) +} + // ecdh creates a shared secret. func ecdh(privkey *ecdsa.PrivateKey, pubkey *ecdsa.PublicKey) []byte { secX, secY := pubkey.ScalarMult(pubkey.X, pubkey.Y, privkey.D.Bytes()) @@ -805,22 +837,3 @@ func decryptGCM(key, nonce, ct, authData []byte) ([]byte, error) { pt := make([]byte, 0, len(ct)) return aesgcm.Open(pt, nonce, ct, authData) } - -// headerMask returns a cipher for 'masking' / 'unmasking' packet headers. -func headerMask(destID enode.ID, input []byte) cipher.Stream { - block, err := aes.NewCipher(destID[:16]) - if err != nil { - panic("can't create cipher") - } - return cipher.NewCTR(block, input[:sizeofMaskingIV]) -} - -// maskOutputPacket applies protocol header masking to a packet sent to destID. -func maskOutputPacket(destID enode.ID, output []byte, headerDataLen int) []byte { - masked := make([]byte, sizeofMaskingIV+len(output)) - crand.Read(masked[:sizeofMaskingIV]) - mask := headerMask(destID, masked) - copy(masked[sizeofMaskingIV:], output) - mask.XORKeyStream(masked[sizeofMaskingIV:], output[:headerDataLen]) - return masked -} diff --git a/p2p/discover/v5_encoding_test.go b/p2p/discover/v5_encoding_test.go index 646ca6096f9..80515e7d4a8 100644 --- a/p2p/discover/v5_encoding_test.go +++ b/p2p/discover/v5_encoding_test.go @@ -42,9 +42,10 @@ import ( var writeTestVectorsFlag = flag.Bool("write-test-vectors", false, "Overwrite discv5 test vectors in testdata/") var ( - testKeyA, _ = crypto.HexToECDSA("eef77acb6c6a6eebc5b363a475ac583ec7eccdb42b6481424c60f59aa326547f") - testKeyB, _ = crypto.HexToECDSA("66fb62bfbd66b9177a138c1e5cddbe4f7c30c343e94e68df8769459cb1cde628") - testIDnonce = [32]byte{5, 6, 7, 8, 9, 10, 11, 12} + testKeyA, _ = crypto.HexToECDSA("eef77acb6c6a6eebc5b363a475ac583ec7eccdb42b6481424c60f59aa326547f") + testKeyB, _ = crypto.HexToECDSA("66fb62bfbd66b9177a138c1e5cddbe4f7c30c343e94e68df8769459cb1cde628") + testEphKey, _ = crypto.HexToECDSA("0288ef00023598499cb6c940146d050d2b1fb914198c327f76aad590bead68b6") + testIDnonce = [32]byte{5, 6, 7, 8, 9, 10, 11, 12} ) func TestDeriveKeysV5(t *testing.T) { @@ -276,10 +277,8 @@ func TestTestVectorsV5(t *testing.T) { } tests := []testVectorTest{ { - name: "v5.1-whoareyou", - packet: &whoareyouV5{ - AuthTag: packetNonce{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}, - }, + name: "v5.1-whoareyou", + packet: challenge0, }, { name: "v5.1-ping-message", @@ -296,7 +295,7 @@ func TestTestVectorsV5(t *testing.T) { name: "v5.1-ping-handshake", packet: &pingV5{ ReqID: []byte{0, 0, 0, 1}, - ENRSeq: 2, + ENRSeq: 1, }, challenge: challenge1, prep: func(net *handshakeTest) { @@ -309,7 +308,7 @@ func TestTestVectorsV5(t *testing.T) { name: "v5.1-ping-handshake-enr", packet: &pingV5{ ReqID: []byte{0, 0, 0, 1}, - ENRSeq: 2, + ENRSeq: 1, }, challenge: challenge0, prep: func(net *handshakeTest) { @@ -324,9 +323,6 @@ func TestTestVectorsV5(t *testing.T) { test := test t.Run(test.name, func(t *testing.T) { net := newHandshakeTest() - net.nodeA.c.sc.nonceFunc = func(counter uint32) packetNonce { - return packetNonce{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF} - } defer net.close() if test.prep != nil { @@ -335,6 +331,17 @@ func TestTestVectorsV5(t *testing.T) { file := filepath.Join("testdata", test.name+".txt") if *writeTestVectorsFlag { + // Override all random inputs. + net.nodeA.c.sc.nonceGen = func(counter uint32) (packetNonce, error) { + return packetNonce{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, nil + } + net.nodeA.c.sc.maskingIVGen = func(buf []byte) error { + return nil // all zero + } + net.nodeA.c.sc.ephemeralKeyGen = func() (*ecdsa.PrivateKey, error) { + return testEphKey, nil + } + // Encode the packet. d, nonce := net.nodeA.encodeWithChallenge(t, net.nodeB, test.challenge, test.packet) comment := testVectorComment(net, test.packet, test.challenge, nonce) writeTestVector(file, comment, d) @@ -348,34 +355,32 @@ func TestTestVectorsV5(t *testing.T) { // testVectorComment creates the commentary for discv5 test vector files. func testVectorComment(net *handshakeTest, p packetV5, challenge *whoareyouV5, nonce packetNonce) string { o := new(strings.Builder) - fmt.Fprintf(o, "src-node-id = %#x\n", net.nodeA.id().Bytes()) - fmt.Fprintf(o, "dest-node-id = %#x\n", net.nodeB.id().Bytes()) - printWhoareyou := func(p *whoareyouV5) { - fmt.Fprintf(o, "whoareyou.auth-tag = %#x\n", p.AuthTag[:]) + fmt.Fprintf(o, "whoareyou.request-nonce = %#x\n", p.AuthTag[:]) fmt.Fprintf(o, "whoareyou.id-nonce = %#x\n", p.IDNonce[:]) fmt.Fprintf(o, "whoareyou.enr-seq = %d\n", p.RecordSeq) } + fmt.Fprintf(o, "src-node-id = %#x\n", net.nodeA.id().Bytes()) + fmt.Fprintf(o, "dest-node-id = %#x\n", net.nodeB.id().Bytes()) switch p := p.(type) { case *whoareyouV5: // WHOAREYOU packet. printWhoareyou(p) case *pingV5: + fmt.Fprintf(o, "nonce = %#x\n", nonce[:]) + fmt.Fprintf(o, "read-key = %#x\n", net.nodeA.c.sc.session(net.nodeB.id(), net.nodeB.addr()).writeKey) + fmt.Fprintf(o, "ping.req-id = %#x\n", p.ReqID) + fmt.Fprintf(o, "ping.enr-seq = %d\n", p.ENRSeq) if challenge != nil { // Handshake message packet. + fmt.Fprint(o, "\nhandshake inputs:\n\n") printWhoareyou(challenge) - } else { - // Ordinary message packet. - fmt.Fprintf(o, "read-key = %#x\n", net.nodeB.c.sc.readKey(net.nodeA.id(), net.nodeA.addr())) + fmt.Fprintf(o, "ephemeral-key = %#x\n", testEphKey.D.Bytes()) } - fmt.Fprintf(o, "auth-tag = %#x\n", nonce[:]) - fmt.Fprintf(o, "ping.req-id = %#x\n", p.ReqID) - fmt.Fprintf(o, "ping.enr-seq = %d", p.ENRSeq) default: panic(fmt.Errorf("unhandled packet type %T", p)) } - return o.String() } diff --git a/p2p/discover/v5_session.go b/p2p/discover/v5_session.go index f0691adcce0..c677ef874fd 100644 --- a/p2p/discover/v5_session.go +++ b/p2p/discover/v5_session.go @@ -17,10 +17,12 @@ package discover import ( + "crypto/ecdsa" crand "crypto/rand" "encoding/binary" "github.com/ethereum/go-ethereum/common/mclock" + "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/p2p/enode" "github.com/hashicorp/golang-lru/simplelru" ) @@ -31,9 +33,10 @@ type sessionCache struct { sessions *simplelru.LRU handshakes map[sessionID]*whoareyouV5 clock mclock.Clock - - // test hook for overriding the random nonce generator. - nonceFunc func(counter uint32) packetNonce + // hooks for overriding randomness. + nonceGen func(uint32) (packetNonce, error) + maskingIVGen func([]byte) error + ephemeralKeyGen func() (*ecdsa.PrivateKey, error) } // sessionID identifies a session or handshake. @@ -60,24 +63,30 @@ func newSessionCache(maxItems int, clock mclock.Clock) *sessionCache { panic("can't create session cache") } return &sessionCache{ - sessions: cache, - handshakes: make(map[sessionID]*whoareyouV5), - clock: clock, - nonceFunc: generateNonce, + sessions: cache, + handshakes: make(map[sessionID]*whoareyouV5), + clock: clock, + nonceGen: generateNonce, + maskingIVGen: generateMaskingIV, + ephemeralKeyGen: crypto.GenerateKey, } } -func generateNonce(counter uint32) (n packetNonce) { - binary.BigEndian.PutUint32(n[:], counter) - crand.Read(n[4:]) - return n +func generateNonce(counter uint32) (n packetNonce, err error) { + binary.BigEndian.PutUint32(n[:4], counter) + _, err = crand.Read(n[4:]) + return n, err +} + +func generateMaskingIV(buf []byte) error { + _, err := crand.Read(buf) + return err } // nextNonce creates a nonce for encrypting a message to the given session. -func (sc *sessionCache) nextNonce(s *session) packetNonce { - n := sc.nonceFunc(s.nonceCounter) +func (sc *sessionCache) nextNonce(s *session) (packetNonce, error) { s.nonceCounter++ - return n + return sc.nonceGen(s.nonceCounter) } // session returns the current session for the given node, if any. From 023702315c3299cc060268240fe40cfcff84d72a Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Thu, 17 Sep 2020 15:40:20 +0200 Subject: [PATCH 16/70] p2p/discover: add ephemeral-pubkey to test vector output --- p2p/discover/testdata/v5.1-ping-handshake-enr.txt | 1 + p2p/discover/testdata/v5.1-ping-handshake.txt | 1 + p2p/discover/v5_encoding_test.go | 1 + 3 files changed, 3 insertions(+) diff --git a/p2p/discover/testdata/v5.1-ping-handshake-enr.txt b/p2p/discover/testdata/v5.1-ping-handshake-enr.txt index b8761e45100..dc72be12520 100644 --- a/p2p/discover/testdata/v5.1-ping-handshake-enr.txt +++ b/p2p/discover/testdata/v5.1-ping-handshake-enr.txt @@ -11,6 +11,7 @@ # whoareyou.id-nonce = 0x0102030405060708090a0b0c0d0e0f1000000000000000000000000000000000 # whoareyou.enr-seq = 0 # ephemeral-key = 0x0288ef00023598499cb6c940146d050d2b1fb914198c327f76aad590bead68b6 +# ephemeral-pubkey = 0x039a003ba6517b473fa0cd74aefe99dadfdb34627f90fec6362df85803908f53a5 00000000000000000000000000000000088b3d4342776668980a4adf72a8fcaa 963f24b27a2f6bb44c7ed5ca10e87de130f94d2390b9853c3dcaa0d51e9472d4 diff --git a/p2p/discover/testdata/v5.1-ping-handshake.txt b/p2p/discover/testdata/v5.1-ping-handshake.txt index 737857b2b88..f23eccf0a68 100644 --- a/p2p/discover/testdata/v5.1-ping-handshake.txt +++ b/p2p/discover/testdata/v5.1-ping-handshake.txt @@ -11,6 +11,7 @@ # whoareyou.id-nonce = 0x0102030405060708090a0b0c0d0e0f1000000000000000000000000000000000 # whoareyou.enr-seq = 1 # ephemeral-key = 0x0288ef00023598499cb6c940146d050d2b1fb914198c327f76aad590bead68b6 +# ephemeral-pubkey = 0x039a003ba6517b473fa0cd74aefe99dadfdb34627f90fec6362df85803908f53a5 00000000000000000000000000000000088b3d4342776668980a4adf72a8fcaa 963f24b27a2f6bb44c7ed5ca10e87de130f94d2390b9853c3dcb21d51e9472d4 diff --git a/p2p/discover/v5_encoding_test.go b/p2p/discover/v5_encoding_test.go index 80515e7d4a8..d09a8cca2c9 100644 --- a/p2p/discover/v5_encoding_test.go +++ b/p2p/discover/v5_encoding_test.go @@ -377,6 +377,7 @@ func testVectorComment(net *handshakeTest, p packetV5, challenge *whoareyouV5, n fmt.Fprint(o, "\nhandshake inputs:\n\n") printWhoareyou(challenge) fmt.Fprintf(o, "ephemeral-key = %#x\n", testEphKey.D.Bytes()) + fmt.Fprintf(o, "ephemeral-pubkey = %#x\n", crypto.CompressPubkey(&testEphKey.PublicKey)) } default: panic(fmt.Errorf("unhandled packet type %T", p)) From e4d93f244b7e73243edbbf3aa8a063c896fbdf12 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Tue, 22 Sep 2020 09:34:22 +0200 Subject: [PATCH 17/70] p2p/discover: move v5 encoding to new sub-package v5wire --- p2p/discover/table_util_test.go | 58 ---- p2p/discover/v5_udp.go | 292 +++++----------- p2p/discover/v5_udp_test.go | 133 ++++--- .../{v5_encoding.go => v5wire/encoding.go} | 325 ++++++------------ .../encoding_test.go} | 227 +++++++----- p2p/discover/v5wire/msg.go | 229 ++++++++++++ .../{v5_session.go => v5wire/session.go} | 35 +- .../testdata/v5.1-ping-handshake-enr.txt | 0 .../testdata/v5.1-ping-handshake.txt | 0 .../testdata/v5.1-ping-message.txt | 0 .../{ => v5wire}/testdata/v5.1-whoareyou.txt | 0 11 files changed, 653 insertions(+), 646 deletions(-) rename p2p/discover/{v5_encoding.go => v5wire/encoding.go} (69%) rename p2p/discover/{v5_encoding_test.go => v5wire/encoding_test.go} (71%) create mode 100644 p2p/discover/v5wire/msg.go rename p2p/discover/{v5_session.go => v5wire/session.go} (78%) rename p2p/discover/{ => v5wire}/testdata/v5.1-ping-handshake-enr.txt (100%) rename p2p/discover/{ => v5wire}/testdata/v5.1-ping-handshake.txt (100%) rename p2p/discover/{ => v5wire}/testdata/v5.1-ping-message.txt (100%) rename p2p/discover/{ => v5wire}/testdata/v5.1-whoareyou.txt (100%) diff --git a/p2p/discover/table_util_test.go b/p2p/discover/table_util_test.go index ed5eaae0f98..54e0b4bccd4 100644 --- a/p2p/discover/table_util_test.go +++ b/p2p/discover/table_util_test.go @@ -22,12 +22,9 @@ import ( "encoding/hex" "errors" "fmt" - "io/ioutil" "math/rand" "net" - "os" "sort" - "strings" "sync" "github.com/ethereum/go-ethereum/crypto" @@ -255,58 +252,3 @@ func hexEncPubkey(h string) (ret encPubkey) { copy(ret[:], b) return ret } - -// hexFile reads the given file and decodes the hex data contained in it. -// Whitespace and any lines beginning with the # character are ignored. -func hexFile(file string) []byte { - fileContent, err := ioutil.ReadFile(file) - if err != nil { - panic(err) - } - - // Gather hex data, ignore comments. - var text []byte - for _, line := range bytes.Split(fileContent, []byte("\n")) { - line = bytes.TrimSpace(line) - if len(line) > 0 && line[0] == '#' { - continue - } - text = append(text, line...) - } - - // Parse the hex. - if bytes.HasPrefix(text, []byte("0x")) { - text = text[2:] - } - data := make([]byte, hex.DecodedLen(len(text))) - if _, err := hex.Decode(data, text); err != nil { - panic("invalid hex in " + file) - } - return data -} - -// writeTestVector writes a test vector file with the given commentary and binary data. -func writeTestVector(file, comment string, data []byte) { - fd, err := os.OpenFile(file, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) - if err != nil { - panic(err) - } - defer fd.Close() - - if len(comment) > 0 { - for _, line := range strings.Split(strings.TrimSpace(comment), "\n") { - fmt.Fprintf(fd, "# %s\n", line) - } - fmt.Fprintln(fd) - } - for len(data) > 0 { - var chunk []byte - if len(data) < 32 { - chunk = data - } else { - chunk = data[:32] - } - data = data[len(chunk):] - fmt.Fprintf(fd, "%x\n", chunk) - } -} diff --git a/p2p/discover/v5_udp.go b/p2p/discover/v5_udp.go index 9a376daabe7..c40c6c8d4e5 100644 --- a/p2p/discover/v5_udp.go +++ b/p2p/discover/v5_udp.go @@ -31,6 +31,7 @@ import ( "github.com/ethereum/go-ethereum/common/mclock" "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/p2p/discover/v5wire" "github.com/ethereum/go-ethereum/p2p/enode" "github.com/ethereum/go-ethereum/p2p/enr" "github.com/ethereum/go-ethereum/p2p/netutil" @@ -45,29 +46,17 @@ const ( respTimeoutV5 = 700 * time.Millisecond ) -// codecV5 is implemented by wireCodec (and testCodec). +// codecV5 is implemented by v5wire.Codec (and testCodec). // // The UDPv5 transport is split into two objects: the codec object deals with // encoding/decoding and with the handshake; the UDPv5 object handles higher-level concerns. type codecV5 interface { - // encode encodes a packet. The 'challenge' parameter is non-nil for calls which got a - // WHOAREYOU response. - encode(fromID enode.ID, fromAddr string, p packetV5, challenge *whoareyouV5) ([]byte, packetNonce, error) + // Encode encodes a packet. + Encode(enode.ID, string, v5wire.Packet, *v5wire.Whoareyou) ([]byte, v5wire.Nonce, error) - // decode decodes a packet. It returns an *unknownV5 packet if decryption fails. - // The fromNode return value is non-nil when the input contains a handshake response. - decode(input []byte, fromAddr string) (fromID enode.ID, fromNode *enode.Node, p packetV5, err error) -} - -// packetV5 is implemented by all discv5 packet type structs. -type packetV5 interface { - // These methods provide information and set the request ID. - name() string - kind() byte - setreqid([]byte) - // handle should perform the appropriate action to handle the packet, i.e. this is the - // place to send the response. - handle(t *UDPv5, fromID enode.ID, fromAddr *net.UDPAddr) + // decode decodes a packet. It returns a *v5wire.Unknown packet if decryption fails. + // The *enode.Node return value is non-nil when the input contains a handshake response. + Decode([]byte, string) (enode.ID, *enode.Node, v5wire.Packet, error) } // UDPv5 is the implementation of protocol version 5. @@ -110,16 +99,16 @@ type UDPv5 struct { // callV5 represents a remote procedure call against another node. type callV5 struct { node *enode.Node - packet packetV5 + packet v5wire.Packet responseType byte // expected packet type of response reqid []byte - ch chan packetV5 // responses sent here - err chan error // errors sent here + ch chan v5wire.Packet // responses sent here + err chan error // errors sent here // Valid for active calls only: - authTag packetNonce // nonce of request packet - handshakeCount int // # times we attempted handshake for this call - challenge *whoareyouV5 // last sent handshake challenge + authTag v5wire.Nonce // nonce of request packet + handshakeCount int // # times we attempted handshake for this call + challenge *v5wire.Whoareyou // last sent handshake challenge timeout mclock.Timer } @@ -164,7 +153,7 @@ func newUDPv5(conn UDPConn, ln *enode.LocalNode, cfg Config) (*UDPv5, error) { callDoneCh: make(chan *callV5), respTimeoutCh: make(chan *callTimeout), // state of dispatch - codec: newWireCodec(ln, cfg.PrivateKey, cfg.Clock), + codec: v5wire.NewCodec(ln, cfg.PrivateKey, cfg.Clock), activeCallByNode: make(map[enode.ID]*callV5), activeCallByAuth: make(map[string]*callV5), callQueue: make(map[enode.ID][]*callV5), @@ -252,11 +241,12 @@ func (t *UDPv5) RegisterTalkHandler(protocol string, handler func([]byte) []byte // TalkRequest sends a talk request to n and waits for a response. func (t *UDPv5) TalkRequest(n *enode.Node, protocol string, request []byte) ([]byte, error) { - resp := t.call(n, p_talkrespV5, &talkreqV5{Protocol: protocol, Message: request}) + req := &v5wire.TalkRequest{Protocol: protocol, Message: request} + resp := t.call(n, v5wire.TalkResponseMsg, req) defer t.callDone(resp) select { case respMsg := <-resp.ch: - return respMsg.(*talkrespV5).Message, nil + return respMsg.(*v5wire.TalkResponse).Message, nil case err := <-resp.err: return nil, err } @@ -342,11 +332,13 @@ func lookupDistances(target, dest enode.ID) (dists []uint) { // ping calls PING on a node and waits for a PONG response. func (t *UDPv5) ping(n *enode.Node) (uint64, error) { - resp := t.call(n, p_pongV5, &pingV5{ENRSeq: t.localNode.Node().Seq()}) + req := &v5wire.Ping{ENRSeq: t.localNode.Node().Seq()} + resp := t.call(n, v5wire.PongMsg, req) defer t.callDone(resp) + select { case pong := <-resp.ch: - return pong.(*pongV5).ENRSeq, nil + return pong.(*v5wire.Pong).ENRSeq, nil case err := <-resp.err: return 0, err } @@ -364,21 +356,9 @@ func (t *UDPv5) RequestENR(n *enode.Node) (*enode.Node, error) { return nodes[0], nil } -// requestTicket calls REQUESTTICKET on a node and waits for a TICKET response. -func (t *UDPv5) requestTicket(n *enode.Node) ([]byte, error) { - resp := t.call(n, p_ticketV5, &pingV5{}) - defer t.callDone(resp) - select { - case response := <-resp.ch: - return response.(*ticketV5).Ticket, nil - case err := <-resp.err: - return nil, err - } -} - // findnode calls FINDNODE on a node and waits for responses. func (t *UDPv5) findnode(n *enode.Node, distances []uint) ([]*enode.Node, error) { - resp := t.call(n, p_nodesV5, &findnodeV5{Distances: distances}) + resp := t.call(n, v5wire.NodesMsg, &v5wire.Findnode{Distances: distances}) return t.waitForNodes(resp, distances) } @@ -394,11 +374,11 @@ func (t *UDPv5) waitForNodes(c *callV5, distances []uint) ([]*enode.Node, error) for { select { case responseP := <-c.ch: - response := responseP.(*nodesV5) + response := responseP.(*v5wire.Nodes) for _, record := range response.Nodes { node, err := t.verifyResponseNode(c, record, distances, seen) if err != nil { - t.log.Debug("Invalid record in "+response.name(), "id", c.node.ID(), "err", err) + t.log.Debug("Invalid record in "+response.Name(), "id", c.node.ID(), "err", err) continue } nodes = append(nodes, node) @@ -449,20 +429,20 @@ func containsUint(x uint, xs []uint) bool { return false } -// call sends the given call and sets up a handler for response packets (of type c.responseType). -// Responses are dispatched to the call's response channel. -func (t *UDPv5) call(node *enode.Node, responseType byte, packet packetV5) *callV5 { +// call sends the given call and sets up a handler for response packets (of message type +// responseType). Responses are dispatched to the call's response channel. +func (t *UDPv5) call(node *enode.Node, responseType byte, packet v5wire.Packet) *callV5 { c := &callV5{ node: node, packet: packet, responseType: responseType, reqid: make([]byte, 8), - ch: make(chan packetV5, 1), + ch: make(chan v5wire.Packet, 1), err: make(chan error, 1), } // Assign request ID. crand.Read(c.reqid) - packet.setreqid(c.reqid) + packet.SetReqID(c.reqid) // Send call to dispatch. select { case t.callCh <- c: @@ -599,21 +579,21 @@ func (t *UDPv5) sendCall(c *callV5) { // sendResponse sends a response packet to the given node. // This doesn't trigger a handshake even if no keys are available. -func (t *UDPv5) sendResponse(toID enode.ID, toAddr *net.UDPAddr, packet packetV5) error { +func (t *UDPv5) sendResponse(toID enode.ID, toAddr *net.UDPAddr, packet v5wire.Packet) error { _, err := t.send(toID, toAddr, packet, nil) return err } // send sends a packet to the given node. -func (t *UDPv5) send(toID enode.ID, toAddr *net.UDPAddr, packet packetV5, c *whoareyouV5) (packetNonce, error) { +func (t *UDPv5) send(toID enode.ID, toAddr *net.UDPAddr, packet v5wire.Packet, c *v5wire.Whoareyou) (v5wire.Nonce, error) { addr := toAddr.String() - enc, authTag, err := t.codec.encode(toID, addr, packet, c) + enc, authTag, err := t.codec.Encode(toID, addr, packet, c) if err != nil { - t.log.Warn(">> "+packet.name(), "id", toID, "addr", addr, "err", err) + t.log.Warn(">> "+packet.Name(), "id", toID, "addr", addr, "err", err) return authTag, err } _, err = t.conn.WriteToUDP(enc, toAddr) - t.log.Trace(">> "+packet.name(), "id", toID, "addr", addr) + t.log.Trace(">> "+packet.Name(), "id", toID, "addr", addr) return authTag, err } @@ -652,7 +632,7 @@ func (t *UDPv5) dispatchReadPacket(from *net.UDPAddr, content []byte) bool { // handlePacket decodes and processes an incoming packet from the network. func (t *UDPv5) handlePacket(rawpacket []byte, fromAddr *net.UDPAddr) error { addr := fromAddr.String() - fromID, fromNode, packet, err := t.codec.decode(rawpacket, addr) + fromID, fromNode, packet, err := t.codec.Decode(rawpacket, addr) if err != nil { t.log.Debug("Bad discv5 packet", "id", fromID, "addr", addr, "err", err) return err @@ -661,27 +641,27 @@ func (t *UDPv5) handlePacket(rawpacket []byte, fromAddr *net.UDPAddr) error { // Handshake succeeded, add to table. t.tab.addSeenNode(wrapNode(fromNode)) } - if packet.kind() != p_whoareyouV5 { - // WHOAREYOU logged separately to report the sender ID. - t.log.Trace("<< "+packet.name(), "id", fromID, "addr", addr) + if packet.Kind() != v5wire.WhoareyouPacket { + // WHOAREYOU logged separately to report errors. + t.log.Trace("<< "+packet.Name(), "id", fromID, "addr", addr) } - packet.handle(t, fromID, fromAddr) + t.handle(packet, fromID, fromAddr) return nil } // handleCallResponse dispatches a response packet to the call waiting for it. -func (t *UDPv5) handleCallResponse(fromID enode.ID, fromAddr *net.UDPAddr, reqid []byte, p packetV5) { +func (t *UDPv5) handleCallResponse(fromID enode.ID, fromAddr *net.UDPAddr, reqid []byte, p v5wire.Packet) { ac := t.activeCallByNode[fromID] if ac == nil || !bytes.Equal(reqid, ac.reqid) { - t.log.Debug(fmt.Sprintf("Unsolicited/late %s response", p.name()), "id", fromID, "addr", fromAddr) + t.log.Debug(fmt.Sprintf("Unsolicited/late %s response", p.Name()), "id", fromID, "addr", fromAddr) return } if !fromAddr.IP.Equal(ac.node.IP()) || fromAddr.Port != ac.node.UDP() { - t.log.Debug(fmt.Sprintf("%s from wrong endpoint", p.name()), "id", fromID, "addr", fromAddr) + t.log.Debug(fmt.Sprintf("%s from wrong endpoint", p.Name()), "id", fromID, "addr", fromAddr) return } - if p.kind() != ac.responseType { - t.log.Debug(fmt.Sprintf("Wrong disv5 response type %s", p.name()), "id", fromID, "addr", fromAddr) + if p.Kind() != ac.responseType { + t.log.Debug(fmt.Sprintf("Wrong discv5 response type %s", p.Name()), "id", fromID, "addr", fromAddr) return } t.startResponseTimeout(ac) @@ -699,51 +679,65 @@ func (t *UDPv5) getNode(id enode.ID) *enode.Node { return nil } -// UNKNOWN - -func (p *unknownV5) name() string { return "UNKNOWN/v5" } -func (p *unknownV5) kind() byte { return p_unknownV5 } -func (p *unknownV5) setreqid(id []byte) {} - -func (p *unknownV5) handle(t *UDPv5, fromID enode.ID, fromAddr *net.UDPAddr) { - challenge := &whoareyouV5{AuthTag: p.AuthTag} +// handle handles incoming packets according to their message type. +func (t *UDPv5) handle(p v5wire.Packet, fromID enode.ID, fromAddr *net.UDPAddr) { + switch p := p.(type) { + case *v5wire.Unknown: + t.handleUnknown(p, fromID, fromAddr) + case *v5wire.Whoareyou: + t.handleWhoareyou(p, fromID, fromAddr) + case *v5wire.Ping: + t.handlePing(p, fromID, fromAddr) + case *v5wire.Pong: + t.localNode.UDPEndpointStatement(fromAddr, &net.UDPAddr{IP: p.ToIP, Port: int(p.ToPort)}) + t.handleCallResponse(fromID, fromAddr, p.ReqID, p) + case *v5wire.Findnode: + t.handleFindnode(p, fromID, fromAddr) + case *v5wire.Nodes: + t.handleCallResponse(fromID, fromAddr, p.ReqID, p) + case *v5wire.TalkRequest: + t.handleTalkRequest(p, fromID, fromAddr) + case *v5wire.TalkResponse: + t.handleCallResponse(fromID, fromAddr, p.ReqID, p) + } +} + +// handleUnknown initiates a handshake by responding with WHOAREYOU. +func (t *UDPv5) handleUnknown(p *v5wire.Unknown, fromID enode.ID, fromAddr *net.UDPAddr) { + challenge := &v5wire.Whoareyou{AuthTag: p.AuthTag} crand.Read(challenge.IDNonce[:]) if n := t.getNode(fromID); n != nil { - challenge.node = n + challenge.Node = n challenge.RecordSeq = n.Seq() } t.sendResponse(fromID, fromAddr, challenge) } -// WHOAREYOU - -func (p *whoareyouV5) name() string { return "WHOAREYOU/v5" } -func (p *whoareyouV5) kind() byte { return p_whoareyouV5 } -func (p *whoareyouV5) setreqid(id []byte) {} +var ( + errChallengeNoCall = errors.New("no matching call") + errChallengeTwice = errors.New("second handshake") +) -func (p *whoareyouV5) handle(t *UDPv5, fromID enode.ID, fromAddr *net.UDPAddr) { - c, err := p.matchWithCall(t, p.AuthTag) +// handleWhoareyou resends the active call as a handshake packet. +func (t *UDPv5) handleWhoareyou(p *v5wire.Whoareyou, fromID enode.ID, fromAddr *net.UDPAddr) { + c, err := t.matchWithCall(fromID, p.AuthTag) if err != nil { - t.log.Debug("Invalid WHOAREYOU/v5", "addr", fromAddr, "err", err) + t.log.Debug("Invalid "+p.Name(), "addr", fromAddr, "err", err) return } + // Resend the call that was answered by WHOAREYOU. - t.log.Trace("<< "+p.name(), "id", c.node.ID(), "addr", fromAddr) + t.log.Trace("<< "+p.Name(), "id", c.node.ID(), "addr", fromAddr) c.handshakeCount++ c.challenge = p - p.node = c.node + p.Node = c.node t.sendCall(c) } -var ( - errChallengeNoCall = errors.New("no matching call") - errChallengeTwice = errors.New("second handshake") -) - -// matchWithCall checks whether the handshake attempt matches the active call. -func (p *whoareyouV5) matchWithCall(t *UDPv5, authTag packetNonce) (*callV5, error) { +// matchWithCall checks whether a handshake attempt matches the active call. +func (t *UDPv5) matchWithCall(fromID enode.ID, authTag v5wire.Nonce) (*callV5, error) { c := t.activeCallByAuth[string(authTag[:])] - if c == nil { + if c == nil || c.node.ID() != fromID { return nil, errChallengeNoCall } if c.handshakeCount > 0 { @@ -752,14 +746,9 @@ func (p *whoareyouV5) matchWithCall(t *UDPv5, authTag packetNonce) (*callV5, err return c, nil } -// PING - -func (p *pingV5) name() string { return "PING/v5" } -func (p *pingV5) kind() byte { return p_pingV5 } -func (p *pingV5) setreqid(id []byte) { p.ReqID = id } - -func (p *pingV5) handle(t *UDPv5, fromID enode.ID, fromAddr *net.UDPAddr) { - t.sendResponse(fromID, fromAddr, &pongV5{ +// handlePing sends a PONG response. +func (t *UDPv5) handlePing(p *v5wire.Ping, fromID enode.ID, fromAddr *net.UDPAddr) { + t.sendResponse(fromID, fromAddr, &v5wire.Pong{ ReqID: p.ReqID, ToIP: fromAddr.IP, ToPort: uint16(fromAddr.Port), @@ -767,24 +756,8 @@ func (p *pingV5) handle(t *UDPv5, fromID enode.ID, fromAddr *net.UDPAddr) { }) } -// PONG - -func (p *pongV5) name() string { return "PONG/v5" } -func (p *pongV5) kind() byte { return p_pongV5 } -func (p *pongV5) setreqid(id []byte) { p.ReqID = id } - -func (p *pongV5) handle(t *UDPv5, fromID enode.ID, fromAddr *net.UDPAddr) { - t.localNode.UDPEndpointStatement(fromAddr, &net.UDPAddr{IP: p.ToIP, Port: int(p.ToPort)}) - t.handleCallResponse(fromID, fromAddr, p.ReqID, p) -} - -// FINDNODE - -func (p *findnodeV5) name() string { return "FINDNODE/v5" } -func (p *findnodeV5) kind() byte { return p_findnodeV5 } -func (p *findnodeV5) setreqid(id []byte) { p.ReqID = id } - -func (p *findnodeV5) handle(t *UDPv5, fromID enode.ID, fromAddr *net.UDPAddr) { +// handleFindnode returns nodes to the requester. +func (t *UDPv5) handleFindnode(p *v5wire.Findnode, fromID enode.ID, fromAddr *net.UDPAddr) { nodes := t.collectTableNodes(fromAddr.IP, p.Distances, findnodeResultLimit) t.sendNodes(fromID, fromAddr, p.ReqID, nodes) } @@ -829,7 +802,7 @@ func (t *UDPv5) collectTableNodes(rip net.IP, distances []uint, limit int) []*en // sendNodes sends the given records in one or more NODES packets. func (t *UDPv5) sendNodes(toID enode.ID, toAddr *net.UDPAddr, reqid []byte, nodes []*enode.Node) { total := uint8(math.Ceil(float64(len(nodes)) / 3)) - resp := &nodesV5{ReqID: reqid, Total: total, Nodes: make([]*enr.Record, 3)} + resp := &v5wire.Nodes{ReqID: reqid, Total: total, Nodes: make([]*enr.Record, 3)} sent := false for len(nodes) > 0 { items := min(nodesResponseItemLimit, len(nodes)) @@ -849,23 +822,8 @@ func (t *UDPv5) sendNodes(toID enode.ID, toAddr *net.UDPAddr, reqid []byte, node } } -// NODES - -func (p *nodesV5) name() string { return "NODES/v5" } -func (p *nodesV5) kind() byte { return p_nodesV5 } -func (p *nodesV5) setreqid(id []byte) { p.ReqID = id } - -func (p *nodesV5) handle(t *UDPv5, fromID enode.ID, fromAddr *net.UDPAddr) { - t.handleCallResponse(fromID, fromAddr, p.ReqID, p) -} - -// TALKREQ - -func (p *talkreqV5) name() string { return "TALKREQ/v5" } -func (p *talkreqV5) kind() byte { return p_talkreqV5 } -func (p *talkreqV5) setreqid(id []byte) { p.ReqID = id } - -func (p *talkreqV5) handle(t *UDPv5, fromID enode.ID, fromAddr *net.UDPAddr) { +// handleTalkRequest runs the talk request handler of the requested protocol. +func (t *UDPv5) handleTalkRequest(p *v5wire.TalkRequest, fromID enode.ID, fromAddr *net.UDPAddr) { t.trlock.Lock() handler := t.trhandlers[p.Protocol] t.trlock.Unlock() @@ -874,66 +832,6 @@ func (p *talkreqV5) handle(t *UDPv5, fromID enode.ID, fromAddr *net.UDPAddr) { if handler != nil { response = handler(p.Message) } - if len(p.ReqID) > 0 { - t.sendResponse(fromID, fromAddr, &talkrespV5{ReqID: p.ReqID, Message: response}) - } -} - -// TALKRESP - -func (p *talkrespV5) name() string { return "TALKRESP/v5" } -func (p *talkrespV5) kind() byte { return p_talkrespV5 } -func (p *talkrespV5) setreqid(id []byte) { p.ReqID = id } - -func (p *talkrespV5) handle(t *UDPv5, fromID enode.ID, fromAddr *net.UDPAddr) { - t.handleCallResponse(fromID, fromAddr, p.ReqID, p) -} - -// REQUESTTICKET - -func (p *requestTicketV5) name() string { return "REQUESTTICKET/v5" } -func (p *requestTicketV5) kind() byte { return p_requestTicketV5 } -func (p *requestTicketV5) setreqid(id []byte) { p.ReqID = id } - -func (p *requestTicketV5) handle(t *UDPv5, fromID enode.ID, fromAddr *net.UDPAddr) { - t.sendResponse(fromID, fromAddr, &ticketV5{ReqID: p.ReqID}) -} - -// TICKET - -func (p *ticketV5) name() string { return "TICKET/v5" } -func (p *ticketV5) kind() byte { return p_ticketV5 } -func (p *ticketV5) setreqid(id []byte) { p.ReqID = id } - -func (p *ticketV5) handle(t *UDPv5, fromID enode.ID, fromAddr *net.UDPAddr) { - t.handleCallResponse(fromID, fromAddr, p.ReqID, p) -} - -// REGTOPIC - -func (p *regtopicV5) name() string { return "REGTOPIC/v5" } -func (p *regtopicV5) kind() byte { return p_regtopicV5 } -func (p *regtopicV5) setreqid(id []byte) { p.ReqID = id } - -func (p *regtopicV5) handle(t *UDPv5, fromID enode.ID, fromAddr *net.UDPAddr) { - t.sendResponse(fromID, fromAddr, ®confirmationV5{ReqID: p.ReqID, Registered: false}) -} - -// REGCONFIRMATION - -func (p *regconfirmationV5) name() string { return "REGCONFIRMATION/v5" } -func (p *regconfirmationV5) kind() byte { return p_regconfirmationV5 } -func (p *regconfirmationV5) setreqid(id []byte) { p.ReqID = id } - -func (p *regconfirmationV5) handle(t *UDPv5, fromID enode.ID, fromAddr *net.UDPAddr) { - t.handleCallResponse(fromID, fromAddr, p.ReqID, p) -} - -// TOPICQUERY - -func (p *topicqueryV5) name() string { return "TOPICQUERY/v5" } -func (p *topicqueryV5) kind() byte { return p_topicqueryV5 } -func (p *topicqueryV5) setreqid(id []byte) { p.ReqID = id } - -func (p *topicqueryV5) handle(t *UDPv5, fromID enode.ID, fromAddr *net.UDPAddr) { + resp := &v5wire.TalkResponse{ReqID: p.ReqID, Message: response} + t.sendResponse(fromID, fromAddr, resp) } diff --git a/p2p/discover/v5_udp_test.go b/p2p/discover/v5_udp_test.go index aad22fc72ec..9b745690f79 100644 --- a/p2p/discover/v5_udp_test.go +++ b/p2p/discover/v5_udp_test.go @@ -29,6 +29,7 @@ import ( "github.com/ethereum/go-ethereum/internal/testlog" "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/p2p/discover/v5wire" "github.com/ethereum/go-ethereum/p2p/enode" "github.com/ethereum/go-ethereum/p2p/enr" "github.com/ethereum/go-ethereum/rlp" @@ -93,8 +94,8 @@ func TestUDPv5_pingHandling(t *testing.T) { test := newUDPV5Test(t) defer test.close() - test.packetIn(&pingV5{ReqID: []byte("foo")}) - test.waitPacketOut(func(p *pongV5, addr *net.UDPAddr, _ packetNonce) { + test.packetIn(&v5wire.Ping{ReqID: []byte("foo")}) + test.waitPacketOut(func(p *v5wire.Pong, addr *net.UDPAddr, _ v5wire.Nonce) { if !bytes.Equal(p.ReqID, []byte("foo")) { t.Error("wrong request ID in response:", p.ReqID) } @@ -110,8 +111,8 @@ func TestUDPv5_unknownPacket(t *testing.T) { test := newUDPV5Test(t) defer test.close() - authTag := packetNonce{1, 2, 3} - check := func(p *whoareyouV5, wantSeq uint64) { + authTag := v5wire.Nonce{1, 2, 3} + check := func(p *v5wire.Whoareyou, wantSeq uint64) { t.Helper() if p.AuthTag != authTag { t.Error("wrong token in WHOAREYOU:", p.AuthTag, authTag) @@ -125,8 +126,8 @@ func TestUDPv5_unknownPacket(t *testing.T) { } // Unknown packet from unknown node. - test.packetIn(&unknownV5{AuthTag: authTag}) - test.waitPacketOut(func(p *whoareyouV5, addr *net.UDPAddr, _ packetNonce) { + test.packetIn(&v5wire.Unknown{AuthTag: authTag}) + test.waitPacketOut(func(p *v5wire.Whoareyou, addr *net.UDPAddr, _ v5wire.Nonce) { check(p, 0) }) @@ -134,8 +135,8 @@ func TestUDPv5_unknownPacket(t *testing.T) { n := test.getNode(test.remotekey, test.remoteaddr).Node() test.table.addSeenNode(wrapNode(n)) - test.packetIn(&unknownV5{AuthTag: authTag}) - test.waitPacketOut(func(p *whoareyouV5, addr *net.UDPAddr, _ packetNonce) { + test.packetIn(&v5wire.Unknown{AuthTag: authTag}) + test.waitPacketOut(func(p *v5wire.Whoareyou, addr *net.UDPAddr, _ v5wire.Nonce) { check(p, n.Seq()) }) } @@ -155,28 +156,28 @@ func TestUDPv5_findnodeHandling(t *testing.T) { fillTable(test.table, wrapNodes(nodes248)) // Requesting with distance zero should return the node's own record. - test.packetIn(&findnodeV5{ReqID: []byte{0}, Distances: []uint{0}}) + test.packetIn(&v5wire.Findnode{ReqID: []byte{0}, Distances: []uint{0}}) test.expectNodes([]byte{0}, 1, []*enode.Node{test.udp.Self()}) // Requesting with distance > 256 shouldn't crash. - test.packetIn(&findnodeV5{ReqID: []byte{1}, Distances: []uint{4234098}}) + test.packetIn(&v5wire.Findnode{ReqID: []byte{1}, Distances: []uint{4234098}}) test.expectNodes([]byte{1}, 1, nil) // Requesting with empty distance list shouldn't crash either. - test.packetIn(&findnodeV5{ReqID: []byte{2}, Distances: []uint{}}) + test.packetIn(&v5wire.Findnode{ReqID: []byte{2}, Distances: []uint{}}) test.expectNodes([]byte{2}, 1, nil) // This request gets no nodes because the corresponding bucket is empty. - test.packetIn(&findnodeV5{ReqID: []byte{3}, Distances: []uint{254}}) + test.packetIn(&v5wire.Findnode{ReqID: []byte{3}, Distances: []uint{254}}) test.expectNodes([]byte{3}, 1, nil) // This request gets all the distance-253 nodes. - test.packetIn(&findnodeV5{ReqID: []byte{4}, Distances: []uint{253}}) + test.packetIn(&v5wire.Findnode{ReqID: []byte{4}, Distances: []uint{253}}) test.expectNodes([]byte{4}, 4, nodes253) // This request gets all the distance-249 nodes and some more at 248 because // the bucket at 249 is not full. - test.packetIn(&findnodeV5{ReqID: []byte{5}, Distances: []uint{249, 248}}) + test.packetIn(&v5wire.Findnode{ReqID: []byte{5}, Distances: []uint{249, 248}}) var nodes []*enode.Node nodes = append(nodes, nodes249...) nodes = append(nodes, nodes248[:10]...) @@ -190,7 +191,7 @@ func (test *udpV5Test) expectNodes(wantReqID []byte, wantTotal uint8, wantNodes } for { - test.waitPacketOut(func(p *nodesV5, addr *net.UDPAddr, _ packetNonce) { + test.waitPacketOut(func(p *v5wire.Nodes, addr *net.UDPAddr, _ v5wire.Nonce) { if !bytes.Equal(p.ReqID, wantReqID) { test.t.Fatalf("wrong request ID %v in response, want %v", p.ReqID, wantReqID) } @@ -232,7 +233,7 @@ func TestUDPv5_pingCall(t *testing.T) { _, err := test.udp.ping(remote) done <- err }() - test.waitPacketOut(func(p *pingV5, addr *net.UDPAddr, _ packetNonce) {}) + test.waitPacketOut(func(p *v5wire.Ping, addr *net.UDPAddr, _ v5wire.Nonce) {}) if err := <-done; err != errTimeout { t.Fatalf("want errTimeout, got %q", err) } @@ -242,8 +243,8 @@ func TestUDPv5_pingCall(t *testing.T) { _, err := test.udp.ping(remote) done <- err }() - test.waitPacketOut(func(p *pingV5, addr *net.UDPAddr, _ packetNonce) { - test.packetInFrom(test.remotekey, test.remoteaddr, &pongV5{ReqID: p.ReqID}) + test.waitPacketOut(func(p *v5wire.Ping, addr *net.UDPAddr, _ v5wire.Nonce) { + test.packetInFrom(test.remotekey, test.remoteaddr, &v5wire.Pong{ReqID: p.ReqID}) }) if err := <-done; err != nil { t.Fatal(err) @@ -254,9 +255,9 @@ func TestUDPv5_pingCall(t *testing.T) { _, err := test.udp.ping(remote) done <- err }() - test.waitPacketOut(func(p *pingV5, addr *net.UDPAddr, _ packetNonce) { + test.waitPacketOut(func(p *v5wire.Ping, addr *net.UDPAddr, _ v5wire.Nonce) { wrongAddr := &net.UDPAddr{IP: net.IP{33, 44, 55, 22}, Port: 10101} - test.packetInFrom(test.remotekey, wrongAddr, &pongV5{ReqID: p.ReqID}) + test.packetInFrom(test.remotekey, wrongAddr, &v5wire.Pong{ReqID: p.ReqID}) }) if err := <-done; err != errTimeout { t.Fatalf("want errTimeout for reply from wrong IP, got %q", err) @@ -285,16 +286,16 @@ func TestUDPv5_findnodeCall(t *testing.T) { }() // Serve the responses: - test.waitPacketOut(func(p *findnodeV5, addr *net.UDPAddr, _ packetNonce) { + test.waitPacketOut(func(p *v5wire.Findnode, addr *net.UDPAddr, _ v5wire.Nonce) { if !reflect.DeepEqual(p.Distances, distances) { t.Fatalf("wrong distances in request: %v", p.Distances) } - test.packetIn(&nodesV5{ + test.packetIn(&v5wire.Nodes{ ReqID: p.ReqID, Total: 2, Nodes: nodesToRecords(nodes[:4]), }) - test.packetIn(&nodesV5{ + test.packetIn(&v5wire.Nodes{ ReqID: p.ReqID, Total: 2, Nodes: nodesToRecords(nodes[4:]), @@ -331,16 +332,16 @@ func TestUDPv5_callResend(t *testing.T) { }() // Ping answered by WHOAREYOU. - test.waitPacketOut(func(p *pingV5, addr *net.UDPAddr, authTag packetNonce) { - test.packetIn(&whoareyouV5{AuthTag: authTag}) + test.waitPacketOut(func(p *v5wire.Ping, addr *net.UDPAddr, authTag v5wire.Nonce) { + test.packetIn(&v5wire.Whoareyou{AuthTag: authTag}) }) // Ping should be re-sent. - test.waitPacketOut(func(p *pingV5, addr *net.UDPAddr, _ packetNonce) { - test.packetIn(&pongV5{ReqID: p.ReqID}) + test.waitPacketOut(func(p *v5wire.Ping, addr *net.UDPAddr, _ v5wire.Nonce) { + test.packetIn(&v5wire.Pong{ReqID: p.ReqID}) }) // Answer the other ping. - test.waitPacketOut(func(p *pingV5, addr *net.UDPAddr, _ packetNonce) { - test.packetIn(&pongV5{ReqID: p.ReqID}) + test.waitPacketOut(func(p *v5wire.Ping, addr *net.UDPAddr, _ v5wire.Nonce) { + test.packetIn(&v5wire.Pong{ReqID: p.ReqID}) }) if err := <-done; err != nil { t.Fatalf("unexpected ping error: %v", err) @@ -364,12 +365,12 @@ func TestUDPv5_multipleHandshakeRounds(t *testing.T) { }() // Ping answered by WHOAREYOU. - test.waitPacketOut(func(p *pingV5, addr *net.UDPAddr, authTag packetNonce) { - test.packetIn(&whoareyouV5{AuthTag: authTag}) + test.waitPacketOut(func(p *v5wire.Ping, addr *net.UDPAddr, authTag v5wire.Nonce) { + test.packetIn(&v5wire.Whoareyou{AuthTag: authTag}) }) // Ping answered by WHOAREYOU again. - test.waitPacketOut(func(p *pingV5, addr *net.UDPAddr, authTag packetNonce) { - test.packetIn(&whoareyouV5{AuthTag: authTag}) + test.waitPacketOut(func(p *v5wire.Ping, addr *net.UDPAddr, authTag v5wire.Nonce) { + test.packetIn(&v5wire.Whoareyou{AuthTag: authTag}) }) if err := <-done; err != errTimeout { t.Fatalf("unexpected ping error: %q", err) @@ -395,16 +396,16 @@ func TestUDPv5_callTimeoutReset(t *testing.T) { }() // Serve two responses, slowly. - test.waitPacketOut(func(p *findnodeV5, addr *net.UDPAddr, _ packetNonce) { + test.waitPacketOut(func(p *v5wire.Findnode, addr *net.UDPAddr, _ v5wire.Nonce) { time.Sleep(respTimeout - 50*time.Millisecond) - test.packetIn(&nodesV5{ + test.packetIn(&v5wire.Nodes{ ReqID: p.ReqID, Total: 2, Nodes: nodesToRecords(nodes[:4]), }) time.Sleep(respTimeout - 50*time.Millisecond) - test.packetIn(&nodesV5{ + test.packetIn(&v5wire.Nodes{ ReqID: p.ReqID, Total: 2, Nodes: nodesToRecords(nodes[4:]), @@ -428,12 +429,12 @@ func TestUDPv5_talkHandling(t *testing.T) { }) // Successful case: - test.packetIn(&talkreqV5{ + test.packetIn(&v5wire.TalkRequest{ ReqID: []byte("foo"), Protocol: "test", Message: []byte("test request"), }) - test.waitPacketOut(func(p *talkrespV5, addr *net.UDPAddr, _ packetNonce) { + test.waitPacketOut(func(p *v5wire.TalkResponse, addr *net.UDPAddr, _ v5wire.Nonce) { if !bytes.Equal(p.ReqID, []byte("foo")) { t.Error("wrong request ID in response:", p.ReqID) } @@ -447,12 +448,12 @@ func TestUDPv5_talkHandling(t *testing.T) { // Check that empty response is returned for unregistered protocols. recvMessage = nil - test.packetIn(&talkreqV5{ + test.packetIn(&v5wire.TalkRequest{ ReqID: []byte("2"), Protocol: "wrong", Message: []byte("test request"), }) - test.waitPacketOut(func(p *talkrespV5, addr *net.UDPAddr, _ packetNonce) { + test.waitPacketOut(func(p *v5wire.TalkResponse, addr *net.UDPAddr, _ v5wire.Nonce) { if !bytes.Equal(p.ReqID, []byte("2")) { t.Error("wrong request ID in response:", p.ReqID) } @@ -479,7 +480,7 @@ func TestUDPv5_talkRequest(t *testing.T) { _, err := test.udp.TalkRequest(remote, "test", []byte("test request")) done <- err }() - test.waitPacketOut(func(p *talkreqV5, addr *net.UDPAddr, _ packetNonce) {}) + test.waitPacketOut(func(p *v5wire.TalkRequest, addr *net.UDPAddr, _ v5wire.Nonce) {}) if err := <-done; err != errTimeout { t.Fatalf("want errTimeout, got %q", err) } @@ -489,14 +490,14 @@ func TestUDPv5_talkRequest(t *testing.T) { _, err := test.udp.TalkRequest(remote, "test", []byte("test request")) done <- err }() - test.waitPacketOut(func(p *talkreqV5, addr *net.UDPAddr, _ packetNonce) { + test.waitPacketOut(func(p *v5wire.TalkRequest, addr *net.UDPAddr, _ v5wire.Nonce) { if p.Protocol != "test" { t.Errorf("wrong protocol ID in talk request: %q", p.Protocol) } if string(p.Message) != "test request" { t.Errorf("wrong message talk request: %q", p.Message) } - test.packetInFrom(test.remotekey, test.remoteaddr, &talkrespV5{ + test.packetInFrom(test.remotekey, test.remoteaddr, &v5wire.TalkResponse{ ReqID: p.ReqID, Message: []byte("test response"), }) @@ -536,14 +537,14 @@ func TestUDPv5_lookup(t *testing.T) { // Answer lookup packets. for done := false; !done; { - done = test.waitPacketOut(func(p packetV5, to *net.UDPAddr, _ packetNonce) { + done = test.waitPacketOut(func(p v5wire.Packet, to *net.UDPAddr, _ v5wire.Nonce) { recipient, key := lookupTestnet.nodeByAddr(to) switch p := p.(type) { - case *pingV5: - test.packetInFrom(key, to, &pongV5{ReqID: p.ReqID}) - case *findnodeV5: + case *v5wire.Ping: + test.packetInFrom(key, to, &v5wire.Pong{ReqID: p.ReqID}) + case *v5wire.Findnode: nodes := lookupTestnet.neighborsAtDistance(recipient, p.Distances[0], 3) - response := &nodesV5{ReqID: p.ReqID, Total: 1, Nodes: nodesToRecords(nodes)} + response := &v5wire.Nodes{ReqID: p.ReqID, Total: 1, Nodes: nodesToRecords(nodes)} test.packetInFrom(key, to, response) } }) @@ -598,46 +599,44 @@ type testCodec struct { type testCodecFrame struct { NodeID enode.ID - AuthTag packetNonce + AuthTag v5wire.Nonce Ptype byte Packet rlp.RawValue } -func (c *testCodec) encode(toID enode.ID, addr string, p packetV5, _ *whoareyouV5) ([]byte, packetNonce, error) { +func (c *testCodec) Encode(toID enode.ID, addr string, p v5wire.Packet, _ *v5wire.Whoareyou) ([]byte, v5wire.Nonce, error) { c.ctr++ - var authTag packetNonce + var authTag v5wire.Nonce binary.BigEndian.PutUint64(authTag[:], c.ctr) + penc, _ := rlp.EncodeToBytes(p) - frame, err := rlp.EncodeToBytes(testCodecFrame{c.id, authTag, p.kind(), penc}) + frame, err := rlp.EncodeToBytes(testCodecFrame{c.id, authTag, p.Kind(), penc}) return frame, authTag, err } -func (c *testCodec) decode(input []byte, addr string) (enode.ID, *enode.Node, packetV5, error) { +func (c *testCodec) Decode(input []byte, addr string) (enode.ID, *enode.Node, v5wire.Packet, error) { frame, p, err := c.decodeFrame(input) if err != nil { return enode.ID{}, nil, nil, err } - if p.kind() == p_whoareyouV5 { - frame.NodeID = enode.ID{} // match wireCodec behavior - } return frame.NodeID, nil, p, nil } -func (c *testCodec) decodeFrame(input []byte) (frame testCodecFrame, p packetV5, err error) { +func (c *testCodec) decodeFrame(input []byte) (frame testCodecFrame, p v5wire.Packet, err error) { if err = rlp.DecodeBytes(input, &frame); err != nil { return frame, nil, fmt.Errorf("invalid frame: %v", err) } switch frame.Ptype { - case p_unknownV5: - dec := new(unknownV5) + case v5wire.UnknownPacket: + dec := new(v5wire.Unknown) err = rlp.DecodeBytes(frame.Packet, &dec) p = dec - case p_whoareyouV5: - dec := new(whoareyouV5) + case v5wire.WhoareyouPacket: + dec := new(v5wire.Whoareyou) err = rlp.DecodeBytes(frame.Packet, &dec) p = dec default: - p, err = decodePacketBodyV5(frame.Ptype, frame.Packet) + p, err = v5wire.DecodeMessage(frame.Ptype, frame.Packet) } return frame, p, err } @@ -670,20 +669,20 @@ func newUDPV5Test(t *testing.T) *udpV5Test { } // handles a packet as if it had been sent to the transport. -func (test *udpV5Test) packetIn(packet packetV5) { +func (test *udpV5Test) packetIn(packet v5wire.Packet) { test.t.Helper() test.packetInFrom(test.remotekey, test.remoteaddr, packet) } // handles a packet as if it had been sent to the transport by the key/endpoint. -func (test *udpV5Test) packetInFrom(key *ecdsa.PrivateKey, addr *net.UDPAddr, packet packetV5) { +func (test *udpV5Test) packetInFrom(key *ecdsa.PrivateKey, addr *net.UDPAddr, packet v5wire.Packet) { test.t.Helper() ln := test.getNode(key, addr) codec := &testCodec{test: test, id: ln.ID()} - enc, _, err := codec.encode(test.udp.Self().ID(), addr.String(), packet, nil) + enc, _, err := codec.Encode(test.udp.Self().ID(), addr.String(), packet, nil) if err != nil { - test.t.Errorf("%s encode error: %v", packet.name(), err) + test.t.Errorf("%s encode error: %v", packet.Name(), err) } if test.udp.dispatchReadPacket(addr, enc) { <-test.udp.readNextCh // unblock UDPv5.dispatch @@ -706,7 +705,7 @@ func (test *udpV5Test) getNode(key *ecdsa.PrivateKey, addr *net.UDPAddr) *enode. } // waitPacketOut waits for the next output packet and handles it using the given 'validate' -// function. The function must be of type func (X, *net.UDPAddr, packetNonce) where X is +// function. The function must be of type func (X, *net.UDPAddr, v5wire.Nonce) where X is // assignable to packetV5. func (test *udpV5Test) waitPacketOut(validate interface{}) (closed bool) { test.t.Helper() diff --git a/p2p/discover/v5_encoding.go b/p2p/discover/v5wire/encoding.go similarity index 69% rename from p2p/discover/v5_encoding.go rename to p2p/discover/v5wire/encoding.go index b7e8e23c5ae..6ac0339531f 100644 --- a/p2p/discover/v5_encoding.go +++ b/p2p/discover/v5wire/encoding.go @@ -14,20 +14,20 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . -package discover +package v5wire import ( "bytes" "crypto/aes" "crypto/cipher" "crypto/ecdsa" + "crypto/elliptic" crand "crypto/rand" "crypto/sha256" "encoding/binary" "errors" "fmt" "hash" - "net" "time" "github.com/ethereum/go-ethereum/common/math" @@ -44,54 +44,38 @@ import ( // TODO add counter to nonce // TODO rehandshake after X packets -// Discovery v5 packet types. -const ( - p_pingV5 byte = iota + 1 - p_pongV5 - p_findnodeV5 - p_nodesV5 - p_talkreqV5 - p_talkrespV5 - p_requestTicketV5 - p_ticketV5 - p_regtopicV5 - p_regconfirmationV5 - p_topicqueryV5 - p_unknownV5 = byte(255) // any non-decryptable packet - p_whoareyouV5 = byte(254) // the WHOAREYOU packet -) +// Nonce represents a nonce used for AES/GCM. +type Nonce [gcmNonceSize]byte // Discovery v5 packet structures. type ( - packetHeaderV5 struct { + packetHeader struct { ProtocolID [8]byte SrcID enode.ID Flags byte AuthSize uint16 } - whoareyouAuthDataV5 struct { - Nonce packetNonce // nonce of request packet - IDNonce [32]byte // ID proof data - RecordSeq uint64 // highest known ENR sequence of requester + whoareyouAuthData struct { + Nonce Nonce // nonce of request packet + IDNonce [32]byte // ID proof data + RecordSeq uint64 // highest known ENR sequence of requester } - handshakeAuthDataV5 struct { + handshakeAuthData struct { h struct { - Version uint8 // protocol version - Nonce packetNonce // AES-GCM nonce of message - SigSize byte // ignature data - PubkeySize byte // offset of + Version uint8 // protocol version + Nonce Nonce // AES-GCM nonce of message + SigSize byte // ignature data + PubkeySize byte // offset of } // Trailing variable-size data. signature, pubkey, record []byte } - messageAuthDataV5 struct { - Nonce packetNonce // AES-GCM nonce of message + messageAuthData struct { + Nonce Nonce // AES-GCM nonce of message } - - packetNonce [gcmNonceSize]byte ) // Packet header flag values. @@ -103,102 +87,13 @@ const ( var ( sizeofMaskingIV = 16 - sizeofPacketHeaderV5 = binary.Size(packetHeaderV5{}) - sizeofWhoareyouAuthDataV5 = binary.Size(whoareyouAuthDataV5{}) - sizeofHandshakeAuthDataV5 = binary.Size(handshakeAuthDataV5{}.h) - sizeofMessageAuthDataV5 = binary.Size(messageAuthDataV5{}) + sizeofPacketHeaderV5 = binary.Size(packetHeader{}) + sizeofWhoareyouAuthDataV5 = binary.Size(whoareyouAuthData{}) + sizeofHandshakeAuthDataV5 = binary.Size(handshakeAuthData{}.h) + sizeofMessageAuthDataV5 = binary.Size(messageAuthData{}) protocolIDV5 = [8]byte{'d', 'i', 's', 'c', 'v', '5', ' ', ' '} ) -// Discovery v5 messages. -type ( - // unknownV5 represents any packet that can't be decrypted. - unknownV5 struct { - AuthTag packetNonce - } - - // WHOAREYOU contains the handshake challenge. - whoareyouV5 struct { - AuthTag packetNonce - IDNonce [32]byte // To be signed by recipient. - RecordSeq uint64 // ENR sequence number of recipient - - node *enode.Node - sent mclock.AbsTime - } - - // PING is sent during liveness checks. - pingV5 struct { - ReqID []byte - ENRSeq uint64 - } - - // PONG is the reply to PING. - pongV5 struct { - ReqID []byte - ENRSeq uint64 - ToIP net.IP // These fields should mirror the UDP envelope address of the ping - ToPort uint16 // packet, which provides a way to discover the the external address (after NAT). - } - - // FINDNODE is a query for nodes in the given bucket. - findnodeV5 struct { - ReqID []byte - Distances []uint - } - - // NODES is the reply to FINDNODE and TOPICQUERY. - nodesV5 struct { - ReqID []byte - Total uint8 - Nodes []*enr.Record - } - - // TALKREQ is an application-level request. - talkreqV5 struct { - ReqID []byte - Protocol string - Message []byte - } - - // TALKRESP is the reply to TALKREQ. - talkrespV5 struct { - ReqID []byte - Message []byte - } - - // REQUESTTICKET requests a ticket for a topic queue. - requestTicketV5 struct { - ReqID []byte - Topic []byte - } - - // TICKET is the response to REQUESTTICKET. - ticketV5 struct { - ReqID []byte - Ticket []byte - } - - // REGTOPIC registers the sender in a topic queue using a ticket. - regtopicV5 struct { - ReqID []byte - Ticket []byte - ENR *enr.Record - } - - // REGCONFIRMATION is the reply to REGTOPIC. - regconfirmationV5 struct { - ReqID []byte - Registered bool - } - - // TOPICQUERY asks for nodes with the given topic. - topicqueryV5 struct { - ReqID []byte - Topic []byte - } -) - const ( // Encryption/authentication parameters. aesKeySize = 16 @@ -217,46 +112,44 @@ var ( errUnexpectedHandshake = errors.New("unexpected auth response, not in handshake") errHandshakeNonceMismatch = errors.New("wrong nonce in auth response") errInvalidAuthKey = errors.New("invalid ephemeral pubkey") - errUnknownAuthScheme = errors.New("unknown auth scheme in handshake") errNoRecord = errors.New("expected ENR in handshake but none sent") errInvalidNonceSig = errors.New("invalid ID nonce signature") errMessageTooShort = errors.New("message contains no data") errMessageDecrypt = errors.New("cannot decrypt message") ) -// wireCodec encodes and decodes discovery v5 packets. -type wireCodec struct { +// Codec encodes and decodes discovery v5 packets. +type Codec struct { sha256 hash.Hash localnode *enode.LocalNode privkey *ecdsa.PrivateKey buf bytes.Buffer // used for encoding of packets msgbuf bytes.Buffer // used for encoding of message content reader bytes.Reader // used for decoding - sc *sessionCache + sc *SessionCache } -// newWireCodec creates a wire codec. -func newWireCodec(ln *enode.LocalNode, key *ecdsa.PrivateKey, clock mclock.Clock) *wireCodec { - c := &wireCodec{ +// NewCodec creates a wire codec. +func NewCodec(ln *enode.LocalNode, key *ecdsa.PrivateKey, clock mclock.Clock) *Codec { + c := &Codec{ sha256: sha256.New(), localnode: ln, privkey: key, - sc: newSessionCache(1024, clock), + sc: NewSessionCache(1024, clock), } return c } -// encode encodes a packet to a node. 'id' and 'addr' specify the destination node. The +// Encode encodes a packet to a node. 'id' and 'addr' specify the destination node. The // 'challenge' parameter should be the most recently received WHOAREYOU packet from that // node. -func (c *wireCodec) encode(id enode.ID, addr string, packet packetV5, challenge *whoareyouV5) ([]byte, packetNonce, error) { - if packet.kind() == p_whoareyouV5 { - p := packet.(*whoareyouV5) +func (c *Codec) Encode(id enode.ID, addr string, packet Packet, challenge *Whoareyou) ([]byte, Nonce, error) { + if p, ok := packet.(*Whoareyou); ok { enc, err := c.encodeWhoareyou(id, p) if err == nil { c.sc.storeSentHandshake(id, addr, p) } - return enc, packetNonce{}, err + return enc, Nonce{}, err } if challenge != nil { @@ -270,7 +163,7 @@ func (c *wireCodec) encode(id enode.ID, addr string, packet packetV5, challenge } // makeHeader creates a packet header. -func (c *wireCodec) makeHeader(toID enode.ID, flags byte, authsizeExtra int) *packetHeaderV5 { +func (c *Codec) makeHeader(toID enode.ID, flags byte, authsizeExtra int) *packetHeader { var authsize int switch flags { case flagMessage: @@ -286,7 +179,7 @@ func (c *wireCodec) makeHeader(toID enode.ID, flags byte, authsizeExtra int) *pa if authsize > int(^uint16(0)) { panic(fmt.Errorf("BUG: auth size %d overflows uint16", authsize)) } - return &packetHeaderV5{ + return &packetHeader{ ProtocolID: protocolIDV5, SrcID: c.localnode.ID(), Flags: flags, @@ -295,8 +188,8 @@ func (c *wireCodec) makeHeader(toID enode.ID, flags byte, authsizeExtra int) *pa } // encodeRandom encodes a packet with random content. -func (c *wireCodec) encodeRandom(toID enode.ID) ([]byte, packetNonce, error) { - var auth messageAuthDataV5 +func (c *Codec) encodeRandom(toID enode.ID) ([]byte, Nonce, error) { + var auth messageAuthData if _, err := crand.Read(auth.Nonce[:]); err != nil { return nil, auth.Nonce, fmt.Errorf("can't get random data: %v", err) } @@ -309,12 +202,12 @@ func (c *wireCodec) encodeRandom(toID enode.ID) ([]byte, packetNonce, error) { } // encodeWhoareyou encodes a WHOAREYOU packet. -func (c *wireCodec) encodeWhoareyou(toID enode.ID, packet *whoareyouV5) ([]byte, error) { +func (c *Codec) encodeWhoareyou(toID enode.ID, packet *Whoareyou) ([]byte, error) { // Sanity check node field to catch misbehaving callers. - if packet.RecordSeq > 0 && packet.node == nil { + if packet.RecordSeq > 0 && packet.Node == nil { panic("BUG: missing node in whoareyouV5 with non-zero seq") } - auth := &whoareyouAuthDataV5{ + auth := &whoareyouAuthData{ Nonce: packet.AuthTag, IDNonce: packet.IDNonce, RecordSeq: packet.RecordSeq, @@ -330,16 +223,16 @@ func (c *wireCodec) encodeWhoareyou(toID enode.ID, packet *whoareyouV5) ([]byte, // encodeHandshakeMessage encodes an encrypted message with a handshake // response header. -func (c *wireCodec) encodeHandshakeMessage(toID enode.ID, addr string, packet packetV5, challenge *whoareyouV5) ([]byte, packetNonce, error) { +func (c *Codec) encodeHandshakeMessage(toID enode.ID, addr string, packet Packet, challenge *Whoareyou) ([]byte, Nonce, error) { // Ensure calling code sets challenge.node. - if challenge.node == nil { - panic("BUG: missing challenge.node in encode") + if challenge.Node == nil { + panic("BUG: missing challenge.Node in encode") } // Generate new secrets. auth, session, err := c.makeHandshakeHeader(toID, addr, challenge) if err != nil { - return nil, packetNonce{}, err + return nil, Nonce{}, err } // TODO: this should happen when the first authenticated message is received @@ -360,7 +253,7 @@ func (c *wireCodec) encodeHandshakeMessage(toID enode.ID, addr string, packet pa // Encrypt packet body. c.msgbuf.Reset() - c.msgbuf.WriteByte(packet.kind()) + c.msgbuf.WriteByte(packet.Kind()) if err := rlp.Encode(&c.msgbuf, packet); err != nil { return nil, auth.h.Nonce, err } @@ -374,28 +267,28 @@ func (c *wireCodec) encodeHandshakeMessage(toID enode.ID, addr string, packet pa } // encodeAuthHeader creates the auth header on a call packet following WHOAREYOU. -func (c *wireCodec) makeHandshakeHeader(toID enode.ID, addr string, challenge *whoareyouV5) (*handshakeAuthDataV5, *session, error) { +func (c *Codec) makeHandshakeHeader(toID enode.ID, addr string, challenge *Whoareyou) (*handshakeAuthData, *session, error) { session := new(session) nonce, err := c.sc.nextNonce(session) if err != nil { return nil, nil, fmt.Errorf("can't generate nonce: %v", err) } - auth := new(handshakeAuthDataV5) + auth := new(handshakeAuthData) auth.h.Version = handshakeVersion auth.h.Nonce = nonce // Create the ephemeral key. This needs to be first because the // key is part of the ID nonce signature. var remotePubkey = new(ecdsa.PublicKey) - if err := challenge.node.Load((*enode.Secp256k1)(remotePubkey)); err != nil { + if err := challenge.Node.Load((*enode.Secp256k1)(remotePubkey)); err != nil { return nil, nil, fmt.Errorf("can't find secp256k1 key for recipient") } ephkey, err := c.sc.ephemeralKeyGen() if err != nil { return nil, nil, fmt.Errorf("can't generate ephemeral key") } - ephpubkey := encodePubkey(&ephkey.PublicKey) + ephpubkey := EncodePubkey(&ephkey.PublicKey) auth.pubkey = ephpubkey[:] auth.h.PubkeySize = byte(len(auth.pubkey)) @@ -415,7 +308,7 @@ func (c *wireCodec) makeHandshakeHeader(toID enode.ID, addr string, challenge *w } // Create session keys. - sec := c.deriveKeys(c.localnode.ID(), challenge.node.ID(), ephkey, remotePubkey, challenge) + sec := c.deriveKeys(c.localnode.ID(), challenge.Node.ID(), ephkey, remotePubkey, challenge) if sec == nil { return nil, nil, fmt.Errorf("key derivation failed") } @@ -423,10 +316,10 @@ func (c *wireCodec) makeHandshakeHeader(toID enode.ID, addr string, challenge *w } // encodeMessage encodes an encrypted message packet. -func (c *wireCodec) encodeMessage(toID enode.ID, s *session, packet packetV5) ([]byte, packetNonce, error) { +func (c *Codec) encodeMessage(toID enode.ID, s *session, packet Packet) ([]byte, Nonce, error) { var ( head = c.makeHeader(toID, flagMessage, 0) - auth messageAuthDataV5 + auth messageAuthData ) // Create the nonce. @@ -444,7 +337,7 @@ func (c *wireCodec) encodeMessage(toID enode.ID, s *session, packet packetV5) ([ // Encode the message plaintext. c.msgbuf.Reset() - c.msgbuf.WriteByte(packet.kind()) + c.msgbuf.WriteByte(packet.Kind()) if err := rlp.Encode(&c.msgbuf, packet); err != nil { return nil, auth.Nonce, err } @@ -459,8 +352,8 @@ func (c *wireCodec) encodeMessage(toID enode.ID, s *session, packet packetV5) ([ return output, auth.Nonce, err } -// decode decodes a discovery packet. -func (c *wireCodec) decode(input []byte, addr string) (src enode.ID, n *enode.Node, p packetV5, err error) { +// Decode decodes a discovery packet. +func (c *Codec) Decode(input []byte, addr string) (src enode.ID, n *enode.Node, p Packet, err error) { // Delete timed-out handshakes. This must happen before decoding to avoid // processing the same handshake twice. c.sc.handshakeGC() @@ -475,7 +368,7 @@ func (c *wireCodec) decode(input []byte, addr string) (src enode.ID, n *enode.No mask.XORKeyStream(headerData, headerData) // Decode and verify the header. - var head packetHeaderV5 + var head packetHeader c.reader.Reset(input) binary.Read(&c.reader, binary.BigEndian, &head) if head.ProtocolID != protocolIDV5 { @@ -503,16 +396,16 @@ func (c *wireCodec) decode(input []byte, addr string) (src enode.ID, n *enode.No } // decodeWhoareyou reads packet data after the header as a WHOAREYOU packet. -func (c *wireCodec) decodeWhoareyou(head *packetHeaderV5) (packetV5, error) { +func (c *Codec) decodeWhoareyou(head *packetHeader) (Packet, error) { if c.reader.Len() < sizeofWhoareyouAuthDataV5 { return nil, errTooShort } if int(head.AuthSize) != sizeofWhoareyouAuthDataV5 { return nil, fmt.Errorf("invalid auth size for whoareyou") } - auth := new(whoareyouAuthDataV5) + auth := new(whoareyouAuthData) binary.Read(&c.reader, binary.BigEndian, auth) - p := &whoareyouV5{ + p := &Whoareyou{ AuthTag: auth.Nonce, IDNonce: auth.IDNonce, RecordSeq: auth.RecordSeq, @@ -520,7 +413,7 @@ func (c *wireCodec) decodeWhoareyou(head *packetHeaderV5) (packetV5, error) { return p, nil } -func (c *wireCodec) decodeHandshakeMessage(fromAddr string, head *packetHeaderV5, input []byte) (n *enode.Node, p packetV5, err error) { +func (c *Codec) decodeHandshakeMessage(fromAddr string, head *packetHeader, input []byte) (n *enode.Node, p Packet, err error) { node, nonce, session, err := c.decodeHandshake(fromAddr, head) if err != nil { return nil, nil, err @@ -538,31 +431,31 @@ func (c *wireCodec) decodeHandshakeMessage(fromAddr string, head *packetHeaderV5 return node, msg, nil } -func (c *wireCodec) decodeHandshake(fromAddr string, head *packetHeaderV5) (*enode.Node, packetNonce, *session, error) { +func (c *Codec) decodeHandshake(fromAddr string, head *packetHeader) (*enode.Node, Nonce, *session, error) { auth, err := c.decodeHandshakeAuthData(head) if err != nil { - return nil, packetNonce{}, nil, err + return nil, Nonce{}, nil, err } // Verify against our last WHOAREYOU. challenge := c.sc.getHandshake(head.SrcID, fromAddr) if challenge == nil { - return nil, packetNonce{}, nil, errUnexpectedHandshake + return nil, Nonce{}, nil, errUnexpectedHandshake } // Get node record. - node, err := c.decodeHandshakeRecord(challenge.node, head.SrcID, auth.record) + node, err := c.decodeHandshakeRecord(challenge.Node, head.SrcID, auth.record) if err != nil { - return nil, packetNonce{}, nil, err + return nil, Nonce{}, nil, err } // Verify ephemeral key is on curve. - ephkey, err := decodePubkey(c.privkey.Curve, auth.pubkey) + ephkey, err := DecodePubkey(c.privkey.Curve, auth.pubkey) if err != nil { - return nil, packetNonce{}, nil, errInvalidAuthKey + return nil, Nonce{}, nil, errInvalidAuthKey } // Verify ID nonce signature. err = c.verifyIDSignature(challenge.IDNonce[:], auth.pubkey, auth.signature, node) if err != nil { - return nil, packetNonce{}, nil, err + return nil, Nonce{}, nil, err } // Derive sesssion keys. session := c.deriveKeys(head.SrcID, c.localnode.ID(), c.privkey, ephkey, challenge) @@ -571,7 +464,7 @@ func (c *wireCodec) decodeHandshake(fromAddr string, head *packetHeaderV5) (*eno } // decodeHandshakeAuthData reads the authdata section of a handshake packet. -func (c *wireCodec) decodeHandshakeAuthData(head *packetHeaderV5) (*handshakeAuthDataV5, error) { +func (c *Codec) decodeHandshakeAuthData(head *packetHeader) (*handshakeAuthData, error) { if int(head.AuthSize) < sizeofHandshakeAuthDataV5 { return nil, fmt.Errorf("header authsize %d too low for handshake", head.AuthSize) } @@ -580,7 +473,7 @@ func (c *wireCodec) decodeHandshakeAuthData(head *packetHeaderV5) (*handshakeAut } // Decode fixed size part. - var auth handshakeAuthDataV5 + var auth handshakeAuthData binary.Read(&c.reader, binary.BigEndian, &auth.h) if auth.h.Version > handshakeVersion || auth.h.Version < minVersion { return nil, fmt.Errorf("invalid handshake version %d", auth.h.Version) @@ -616,7 +509,7 @@ func readNew(data *[]byte, length int, r *bytes.Reader) bool { // decodeHandshakeRecord verifies the node record contained in a handshake packet. The // remote node should include the record if we don't have one or if ours is older than the // latest sequence number. -func (c *wireCodec) decodeHandshakeRecord(local *enode.Node, wantID enode.ID, remote []byte) (node *enode.Node, err error) { +func (c *Codec) decodeHandshakeRecord(local *enode.Node, wantID enode.ID, remote []byte) (node *enode.Node, err error) { node = local if len(remote) > 0 { var record enr.Record @@ -641,11 +534,11 @@ func (c *wireCodec) decodeHandshakeRecord(local *enode.Node, wantID enode.ID, re } // decodeMessage reads packet data following the header as an ordinary message packet. -func (c *wireCodec) decodeMessage(fromAddr string, head *packetHeaderV5, input []byte) (packetV5, error) { +func (c *Codec) decodeMessage(fromAddr string, head *packetHeader, input []byte) (Packet, error) { if c.reader.Len() < sizeofMessageAuthDataV5 { return nil, errTooShort } - auth := new(messageAuthDataV5) + auth := new(messageAuthData) binary.Read(&c.reader, binary.BigEndian, auth) // Try decrypting the message. @@ -653,12 +546,12 @@ func (c *wireCodec) decodeMessage(fromAddr string, head *packetHeaderV5, input [ msg, err := c.decryptMessage(input, auth.Nonce, key) if err == errMessageDecrypt { // It didn't work. Start the handshake since this is an ordinary message packet. - return &unknownV5{AuthTag: auth.Nonce}, nil + return &Unknown{AuthTag: auth.Nonce}, nil } return msg, err } -func (c *wireCodec) decryptMessage(input []byte, nonce packetNonce, readKey []byte) (packetV5, error) { +func (c *Codec) decryptMessage(input []byte, nonce Nonce, readKey []byte) (Packet, error) { headerData := input[:len(input)-c.reader.Len()] messageCT := input[len(headerData):] message, err := decryptGCM(readKey, nonce[:], messageCT, headerData) @@ -668,46 +561,11 @@ func (c *wireCodec) decryptMessage(input []byte, nonce packetNonce, readKey []by if len(message) == 0 { return nil, errMessageTooShort } - return decodePacketBodyV5(message[0], message[1:]) -} - -// decodePacketBody decodes the body of an encrypted discovery packet. -func decodePacketBodyV5(ptype byte, body []byte) (packetV5, error) { - var dec packetV5 - switch ptype { - case p_pingV5: - dec = new(pingV5) - case p_pongV5: - dec = new(pongV5) - case p_findnodeV5: - dec = new(findnodeV5) - case p_nodesV5: - dec = new(nodesV5) - case p_talkreqV5: - dec = new(talkreqV5) - case p_talkrespV5: - dec = new(talkrespV5) - case p_requestTicketV5: - dec = new(requestTicketV5) - case p_ticketV5: - dec = new(ticketV5) - case p_regtopicV5: - dec = new(regtopicV5) - case p_regconfirmationV5: - dec = new(regconfirmationV5) - case p_topicqueryV5: - dec = new(topicqueryV5) - default: - return nil, fmt.Errorf("unknown packet type %d", ptype) - } - if err := rlp.DecodeBytes(body, dec); err != nil { - return nil, err - } - return dec, nil + return DecodeMessage(message[0], message[1:]) } // signIDNonce creates the ID nonce signature. -func (c *wireCodec) signIDNonce(nonce, ephkey []byte) ([]byte, error) { +func (c *Codec) signIDNonce(nonce, ephkey []byte) ([]byte, error) { idsig, err := crypto.Sign(c.idNonceHash(nonce, ephkey), c.privkey) if err != nil { return nil, fmt.Errorf("can't sign: %v", err) @@ -716,7 +574,7 @@ func (c *wireCodec) signIDNonce(nonce, ephkey []byte) ([]byte, error) { } // idNonceHash computes the hash of id nonce with prefix. -func (c *wireCodec) idNonceHash(nonce, ephkey []byte) []byte { +func (c *Codec) idNonceHash(nonce, ephkey []byte) []byte { h := c.sha256reset() h.Write([]byte(idNoncePrefix)) h.Write(nonce) @@ -725,7 +583,7 @@ func (c *wireCodec) idNonceHash(nonce, ephkey []byte) []byte { } // verifyIDSignature checks that signature over idnonce was made by the node with given record. -func (c *wireCodec) verifyIDSignature(nonce, ephkey, sig []byte, n *enode.Node) error { +func (c *Codec) verifyIDSignature(nonce, ephkey, sig []byte, n *enode.Node) error { switch idscheme := n.Record().IdentityScheme(); idscheme { case "v4": var pk ecdsa.PublicKey @@ -740,7 +598,7 @@ func (c *wireCodec) verifyIDSignature(nonce, ephkey, sig []byte, n *enode.Node) } // deriveKeys generates session keys using elliptic-curve Diffie-Hellman key agreement. -func (c *wireCodec) deriveKeys(n1, n2 enode.ID, priv *ecdsa.PrivateKey, pub *ecdsa.PublicKey, challenge *whoareyouV5) *session { +func (c *Codec) deriveKeys(n1, n2 enode.ID, priv *ecdsa.PrivateKey, pub *ecdsa.PublicKey, challenge *Whoareyou) *session { eph := ecdh(priv, pub) if eph == nil { return nil @@ -763,13 +621,13 @@ func (c *wireCodec) deriveKeys(n1, n2 enode.ID, priv *ecdsa.PrivateKey, pub *ecd } // sha256reset returns the shared hash instance. -func (c *wireCodec) sha256reset() hash.Hash { +func (c *Codec) sha256reset() hash.Hash { c.sha256.Reset() return c.sha256 } // sha256sum computes sha256 on the concatenation of inputs. -func (c *wireCodec) sha256sum(inputs ...[]byte) []byte { +func (c *Codec) sha256sum(inputs ...[]byte) []byte { c.sha256.Reset() for _, b := range inputs { c.sha256.Write(b) @@ -778,7 +636,7 @@ func (c *wireCodec) sha256sum(inputs ...[]byte) []byte { } // maskOutputPacket applies protocol header masking to a packet sent to destID. -func (c *wireCodec) maskOutputPacket(destID enode.ID, output []byte, headerDataLen int) []byte { +func (c *Codec) maskOutputPacket(destID enode.ID, output []byte, headerDataLen int) []byte { masked := make([]byte, sizeofMaskingIV+len(output)) c.sc.maskingIVGen(masked[:sizeofMaskingIV]) mask := headerMask(destID, masked) @@ -837,3 +695,24 @@ func decryptGCM(key, nonce, ct, authData []byte) ([]byte, error) { pt := make([]byte, 0, len(ct)) return aesgcm.Open(pt, nonce, ct, authData) } + +// Pubkey represents an encoded public key. +type Pubkey [33]byte + +// EncodePubkey encodes a public key into the 33-byte compressed format. +func EncodePubkey(key *ecdsa.PublicKey) Pubkey { + var enc Pubkey + copy(enc[:], crypto.CompressPubkey(key)) + return enc +} + +// DecodePubkey decodes a public key from the 33-byte compressed format. +func DecodePubkey(curve elliptic.Curve, e []byte) (*ecdsa.PublicKey, error) { + if len(e) != len(Pubkey{}) { + return nil, errors.New("wrong size public key data") + } + if curve != crypto.S256() { + return nil, errors.New("curves other than secp256k1 are not supported") + } + return crypto.DecompressPubkey(e) +} diff --git a/p2p/discover/v5_encoding_test.go b/p2p/discover/v5wire/encoding_test.go similarity index 71% rename from p2p/discover/v5_encoding_test.go rename to p2p/discover/v5wire/encoding_test.go index d09a8cca2c9..be8a20d43b6 100644 --- a/p2p/discover/v5_encoding_test.go +++ b/p2p/discover/v5wire/encoding_test.go @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . -package discover +package v5wire import ( "bytes" @@ -22,7 +22,9 @@ import ( "encoding/hex" "flag" "fmt" + "io/ioutil" "net" + "os" "path/filepath" "reflect" "strings" @@ -54,10 +56,10 @@ func TestDeriveKeysV5(t *testing.T) { var ( n1 = enode.ID{1} n2 = enode.ID{2} - challenge = &whoareyouV5{} + challenge = &Whoareyou{} db, _ = enode.OpenDB("") ln = enode.NewLocalNode(db, testKeyA) - c = newWireCodec(ln, testKeyA, mclock.System{}) + c = NewCodec(ln, testKeyA, mclock.System{}) ) defer db.Close() @@ -78,28 +80,28 @@ func TestHandshakeV5(t *testing.T) { defer net.close() // A -> B RANDOM PACKET - packet, _ := net.nodeA.encode(t, net.nodeB, &findnodeV5{}) - resp := net.nodeB.expectDecode(t, p_unknownV5, packet) + packet, _ := net.nodeA.encode(t, net.nodeB, &Findnode{}) + resp := net.nodeB.expectDecode(t, UnknownPacket, packet) // A <- B WHOAREYOU - challenge := &whoareyouV5{ - AuthTag: resp.(*unknownV5).AuthTag, + challenge := &Whoareyou{ + AuthTag: resp.(*Unknown).AuthTag, IDNonce: testIDnonce, RecordSeq: 0, } whoareyou, _ := net.nodeB.encode(t, net.nodeA, challenge) - net.nodeA.expectDecode(t, p_whoareyouV5, whoareyou) + net.nodeA.expectDecode(t, WhoareyouPacket, whoareyou) // A -> B FINDNODE (handshake packet) - findnode, _ := net.nodeA.encodeWithChallenge(t, net.nodeB, challenge, &findnodeV5{}) - net.nodeB.expectDecode(t, p_findnodeV5, findnode) + findnode, _ := net.nodeA.encodeWithChallenge(t, net.nodeB, challenge, &Findnode{}) + net.nodeB.expectDecode(t, FindnodeMsg, findnode) if len(net.nodeB.c.sc.handshakes) > 0 { t.Fatalf("node B didn't remove handshake from challenge map") } // A <- B NODES - nodes, _ := net.nodeB.encode(t, net.nodeA, &nodesV5{Total: 1}) - net.nodeA.expectDecode(t, p_nodesV5, nodes) + nodes, _ := net.nodeB.encode(t, net.nodeA, &Nodes{Total: 1}) + net.nodeA.expectDecode(t, NodesMsg, nodes) } // This test checks that handshake attempts are removed within the timeout. @@ -109,21 +111,21 @@ func TestHandshakeV5_timeout(t *testing.T) { defer net.close() // A -> B RANDOM PACKET - packet, _ := net.nodeA.encode(t, net.nodeB, &findnodeV5{}) - resp := net.nodeB.expectDecode(t, p_unknownV5, packet) + packet, _ := net.nodeA.encode(t, net.nodeB, &Findnode{}) + resp := net.nodeB.expectDecode(t, UnknownPacket, packet) // A <- B WHOAREYOU - challenge := &whoareyouV5{ - AuthTag: resp.(*unknownV5).AuthTag, + challenge := &Whoareyou{ + AuthTag: resp.(*Unknown).AuthTag, IDNonce: testIDnonce, RecordSeq: 0, } whoareyou, _ := net.nodeB.encode(t, net.nodeA, challenge) - net.nodeA.expectDecode(t, p_whoareyouV5, whoareyou) + net.nodeA.expectDecode(t, WhoareyouPacket, whoareyou) // A -> B FINDNODE (handshake packet) after timeout net.clock.Run(handshakeTimeout + 1) - findnode, _ := net.nodeA.encodeWithChallenge(t, net.nodeB, challenge, &findnodeV5{}) + findnode, _ := net.nodeA.encodeWithChallenge(t, net.nodeB, challenge, &Findnode{}) net.nodeB.expectDecodeErr(t, errUnexpectedHandshake, findnode) } @@ -134,30 +136,30 @@ func TestHandshakeV5_norecord(t *testing.T) { defer net.close() // A -> B RANDOM PACKET - packet, _ := net.nodeA.encode(t, net.nodeB, &findnodeV5{}) - resp := net.nodeB.expectDecode(t, p_unknownV5, packet) + packet, _ := net.nodeA.encode(t, net.nodeB, &Findnode{}) + resp := net.nodeB.expectDecode(t, UnknownPacket, packet) // A <- B WHOAREYOU nodeA := net.nodeA.n() if nodeA.Seq() == 0 { t.Fatal("need non-zero sequence number") } - challenge := &whoareyouV5{ - AuthTag: resp.(*unknownV5).AuthTag, + challenge := &Whoareyou{ + AuthTag: resp.(*Unknown).AuthTag, IDNonce: testIDnonce, RecordSeq: nodeA.Seq(), - node: nodeA, + Node: nodeA, } whoareyou, _ := net.nodeB.encode(t, net.nodeA, challenge) - net.nodeA.expectDecode(t, p_whoareyouV5, whoareyou) + net.nodeA.expectDecode(t, WhoareyouPacket, whoareyou) // A -> B FINDNODE - findnode, _ := net.nodeA.encodeWithChallenge(t, net.nodeB, challenge, &findnodeV5{}) - net.nodeB.expectDecode(t, p_findnodeV5, findnode) + findnode, _ := net.nodeA.encodeWithChallenge(t, net.nodeB, challenge, &Findnode{}) + net.nodeB.expectDecode(t, FindnodeMsg, findnode) // A <- B NODES - nodes, _ := net.nodeB.encode(t, net.nodeA, &nodesV5{Total: 1}) - net.nodeA.expectDecode(t, p_nodesV5, nodes) + nodes, _ := net.nodeB.encode(t, net.nodeA, &Nodes{Total: 1}) + net.nodeA.expectDecode(t, NodesMsg, nodes) } // In this test, A tries to send FINDNODE with existing secrets but B doesn't know @@ -174,13 +176,13 @@ func TestHandshakeV5_rekey(t *testing.T) { net.nodeA.c.sc.storeNewSession(net.nodeB.id(), net.nodeB.addr(), session) // A -> B FINDNODE (encrypted with zero keys) - findnode, authTag := net.nodeA.encode(t, net.nodeB, &findnodeV5{}) - net.nodeB.expectDecode(t, p_unknownV5, findnode) + findnode, authTag := net.nodeA.encode(t, net.nodeB, &Findnode{}) + net.nodeB.expectDecode(t, UnknownPacket, findnode) // A <- B WHOAREYOU - challenge := &whoareyouV5{AuthTag: authTag, IDNonce: testIDnonce} + challenge := &Whoareyou{AuthTag: authTag, IDNonce: testIDnonce} whoareyou, _ := net.nodeB.encode(t, net.nodeA, challenge) - net.nodeA.expectDecode(t, p_whoareyouV5, whoareyou) + net.nodeA.expectDecode(t, WhoareyouPacket, whoareyou) // Check that new keys haven't been stored yet. sa := net.nodeA.c.sc.session(net.nodeB.id(), net.nodeB.addr()) @@ -192,12 +194,12 @@ func TestHandshakeV5_rekey(t *testing.T) { } // A -> B FINDNODE encrypted with new keys - findnode, _ = net.nodeA.encodeWithChallenge(t, net.nodeB, challenge, &findnodeV5{}) - net.nodeB.expectDecode(t, p_findnodeV5, findnode) + findnode, _ = net.nodeA.encodeWithChallenge(t, net.nodeB, challenge, &Findnode{}) + net.nodeB.expectDecode(t, FindnodeMsg, findnode) // A <- B NODES - nodes, _ := net.nodeB.encode(t, net.nodeA, &nodesV5{Total: 1}) - net.nodeA.expectDecode(t, p_nodesV5, nodes) + nodes, _ := net.nodeB.encode(t, net.nodeA, &Nodes{Total: 1}) + net.nodeA.expectDecode(t, NodesMsg, nodes) } // In this test A and B have different keys before the handshake. @@ -218,21 +220,21 @@ func TestHandshakeV5_rekey2(t *testing.T) { net.nodeB.c.sc.storeNewSession(net.nodeA.id(), net.nodeA.addr(), initKeysB) // A -> B FINDNODE encrypted with initKeysA - findnode, authTag := net.nodeA.encode(t, net.nodeB, &findnodeV5{Distances: []uint{3}}) - net.nodeB.expectDecode(t, p_unknownV5, findnode) + findnode, authTag := net.nodeA.encode(t, net.nodeB, &Findnode{Distances: []uint{3}}) + net.nodeB.expectDecode(t, UnknownPacket, findnode) // A <- B WHOAREYOU - challenge := &whoareyouV5{AuthTag: authTag, IDNonce: testIDnonce} + challenge := &Whoareyou{AuthTag: authTag, IDNonce: testIDnonce} whoareyou, _ := net.nodeB.encode(t, net.nodeA, challenge) - net.nodeA.expectDecode(t, p_whoareyouV5, whoareyou) + net.nodeA.expectDecode(t, WhoareyouPacket, whoareyou) // A -> B FINDNODE (handshake packet) - findnode, _ = net.nodeA.encodeWithChallenge(t, net.nodeB, challenge, &findnodeV5{}) - net.nodeB.expectDecode(t, p_findnodeV5, findnode) + findnode, _ = net.nodeA.encodeWithChallenge(t, net.nodeB, challenge, &Findnode{}) + net.nodeB.expectDecode(t, FindnodeMsg, findnode) // A <- B NODES - nodes, _ := net.nodeB.encode(t, net.nodeA, &nodesV5{Total: 1}) - net.nodeA.expectDecode(t, p_nodesV5, nodes) + nodes, _ := net.nodeB.encode(t, net.nodeA, &Nodes{Total: 1}) + net.nodeA.expectDecode(t, NodesMsg, nodes) } // This test checks some malformed packets. @@ -257,13 +259,13 @@ func TestTestVectorsV5(t *testing.T) { writeKey: hexutil.MustDecode("0x00000000000000000000000000000000"), readKey: hexutil.MustDecode("0x01010101010101010101010101010101"), } - challenge0 = &whoareyouV5{ - AuthTag: packetNonce{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}, + challenge0 = &Whoareyou{ + AuthTag: Nonce{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}, IDNonce: [32]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, RecordSeq: 0, } - challenge1 = &whoareyouV5{ - AuthTag: packetNonce{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}, + challenge1 = &Whoareyou{ + AuthTag: Nonce{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}, IDNonce: [32]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, RecordSeq: 1, } @@ -271,8 +273,8 @@ func TestTestVectorsV5(t *testing.T) { type testVectorTest struct { name string // test vector name - packet packetV5 // the packet to be encoded - challenge *whoareyouV5 // handshake challenge passed to encoder + packet Packet // the packet to be encoded + challenge *Whoareyou // handshake challenge passed to encoder prep func(*handshakeTest) // called before encode/decode } tests := []testVectorTest{ @@ -282,7 +284,7 @@ func TestTestVectorsV5(t *testing.T) { }, { name: "v5.1-ping-message", - packet: &pingV5{ + packet: &Ping{ ReqID: []byte{0, 0, 0, 1}, ENRSeq: 2, }, @@ -293,27 +295,27 @@ func TestTestVectorsV5(t *testing.T) { }, { name: "v5.1-ping-handshake", - packet: &pingV5{ + packet: &Ping{ ReqID: []byte{0, 0, 0, 1}, ENRSeq: 1, }, challenge: challenge1, prep: func(net *handshakeTest) { c := *challenge1 - c.node = net.nodeA.n() + c.Node = net.nodeA.n() net.nodeB.c.sc.storeSentHandshake(idA, addr, &c) }, }, { name: "v5.1-ping-handshake-enr", - packet: &pingV5{ + packet: &Ping{ ReqID: []byte{0, 0, 0, 1}, ENRSeq: 1, }, challenge: challenge0, prep: func(net *handshakeTest) { c := *challenge0 - c.node = net.nodeA.n() + c.Node = net.nodeA.n() net.nodeB.c.sc.storeSentHandshake(idA, addr, &c) }, }, @@ -332,8 +334,8 @@ func TestTestVectorsV5(t *testing.T) { file := filepath.Join("testdata", test.name+".txt") if *writeTestVectorsFlag { // Override all random inputs. - net.nodeA.c.sc.nonceGen = func(counter uint32) (packetNonce, error) { - return packetNonce{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, nil + net.nodeA.c.sc.nonceGen = func(counter uint32) (Nonce, error) { + return Nonce{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, nil } net.nodeA.c.sc.maskingIVGen = func(buf []byte) error { return nil // all zero @@ -347,15 +349,15 @@ func TestTestVectorsV5(t *testing.T) { writeTestVector(file, comment, d) } enc := hexFile(file) - net.nodeB.expectDecode(t, test.packet.kind(), enc) + net.nodeB.expectDecode(t, test.packet.Kind(), enc) }) } } // testVectorComment creates the commentary for discv5 test vector files. -func testVectorComment(net *handshakeTest, p packetV5, challenge *whoareyouV5, nonce packetNonce) string { +func testVectorComment(net *handshakeTest, p Packet, challenge *Whoareyou, nonce Nonce) string { o := new(strings.Builder) - printWhoareyou := func(p *whoareyouV5) { + printWhoareyou := func(p *Whoareyou) { fmt.Fprintf(o, "whoareyou.request-nonce = %#x\n", p.AuthTag[:]) fmt.Fprintf(o, "whoareyou.id-nonce = %#x\n", p.IDNonce[:]) fmt.Fprintf(o, "whoareyou.enr-seq = %d\n", p.RecordSeq) @@ -364,10 +366,10 @@ func testVectorComment(net *handshakeTest, p packetV5, challenge *whoareyouV5, n fmt.Fprintf(o, "src-node-id = %#x\n", net.nodeA.id().Bytes()) fmt.Fprintf(o, "dest-node-id = %#x\n", net.nodeB.id().Bytes()) switch p := p.(type) { - case *whoareyouV5: + case *Whoareyou: // WHOAREYOU packet. printWhoareyou(p) - case *pingV5: + case *Ping: fmt.Fprintf(o, "nonce = %#x\n", nonce[:]) fmt.Fprintf(o, "read-key = %#x\n", net.nodeA.c.sc.session(net.nodeB.id(), net.nodeB.addr()).writeKey) fmt.Fprintf(o, "ping.req-id = %#x\n", p.ReqID) @@ -392,21 +394,21 @@ func BenchmarkV5_DecodeHandshakePingSecp256k1(b *testing.B) { var ( idA = net.nodeA.id() - challenge = &whoareyouV5{node: net.nodeB.n()} - message = &pingV5{ReqID: []byte("reqid")} + challenge = &Whoareyou{Node: net.nodeB.n()} + message = &Ping{ReqID: []byte("reqid")} ) - enc, _, err := net.nodeA.c.encode(net.nodeB.id(), "", message, challenge) + enc, _, err := net.nodeA.c.Encode(net.nodeB.id(), "", message, challenge) if err != nil { b.Fatal("can't encode handshake packet") } - challenge.node = nil // force ENR signature verification in decoder + challenge.Node = nil // force ENR signature verification in decoder b.ResetTimer() input := make([]byte, len(enc)) for i := 0; i < b.N; i++ { copy(input, enc) net.nodeB.c.sc.storeSentHandshake(idA, "", challenge) - _, _, _, err := net.nodeB.c.decode(input, "") + _, _, _, err := net.nodeB.c.Decode(input, "") if err != nil { b.Fatal(err) } @@ -425,8 +427,8 @@ func BenchmarkV5_DecodePing(b *testing.B) { net.nodeA.c.sc.storeNewSession(net.nodeB.id(), net.nodeB.addr(), session) net.nodeB.c.sc.storeNewSession(net.nodeA.id(), net.nodeA.addr(), session.keysFlipped()) addrB := net.nodeA.addr() - ping := &pingV5{ReqID: []byte("reqid"), ENRSeq: 5} - enc, _, err := net.nodeA.c.encode(net.nodeB.id(), addrB, ping, nil) + ping := &Ping{ReqID: []byte("reqid"), ENRSeq: 5} + enc, _, err := net.nodeA.c.Encode(net.nodeB.id(), addrB, ping, nil) if err != nil { b.Fatalf("can't encode: %v", err) } @@ -435,8 +437,8 @@ func BenchmarkV5_DecodePing(b *testing.B) { input := make([]byte, len(enc)) for i := 0; i < b.N; i++ { copy(input, enc) - _, _, packet, _ := net.nodeB.c.decode(input, addrB) - if _, ok := packet.(*pingV5); !ok { + _, _, packet, _ := net.nodeB.c.Decode(input, addrB) + if _, ok := packet.(*Ping); !ok { b.Fatalf("wrong packet type %T", packet) } } @@ -451,7 +453,7 @@ type handshakeTest struct { type handshakeTestNode struct { ln *enode.LocalNode - c *wireCodec + c *Codec } func newHandshakeTest() *handshakeTest { @@ -473,41 +475,43 @@ func (n *handshakeTestNode) init(key *ecdsa.PrivateKey, ip net.IP, clock mclock. if n.ln.Node().Seq() != 1 { panic(fmt.Errorf("unexpected seq %d", n.ln.Node().Seq())) } - n.c = newWireCodec(n.ln, key, clock) + n.c = NewCodec(n.ln, key, clock) } -func (n *handshakeTestNode) encode(t testing.TB, to handshakeTestNode, p packetV5) ([]byte, packetNonce) { +func (n *handshakeTestNode) encode(t testing.TB, to handshakeTestNode, p Packet) ([]byte, Nonce) { t.Helper() return n.encodeWithChallenge(t, to, nil, p) } -func (n *handshakeTestNode) encodeWithChallenge(t testing.TB, to handshakeTestNode, c *whoareyouV5, p packetV5) ([]byte, packetNonce) { +func (n *handshakeTestNode) encodeWithChallenge(t testing.TB, to handshakeTestNode, c *Whoareyou, p Packet) ([]byte, Nonce) { t.Helper() + // Copy challenge and add destination node. This avoids sharing 'c' among the two codecs. - var challenge *whoareyouV5 + var challenge *Whoareyou if c != nil { challengeCopy := *c challenge = &challengeCopy - challenge.node = to.n() + challenge.Node = to.n() } // Encode to destination. - enc, nonce, err := n.c.encode(to.id(), to.addr(), p, challenge) + enc, nonce, err := n.c.Encode(to.id(), to.addr(), p, challenge) if err != nil { t.Fatal(fmt.Errorf("(%s) %v", n.ln.ID().TerminalString(), err)) } - t.Logf("(%s) -> (%s) %s\n%s", n.ln.ID().TerminalString(), to.id().TerminalString(), p.name(), hex.Dump(enc)) + t.Logf("(%s) -> (%s) %s\n%s", n.ln.ID().TerminalString(), to.id().TerminalString(), p.Name(), hex.Dump(enc)) return enc, nonce } -func (n *handshakeTestNode) expectDecode(t *testing.T, ptype byte, p []byte) packetV5 { +func (n *handshakeTestNode) expectDecode(t *testing.T, ptype byte, p []byte) Packet { t.Helper() + dec, err := n.decode(p) if err != nil { t.Fatal(fmt.Errorf("(%s) %v", n.ln.ID().TerminalString(), err)) } t.Logf("(%s) %#v", n.ln.ID().TerminalString(), pp.NewFormatter(dec)) - if dec.kind() != ptype { - t.Fatalf("expected packet type %d, got %d", ptype, dec.kind()) + if dec.Kind() != ptype { + t.Fatalf("expected packet type %d, got %d", ptype, dec.Kind()) } return dec } @@ -519,8 +523,8 @@ func (n *handshakeTestNode) expectDecodeErr(t *testing.T, wantErr error, p []byt } } -func (n *handshakeTestNode) decode(input []byte) (packetV5, error) { - _, _, p, err := n.c.decode(input, "127.0.0.1") +func (n *handshakeTestNode) decode(input []byte) (Packet, error) { + _, _, p, err := n.c.Decode(input, "127.0.0.1") return p, err } @@ -535,3 +539,58 @@ func (n *handshakeTestNode) addr() string { func (n *handshakeTestNode) id() enode.ID { return n.ln.ID() } + +// hexFile reads the given file and decodes the hex data contained in it. +// Whitespace and any lines beginning with the # character are ignored. +func hexFile(file string) []byte { + fileContent, err := ioutil.ReadFile(file) + if err != nil { + panic(err) + } + + // Gather hex data, ignore comments. + var text []byte + for _, line := range bytes.Split(fileContent, []byte("\n")) { + line = bytes.TrimSpace(line) + if len(line) > 0 && line[0] == '#' { + continue + } + text = append(text, line...) + } + + // Parse the hex. + if bytes.HasPrefix(text, []byte("0x")) { + text = text[2:] + } + data := make([]byte, hex.DecodedLen(len(text))) + if _, err := hex.Decode(data, text); err != nil { + panic("invalid hex in " + file) + } + return data +} + +// writeTestVector writes a test vector file with the given commentary and binary data. +func writeTestVector(file, comment string, data []byte) { + fd, err := os.OpenFile(file, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + panic(err) + } + defer fd.Close() + + if len(comment) > 0 { + for _, line := range strings.Split(strings.TrimSpace(comment), "\n") { + fmt.Fprintf(fd, "# %s\n", line) + } + fmt.Fprintln(fd) + } + for len(data) > 0 { + var chunk []byte + if len(data) < 32 { + chunk = data + } else { + chunk = data[:32] + } + data = data[len(chunk):] + fmt.Fprintf(fd, "%x\n", chunk) + } +} diff --git a/p2p/discover/v5wire/msg.go b/p2p/discover/v5wire/msg.go new file mode 100644 index 00000000000..24411d0b5b5 --- /dev/null +++ b/p2p/discover/v5wire/msg.go @@ -0,0 +1,229 @@ +// Copyright 2019 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package v5wire + +import ( + "fmt" + "net" + + "github.com/ethereum/go-ethereum/common/mclock" + "github.com/ethereum/go-ethereum/p2p/enode" + "github.com/ethereum/go-ethereum/p2p/enr" + "github.com/ethereum/go-ethereum/rlp" +) + +// Packet is implemented by all message types. +type Packet interface { + // ??? Src() enode.ID // Src returns the source node ID of the packet. + Name() string // Name returns a string corresponding to the message type. + Kind() byte // Kind returns the message type. + SetReqID([]byte) // Sets the request ID. +} + +// Message types. +const ( + PingMsg byte = iota + 1 + PongMsg + FindnodeMsg + NodesMsg + TalkRequestMsg + TalkResponseMsg + RequestTicketMsg + TicketMsg + RegtopicMsg + RegconfirmationMsg + TopicQueryMsg + + UnknownPacket = byte(255) // any non-decryptable packet + WhoareyouPacket = byte(254) // the WHOAREYOU packet +) + +// Protocol messages. +type ( + // Unknown represents any packet that can't be decrypted. + Unknown struct { + AuthTag Nonce + } + + // WHOAREYOU contains the handshake challenge. + Whoareyou struct { + AuthTag Nonce + IDNonce [32]byte // To be signed by recipient. + RecordSeq uint64 // ENR sequence number of recipient + + Node *enode.Node // Locally known node records of recipient. + sent mclock.AbsTime + } + + // PING is sent during liveness checks. + Ping struct { + ReqID []byte + ENRSeq uint64 + } + + // PONG is the reply to PING. + Pong struct { + ReqID []byte + ENRSeq uint64 + ToIP net.IP // These fields should mirror the UDP envelope address of the ping + ToPort uint16 // packet, which provides a way to discover the the external address (after NAT). + } + + // FINDNODE is a query for nodes in the given bucket. + Findnode struct { + ReqID []byte + Distances []uint + } + + // NODES is the reply to FINDNODE and TOPICQUERY. + Nodes struct { + ReqID []byte + Total uint8 + Nodes []*enr.Record + } + + // TALKREQ is an application-level request. + TalkRequest struct { + ReqID []byte + Protocol string + Message []byte + } + + // TALKRESP is the reply to TALKREQ. + TalkResponse struct { + ReqID []byte + Message []byte + } + + // REQUESTTICKET requests a ticket for a topic queue. + RequestTicket struct { + ReqID []byte + Topic []byte + } + + // TICKET is the response to REQUESTTICKET. + Ticket struct { + ReqID []byte + Ticket []byte + } + + // REGTOPIC registers the sender in a topic queue using a ticket. + Regtopic struct { + ReqID []byte + Ticket []byte + ENR *enr.Record + } + + // REGCONFIRMATION is the reply to REGTOPIC. + Regconfirmation struct { + ReqID []byte + Registered bool + } + + // TOPICQUERY asks for nodes with the given topic. + TopicQuery struct { + ReqID []byte + Topic []byte + } +) + +// DecodeMessage decodes the message body of a packet. +func DecodeMessage(ptype byte, body []byte) (Packet, error) { + var dec Packet + switch ptype { + case PingMsg: + dec = new(Ping) + case PongMsg: + dec = new(Pong) + case FindnodeMsg: + dec = new(Findnode) + case NodesMsg: + dec = new(Nodes) + case TalkRequestMsg: + dec = new(TalkRequest) + case TalkResponseMsg: + dec = new(TalkResponse) + case RequestTicketMsg: + dec = new(RequestTicket) + case TicketMsg: + dec = new(Ticket) + case RegtopicMsg: + dec = new(Regtopic) + case RegconfirmationMsg: + dec = new(Regconfirmation) + case TopicQueryMsg: + dec = new(TopicQuery) + default: + return nil, fmt.Errorf("unknown packet type %d", ptype) + } + if err := rlp.DecodeBytes(body, dec); err != nil { + return nil, err + } + return dec, nil +} + +func (*Whoareyou) Name() string { return "WHOAREYOU/v5" } +func (*Whoareyou) Kind() byte { return WhoareyouPacket } +func (*Whoareyou) SetReqID([]byte) {} + +func (*Unknown) Name() string { return "UNKNOWN/v5" } +func (*Unknown) Kind() byte { return UnknownPacket } +func (*Unknown) SetReqID([]byte) {} + +func (*Ping) Name() string { return "PING/v5" } +func (*Ping) Kind() byte { return PingMsg } +func (p *Ping) SetReqID(id []byte) { p.ReqID = id } + +func (*Pong) Name() string { return "PONG/v5" } +func (*Pong) Kind() byte { return PongMsg } +func (p *Pong) SetReqID(id []byte) { p.ReqID = id } + +func (*Findnode) Name() string { return "FINDNODE/v5" } +func (*Findnode) Kind() byte { return FindnodeMsg } +func (p *Findnode) SetReqID(id []byte) { p.ReqID = id } + +func (*Nodes) Name() string { return "NODES/v5" } +func (*Nodes) Kind() byte { return NodesMsg } +func (p *Nodes) SetReqID(id []byte) { p.ReqID = id } + +func (*TalkRequest) Name() string { return "TALKREQ/v5" } +func (*TalkRequest) Kind() byte { return TalkRequestMsg } +func (p *TalkRequest) SetReqID(id []byte) { p.ReqID = id } + +func (*TalkResponse) Name() string { return "TALKRESP/v5" } +func (*TalkResponse) Kind() byte { return TalkResponseMsg } +func (p *TalkResponse) SetReqID(id []byte) { p.ReqID = id } + +func (*RequestTicket) Name() string { return "REQTICKET/v5" } +func (*RequestTicket) Kind() byte { return RequestTicketMsg } +func (p *RequestTicket) SetReqID(id []byte) { p.ReqID = id } + +func (*Regtopic) Name() string { return "REGTOPIC/v5" } +func (*Regtopic) Kind() byte { return RegtopicMsg } +func (p *Regtopic) SetReqID(id []byte) { p.ReqID = id } + +func (*Ticket) Name() string { return "TICKET/v5" } +func (*Ticket) Kind() byte { return TicketMsg } +func (p *Ticket) SetReqID(id []byte) { p.ReqID = id } + +func (*Regconfirmation) Name() string { return "REGCONFIRMATION/v5" } +func (*Regconfirmation) Kind() byte { return RegconfirmationMsg } +func (p *Regconfirmation) SetReqID(id []byte) { p.ReqID = id } + +func (*TopicQuery) Name() string { return "TOPICQUERY/v5" } +func (*TopicQuery) Kind() byte { return TopicQueryMsg } +func (p *TopicQuery) SetReqID(id []byte) { p.ReqID = id } diff --git a/p2p/discover/v5_session.go b/p2p/discover/v5wire/session.go similarity index 78% rename from p2p/discover/v5_session.go rename to p2p/discover/v5wire/session.go index c677ef874fd..36e38cca9f0 100644 --- a/p2p/discover/v5_session.go +++ b/p2p/discover/v5wire/session.go @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . -package discover +package v5wire import ( "crypto/ecdsa" @@ -27,14 +27,15 @@ import ( "github.com/hashicorp/golang-lru/simplelru" ) -// The sessionCache keeps negotiated encryption keys and +// The SessionCache keeps negotiated encryption keys and // state for in-progress handshakes in the Discovery v5 wire protocol. -type sessionCache struct { +type SessionCache struct { sessions *simplelru.LRU - handshakes map[sessionID]*whoareyouV5 + handshakes map[sessionID]*Whoareyou clock mclock.Clock + // hooks for overriding randomness. - nonceGen func(uint32) (packetNonce, error) + nonceGen func(uint32) (Nonce, error) maskingIVGen func([]byte) error ephemeralKeyGen func() (*ecdsa.PrivateKey, error) } @@ -57,14 +58,14 @@ func (s *session) keysFlipped() *session { return &session{s.readKey, s.writeKey, s.nonceCounter} } -func newSessionCache(maxItems int, clock mclock.Clock) *sessionCache { +func NewSessionCache(maxItems int, clock mclock.Clock) *SessionCache { cache, err := simplelru.NewLRU(maxItems, nil) if err != nil { panic("can't create session cache") } - return &sessionCache{ + return &SessionCache{ sessions: cache, - handshakes: make(map[sessionID]*whoareyouV5), + handshakes: make(map[sessionID]*Whoareyou), clock: clock, nonceGen: generateNonce, maskingIVGen: generateMaskingIV, @@ -72,7 +73,7 @@ func newSessionCache(maxItems int, clock mclock.Clock) *sessionCache { } } -func generateNonce(counter uint32) (n packetNonce, err error) { +func generateNonce(counter uint32) (n Nonce, err error) { binary.BigEndian.PutUint32(n[:4], counter) _, err = crand.Read(n[4:]) return n, err @@ -84,13 +85,13 @@ func generateMaskingIV(buf []byte) error { } // nextNonce creates a nonce for encrypting a message to the given session. -func (sc *sessionCache) nextNonce(s *session) (packetNonce, error) { +func (sc *SessionCache) nextNonce(s *session) (Nonce, error) { s.nonceCounter++ return sc.nonceGen(s.nonceCounter) } // session returns the current session for the given node, if any. -func (sc *sessionCache) session(id enode.ID, addr string) *session { +func (sc *SessionCache) session(id enode.ID, addr string) *session { item, ok := sc.sessions.Get(sessionID{id, addr}) if !ok { return nil @@ -99,7 +100,7 @@ func (sc *sessionCache) session(id enode.ID, addr string) *session { } // readKey returns the current read key for the given node. -func (sc *sessionCache) readKey(id enode.ID, addr string) []byte { +func (sc *SessionCache) readKey(id enode.ID, addr string) []byte { if s := sc.session(id, addr); s != nil { return s.readKey } @@ -107,28 +108,28 @@ func (sc *sessionCache) readKey(id enode.ID, addr string) []byte { } // storeNewSession stores new encryption keys in the cache. -func (sc *sessionCache) storeNewSession(id enode.ID, addr string, s *session) { +func (sc *SessionCache) storeNewSession(id enode.ID, addr string, s *session) { sc.sessions.Add(sessionID{id, addr}, s) } // getHandshake gets the handshake challenge we previously sent to the given remote node. -func (sc *sessionCache) getHandshake(id enode.ID, addr string) *whoareyouV5 { +func (sc *SessionCache) getHandshake(id enode.ID, addr string) *Whoareyou { return sc.handshakes[sessionID{id, addr}] } // storeSentHandshake stores the handshake challenge sent to the given remote node. -func (sc *sessionCache) storeSentHandshake(id enode.ID, addr string, challenge *whoareyouV5) { +func (sc *SessionCache) storeSentHandshake(id enode.ID, addr string, challenge *Whoareyou) { challenge.sent = sc.clock.Now() sc.handshakes[sessionID{id, addr}] = challenge } // deleteHandshake deletes handshake data for the given node. -func (sc *sessionCache) deleteHandshake(id enode.ID, addr string) { +func (sc *SessionCache) deleteHandshake(id enode.ID, addr string) { delete(sc.handshakes, sessionID{id, addr}) } // handshakeGC deletes timed-out handshakes. -func (sc *sessionCache) handshakeGC() { +func (sc *SessionCache) handshakeGC() { deadline := sc.clock.Now().Add(-handshakeTimeout) for key, challenge := range sc.handshakes { if challenge.sent < deadline { diff --git a/p2p/discover/testdata/v5.1-ping-handshake-enr.txt b/p2p/discover/v5wire/testdata/v5.1-ping-handshake-enr.txt similarity index 100% rename from p2p/discover/testdata/v5.1-ping-handshake-enr.txt rename to p2p/discover/v5wire/testdata/v5.1-ping-handshake-enr.txt diff --git a/p2p/discover/testdata/v5.1-ping-handshake.txt b/p2p/discover/v5wire/testdata/v5.1-ping-handshake.txt similarity index 100% rename from p2p/discover/testdata/v5.1-ping-handshake.txt rename to p2p/discover/v5wire/testdata/v5.1-ping-handshake.txt diff --git a/p2p/discover/testdata/v5.1-ping-message.txt b/p2p/discover/v5wire/testdata/v5.1-ping-message.txt similarity index 100% rename from p2p/discover/testdata/v5.1-ping-message.txt rename to p2p/discover/v5wire/testdata/v5.1-ping-message.txt diff --git a/p2p/discover/testdata/v5.1-whoareyou.txt b/p2p/discover/v5wire/testdata/v5.1-whoareyou.txt similarity index 100% rename from p2p/discover/testdata/v5.1-whoareyou.txt rename to p2p/discover/v5wire/testdata/v5.1-whoareyou.txt From 38091a9aa1b77acf465e3788938493932999a561 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Tue, 22 Sep 2020 09:37:02 +0200 Subject: [PATCH 18/70] p2p/discover/v5wire: regenerate handshake test vectors --- .../testdata/v5.1-ping-handshake-enr.txt | 21 +++++++++---------- .../v5wire/testdata/v5.1-ping-handshake.txt | 13 ++++++------ 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/p2p/discover/v5wire/testdata/v5.1-ping-handshake-enr.txt b/p2p/discover/v5wire/testdata/v5.1-ping-handshake-enr.txt index dc72be12520..b9d65092bc4 100644 --- a/p2p/discover/v5wire/testdata/v5.1-ping-handshake-enr.txt +++ b/p2p/discover/v5wire/testdata/v5.1-ping-handshake-enr.txt @@ -14,14 +14,13 @@ # ephemeral-pubkey = 0x039a003ba6517b473fa0cd74aefe99dadfdb34627f90fec6362df85803908f53a5 00000000000000000000000000000000088b3d4342776668980a4adf72a8fcaa -963f24b27a2f6bb44c7ed5ca10e87de130f94d2390b9853c3dcaa0d51e9472d4 -3c9ae48d04689ef4d3d2602a5e89ac340f9e81e722b1d7dac2578d520dd5bc6d -c1e38ad3ab33012be1a5d259267a0947bf242219834c5702d1c694c0ceb4a6a2 -7b5d68bd2c2e32e6cb9696706adff216ab862a9186875f9494150c4ae06fa4d1 -f0396c93f215fa4ef52417d9c40a31564e8d5f31a7f08c38045ff5e30d966183 -8b1eabee9f1e561120bcc4d9f2f9c839152b4ab970e029b2395b97e8c3aa8d3b -497ee98a15e865bcd34effa8b83eb6396bca60ad8f0bff1e047e278454bc2b3d -6404c12106a9d0b6107fc2383976fc05fbda2c954d402c28c8fb53a2b3a4b111 -c286ba2ac4ff880168323c6e97b01dbcbeef4f234e5849f75ab007217c919820 -aaa1c8a7926d3625917fccc3d4569a69fd8aca026be87afab8e8e645d1ee8889 -92 +963f24b27a2f6bb44c7ed5ca10e87de130f94d2390b9853c3dcb41d51e9472d4 +3c9ae48d04689ef4d3b340a9cb02d3f5cb5c73f266876372a497ef20dccc83ee +bcf61f61bc2bb13655118c2dddd4fa7f66210832e7c45c2af87b635121ae1320 +57cce99aa7d2760b31390fea5142053c97feb5fc3f5d0ff3d71008a5b6724bbf +c8c97746524e695129d2bd68c13b95e8687264923226593c1cdb64d1377d3b68 +b3b755f98ed4631901e5e67c75b838b759df976df74dc60a07cc7c20a3102303 +bf6e56b02560c31d8383f2804c32fec46eef5d0b79b4c30b247350069605025c +86c70190214e6ebda6a9c61c90401dafe99dcf42189ad5f1f9a57322e640c851 +db1247e69e618d3947987fccc3d4569a69fdaf27278e478b0689761155de48e0 +3055 diff --git a/p2p/discover/v5wire/testdata/v5.1-ping-handshake.txt b/p2p/discover/v5wire/testdata/v5.1-ping-handshake.txt index f23eccf0a68..57e1561f18d 100644 --- a/p2p/discover/v5wire/testdata/v5.1-ping-handshake.txt +++ b/p2p/discover/v5wire/testdata/v5.1-ping-handshake.txt @@ -14,10 +14,9 @@ # ephemeral-pubkey = 0x039a003ba6517b473fa0cd74aefe99dadfdb34627f90fec6362df85803908f53a5 00000000000000000000000000000000088b3d4342776668980a4adf72a8fcaa -963f24b27a2f6bb44c7ed5ca10e87de130f94d2390b9853c3dcb21d51e9472d4 -3c9ae48d04689ef4d3d2602a5e89ac340f9e81e722b1d7dac2578d520dd5bc6d -c1e38ad3ab33012be1a5d259267a0947bf242219834c5702d1c694c0ceb4a6a2 -7b5d68bd2c2e32e6cb9696706adff216ab862a9186875f9494150c4ae06fa4d1 -f0396c93f215fa4ef52417d9c40a31564e8d5f31a7f08c38045ff5e30d966183 -8b1eabee9f1e561120bc7fccc3d4569a69fdf04f31230ae4be20404467d9ea9a -b3cd +963f24b27a2f6bb44c7ed5ca10e87de130f94d2390b9853c3dcbded51e9472d4 +3c9ae48d04689ef4d3b340a9cb02d3f5cb5c73f266876372a497ef20dccc83ee +bcf61f61bc2bb13655118c2dddd4fa7f66210832e7c45c2af87b635121ae1320 +57cce99aa7d2760b31390fea5142053c97feb5fc3f5d0ff3d71008a5b6724bbf +c8c97746524e695129d2bd7fccc3d4569a69fd8a783849a117bd23ec5b5d02be +0a0c57 From d5d2f36925b38e760bd48955fe65790e1409cfc9 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Tue, 22 Sep 2020 09:54:03 +0200 Subject: [PATCH 19/70] p2p/discover/v5wire: remove unused code --- p2p/discover/v5wire/encoding.go | 26 ++++++++------------------ 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/p2p/discover/v5wire/encoding.go b/p2p/discover/v5wire/encoding.go index 6ac0339531f..3713295e9d4 100644 --- a/p2p/discover/v5wire/encoding.go +++ b/p2p/discover/v5wire/encoding.go @@ -107,15 +107,14 @@ const ( ) var ( - errTooShort = errors.New("packet too short") - errInvalidHeader = errors.New("invalid packet header") - errUnexpectedHandshake = errors.New("unexpected auth response, not in handshake") - errHandshakeNonceMismatch = errors.New("wrong nonce in auth response") - errInvalidAuthKey = errors.New("invalid ephemeral pubkey") - errNoRecord = errors.New("expected ENR in handshake but none sent") - errInvalidNonceSig = errors.New("invalid ID nonce signature") - errMessageTooShort = errors.New("message contains no data") - errMessageDecrypt = errors.New("cannot decrypt message") + errTooShort = errors.New("packet too short") + errInvalidHeader = errors.New("invalid packet header") + errUnexpectedHandshake = errors.New("unexpected auth response, not in handshake") + errInvalidAuthKey = errors.New("invalid ephemeral pubkey") + errNoRecord = errors.New("expected ENR in handshake but none sent") + errInvalidNonceSig = errors.New("invalid ID nonce signature") + errMessageTooShort = errors.New("message contains no data") + errMessageDecrypt = errors.New("cannot decrypt message") ) // Codec encodes and decodes discovery v5 packets. @@ -626,15 +625,6 @@ func (c *Codec) sha256reset() hash.Hash { return c.sha256 } -// sha256sum computes sha256 on the concatenation of inputs. -func (c *Codec) sha256sum(inputs ...[]byte) []byte { - c.sha256.Reset() - for _, b := range inputs { - c.sha256.Write(b) - } - return c.sha256.Sum(nil) -} - // maskOutputPacket applies protocol header masking to a packet sent to destID. func (c *Codec) maskOutputPacket(destID enode.ID, output []byte, headerDataLen int) []byte { masked := make([]byte, sizeofMaskingIV+len(output)) From 4f1a20c107a6d7f6dffb09bf7784f69bc6f91bac Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Tue, 22 Sep 2020 09:55:59 +0200 Subject: [PATCH 20/70] p2p/discover: remove unused method in test --- p2p/discover/table_util_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/p2p/discover/table_util_test.go b/p2p/discover/table_util_test.go index 54e0b4bccd4..11bf9147845 100644 --- a/p2p/discover/table_util_test.go +++ b/p2p/discover/table_util_test.go @@ -146,7 +146,6 @@ func (t *pingRecorder) updateRecord(n *enode.Node) { func (t *pingRecorder) Self() *enode.Node { return nullNode } func (t *pingRecorder) lookupSelf() []*enode.Node { return nil } func (t *pingRecorder) lookupRandom() []*enode.Node { return nil } -func (t *pingRecorder) close() {} // ping simulates a ping request. func (t *pingRecorder) ping(n *enode.Node) (seq uint64, err error) { From f88786f14bb8326f7edfd95bf5b7eed76f6a7cb6 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Tue, 22 Sep 2020 11:23:31 +0200 Subject: [PATCH 21/70] p2p/discover: fix some issues in lookup tests checkNodesEqual only checked the first node, which made the lookup test pass even though it was actually returning an incorrect result. --- p2p/discover/table_util_test.go | 3 ++- p2p/discover/v4_lookup_test.go | 14 +++++++---- p2p/discover/v5_udp.go | 33 ++++++++++++------------- p2p/discover/v5_udp_test.go | 43 ++++++++++++++++++++++++--------- 4 files changed, 59 insertions(+), 34 deletions(-) diff --git a/p2p/discover/table_util_test.go b/p2p/discover/table_util_test.go index 11bf9147845..47a2e7ac3ca 100644 --- a/p2p/discover/table_util_test.go +++ b/p2p/discover/table_util_test.go @@ -187,15 +187,16 @@ func hasDuplicates(slice []*node) bool { return false } +// checkNodesEqual checks whether the two given node lists contain the same nodes. func checkNodesEqual(got, want []*enode.Node) error { if len(got) == len(want) { for i := range got { if !nodeEqual(got[i], want[i]) { goto NotEqual } - return nil } } + return nil NotEqual: output := new(bytes.Buffer) diff --git a/p2p/discover/v4_lookup_test.go b/p2p/discover/v4_lookup_test.go index 52d7323d3ac..a00de9ca18c 100644 --- a/p2p/discover/v4_lookup_test.go +++ b/p2p/discover/v4_lookup_test.go @@ -279,17 +279,21 @@ func (tn *preminedTestnet) nodesAtDistance(dist int) []v4wire.Node { return result } -func (tn *preminedTestnet) neighborsAtDistance(base *enode.Node, distance uint, elems int) []*enode.Node { - nodes := nodesByDistance{target: base.ID()} +func (tn *preminedTestnet) neighborsAtDistances(base *enode.Node, distances []uint, elems int) []*enode.Node { + var result []*enode.Node for d := range lookupTestnet.dists { for i := range lookupTestnet.dists[d] { n := lookupTestnet.node(d, i) - if uint(enode.LogDist(n.ID(), base.ID())) == distance { - nodes.push(wrapNode(n), elems) + d := enode.LogDist(base.ID(), n.ID()) + if containsUint(uint(d), distances) { + result = append(result, n) + if len(result) >= elems { + return result + } } } } - return unwrapNodes(nodes.entries) + return result } func (tn *preminedTestnet) closest(n int) (nodes []*enode.Node) { diff --git a/p2p/discover/v5_udp.go b/p2p/discover/v5_udp.go index c40c6c8d4e5..d0b8008241e 100644 --- a/p2p/discover/v5_udp.go +++ b/p2p/discover/v5_udp.go @@ -679,7 +679,7 @@ func (t *UDPv5) getNode(id enode.ID) *enode.Node { return nil } -// handle handles incoming packets according to their message type. +// handle processes incoming packets according to their message type. func (t *UDPv5) handle(p v5wire.Packet, fromID enode.ID, fromAddr *net.UDPAddr) { switch p := p.(type) { case *v5wire.Unknown: @@ -722,7 +722,7 @@ var ( func (t *UDPv5) handleWhoareyou(p *v5wire.Whoareyou, fromID enode.ID, fromAddr *net.UDPAddr) { c, err := t.matchWithCall(fromID, p.AuthTag) if err != nil { - t.log.Debug("Invalid "+p.Name(), "addr", fromAddr, "err", err) + t.log.Debug("Invalid "+p.Name(), "id", fromID, "addr", fromAddr, "err", err) return } @@ -759,7 +759,9 @@ func (t *UDPv5) handlePing(p *v5wire.Ping, fromID enode.ID, fromAddr *net.UDPAdd // handleFindnode returns nodes to the requester. func (t *UDPv5) handleFindnode(p *v5wire.Findnode, fromID enode.ID, fromAddr *net.UDPAddr) { nodes := t.collectTableNodes(fromAddr.IP, p.Distances, findnodeResultLimit) - t.sendNodes(fromID, fromAddr, p.ReqID, nodes) + for _, resp := range packNodes(p.ReqID, nodes) { + t.sendResponse(fromID, fromAddr, resp) + } } // collectTableNodes creates a FINDNODE result set for the given distances. @@ -799,27 +801,24 @@ func (t *UDPv5) collectTableNodes(rip net.IP, distances []uint, limit int) []*en return nodes } -// sendNodes sends the given records in one or more NODES packets. -func (t *UDPv5) sendNodes(toID enode.ID, toAddr *net.UDPAddr, reqid []byte, nodes []*enode.Node) { +// packNodes creates NODES response packets for the given node list. +func packNodes(reqid []byte, nodes []*enode.Node) []*v5wire.Nodes { + if len(nodes) == 0 { + return []*v5wire.Nodes{{ReqID: reqid, Total: 1}} + } + total := uint8(math.Ceil(float64(len(nodes)) / 3)) - resp := &v5wire.Nodes{ReqID: reqid, Total: total, Nodes: make([]*enr.Record, 3)} - sent := false + var resp []*v5wire.Nodes for len(nodes) > 0 { + p := &v5wire.Nodes{ReqID: reqid, Total: total} items := min(nodesResponseItemLimit, len(nodes)) - resp.Nodes = resp.Nodes[:items] for i := 0; i < items; i++ { - resp.Nodes[i] = nodes[i].Record() + p.Nodes = append(p.Nodes, nodes[i].Record()) } - t.sendResponse(toID, toAddr, resp) nodes = nodes[items:] - sent = true - } - // Ensure at least one response is sent. - if !sent { - resp.Total = 1 - resp.Nodes = nil - t.sendResponse(toID, toAddr, resp) + resp = append(resp, p) } + return resp } // handleTalkRequest runs the talk request handler of the requested protocol. diff --git a/p2p/discover/v5_udp_test.go b/p2p/discover/v5_udp_test.go index 9b745690f79..fbbc1b413d8 100644 --- a/p2p/discover/v5_udp_test.go +++ b/p2p/discover/v5_udp_test.go @@ -24,6 +24,7 @@ import ( "math/rand" "net" "reflect" + "sort" "testing" "time" @@ -36,11 +37,12 @@ import ( ) // Real sockets, real crypto: this test checks end-to-end connectivity for UDPv5. -func TestEndToEndV5(t *testing.T) { +func TestUDPv5_lookupE2E(t *testing.T) { t.Parallel() + const N = 5 var nodes []*UDPv5 - for i := 0; i < 5; i++ { + for i := 0; i < N; i++ { var cfg Config if len(nodes) > 0 { bn := nodes[0].Self() @@ -50,12 +52,22 @@ func TestEndToEndV5(t *testing.T) { nodes = append(nodes, node) defer node.Close() } + last := nodes[N-1] + target := nodes[rand.Intn(N-2)].Self() - last := nodes[len(nodes)-1] - target := nodes[rand.Intn(len(nodes)-2)].Self() + // It is expected that all nodes can be found. + expectedResult := make([]*enode.Node, len(nodes)) + for i := range nodes { + expectedResult[i] = nodes[i].Self() + } + sort.Slice(expectedResult, func(i, j int) bool { + return enode.DistCmp(target.ID(), expectedResult[i].ID(), expectedResult[j].ID()) < 0 + }) + + // Do the lookup. results := last.Lookup(target.ID()) - if len(results) == 0 || results[0].ID() != target.ID() { - t.Fatalf("lookup returned wrong results: %v", results) + if err := checkNodesEqual(results, expectedResult); err != nil { + t.Fatalf("lookup returned wrong results: %v", err) } } @@ -526,7 +538,8 @@ func TestUDPv5_lookup(t *testing.T) { } // Seed table with initial node. - fillTable(test.table, []*node{wrapNode(lookupTestnet.node(256, 0))}) + initialNode := lookupTestnet.node(256, 0) + fillTable(test.table, []*node{wrapNode(initialNode)}) // Start the lookup. resultC := make(chan []*enode.Node, 1) @@ -536,6 +549,7 @@ func TestUDPv5_lookup(t *testing.T) { }() // Answer lookup packets. + asked := make(map[enode.ID]bool) for done := false; !done; { done = test.waitPacketOut(func(p v5wire.Packet, to *net.UDPAddr, _ v5wire.Nonce) { recipient, key := lookupTestnet.nodeByAddr(to) @@ -543,15 +557,22 @@ func TestUDPv5_lookup(t *testing.T) { case *v5wire.Ping: test.packetInFrom(key, to, &v5wire.Pong{ReqID: p.ReqID}) case *v5wire.Findnode: - nodes := lookupTestnet.neighborsAtDistance(recipient, p.Distances[0], 3) - response := &v5wire.Nodes{ReqID: p.ReqID, Total: 1, Nodes: nodesToRecords(nodes)} - test.packetInFrom(key, to, response) + if asked[recipient.ID()] { + t.Error("Asked node", recipient.ID(), "twice") + } + asked[recipient.ID()] = true + nodes := lookupTestnet.neighborsAtDistances(recipient, p.Distances, 16) + t.Logf("Got FINDNODE for %v, returning %d nodes", p.Distances, len(nodes)) + for _, resp := range packNodes(p.ReqID, nodes) { + test.packetInFrom(key, to, resp) + } } }) } // Verify result nodes. - checkLookupResults(t, lookupTestnet, <-resultC) + results := <-resultC + checkLookupResults(t, lookupTestnet, results) } // This test checks the local node can be utilised to set key-values. From 90443f3567ece1973f27d2032dd86354ea832efb Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Tue, 22 Sep 2020 16:27:23 +0200 Subject: [PATCH 22/70] cmd/devp2p: add basic interactive discv5 test suite --- cmd/devp2p/README.md | 86 +++++++++++++ cmd/devp2p/discv4cmd.go | 6 +- cmd/devp2p/discv5cmd.go | 35 ++++++ cmd/devp2p/internal/v5test/discv5tests.go | 141 +++++++++++++++++++++ cmd/devp2p/internal/v5test/framework.go | 142 ++++++++++++++++++++++ 5 files changed, 409 insertions(+), 1 deletion(-) create mode 100644 cmd/devp2p/README.md create mode 100644 cmd/devp2p/internal/v5test/discv5tests.go create mode 100644 cmd/devp2p/internal/v5test/framework.go diff --git a/cmd/devp2p/README.md b/cmd/devp2p/README.md new file mode 100644 index 00000000000..2763c75085a --- /dev/null +++ b/cmd/devp2p/README.md @@ -0,0 +1,86 @@ +# The devp2p command + +The devp2p command line tool is a utility for low-level peer-to-peer debugging and +protocol development purposes. It can do many things. + +### ENR Decoding + +Use `devp2p enrdump ` to verify and display an Ethereum Node Record. + +### Node Key Management + +The `devp2p key ...` command family deals with node key files. + +Run `devp2p key generate mynode.key` to create a new node key in the `mynode.key` file. + +Run `devp2p key to-enode mynode.key -ip 127.0.0.1 -tcp 30303` to create an enode:// URL +corresponding to the given node key and address information. + +### Maintaining DNS Discovery Node Lists + +The devp2p command can create and publish DNS discovery node lists. + +Run `devp2p dns sign ` to update the signature of a DNS discovery tree. + +Run `devp2p dns sync ` to download a complete DNS discovery tree. + +Run `devp2p dns to-cloudflare ` to publish a tree to CloudFlare DNS. + +Run `devp2p dns to-route53 ` to publish a tree to Amazon Route53. + +You can find more information about these commands in the [DNS Discovery Setup Guide][dns-tutorial]. + +### Discovery v4 Utilities + +The `devp2p discv4 ...` command family deals with the [Node Discovery v4][discv4] +protocol. + +Run `devp2p discv4 ping ` to ping a node. + +Run `devp2p discv4 resolve ` to find the most recent node record of a node in +the DHT. + +Run `devp2p discv4 crawl ` to create or update a JSON node set. + +### Discovery v5 Utilities + +The `devp2p discv5 ...` command family deals with the [Node Discovery v5][discv5] +protocol. This protocol is currently under active development. + +Run `devp2p discv5 ping ` to ping a node. + +Run `devp2p discv5 resolve ` to find the most recent node record of a node in +the discv5 DHT. + +Run `devp2p discv5 listen` to run a Discovery v5 node. + +Run `devp2p discv5 crawl ` to create or update a JSON node set containing +discv5 nodes. + +### Discovery Test Suites + +The devp2p command also contains interactive test suites for Discovery v4 and Discovery +v5. + +To run these tests against your implementation, you need to set up a networking +environment where two separate UDP listening addresses are available on the same machine. +The two listening addresses must also be routed such that they are able to reach the node +you want to test. + +For example, if you want to run the test on your local host, and the node under test is +also on the local host, you need to assign two IP addresses (or a larger range) to your +loopback interface. On macOS, this can be done by executing the following command: + + sudo ifconfig lo0 add 127.0.0.2 + +You can now run either test suite as follows: Start the node under test first, ensuring +that it won't talk to the Internet (i.e. disable bootstrapping). An easy way to prevent +unintended connections to the global DHT is listening on `127.0.0.1`. + +Now get the ENR of your node and store it in the `NODE` environment variable. + +Start the test by running `devp2p discv5 test -listen1 127.0.0.1 -listen2 127.0.0.2 $NODE`. + +[dns-tutorial]: https://geth.ethereum.org/docs/developers/dns-discovery-setup +[discv4]: https://github.com/ethereum/devp2p/tree/master/discv4.md +[discv5]: https://github.com/ethereum/devp2p/tree/master/discv5/discv5.md diff --git a/cmd/devp2p/discv4cmd.go b/cmd/devp2p/discv4cmd.go index 99b0957ab37..13bc098b92e 100644 --- a/cmd/devp2p/discv4cmd.go +++ b/cmd/devp2p/discv4cmd.go @@ -294,7 +294,11 @@ func listen(ln *enode.LocalNode, addr string) *net.UDPConn { func parseBootnodes(ctx *cli.Context) ([]*enode.Node, error) { s := params.RinkebyBootnodes if ctx.IsSet(bootnodesFlag.Name) { - s = strings.Split(ctx.String(bootnodesFlag.Name), ",") + input := ctx.String(bootnodesFlag.Name) + if input == "" { + return nil, nil + } + s = strings.Split(input, ",") } nodes := make([]*enode.Node, len(s)) var err error diff --git a/cmd/devp2p/discv5cmd.go b/cmd/devp2p/discv5cmd.go index f871821ea22..66efa25e655 100644 --- a/cmd/devp2p/discv5cmd.go +++ b/cmd/devp2p/discv5cmd.go @@ -18,9 +18,13 @@ package main import ( "fmt" + "os" "time" + "github.com/ethereum/go-ethereum/cmd/devp2p/internal/v5test" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/internal/utesting" + "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/p2p/discover" "gopkg.in/urfave/cli.v1" ) @@ -33,6 +37,7 @@ var ( discv5PingCommand, discv5ResolveCommand, discv5CrawlCommand, + discv5TestCommand, discv5ListenCommand, }, } @@ -53,6 +58,12 @@ var ( Action: discv5Crawl, Flags: []cli.Flag{bootnodesFlag, crawlTimeoutFlag}, } + discv5TestCommand = cli.Command{ + Name: "test", + Usage: "Runs tests against a node", + Action: discv5Test, + Flags: []cli.Flag{testPatternFlag, testListen1Flag, testListen2Flag}, + } discv5ListenCommand = cli.Command{ Name: "listen", Usage: "Runs a node", @@ -103,6 +114,30 @@ func discv5Crawl(ctx *cli.Context) error { return nil } +func discv5Test(ctx *cli.Context) error { + // Disable logging unless explicitly enabled. + if !ctx.GlobalIsSet("verbosity") && !ctx.GlobalIsSet("vmodule") { + log.Root().SetHandler(log.DiscardHandler()) + } + + // Filter and run test cases. + suite := &v5test.Suite{ + Dest: getNodeArg(ctx), + Listen1: ctx.String(testListen1Flag.Name), + Listen2: ctx.String(testListen2Flag.Name), + } + tests := suite.AllTests() + if ctx.IsSet(testPatternFlag.Name) { + tests = utesting.MatchTests(tests, ctx.String(testPatternFlag.Name)) + } + results := utesting.RunTests(tests, os.Stdout) + if fails := utesting.CountFailures(results); fails > 0 { + return fmt.Errorf("%v/%v tests passed.", len(tests)-fails, len(tests)) + } + fmt.Printf("%v/%v passed\n", len(tests), len(tests)) + return nil +} + func discv5Listen(ctx *cli.Context) error { disc := startV5(ctx) defer disc.Close() diff --git a/cmd/devp2p/internal/v5test/discv5tests.go b/cmd/devp2p/internal/v5test/discv5tests.go new file mode 100644 index 00000000000..6ce1a975dbf --- /dev/null +++ b/cmd/devp2p/internal/v5test/discv5tests.go @@ -0,0 +1,141 @@ +// Copyright 2020 The go-ethereum Authors +// This file is part of go-ethereum. +// +// go-ethereum is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// go-ethereum is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with go-ethereum. If not, see . + +package v5test + +import ( + "bytes" + + "github.com/ethereum/go-ethereum/internal/utesting" + "github.com/ethereum/go-ethereum/p2p/discover/v5wire" + "github.com/ethereum/go-ethereum/p2p/enode" + "github.com/ethereum/go-ethereum/p2p/enr" +) + +// Suite is the discv5 test suite. +type Suite struct { + Dest *enode.Node + Listen1, Listen2 string // listening addresses +} + +func (s *Suite) listen() *testenv { + return newTestEnv(s.Dest, s.Listen1, s.Listen2) +} + +func (s *Suite) AllTests() []utesting.Test { + return []utesting.Test{ + {Name: "Ping", Fn: s.TestPing}, + {Name: "TalkRequest", Fn: s.TestTalkRequest}, + {Name: "FindnodeZeroDistance", Fn: s.TestFindnodeZeroDistance}, + } +} + +// This test sends PING and expects a PONG response. +func (s *Suite) TestPing(t *utesting.T) { + te := s.listen() + defer te.close() + + id := te.nextReqID() + resp := te.reqresp(te.l1, &v5wire.Ping{ReqID: id}) + switch resp := resp.(type) { + case *v5wire.Pong: + if !bytes.Equal(resp.ReqID, id) { + t.Fatalf("wrong request ID %x in PONG, want %x", resp.ReqID, id) + } + if !resp.ToIP.Equal(laddr(te.l1).IP) { + t.Fatalf("wrong destination IP %v in PONG, want %v", resp.ToIP, laddr(te.l1).IP) + } + if int(resp.ToPort) != laddr(te.l1).Port { + t.Fatalf("wrong destination port %v in PONG, want %v", resp.ToPort, laddr(te.l1).Port) + } + default: + t.Fatal("expected PONG, got", resp.Name()) + } +} + +// This test sends TALKREQ and expects an empty TALKRESP response. +func (s *Suite) TestTalkRequest(t *utesting.T) { + te := s.listen() + defer te.close() + + // Non-empty request ID. + id := te.nextReqID() + resp := te.reqresp(te.l1, &v5wire.TalkRequest{ReqID: id, Protocol: "test-protocol"}) + switch resp := resp.(type) { + case *v5wire.TalkResponse: + if !bytes.Equal(resp.ReqID, id) { + t.Fatalf("wrong request ID %x in TALKRESP, want %x", resp.ReqID, id) + } + if len(resp.Message) > 0 { + t.Fatalf("non-empty message %x in TALKRESP", resp.Message) + } + default: + t.Fatal("expected TALKRESP, got", resp.Name()) + } + + // Empty request ID. + resp = te.reqresp(te.l1, &v5wire.TalkRequest{Protocol: "test-protocol"}) + switch resp := resp.(type) { + case *v5wire.TalkResponse: + if len(resp.ReqID) > 0 { + t.Fatalf("wrong request ID %x in TALKRESP, want empty byte array", resp.ReqID) + } + if len(resp.Message) > 0 { + t.Fatalf("non-empty message %x in TALKRESP", resp.Message) + } + default: + t.Fatal("expected TALKRESP, got", resp.Name()) + } +} + +// This test checks that the remote node returns itself for FINDNODE with distance zero. +func (s *Suite) TestFindnodeZeroDistance(t *utesting.T) { + te := s.listen() + defer te.close() + + id := te.nextReqID() + resp := te.reqresp(te.l1, &v5wire.Findnode{ReqID: id, Distances: []uint{0}}) + switch resp := resp.(type) { + case *v5wire.Nodes: + if !bytes.Equal(resp.ReqID, id) { + t.Fatalf("wrong request ID %x in NODES, want %x", resp.ReqID, id) + } + if len(resp.Nodes) != 1 { + t.Error("invalid number of entries in NODES response") + } + nodes, err := checkRecords(resp.Nodes) + if err != nil { + t.Errorf("invalid node in NODES response: %v", err) + } + if nodes[0].ID() != te.remote.ID() { + t.Errorf("ID of response node is %v, want %v", nodes[0].ID(), te.remote.ID()) + } + default: + t.Fatal("expected NODES, got", resp.Name()) + } +} + +func checkRecords(records []*enr.Record) ([]*enode.Node, error) { + nodes := make([]*enode.Node, len(records)) + for i := range records { + n, err := enode.New(enode.ValidSchemes, records[i]) + if err != nil { + return nil, err + } + nodes[i] = n + } + return nodes, nil +} diff --git a/cmd/devp2p/internal/v5test/framework.go b/cmd/devp2p/internal/v5test/framework.go new file mode 100644 index 00000000000..2ecc0da474a --- /dev/null +++ b/cmd/devp2p/internal/v5test/framework.go @@ -0,0 +1,142 @@ +// Copyright 2020 The go-ethereum Authors +// This file is part of go-ethereum. +// +// go-ethereum is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// go-ethereum is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with go-ethereum. If not, see . + +package v5test + +import ( + "crypto/ecdsa" + "encoding/binary" + "fmt" + "net" + "time" + + "github.com/ethereum/go-ethereum/common/mclock" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/p2p/discover/v5wire" + "github.com/ethereum/go-ethereum/p2p/enode" +) + +const waitTime = 300 * time.Millisecond + +type testenv struct { + l1, l2 net.PacketConn + localNode *enode.LocalNode + localKey *ecdsa.PrivateKey + remote *enode.Node + remoteAddr *net.UDPAddr + + codec *v5wire.Codec + lastRequest v5wire.Packet + lastChallenge *v5wire.Whoareyou + idCounter uint32 +} + +type errorPacket struct { + err error +} + +func (p *errorPacket) Kind() byte { return 99 } +func (p *errorPacket) Name() string { return fmt.Sprintf("error: %v", p.err) } +func (p *errorPacket) SetReqID([]byte) {} +func (p *errorPacket) Error() string { return p.err.Error() } +func (p *errorPacket) Unwrap() error { return p.err } + +func newTestEnv(dest *enode.Node, listen1, listen2 string) *testenv { + l1, err := net.ListenPacket("udp", fmt.Sprintf("%v:0", listen1)) + if err != nil { + panic(err) + } + l2, err := net.ListenPacket("udp", fmt.Sprintf("%v:0", listen2)) + if err != nil { + panic(err) + } + key, err := crypto.GenerateKey() + if err != nil { + panic(err) + } + db, err := enode.OpenDB("") + if err != nil { + panic(err) + } + ln := enode.NewLocalNode(db, key) + ln.SetStaticIP(laddr(l1).IP) + ln.SetFallbackUDP(laddr(l1).Port) + + return &testenv{ + l1: l1, + l2: l2, + localKey: key, + localNode: ln, + remote: dest, + remoteAddr: &net.UDPAddr{IP: dest.IP(), Port: dest.UDP()}, + codec: v5wire.NewCodec(ln, key, mclock.System{}), + } +} + +func (te *testenv) close() { + te.l1.Close() + te.l2.Close() + te.localNode.Database().Close() +} + +func (te *testenv) nextReqID() []byte { + id := make([]byte, 4) + te.idCounter++ + binary.BigEndian.PutUint32(id, te.idCounter) + return id +} + +func (te *testenv) reqresp(c net.PacketConn, req v5wire.Packet) v5wire.Packet { + te.write(c, req, nil) + resp := te.read(c) + if resp.Kind() == v5wire.WhoareyouPacket { + challenge := resp.(*v5wire.Whoareyou) + challenge.Node = te.remote + te.write(c, req, challenge) + return te.read(c) + } + return resp +} + +func (te *testenv) write(c net.PacketConn, p v5wire.Packet, challenge *v5wire.Whoareyou) { + packet, _, err := te.codec.Encode(te.remote.ID(), te.remoteAddr.String(), p, challenge) + if err != nil { + panic(fmt.Errorf("can't encode %v packet: %v", p.Name(), err)) + } + if _, err := c.WriteTo(packet, te.remoteAddr); err != nil { + panic(fmt.Errorf("can't send %v: %v", p.Name(), err)) + } +} + +func (te *testenv) read(c net.PacketConn) v5wire.Packet { + buf := make([]byte, 1280) + if err := c.SetReadDeadline(time.Now().Add(waitTime)); err != nil { + return &errorPacket{err} + } + n, fromAddr, err := c.ReadFrom(buf) + if err != nil { + return &errorPacket{err} + } + _, _, p, err := te.codec.Decode(buf[:n], fromAddr.String()) + if err != nil { + return &errorPacket{err} + } + return p +} + +func laddr(c net.PacketConn) *net.UDPAddr { + return c.LocalAddr().(*net.UDPAddr) +} From 0e120a6d624466b77722ec7387aa0794e8622a79 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Tue, 22 Sep 2020 16:32:26 +0200 Subject: [PATCH 23/70] cmd/devp2p: improve usage of discv5 test --- cmd/devp2p/discv5cmd.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/devp2p/discv5cmd.go b/cmd/devp2p/discv5cmd.go index 66efa25e655..1d7442144fd 100644 --- a/cmd/devp2p/discv5cmd.go +++ b/cmd/devp2p/discv5cmd.go @@ -60,7 +60,7 @@ var ( } discv5TestCommand = cli.Command{ Name: "test", - Usage: "Runs tests against a node", + Usage: "Runs protocol tests against a node", Action: discv5Test, Flags: []cli.Flag{testPatternFlag, testListen1Flag, testListen2Flag}, } From f337817bcc23e4e794582745cadd438578546682 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Tue, 22 Sep 2020 16:36:22 +0200 Subject: [PATCH 24/70] p2p/discover/v5wire: check flag values better --- p2p/discover/v5wire/encoding.go | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/p2p/discover/v5wire/encoding.go b/p2p/discover/v5wire/encoding.go index 3713295e9d4..fff43b0f875 100644 --- a/p2p/discover/v5wire/encoding.go +++ b/p2p/discover/v5wire/encoding.go @@ -52,7 +52,7 @@ type ( packetHeader struct { ProtocolID [8]byte SrcID enode.ID - Flags byte + Flag byte AuthSize uint16 } @@ -109,6 +109,7 @@ const ( var ( errTooShort = errors.New("packet too short") errInvalidHeader = errors.New("invalid packet header") + errInvalidFlag = errors.New("invalid flag value in header") errUnexpectedHandshake = errors.New("unexpected auth response, not in handshake") errInvalidAuthKey = errors.New("invalid ephemeral pubkey") errNoRecord = errors.New("expected ENR in handshake but none sent") @@ -181,7 +182,7 @@ func (c *Codec) makeHeader(toID enode.ID, flags byte, authsizeExtra int) *packet return &packetHeader{ ProtocolID: protocolIDV5, SrcID: c.localnode.ID(), - Flags: flags, + Flag: flags, AuthSize: uint16(authsize), } } @@ -383,13 +384,15 @@ func (c *Codec) Decode(input []byte, addr string) (src enode.ID, n *enode.Node, mask.XORKeyStream(authData, authData) // Decode auth part and message. - switch { - case head.Flags&flagWhoareyou != 0: + switch head.Flag { + case flagWhoareyou: p, err = c.decodeWhoareyou(&head) - case head.Flags&flagHandshake != 0: + case flagHandshake: n, p, err = c.decodeHandshakeMessage(addr, &head, input) - default: + case flagMessage: p, err = c.decodeMessage(addr, &head, input) + default: + err = errInvalidFlag } return src, n, p, err } From e166b4c2298945ba15272de04c7946d8e9170d9b Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Tue, 22 Sep 2020 18:05:30 +0200 Subject: [PATCH 25/70] p2p/discover/v5wire: move low-level crypto to its own file --- p2p/discover/v5wire/crypto.go | 130 ++++++++++++++++++++++++++++++++ p2p/discover/v5wire/encoding.go | 115 +--------------------------- 2 files changed, 134 insertions(+), 111 deletions(-) create mode 100644 p2p/discover/v5wire/crypto.go diff --git a/p2p/discover/v5wire/crypto.go b/p2p/discover/v5wire/crypto.go new file mode 100644 index 00000000000..9d6d03683bf --- /dev/null +++ b/p2p/discover/v5wire/crypto.go @@ -0,0 +1,130 @@ +package v5wire + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/ecdsa" + "crypto/elliptic" + "errors" + "fmt" + "hash" + + "github.com/ethereum/go-ethereum/common/math" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/p2p/enode" +) + +const ( + // Encryption/authentication parameters. + aesKeySize = 16 + gcmNonceSize = 12 + idNoncePrefix = "discovery-id-nonce" +) + +// Nonce represents a nonce used for AES/GCM. +type Nonce [gcmNonceSize]byte + +// EncodePubkey encodes a public key. +func EncodePubkey(key *ecdsa.PublicKey) []byte { + switch key.Curve { + case crypto.S256(): + return crypto.CompressPubkey(key) + default: + panic("unsupported curve " + key.Curve.Params().Name + " in EncodePubkey") + } +} + +// DecodePubkey decodes a public key in compressed format. +func DecodePubkey(curve elliptic.Curve, e []byte) (*ecdsa.PublicKey, error) { + switch curve { + case crypto.S256(): + if len(e) != 33 { + return nil, errors.New("wrong size public key data") + } + return crypto.DecompressPubkey(e) + default: + return nil, fmt.Errorf("unsupported curve %s in DecodePubkey", curve.Params().Name) + } +} + +// idNonceHash computes the ID signature hash used in the handshake. +func idNonceHash(h hash.Hash, nonce, ephkey []byte) []byte { + h.Reset() + h.Write([]byte(idNoncePrefix)) + h.Write(nonce) + h.Write(ephkey) + return h.Sum(nil) +} + +// makeIDSignature creates the ID nonce signature. +func makeIDSignature(hash hash.Hash, key *ecdsa.PrivateKey, nonce, ephkey []byte) ([]byte, error) { + switch key.Curve { + case crypto.S256(): + input := idNonceHash(hash, nonce, ephkey) + idsig, err := crypto.Sign(input, key) + if err != nil { + return nil, err + } + return idsig[:len(idsig)-1], nil // remove recovery ID + default: + return nil, fmt.Errorf("unsupported curve %s", key.Curve.Params().Name) + } +} + +// verifyIDSignature checks that signature over idnonce was made by the given node. +func verifyIDSignature(hash hash.Hash, nonce, ephkey, sig []byte, n *enode.Node) error { + switch idscheme := n.Record().IdentityScheme(); idscheme { + case "v4": + var pk ecdsa.PublicKey + n.Load((*enode.Secp256k1)(&pk)) // cannot fail because record is valid + input := idNonceHash(hash, nonce, ephkey) + if !crypto.VerifySignature(crypto.FromECDSAPub(&pk), input, sig) { + return errInvalidNonceSig + } + return nil + default: + return fmt.Errorf("can't verify ID nonce signature against scheme %q", idscheme) + } +} + +// ecdh creates a shared secret. +func ecdh(privkey *ecdsa.PrivateKey, pubkey *ecdsa.PublicKey) []byte { + secX, secY := pubkey.ScalarMult(pubkey.X, pubkey.Y, privkey.D.Bytes()) + if secX == nil { + return nil + } + sec := make([]byte, 33) + sec[0] = 0x02 | byte(secY.Bit(0)) + math.ReadBits(secX, sec[1:]) + return sec +} + +// encryptGCM encrypts pt using AES-GCM with the given key and nonce. +func encryptGCM(dest, key, nonce, pt, authData []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + panic(fmt.Errorf("can't create block cipher: %v", err)) + } + aesgcm, err := cipher.NewGCMWithNonceSize(block, gcmNonceSize) + if err != nil { + panic(fmt.Errorf("can't create GCM: %v", err)) + } + return aesgcm.Seal(dest, nonce, pt, authData), nil +} + +// decryptGCM decrypts ct using AES-GCM with the given key and nonce. +func decryptGCM(key, nonce, ct, authData []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, fmt.Errorf("can't create block cipher: %v", err) + } + if len(nonce) != gcmNonceSize { + return nil, fmt.Errorf("invalid GCM nonce size: %d", len(nonce)) + } + aesgcm, err := cipher.NewGCMWithNonceSize(block, gcmNonceSize) + if err != nil { + return nil, fmt.Errorf("can't create GCM: %v", err) + } + pt := make([]byte, 0, len(ct)) + return aesgcm.Open(pt, nonce, ct, authData) +} diff --git a/p2p/discover/v5wire/encoding.go b/p2p/discover/v5wire/encoding.go index fff43b0f875..48bf96d95a5 100644 --- a/p2p/discover/v5wire/encoding.go +++ b/p2p/discover/v5wire/encoding.go @@ -21,7 +21,6 @@ import ( "crypto/aes" "crypto/cipher" "crypto/ecdsa" - "crypto/elliptic" crand "crypto/rand" "crypto/sha256" "encoding/binary" @@ -30,9 +29,7 @@ import ( "hash" "time" - "github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/common/mclock" - "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/p2p/enode" "github.com/ethereum/go-ethereum/p2p/enr" "github.com/ethereum/go-ethereum/rlp" @@ -44,9 +41,6 @@ import ( // TODO add counter to nonce // TODO rehandshake after X packets -// Nonce represents a nonce used for AES/GCM. -type Nonce [gcmNonceSize]byte - // Discovery v5 packet structures. type ( packetHeader struct { @@ -95,13 +89,8 @@ var ( ) const ( - // Encryption/authentication parameters. - aesKeySize = 16 - gcmNonceSize = 12 - idNoncePrefix = "discovery-id-nonce" - handshakeTimeout = time.Second - // Protocol constants. + handshakeTimeout = time.Second handshakeVersion = 1 minVersion = 1 ) @@ -293,7 +282,7 @@ func (c *Codec) makeHandshakeHeader(toID enode.ID, addr string, challenge *Whoar auth.h.PubkeySize = byte(len(auth.pubkey)) // Add ID nonce signature to response. - idsig, err := c.signIDNonce(challenge.IDNonce[:], ephpubkey[:]) + idsig, err := makeIDSignature(c.sha256, c.privkey, challenge.IDNonce[:], ephpubkey[:]) if err != nil { return nil, nil, fmt.Errorf("can't sign: %v", err) } @@ -455,7 +444,7 @@ func (c *Codec) decodeHandshake(fromAddr string, head *packetHeader) (*enode.Nod return nil, Nonce{}, nil, errInvalidAuthKey } // Verify ID nonce signature. - err = c.verifyIDSignature(challenge.IDNonce[:], auth.pubkey, auth.signature, node) + err = verifyIDSignature(c.sha256, challenge.IDNonce[:], auth.pubkey, auth.signature, node) if err != nil { return nil, Nonce{}, nil, err } @@ -566,39 +555,6 @@ func (c *Codec) decryptMessage(input []byte, nonce Nonce, readKey []byte) (Packe return DecodeMessage(message[0], message[1:]) } -// signIDNonce creates the ID nonce signature. -func (c *Codec) signIDNonce(nonce, ephkey []byte) ([]byte, error) { - idsig, err := crypto.Sign(c.idNonceHash(nonce, ephkey), c.privkey) - if err != nil { - return nil, fmt.Errorf("can't sign: %v", err) - } - return idsig[:len(idsig)-1], nil // remove recovery ID -} - -// idNonceHash computes the hash of id nonce with prefix. -func (c *Codec) idNonceHash(nonce, ephkey []byte) []byte { - h := c.sha256reset() - h.Write([]byte(idNoncePrefix)) - h.Write(nonce) - h.Write(ephkey) - return h.Sum(nil) -} - -// verifyIDSignature checks that signature over idnonce was made by the node with given record. -func (c *Codec) verifyIDSignature(nonce, ephkey, sig []byte, n *enode.Node) error { - switch idscheme := n.Record().IdentityScheme(); idscheme { - case "v4": - var pk ecdsa.PublicKey - n.Load((*enode.Secp256k1)(&pk)) // cannot fail because record is valid - if !crypto.VerifySignature(crypto.FromECDSAPub(&pk), c.idNonceHash(nonce, ephkey), sig) { - return errInvalidNonceSig - } - return nil - default: - return fmt.Errorf("can't verify ID nonce signature against scheme %q", idscheme) - } -} - // deriveKeys generates session keys using elliptic-curve Diffie-Hellman key agreement. func (c *Codec) deriveKeys(n1, n2 enode.ID, priv *ecdsa.PrivateKey, pub *ecdsa.PublicKey, challenge *Whoareyou) *session { eph := ecdh(priv, pub) @@ -622,7 +578,7 @@ func (c *Codec) deriveKeys(n1, n2 enode.ID, priv *ecdsa.PrivateKey, pub *ecdsa.P return &sec } -// sha256reset returns the shared hash instance. +// sha256 returns the shared hash instance. func (c *Codec) sha256reset() hash.Hash { c.sha256.Reset() return c.sha256 @@ -646,66 +602,3 @@ func headerMask(destID enode.ID, input []byte) cipher.Stream { } return cipher.NewCTR(block, input[:sizeofMaskingIV]) } - -// ecdh creates a shared secret. -func ecdh(privkey *ecdsa.PrivateKey, pubkey *ecdsa.PublicKey) []byte { - secX, secY := pubkey.ScalarMult(pubkey.X, pubkey.Y, privkey.D.Bytes()) - if secX == nil { - return nil - } - sec := make([]byte, 33) - sec[0] = 0x02 | byte(secY.Bit(0)) - math.ReadBits(secX, sec[1:]) - return sec -} - -// encryptGCM encrypts pt using AES-GCM with the given key and nonce. -func encryptGCM(dest, key, nonce, pt, authData []byte) ([]byte, error) { - block, err := aes.NewCipher(key) - if err != nil { - panic(fmt.Errorf("can't create block cipher: %v", err)) - } - aesgcm, err := cipher.NewGCMWithNonceSize(block, gcmNonceSize) - if err != nil { - panic(fmt.Errorf("can't create GCM: %v", err)) - } - return aesgcm.Seal(dest, nonce, pt, authData), nil -} - -// decryptGCM decrypts ct using AES-GCM with the given key and nonce. -func decryptGCM(key, nonce, ct, authData []byte) ([]byte, error) { - block, err := aes.NewCipher(key) - if err != nil { - return nil, fmt.Errorf("can't create block cipher: %v", err) - } - if len(nonce) != gcmNonceSize { - return nil, fmt.Errorf("invalid GCM nonce size: %d", len(nonce)) - } - aesgcm, err := cipher.NewGCMWithNonceSize(block, gcmNonceSize) - if err != nil { - return nil, fmt.Errorf("can't create GCM: %v", err) - } - pt := make([]byte, 0, len(ct)) - return aesgcm.Open(pt, nonce, ct, authData) -} - -// Pubkey represents an encoded public key. -type Pubkey [33]byte - -// EncodePubkey encodes a public key into the 33-byte compressed format. -func EncodePubkey(key *ecdsa.PublicKey) Pubkey { - var enc Pubkey - copy(enc[:], crypto.CompressPubkey(key)) - return enc -} - -// DecodePubkey decodes a public key from the 33-byte compressed format. -func DecodePubkey(curve elliptic.Curve, e []byte) (*ecdsa.PublicKey, error) { - if len(e) != len(Pubkey{}) { - return nil, errors.New("wrong size public key data") - } - if curve != crypto.S256() { - return nil, errors.New("curves other than secp256k1 are not supported") - } - return crypto.DecompressPubkey(e) -} From ef46177f7d2b5ac8528231c4132abc8e909e4306 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Tue, 22 Sep 2020 18:11:40 +0200 Subject: [PATCH 26/70] p2p/discover/v5wire: avoid ENR pubkey decompression for signature verification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes 10µs from handshake benchmark. Probably not worth it, but it's easy to do. --- p2p/discover/v5wire/crypto.go | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/p2p/discover/v5wire/crypto.go b/p2p/discover/v5wire/crypto.go index 9d6d03683bf..dd48e65fc7e 100644 --- a/p2p/discover/v5wire/crypto.go +++ b/p2p/discover/v5wire/crypto.go @@ -71,14 +71,21 @@ func makeIDSignature(hash hash.Hash, key *ecdsa.PrivateKey, nonce, ephkey []byte } } +// s256raw is an unparsed secp256k1 public key ENR entry. +type s256raw []byte + +func (s256raw) ENRKey() string { return "secp256k1" } + // verifyIDSignature checks that signature over idnonce was made by the given node. func verifyIDSignature(hash hash.Hash, nonce, ephkey, sig []byte, n *enode.Node) error { switch idscheme := n.Record().IdentityScheme(); idscheme { case "v4": - var pk ecdsa.PublicKey - n.Load((*enode.Secp256k1)(&pk)) // cannot fail because record is valid + var pubkey s256raw + if n.Load(&pubkey) != nil { + return errors.New("no secp256k1 public key in record") + } input := idNonceHash(hash, nonce, ephkey) - if !crypto.VerifySignature(crypto.FromECDSAPub(&pk), input, sig) { + if !crypto.VerifySignature(pubkey, input, sig) { return errInvalidNonceSig } return nil From c82c795e8fea5fdbe3c0c4352bdd605f3faf3718 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Mon, 28 Sep 2020 13:40:45 +0200 Subject: [PATCH 27/70] p2p/discover/v5wire: implement id-sig-input change --- p2p/discover/v5wire/crypto.go | 11 ++++++----- p2p/discover/v5wire/encoding.go | 4 ++-- .../v5wire/testdata/v5.1-ping-handshake-enr.txt | 10 +++++----- p2p/discover/v5wire/testdata/v5.1-ping-handshake.txt | 10 +++++----- 4 files changed, 18 insertions(+), 17 deletions(-) diff --git a/p2p/discover/v5wire/crypto.go b/p2p/discover/v5wire/crypto.go index dd48e65fc7e..613dcda1830 100644 --- a/p2p/discover/v5wire/crypto.go +++ b/p2p/discover/v5wire/crypto.go @@ -48,19 +48,20 @@ func DecodePubkey(curve elliptic.Curve, e []byte) (*ecdsa.PublicKey, error) { } // idNonceHash computes the ID signature hash used in the handshake. -func idNonceHash(h hash.Hash, nonce, ephkey []byte) []byte { +func idNonceHash(h hash.Hash, destID enode.ID, nonce, ephkey []byte) []byte { h.Reset() h.Write([]byte(idNoncePrefix)) h.Write(nonce) h.Write(ephkey) + h.Write(destID[:]) return h.Sum(nil) } // makeIDSignature creates the ID nonce signature. -func makeIDSignature(hash hash.Hash, key *ecdsa.PrivateKey, nonce, ephkey []byte) ([]byte, error) { +func makeIDSignature(hash hash.Hash, key *ecdsa.PrivateKey, destID enode.ID, nonce, ephkey []byte) ([]byte, error) { switch key.Curve { case crypto.S256(): - input := idNonceHash(hash, nonce, ephkey) + input := idNonceHash(hash, destID, nonce, ephkey) idsig, err := crypto.Sign(input, key) if err != nil { return nil, err @@ -77,14 +78,14 @@ type s256raw []byte func (s256raw) ENRKey() string { return "secp256k1" } // verifyIDSignature checks that signature over idnonce was made by the given node. -func verifyIDSignature(hash hash.Hash, nonce, ephkey, sig []byte, n *enode.Node) error { +func verifyIDSignature(hash hash.Hash, destID enode.ID, nonce, ephkey, sig []byte, n *enode.Node) error { switch idscheme := n.Record().IdentityScheme(); idscheme { case "v4": var pubkey s256raw if n.Load(&pubkey) != nil { return errors.New("no secp256k1 public key in record") } - input := idNonceHash(hash, nonce, ephkey) + input := idNonceHash(hash, destID, nonce, ephkey) if !crypto.VerifySignature(pubkey, input, sig) { return errInvalidNonceSig } diff --git a/p2p/discover/v5wire/encoding.go b/p2p/discover/v5wire/encoding.go index 48bf96d95a5..2dbb9705832 100644 --- a/p2p/discover/v5wire/encoding.go +++ b/p2p/discover/v5wire/encoding.go @@ -282,7 +282,7 @@ func (c *Codec) makeHandshakeHeader(toID enode.ID, addr string, challenge *Whoar auth.h.PubkeySize = byte(len(auth.pubkey)) // Add ID nonce signature to response. - idsig, err := makeIDSignature(c.sha256, c.privkey, challenge.IDNonce[:], ephpubkey[:]) + idsig, err := makeIDSignature(c.sha256, c.privkey, toID, challenge.IDNonce[:], ephpubkey[:]) if err != nil { return nil, nil, fmt.Errorf("can't sign: %v", err) } @@ -444,7 +444,7 @@ func (c *Codec) decodeHandshake(fromAddr string, head *packetHeader) (*enode.Nod return nil, Nonce{}, nil, errInvalidAuthKey } // Verify ID nonce signature. - err = verifyIDSignature(c.sha256, challenge.IDNonce[:], auth.pubkey, auth.signature, node) + err = verifyIDSignature(c.sha256, c.localnode.ID(), challenge.IDNonce[:], auth.pubkey, auth.signature, node) if err != nil { return nil, Nonce{}, nil, err } diff --git a/p2p/discover/v5wire/testdata/v5.1-ping-handshake-enr.txt b/p2p/discover/v5wire/testdata/v5.1-ping-handshake-enr.txt index b9d65092bc4..3dd9b036fe0 100644 --- a/p2p/discover/v5wire/testdata/v5.1-ping-handshake-enr.txt +++ b/p2p/discover/v5wire/testdata/v5.1-ping-handshake-enr.txt @@ -15,12 +15,12 @@ 00000000000000000000000000000000088b3d4342776668980a4adf72a8fcaa 963f24b27a2f6bb44c7ed5ca10e87de130f94d2390b9853c3dcb41d51e9472d4 -3c9ae48d04689ef4d3b340a9cb02d3f5cb5c73f266876372a497ef20dccc83ee -bcf61f61bc2bb13655118c2dddd4fa7f66210832e7c45c2af87b635121ae1320 -57cce99aa7d2760b31390fea5142053c97feb5fc3f5d0ff3d71008a5b6724bbf +3c9ae48d04689ef4d3b3bfd49cc477b9942ef2fb9f4221652369f24e988582a4 +642bb475ad544bbc5e5bf9a0de9a9f33760fb4eb9b6b6d675f1607c5c5eacae6 +06607548e66eb345fd820fea5142053c97feb5fc3f5d0ff3d71008a5b6724bbf c8c97746524e695129d2bd68c13b95e8687264923226593c1cdb64d1377d3b68 b3b755f98ed4631901e5e67c75b838b759df976df74dc60a07cc7c20a3102303 bf6e56b02560c31d8383f2804c32fec46eef5d0b79b4c30b247350069605025c 86c70190214e6ebda6a9c61c90401dafe99dcf42189ad5f1f9a57322e640c851 -db1247e69e618d3947987fccc3d4569a69fdaf27278e478b0689761155de48e0 -3055 +db1247e69e618d3947987fccc3d4569a69fd8c1f49cf2d5620e27a116b0df169 +4604 diff --git a/p2p/discover/v5wire/testdata/v5.1-ping-handshake.txt b/p2p/discover/v5wire/testdata/v5.1-ping-handshake.txt index 57e1561f18d..33274175405 100644 --- a/p2p/discover/v5wire/testdata/v5.1-ping-handshake.txt +++ b/p2p/discover/v5wire/testdata/v5.1-ping-handshake.txt @@ -15,8 +15,8 @@ 00000000000000000000000000000000088b3d4342776668980a4adf72a8fcaa 963f24b27a2f6bb44c7ed5ca10e87de130f94d2390b9853c3dcbded51e9472d4 -3c9ae48d04689ef4d3b340a9cb02d3f5cb5c73f266876372a497ef20dccc83ee -bcf61f61bc2bb13655118c2dddd4fa7f66210832e7c45c2af87b635121ae1320 -57cce99aa7d2760b31390fea5142053c97feb5fc3f5d0ff3d71008a5b6724bbf -c8c97746524e695129d2bd7fccc3d4569a69fd8a783849a117bd23ec5b5d02be -0a0c57 +3c9ae48d04689ef4d3b3bfd49cc477b9942ef2fb9f4221652369f24e988582a4 +642bb475ad544bbc5e5bf9a0de9a9f33760fb4eb9b6b6d675f1607c5c5eacae6 +06607548e66eb345fd820fea5142053c97feb5fc3f5d0ff3d71008a5b6724bbf +c8c97746524e695129d2bd7fccc3d4569a69fd62f653216b647dcea5f93b5845 +ded334 From c812ec3c6f497080f7497343a034c35b3eabde5a Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Tue, 29 Sep 2020 12:49:23 +0200 Subject: [PATCH 28/70] p2p/discover/v5wire: implement version in protocol-id --- p2p/discover/v5wire/encoding.go | 71 +++++++++++-------- .../testdata/v5.1-ping-handshake-enr.txt | 22 +++--- .../v5wire/testdata/v5.1-ping-handshake.txt | 14 ++-- .../v5wire/testdata/v5.1-ping-message.txt | 4 +- .../v5wire/testdata/v5.1-whoareyou.txt | 2 +- 5 files changed, 61 insertions(+), 52 deletions(-) diff --git a/p2p/discover/v5wire/encoding.go b/p2p/discover/v5wire/encoding.go index 2dbb9705832..6b62a7424b9 100644 --- a/p2p/discover/v5wire/encoding.go +++ b/p2p/discover/v5wire/encoding.go @@ -58,7 +58,6 @@ type ( handshakeAuthData struct { h struct { - Version uint8 // protocol version Nonce Nonce // AES-GCM nonce of message SigSize byte // ignature data PubkeySize byte // offset of @@ -79,22 +78,25 @@ const ( flagHandshake = 2 ) +// Protocol constants. +const ( + handshakeTimeout = time.Second + handshakeVersion = 1 + minVersion = 1 + protocolID = "discv5" + versionOffset = 7 +) + +// Packet sizes. var ( sizeofMaskingIV = 16 sizeofPacketHeaderV5 = binary.Size(packetHeader{}) sizeofWhoareyouAuthDataV5 = binary.Size(whoareyouAuthData{}) sizeofHandshakeAuthDataV5 = binary.Size(handshakeAuthData{}.h) sizeofMessageAuthDataV5 = binary.Size(messageAuthData{}) - protocolIDV5 = [8]byte{'d', 'i', 's', 'c', 'v', '5', ' ', ' '} -) - -const ( - // Protocol constants. - handshakeTimeout = time.Second - handshakeVersion = 1 - minVersion = 1 ) +// Errors. var ( errTooShort = errors.New("packet too short") errInvalidHeader = errors.New("invalid packet header") @@ -152,9 +154,13 @@ func (c *Codec) Encode(id enode.ID, addr string, packet Packet, challenge *Whoar } // makeHeader creates a packet header. -func (c *Codec) makeHeader(toID enode.ID, flags byte, authsizeExtra int) *packetHeader { +func (c *Codec) makeHeader(toID enode.ID, flag byte, authsizeExtra int) *packetHeader { + h := &packetHeader{ + SrcID: c.localnode.ID(), + Flag: flag, + } var authsize int - switch flags { + switch flag { case flagMessage: authsize = sizeofMessageAuthDataV5 case flagWhoareyou: @@ -162,18 +168,16 @@ func (c *Codec) makeHeader(toID enode.ID, flags byte, authsizeExtra int) *packet case flagHandshake: authsize = sizeofHandshakeAuthDataV5 default: - panic(fmt.Errorf("BUG: invalid packet header flags %x", flags)) + panic(fmt.Errorf("BUG: invalid packet header flag %x", flag)) } authsize += authsizeExtra if authsize > int(^uint16(0)) { panic(fmt.Errorf("BUG: auth size %d overflows uint16", authsize)) } - return &packetHeader{ - ProtocolID: protocolIDV5, - SrcID: c.localnode.ID(), - Flag: flags, - AuthSize: uint16(authsize), - } + h.AuthSize = uint16(authsize) + copy(h.ProtocolID[:], protocolID) + h.ProtocolID[versionOffset] = handshakeVersion + return h } // encodeRandom encodes a packet with random content. @@ -264,7 +268,6 @@ func (c *Codec) makeHandshakeHeader(toID enode.ID, addr string, challenge *Whoar } auth := new(handshakeAuthData) - auth.h.Version = handshakeVersion auth.h.Nonce = nonce // Create the ephemeral key. This needs to be first because the @@ -360,10 +363,7 @@ func (c *Codec) Decode(input []byte, addr string) (src enode.ID, n *enode.Node, var head packetHeader c.reader.Reset(input) binary.Read(&c.reader, binary.BigEndian, &head) - if head.ProtocolID != protocolIDV5 { - return enode.ID{}, nil, nil, errInvalidHeader - } - if int(head.AuthSize) > c.reader.Len() { + if !head.isValid(c.reader.Len()) { return enode.ID{}, nil, nil, errInvalidHeader } src = head.SrcID @@ -386,6 +386,18 @@ func (c *Codec) Decode(input []byte, addr string) (src enode.ID, n *enode.Node, return src, n, p, err } +func (h *packetHeader) isValid(packetLen int) bool { + for i := range protocolID { + if h.ProtocolID[i] != protocolID[i] { + return false + } + } + if h.ProtocolID[versionOffset] < minVersion { + return false + } + return int(h.AuthSize) <= packetLen +} + // decodeWhoareyou reads packet data after the header as a WHOAREYOU packet. func (c *Codec) decodeWhoareyou(head *packetHeader) (Packet, error) { if c.reader.Len() < sizeofWhoareyouAuthDataV5 { @@ -456,20 +468,17 @@ func (c *Codec) decodeHandshake(fromAddr string, head *packetHeader) (*enode.Nod // decodeHandshakeAuthData reads the authdata section of a handshake packet. func (c *Codec) decodeHandshakeAuthData(head *packetHeader) (*handshakeAuthData, error) { + // Decode fixed size part. if int(head.AuthSize) < sizeofHandshakeAuthDataV5 { return nil, fmt.Errorf("header authsize %d too low for handshake", head.AuthSize) } - if c.reader.Len() < int(head.AuthSize) { - return nil, errTooShort - } - - // Decode fixed size part. var auth handshakeAuthData binary.Read(&c.reader, binary.BigEndian, &auth.h) - if auth.h.Version > handshakeVersion || auth.h.Version < minVersion { - return nil, fmt.Errorf("invalid handshake version %d", auth.h.Version) - } + // Decode variable-size part. + if c.reader.Len() < int(head.AuthSize) { + return nil, errTooShort + } varspace := int(head.AuthSize) - sizeofHandshakeAuthDataV5 if int(auth.h.SigSize)+int(auth.h.PubkeySize) > varspace { return nil, fmt.Errorf("invalid handshake data sizes (%d+%d > %d)", auth.h.SigSize, auth.h.PubkeySize, varspace) diff --git a/p2p/discover/v5wire/testdata/v5.1-ping-handshake-enr.txt b/p2p/discover/v5wire/testdata/v5.1-ping-handshake-enr.txt index 3dd9b036fe0..10ee435affb 100644 --- a/p2p/discover/v5wire/testdata/v5.1-ping-handshake-enr.txt +++ b/p2p/discover/v5wire/testdata/v5.1-ping-handshake-enr.txt @@ -13,14 +13,14 @@ # ephemeral-key = 0x0288ef00023598499cb6c940146d050d2b1fb914198c327f76aad590bead68b6 # ephemeral-pubkey = 0x039a003ba6517b473fa0cd74aefe99dadfdb34627f90fec6362df85803908f53a5 -00000000000000000000000000000000088b3d4342776668980a4adf72a8fcaa -963f24b27a2f6bb44c7ed5ca10e87de130f94d2390b9853c3dcb41d51e9472d4 -3c9ae48d04689ef4d3b3bfd49cc477b9942ef2fb9f4221652369f24e988582a4 -642bb475ad544bbc5e5bf9a0de9a9f33760fb4eb9b6b6d675f1607c5c5eacae6 -06607548e66eb345fd820fea5142053c97feb5fc3f5d0ff3d71008a5b6724bbf -c8c97746524e695129d2bd68c13b95e8687264923226593c1cdb64d1377d3b68 -b3b755f98ed4631901e5e67c75b838b759df976df74dc60a07cc7c20a3102303 -bf6e56b02560c31d8383f2804c32fec46eef5d0b79b4c30b247350069605025c -86c70190214e6ebda6a9c61c90401dafe99dcf42189ad5f1f9a57322e640c851 -db1247e69e618d3947987fccc3d4569a69fd8c1f49cf2d5620e27a116b0df169 -4604 +00000000000000000000000000000000088b3d4342774649980a4adf72a8fcaa +963f24b27a2f6bb44c7ed5ca10e87de130f94d2390b9853c3dcb402b1e9472d4 +3c9ae48d04689e4bb28c15e69676a8d185b20cea81ece9fe381f5df082418ce9 +a55a707ebeac1cf993f38bdc4bd131089494fe3bcffb9adddc372106aee116f4 +c145d9a715618b37f05796706adff216ab862a9186875f9494150c4ae06fa4d1 +f0396c93f215fa4ef524e0ed04c3c21e39b1868e1ca8105e585ec17315e755e6 +cfc4dd6cb7fd8e1a1f55e49b4b5eb024221482105346f3c82b15fdaae36a3bb1 +2a494683b4a3c7f2ae41306252fed84785e2bbff3b022812d0882f06978df84a +80d443972213342d04b9048fc3b1d5fcb1df0f822152eced6da4d3f6df27e70e +4539717307a0208cd27fccc3d4569a69fd694853b50fd0af6d021c161f5139c4 +33 diff --git a/p2p/discover/v5wire/testdata/v5.1-ping-handshake.txt b/p2p/discover/v5wire/testdata/v5.1-ping-handshake.txt index 33274175405..909bfd7ff9c 100644 --- a/p2p/discover/v5wire/testdata/v5.1-ping-handshake.txt +++ b/p2p/discover/v5wire/testdata/v5.1-ping-handshake.txt @@ -13,10 +13,10 @@ # ephemeral-key = 0x0288ef00023598499cb6c940146d050d2b1fb914198c327f76aad590bead68b6 # ephemeral-pubkey = 0x039a003ba6517b473fa0cd74aefe99dadfdb34627f90fec6362df85803908f53a5 -00000000000000000000000000000000088b3d4342776668980a4adf72a8fcaa -963f24b27a2f6bb44c7ed5ca10e87de130f94d2390b9853c3dcbded51e9472d4 -3c9ae48d04689ef4d3b3bfd49cc477b9942ef2fb9f4221652369f24e988582a4 -642bb475ad544bbc5e5bf9a0de9a9f33760fb4eb9b6b6d675f1607c5c5eacae6 -06607548e66eb345fd820fea5142053c97feb5fc3f5d0ff3d71008a5b6724bbf -c8c97746524e695129d2bd7fccc3d4569a69fd62f653216b647dcea5f93b5845 -ded334 +00000000000000000000000000000000088b3d4342774649980a4adf72a8fcaa +963f24b27a2f6bb44c7ed5ca10e87de130f94d2390b9853c3dcbc12b1e9472d4 +3c9ae48d04689e4bb28c15e69676a8d185b20cea81ece9fe381f5df082418ce9 +a55a707ebeac1cf993f38bdc4bd131089494fe3bcffb9adddc372106aee116f4 +c145d9a715618b37f05796706adff216ab862a9186875f9494150c4ae06fa4d1 +f0396c93f215fa4ef5247fccc3d4569a69fd3c0554ce16da107e370a9298d984 +971a diff --git a/p2p/discover/v5wire/testdata/v5.1-ping-message.txt b/p2p/discover/v5wire/testdata/v5.1-ping-message.txt index 4a542173c42..d816d32d9d1 100644 --- a/p2p/discover/v5wire/testdata/v5.1-ping-message.txt +++ b/p2p/discover/v5wire/testdata/v5.1-ping-message.txt @@ -5,6 +5,6 @@ # ping.req-id = 0x00000001 # ping.enr-seq = 2 -00000000000000000000000000000000088b3d4342776668980a4adf72a8fcaa +00000000000000000000000000000000088b3d4342774649980a4adf72a8fcaa 963f24b27a2f6bb44c7ed5ca10e87de130f94d2390b9853c3fcba22b1e9472d4 -3c9ae48d04689eb84102ed931f66d180cbb4219f369a24f4e6b24d7bdc2a04 +3c9ae48d04689eb84102ed931f66d1d430427642248e344d6e97d20a6b76dd diff --git a/p2p/discover/v5wire/testdata/v5.1-whoareyou.txt b/p2p/discover/v5wire/testdata/v5.1-whoareyou.txt index 85cfdab4f0a..7bbf2cda13c 100644 --- a/p2p/discover/v5wire/testdata/v5.1-whoareyou.txt +++ b/p2p/discover/v5wire/testdata/v5.1-whoareyou.txt @@ -4,7 +4,7 @@ # whoareyou.id-nonce = 0x0102030405060708090a0b0c0d0e0f1000000000000000000000000000000000 # whoareyou.enr-seq = 0 -00000000000000000000000000000000088b3d4342776668980a4adf72a8fcaa +00000000000000000000000000000000088b3d4342774649980a4adf72a8fcaa 963f24b27a2f6bb44c7ed5ca10e87de130f94d2390b9853c3ecb9ad5e368892e c562137bf19c6d0a9191a5651c4f415117bdfa0c7ab86af62b7a9784eceb2800 8d03ede83bd1369631f9f3d8da0b45 From 13bcca6e1a5d7f3dc774e26beb4386a213a3b537 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Wed, 30 Sep 2020 13:18:39 +0200 Subject: [PATCH 29/70] p2p/discover/v5wire: use WHOAREYOU IV in ID proof This shrinks WHOAREYOU by 16 bytes. --- p2p/discover/v5wire/crypto.go | 21 +- p2p/discover/v5wire/encoding.go | 346 ++++++++++-------- p2p/discover/v5wire/encoding_test.go | 36 +- p2p/discover/v5wire/msg.go | 15 +- .../testdata/v5.1-ping-handshake-enr.txt | 14 +- .../v5wire/testdata/v5.1-ping-handshake.txt | 14 +- .../v5wire/testdata/v5.1-ping-message.txt | 2 +- .../v5wire/testdata/v5.1-whoareyou.txt | 9 +- 8 files changed, 256 insertions(+), 201 deletions(-) diff --git a/p2p/discover/v5wire/crypto.go b/p2p/discover/v5wire/crypto.go index 613dcda1830..0cf1752e1f8 100644 --- a/p2p/discover/v5wire/crypto.go +++ b/p2p/discover/v5wire/crypto.go @@ -48,20 +48,21 @@ func DecodePubkey(curve elliptic.Curve, e []byte) (*ecdsa.PublicKey, error) { } // idNonceHash computes the ID signature hash used in the handshake. -func idNonceHash(h hash.Hash, destID enode.ID, nonce, ephkey []byte) []byte { +func idNonceHash(h hash.Hash, destID enode.ID, iv, authdata, ephkey []byte) []byte { h.Reset() h.Write([]byte(idNoncePrefix)) - h.Write(nonce) + h.Write(iv) + h.Write(authdata) h.Write(ephkey) h.Write(destID[:]) return h.Sum(nil) } // makeIDSignature creates the ID nonce signature. -func makeIDSignature(hash hash.Hash, key *ecdsa.PrivateKey, destID enode.ID, nonce, ephkey []byte) ([]byte, error) { +func makeIDSignature(hash hash.Hash, key *ecdsa.PrivateKey, destID enode.ID, ephkey []byte, challenge *Header) ([]byte, error) { switch key.Curve { case crypto.S256(): - input := idNonceHash(hash, destID, nonce, ephkey) + input := idNonceHash(hash, destID, challenge.IV[:], challenge.AuthData, ephkey) idsig, err := crypto.Sign(input, key) if err != nil { return nil, err @@ -78,14 +79,14 @@ type s256raw []byte func (s256raw) ENRKey() string { return "secp256k1" } // verifyIDSignature checks that signature over idnonce was made by the given node. -func verifyIDSignature(hash hash.Hash, destID enode.ID, nonce, ephkey, sig []byte, n *enode.Node) error { +func verifyIDSignature(hash hash.Hash, sig []byte, n *enode.Node, destID enode.ID, ephkey []byte, challenge *Header) error { switch idscheme := n.Record().IdentityScheme(); idscheme { case "v4": var pubkey s256raw if n.Load(&pubkey) != nil { return errors.New("no secp256k1 public key in record") } - input := idNonceHash(hash, destID, nonce, ephkey) + input := idNonceHash(hash, destID, challenge.IV[:], challenge.AuthData, ephkey) if !crypto.VerifySignature(pubkey, input, sig) { return errInvalidNonceSig } @@ -107,8 +108,10 @@ func ecdh(privkey *ecdsa.PrivateKey, pubkey *ecdsa.PublicKey) []byte { return sec } -// encryptGCM encrypts pt using AES-GCM with the given key and nonce. -func encryptGCM(dest, key, nonce, pt, authData []byte) ([]byte, error) { +// encryptGCM encrypts pt using AES-GCM with the given key and nonce. The ciphertext is +// appended to dest, which must not overlap with plaintext. The resulting ciphertext is 16 +// bytes longer than plaintext because it contains an authentication tag. +func encryptGCM(dest, key, nonce, plaintext, authData []byte) ([]byte, error) { block, err := aes.NewCipher(key) if err != nil { panic(fmt.Errorf("can't create block cipher: %v", err)) @@ -117,7 +120,7 @@ func encryptGCM(dest, key, nonce, pt, authData []byte) ([]byte, error) { if err != nil { panic(fmt.Errorf("can't create GCM: %v", err)) } - return aesgcm.Seal(dest, nonce, pt, authData), nil + return aesgcm.Seal(dest, nonce, plaintext, authData), nil } // decryptGCM decrypts ct using AES-GCM with the given key and nonce. diff --git a/p2p/discover/v5wire/encoding.go b/p2p/discover/v5wire/encoding.go index 6b62a7424b9..7ccb49c4837 100644 --- a/p2p/discover/v5wire/encoding.go +++ b/p2p/discover/v5wire/encoding.go @@ -41,18 +41,27 @@ import ( // TODO add counter to nonce // TODO rehandshake after X packets -// Discovery v5 packet structures. -type ( - packetHeader struct { - ProtocolID [8]byte - SrcID enode.ID - Flag byte - AuthSize uint16 - } +// Header represents a packet header. +type Header struct { + IV [sizeofMaskingIV]byte + HeaderData + Nonce Nonce + AuthData []byte +} + +// HeaderData contains the static fields of a packet header. +type HeaderData struct { + ProtocolID [8]byte + SrcID enode.ID + Flag byte + AuthSize uint16 +} +// Authdata layouts. +type ( whoareyouAuthData struct { Nonce Nonce // nonce of request packet - IDNonce [32]byte // ID proof data + IDNonce [16]byte // ID proof data RecordSeq uint64 // highest known ENR sequence of requester } @@ -65,10 +74,6 @@ type ( // Trailing variable-size data. signature, pubkey, record []byte } - - messageAuthData struct { - Nonce Nonce // AES-GCM nonce of message - } ) // Packet header flag values. @@ -85,15 +90,7 @@ const ( minVersion = 1 protocolID = "discv5" versionOffset = 7 -) - -// Packet sizes. -var ( - sizeofMaskingIV = 16 - sizeofPacketHeaderV5 = binary.Size(packetHeader{}) - sizeofWhoareyouAuthDataV5 = binary.Size(whoareyouAuthData{}) - sizeofHandshakeAuthDataV5 = binary.Size(handshakeAuthData{}.h) - sizeofMessageAuthDataV5 = binary.Size(messageAuthData{}) + sizeofMaskingIV = 16 ) // Errors. @@ -109,6 +106,24 @@ var ( errMessageDecrypt = errors.New("cannot decrypt message") ) +// Packet sizes. +var ( + sizeofHeaderData = binary.Size(HeaderData{}) + sizeofWhoareyouAuthData = binary.Size(whoareyouAuthData{}) + sizeofHandshakeAuthData = binary.Size(handshakeAuthData{}.h) + sizeofMessageAuthData = len(Nonce{}) +) + +// func init() { +// var ( +// sizeofGCMTag = 16 +// sizeofWhoareyou = sizeofHeaderData + sizeofWhoareyouAuthData +// sizeofEmptyMsg = sizeofHeaderData + sizeofMessageAuthData + sizeofGCMTag +// ) +// fmt.Println("WHOAREYOU size:", sizeofWhoareyou) +// fmt.Println("EMPTY msg size:", sizeofEmptyMsg) +// } + // Codec encodes and decodes discovery v5 packets. type Codec struct { sha256 hash.Hash @@ -135,38 +150,74 @@ func NewCodec(ln *enode.LocalNode, key *ecdsa.PrivateKey, clock mclock.Clock) *C // 'challenge' parameter should be the most recently received WHOAREYOU packet from that // node. func (c *Codec) Encode(id enode.ID, addr string, packet Packet, challenge *Whoareyou) ([]byte, Nonce, error) { + var ( + header Header + msgdata []byte + err error + ) if p, ok := packet.(*Whoareyou); ok { - enc, err := c.encodeWhoareyou(id, p) + // WHOAREYOU has special encoding. + header, err = c.encodeWhoareyou(id, p) if err == nil { c.sc.storeSentHandshake(id, addr, p) } - return enc, Nonce{}, err + } else if challenge != nil { + // Remote sent a challenge, answer it with a handshake. + header, msgdata, err = c.encodeHandshakeMessage(id, addr, packet, challenge) + } else if session := c.sc.session(id, addr); session != nil { + // There is a session, use it. + header, msgdata, err = c.encodeMessage(id, session, packet) + } else { + // No keys, no handshake: send random data to kick off the handshake. + header, msgdata, err = c.encodeRandom(id) } - if challenge != nil { - return c.encodeHandshakeMessage(id, addr, packet, challenge) + if err != nil { + return nil, Nonce{}, err } - if session := c.sc.session(id, addr); session != nil { - return c.encodeMessage(id, session, packet) + enc, err := c.EncodeRaw(id, header, msgdata) + if err != nil { + return nil, Nonce{}, err } - // No keys, no handshake: send random data to kick off the handshake. - return c.encodeRandom(id) + return enc, header.Nonce, nil +} + +func (c *Codec) EncodeRaw(id enode.ID, head Header, msgdata []byte) ([]byte, error) { + // Generate masking IV. + if err := c.sc.maskingIVGen(head.IV[:]); err != nil { + return nil, err + } + + // Encode the packet. + c.buf.Reset() + c.buf.Write(head.IV[:]) + binary.Write(&c.buf, binary.BigEndian, &head.HeaderData) + c.buf.Write(head.AuthData) + + // Apply masking. + masked := c.buf.Bytes()[sizeofMaskingIV:] + mask := head.mask(id) + mask.XORKeyStream(masked[:], masked[:]) + + // Write message data. + c.buf.Write(msgdata) + return c.buf.Bytes(), nil } // makeHeader creates a packet header. -func (c *Codec) makeHeader(toID enode.ID, flag byte, authsizeExtra int) *packetHeader { - h := &packetHeader{ +func (c *Codec) makeHeader(toID enode.ID, flag byte, authsizeExtra int) Header { + data := HeaderData{ SrcID: c.localnode.ID(), Flag: flag, } var authsize int switch flag { case flagMessage: - authsize = sizeofMessageAuthDataV5 + authsize = sizeofMessageAuthData case flagWhoareyou: - authsize = sizeofWhoareyouAuthDataV5 + authsize = sizeofWhoareyouAuthData case flagHandshake: - authsize = sizeofHandshakeAuthDataV5 + authsize = sizeofHandshakeAuthData default: panic(fmt.Errorf("BUG: invalid packet header flag %x", flag)) } @@ -174,93 +225,96 @@ func (c *Codec) makeHeader(toID enode.ID, flag byte, authsizeExtra int) *packetH if authsize > int(^uint16(0)) { panic(fmt.Errorf("BUG: auth size %d overflows uint16", authsize)) } - h.AuthSize = uint16(authsize) - copy(h.ProtocolID[:], protocolID) - h.ProtocolID[versionOffset] = handshakeVersion - return h + data.AuthSize = uint16(authsize) + copy(data.ProtocolID[:], protocolID) + data.ProtocolID[versionOffset] = handshakeVersion + return Header{HeaderData: data} } // encodeRandom encodes a packet with random content. -func (c *Codec) encodeRandom(toID enode.ID) ([]byte, Nonce, error) { - var auth messageAuthData - if _, err := crand.Read(auth.Nonce[:]); err != nil { - return nil, auth.Nonce, fmt.Errorf("can't get random data: %v", err) - } - - c.buf.Reset() - binary.Write(&c.buf, binary.BigEndian, c.makeHeader(toID, flagMessage, 0)) - binary.Write(&c.buf, binary.BigEndian, &auth) - output := c.maskOutputPacket(toID, c.buf.Bytes(), c.buf.Len()) - return output, auth.Nonce, nil +func (c *Codec) encodeRandom(toID enode.ID) (Header, []byte, error) { + head := c.makeHeader(toID, flagMessage, 0) + if _, err := crand.Read(head.Nonce[:]); err != nil { + return head, nil, fmt.Errorf("can't get random data: %v", err) + } + head.AuthData = head.Nonce[:] + msgdata := make([]byte, 16) + return head, msgdata, nil } // encodeWhoareyou encodes a WHOAREYOU packet. -func (c *Codec) encodeWhoareyou(toID enode.ID, packet *Whoareyou) ([]byte, error) { +func (c *Codec) encodeWhoareyou(toID enode.ID, packet *Whoareyou) (Header, error) { // Sanity check node field to catch misbehaving callers. if packet.RecordSeq > 0 && packet.Node == nil { panic("BUG: missing node in whoareyouV5 with non-zero seq") } + + // Encode auth data. auth := &whoareyouAuthData{ - Nonce: packet.AuthTag, + Nonce: packet.Nonce, IDNonce: packet.IDNonce, RecordSeq: packet.RecordSeq, } - head := c.makeHeader(toID, flagWhoareyou, 0) - c.buf.Reset() - binary.Write(&c.buf, binary.BigEndian, head) binary.Write(&c.buf, binary.BigEndian, auth) - output := c.maskOutputPacket(toID, c.buf.Bytes(), c.buf.Len()) - return output, nil + + // Create header. + head := c.makeHeader(toID, flagWhoareyou, 0) + head.AuthData = make([]byte, c.buf.Len()) + copy(head.AuthData, c.buf.Bytes()) + + // Update header in packet. + packet.Header = head + return head, nil } // encodeHandshakeMessage encodes an encrypted message with a handshake // response header. -func (c *Codec) encodeHandshakeMessage(toID enode.ID, addr string, packet Packet, challenge *Whoareyou) ([]byte, Nonce, error) { +func (c *Codec) encodeHandshakeMessage(toID enode.ID, addr string, packet Packet, challenge *Whoareyou) (Header, []byte, error) { // Ensure calling code sets challenge.node. if challenge.Node == nil { panic("BUG: missing challenge.Node in encode") } // Generate new secrets. - auth, session, err := c.makeHandshakeHeader(toID, addr, challenge) + auth, session, err := c.makeHandshakeAuth(toID, addr, challenge) if err != nil { - return nil, Nonce{}, err + return Header{}, nil, err } // TODO: this should happen when the first authenticated message is received c.sc.storeNewSession(toID, addr, session) - // Encode header and auth header. + // Encode the auth header. var ( authsizeExtra = len(auth.pubkey) + len(auth.signature) + len(auth.record) head = c.makeHeader(toID, flagHandshake, authsizeExtra) ) c.buf.Reset() - binary.Write(&c.buf, binary.BigEndian, head) binary.Write(&c.buf, binary.BigEndian, &auth.h) c.buf.Write(auth.signature) c.buf.Write(auth.pubkey) c.buf.Write(auth.record) - output := c.buf.Bytes() + + // Copy auth into packet header. + head.AuthData = make([]byte, c.buf.Len()) + copy(head.AuthData, c.buf.Bytes()) + head.Nonce = auth.h.Nonce // Encrypt packet body. c.msgbuf.Reset() c.msgbuf.WriteByte(packet.Kind()) if err := rlp.Encode(&c.msgbuf, packet); err != nil { - return nil, auth.h.Nonce, err + return head, nil, err } messagePT := c.msgbuf.Bytes() - headerData := output - output, err = encryptGCM(output, session.writeKey, auth.h.Nonce[:], messagePT, headerData) - if err == nil { - output = c.maskOutputPacket(toID, output, len(headerData)) - } - return output, auth.h.Nonce, err + var msgdata []byte + msgdata, err = encryptGCM(msgdata, session.writeKey, head.Nonce[:], messagePT, nil) + return head, msgdata, err } -// encodeAuthHeader creates the auth header on a call packet following WHOAREYOU. -func (c *Codec) makeHandshakeHeader(toID enode.ID, addr string, challenge *Whoareyou) (*handshakeAuthData, *session, error) { +// encodeAuthHeader creates the auth header on a request packet following WHOAREYOU. +func (c *Codec) makeHandshakeAuth(toID enode.ID, addr string, challenge *Whoareyou) (*handshakeAuthData, *session, error) { session := new(session) nonce, err := c.sc.nextNonce(session) if err != nil { @@ -285,7 +339,7 @@ func (c *Codec) makeHandshakeHeader(toID enode.ID, addr string, challenge *Whoar auth.h.PubkeySize = byte(len(auth.pubkey)) // Add ID nonce signature to response. - idsig, err := makeIDSignature(c.sha256, c.privkey, toID, challenge.IDNonce[:], ephpubkey[:]) + idsig, err := makeIDSignature(c.sha256, c.privkey, toID, ephpubkey[:], &challenge.Header) if err != nil { return nil, nil, fmt.Errorf("can't sign: %v", err) } @@ -308,40 +362,29 @@ func (c *Codec) makeHandshakeHeader(toID enode.ID, addr string, challenge *Whoar } // encodeMessage encodes an encrypted message packet. -func (c *Codec) encodeMessage(toID enode.ID, s *session, packet Packet) ([]byte, Nonce, error) { - var ( - head = c.makeHeader(toID, flagMessage, 0) - auth messageAuthData - ) +func (c *Codec) encodeMessage(toID enode.ID, s *session, packet Packet) (Header, []byte, error) { + head := c.makeHeader(toID, flagMessage, 0) // Create the nonce. nonce, err := c.sc.nextNonce(s) if err != nil { - return nil, auth.Nonce, fmt.Errorf("can't generate nonce: %v", err) + return Header{}, nil, fmt.Errorf("can't generate nonce: %v", err) } - auth.Nonce = nonce - - // Encode the header. - c.buf.Reset() - binary.Write(&c.buf, binary.BigEndian, head) - binary.Write(&c.buf, binary.BigEndian, &auth) - output := c.buf.Bytes() + head.Nonce = nonce + head.AuthData = head.Nonce[:] // Encode the message plaintext. c.msgbuf.Reset() c.msgbuf.WriteByte(packet.Kind()) if err := rlp.Encode(&c.msgbuf, packet); err != nil { - return nil, auth.Nonce, err + return head, nil, err } messagePT := c.msgbuf.Bytes() // Encrypt the message. - headerData := output - output, err = encryptGCM(output, s.writeKey, nonce[:], messagePT, headerData) - if err == nil { - output = c.maskOutputPacket(toID, output, len(headerData)) - } - return output, auth.Nonce, err + var msgdata []byte + msgdata, err = encryptGCM(msgdata, s.writeKey, nonce[:], messagePT, nil) + return head, msgdata, err } // Decode decodes a discovery packet. @@ -350,80 +393,73 @@ func (c *Codec) Decode(input []byte, addr string) (src enode.ID, n *enode.Node, // processing the same handshake twice. c.sc.handshakeGC() - // Unmask the header. - if len(input) < sizeofPacketHeaderV5+sizeofMaskingIV { + // Unmask the static header. + if len(input) < sizeofHeaderData+sizeofMaskingIV { return enode.ID{}, nil, nil, errTooShort } - mask := headerMask(c.localnode.ID(), input) + var head Header + copy(head.IV[:], input[:sizeofMaskingIV]) + mask := head.mask(c.localnode.ID()) input = input[sizeofMaskingIV:] - headerData := input[:sizeofPacketHeaderV5] + headerData := input[:sizeofHeaderData] mask.XORKeyStream(headerData, headerData) // Decode and verify the header. - var head packetHeader c.reader.Reset(input) - binary.Read(&c.reader, binary.BigEndian, &head) - if !head.isValid(c.reader.Len()) { + err = binary.Read(&c.reader, binary.BigEndian, &head.HeaderData) + if err != nil || !head.isValid(c.reader.Len()) { return enode.ID{}, nil, nil, errInvalidHeader } - src = head.SrcID - // Unmask auth data. - authData := input[sizeofPacketHeaderV5 : sizeofPacketHeaderV5+int(head.AuthSize)] - mask.XORKeyStream(authData, authData) + head.AuthData = make([]byte, head.AuthSize) + copy(head.AuthData, input[sizeofHeaderData:]) + mask.XORKeyStream(head.AuthData, head.AuthData) // Decode auth part and message. + msgdata := input[sizeofHeaderData+len(head.AuthData):] switch head.Flag { case flagWhoareyou: p, err = c.decodeWhoareyou(&head) case flagHandshake: - n, p, err = c.decodeHandshakeMessage(addr, &head, input) + n, p, err = c.decodeHandshakeMessage(addr, &head, msgdata) case flagMessage: - p, err = c.decodeMessage(addr, &head, input) + p, err = c.decodeMessage(addr, &head, msgdata) default: err = errInvalidFlag } - return src, n, p, err -} - -func (h *packetHeader) isValid(packetLen int) bool { - for i := range protocolID { - if h.ProtocolID[i] != protocolID[i] { - return false - } - } - if h.ProtocolID[versionOffset] < minVersion { - return false - } - return int(h.AuthSize) <= packetLen + return head.SrcID, n, p, err } // decodeWhoareyou reads packet data after the header as a WHOAREYOU packet. -func (c *Codec) decodeWhoareyou(head *packetHeader) (Packet, error) { - if c.reader.Len() < sizeofWhoareyouAuthDataV5 { +func (c *Codec) decodeWhoareyou(head *Header) (Packet, error) { + if c.reader.Len() < sizeofWhoareyouAuthData { return nil, errTooShort } - if int(head.AuthSize) != sizeofWhoareyouAuthDataV5 { + if int(head.AuthSize) != sizeofWhoareyouAuthData { return nil, fmt.Errorf("invalid auth size for whoareyou") } + c.reader.Reset(head.AuthData) auth := new(whoareyouAuthData) binary.Read(&c.reader, binary.BigEndian, auth) + head.Nonce = auth.Nonce p := &Whoareyou{ - AuthTag: auth.Nonce, + Header: *head, + Nonce: auth.Nonce, IDNonce: auth.IDNonce, RecordSeq: auth.RecordSeq, } return p, nil } -func (c *Codec) decodeHandshakeMessage(fromAddr string, head *packetHeader, input []byte) (n *enode.Node, p Packet, err error) { +func (c *Codec) decodeHandshakeMessage(fromAddr string, head *Header, msgdata []byte) (n *enode.Node, p Packet, err error) { node, nonce, session, err := c.decodeHandshake(fromAddr, head) if err != nil { return nil, nil, err } + head.Nonce = nonce // Decrypt the message using the new session keys. - msg, err := c.decryptMessage(input, nonce, session.readKey) + msg, err := c.decryptMessage(msgdata, nonce[:], session.readKey) if err != nil { return node, msg, err } @@ -434,7 +470,7 @@ func (c *Codec) decodeHandshakeMessage(fromAddr string, head *packetHeader, inpu return node, msg, nil } -func (c *Codec) decodeHandshake(fromAddr string, head *packetHeader) (*enode.Node, Nonce, *session, error) { +func (c *Codec) decodeHandshake(fromAddr string, head *Header) (*enode.Node, Nonce, *session, error) { auth, err := c.decodeHandshakeAuthData(head) if err != nil { return nil, Nonce{}, nil, err @@ -456,7 +492,8 @@ func (c *Codec) decodeHandshake(fromAddr string, head *packetHeader) (*enode.Nod return nil, Nonce{}, nil, errInvalidAuthKey } // Verify ID nonce signature. - err = verifyIDSignature(c.sha256, c.localnode.ID(), challenge.IDNonce[:], auth.pubkey, auth.signature, node) + sig := auth.signature + err = verifyIDSignature(c.sha256, sig, node, c.localnode.ID(), auth.pubkey, &challenge.Header) if err != nil { return nil, Nonce{}, nil, err } @@ -467,19 +504,20 @@ func (c *Codec) decodeHandshake(fromAddr string, head *packetHeader) (*enode.Nod } // decodeHandshakeAuthData reads the authdata section of a handshake packet. -func (c *Codec) decodeHandshakeAuthData(head *packetHeader) (*handshakeAuthData, error) { +func (c *Codec) decodeHandshakeAuthData(head *Header) (*handshakeAuthData, error) { // Decode fixed size part. - if int(head.AuthSize) < sizeofHandshakeAuthDataV5 { + if int(head.AuthSize) < sizeofHandshakeAuthData { return nil, fmt.Errorf("header authsize %d too low for handshake", head.AuthSize) } + c.reader.Reset(head.AuthData) var auth handshakeAuthData binary.Read(&c.reader, binary.BigEndian, &auth.h) // Decode variable-size part. - if c.reader.Len() < int(head.AuthSize) { + varspace := int(head.AuthSize) - sizeofHandshakeAuthData + if c.reader.Len() < varspace { return nil, errTooShort } - varspace := int(head.AuthSize) - sizeofHandshakeAuthDataV5 if int(auth.h.SigSize)+int(auth.h.PubkeySize) > varspace { return nil, fmt.Errorf("invalid handshake data sizes (%d+%d > %d)", auth.h.SigSize, auth.h.PubkeySize, varspace) } @@ -534,34 +572,31 @@ func (c *Codec) decodeHandshakeRecord(local *enode.Node, wantID enode.ID, remote } // decodeMessage reads packet data following the header as an ordinary message packet. -func (c *Codec) decodeMessage(fromAddr string, head *packetHeader, input []byte) (Packet, error) { - if c.reader.Len() < sizeofMessageAuthDataV5 { +func (c *Codec) decodeMessage(fromAddr string, head *Header, msgdata []byte) (Packet, error) { + if len(head.AuthData) < sizeofMessageAuthData { return nil, errTooShort } - auth := new(messageAuthData) - binary.Read(&c.reader, binary.BigEndian, auth) + copy(head.Nonce[:], head.AuthData) // Try decrypting the message. key := c.sc.readKey(head.SrcID, fromAddr) - msg, err := c.decryptMessage(input, auth.Nonce, key) + msg, err := c.decryptMessage(msgdata, head.Nonce[:], key) if err == errMessageDecrypt { // It didn't work. Start the handshake since this is an ordinary message packet. - return &Unknown{AuthTag: auth.Nonce}, nil + return &Unknown{Nonce: head.Nonce}, nil } return msg, err } -func (c *Codec) decryptMessage(input []byte, nonce Nonce, readKey []byte) (Packet, error) { - headerData := input[:len(input)-c.reader.Len()] - messageCT := input[len(headerData):] - message, err := decryptGCM(readKey, nonce[:], messageCT, headerData) +func (c *Codec) decryptMessage(input []byte, nonce []byte, readKey []byte) (Packet, error) { + msgdata, err := decryptGCM(readKey, nonce, input, nil) if err != nil { return nil, errMessageDecrypt } - if len(message) == 0 { + if len(msgdata) == 0 { return nil, errMessageTooShort } - return DecodeMessage(message[0], message[1:]) + return DecodeMessage(msgdata[0], msgdata[1:]) } // deriveKeys generates session keys using elliptic-curve Diffie-Hellman key agreement. @@ -594,20 +629,27 @@ func (c *Codec) sha256reset() hash.Hash { } // maskOutputPacket applies protocol header masking to a packet sent to destID. -func (c *Codec) maskOutputPacket(destID enode.ID, output []byte, headerDataLen int) []byte { - masked := make([]byte, sizeofMaskingIV+len(output)) - c.sc.maskingIVGen(masked[:sizeofMaskingIV]) - mask := headerMask(destID, masked) - copy(masked[sizeofMaskingIV:], output) - mask.XORKeyStream(masked[sizeofMaskingIV:], output[:headerDataLen]) - return masked +func (c *Codec) maskOutputPacket(destID enode.ID, head *Header, headerBytes []byte) { +} + +// isValid returns true if h contains a valid protocol ID and auth size. +func (h *HeaderData) isValid(packetLen int) bool { + for i := range protocolID { + if h.ProtocolID[i] != protocolID[i] { + return false + } + } + if h.ProtocolID[versionOffset] < minVersion { + return false + } + return int(h.AuthSize) <= packetLen } // headerMask returns a cipher for 'masking' / 'unmasking' packet headers. -func headerMask(destID enode.ID, input []byte) cipher.Stream { +func (h *Header) mask(destID enode.ID) cipher.Stream { block, err := aes.NewCipher(destID[:16]) if err != nil { panic("can't create cipher") } - return cipher.NewCTR(block, input[:sizeofMaskingIV]) + return cipher.NewCTR(block, h.IV[:]) } diff --git a/p2p/discover/v5wire/encoding_test.go b/p2p/discover/v5wire/encoding_test.go index be8a20d43b6..aa63e4508ab 100644 --- a/p2p/discover/v5wire/encoding_test.go +++ b/p2p/discover/v5wire/encoding_test.go @@ -47,7 +47,7 @@ var ( testKeyA, _ = crypto.HexToECDSA("eef77acb6c6a6eebc5b363a475ac583ec7eccdb42b6481424c60f59aa326547f") testKeyB, _ = crypto.HexToECDSA("66fb62bfbd66b9177a138c1e5cddbe4f7c30c343e94e68df8769459cb1cde628") testEphKey, _ = crypto.HexToECDSA("0288ef00023598499cb6c940146d050d2b1fb914198c327f76aad590bead68b6") - testIDnonce = [32]byte{5, 6, 7, 8, 9, 10, 11, 12} + testIDnonce = [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16} ) func TestDeriveKeysV5(t *testing.T) { @@ -85,7 +85,7 @@ func TestHandshakeV5(t *testing.T) { // A <- B WHOAREYOU challenge := &Whoareyou{ - AuthTag: resp.(*Unknown).AuthTag, + Nonce: resp.(*Unknown).Nonce, IDNonce: testIDnonce, RecordSeq: 0, } @@ -116,7 +116,7 @@ func TestHandshakeV5_timeout(t *testing.T) { // A <- B WHOAREYOU challenge := &Whoareyou{ - AuthTag: resp.(*Unknown).AuthTag, + Nonce: resp.(*Unknown).Nonce, IDNonce: testIDnonce, RecordSeq: 0, } @@ -145,7 +145,7 @@ func TestHandshakeV5_norecord(t *testing.T) { t.Fatal("need non-zero sequence number") } challenge := &Whoareyou{ - AuthTag: resp.(*Unknown).AuthTag, + Nonce: resp.(*Unknown).Nonce, IDNonce: testIDnonce, RecordSeq: nodeA.Seq(), Node: nodeA, @@ -180,7 +180,7 @@ func TestHandshakeV5_rekey(t *testing.T) { net.nodeB.expectDecode(t, UnknownPacket, findnode) // A <- B WHOAREYOU - challenge := &Whoareyou{AuthTag: authTag, IDNonce: testIDnonce} + challenge := &Whoareyou{Nonce: authTag, IDNonce: testIDnonce} whoareyou, _ := net.nodeB.encode(t, net.nodeA, challenge) net.nodeA.expectDecode(t, WhoareyouPacket, whoareyou) @@ -224,7 +224,7 @@ func TestHandshakeV5_rekey2(t *testing.T) { net.nodeB.expectDecode(t, UnknownPacket, findnode) // A <- B WHOAREYOU - challenge := &Whoareyou{AuthTag: authTag, IDNonce: testIDnonce} + challenge := &Whoareyou{Nonce: authTag, IDNonce: testIDnonce} whoareyou, _ := net.nodeB.encode(t, net.nodeA, challenge) net.nodeA.expectDecode(t, WhoareyouPacket, whoareyou) @@ -260,13 +260,13 @@ func TestTestVectorsV5(t *testing.T) { readKey: hexutil.MustDecode("0x01010101010101010101010101010101"), } challenge0 = &Whoareyou{ - AuthTag: Nonce{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}, - IDNonce: [32]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, + Nonce: Nonce{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}, + IDNonce: testIDnonce, RecordSeq: 0, } challenge1 = &Whoareyou{ - AuthTag: Nonce{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}, - IDNonce: [32]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, + Nonce: Nonce{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}, + IDNonce: testIDnonce, RecordSeq: 1, } ) @@ -301,9 +301,9 @@ func TestTestVectorsV5(t *testing.T) { }, challenge: challenge1, prep: func(net *handshakeTest) { - c := *challenge1 - c.Node = net.nodeA.n() - net.nodeB.c.sc.storeSentHandshake(idA, addr, &c) + challenge1.Node = net.nodeA.n() + net.nodeA.encode(t, net.nodeB, challenge1) + net.nodeB.c.sc.storeSentHandshake(idA, addr, challenge1) }, }, { @@ -314,9 +314,9 @@ func TestTestVectorsV5(t *testing.T) { }, challenge: challenge0, prep: func(net *handshakeTest) { - c := *challenge0 - c.Node = net.nodeA.n() - net.nodeB.c.sc.storeSentHandshake(idA, addr, &c) + challenge0.Node = net.nodeA.n() + net.nodeA.encode(t, net.nodeB, challenge0) + net.nodeB.c.sc.storeSentHandshake(idA, addr, challenge0) }, }, } @@ -358,7 +358,9 @@ func TestTestVectorsV5(t *testing.T) { func testVectorComment(net *handshakeTest, p Packet, challenge *Whoareyou, nonce Nonce) string { o := new(strings.Builder) printWhoareyou := func(p *Whoareyou) { - fmt.Fprintf(o, "whoareyou.request-nonce = %#x\n", p.AuthTag[:]) + fmt.Fprintf(o, "whoareyou.iv = %#x\n", p.Header.IV[:]) + fmt.Fprintf(o, "whoareyou.authdata = %#x\n", p.Header.AuthData[:]) + fmt.Fprintf(o, "whoareyou.request-nonce = %#x\n", p.Nonce[:]) fmt.Fprintf(o, "whoareyou.id-nonce = %#x\n", p.IDNonce[:]) fmt.Fprintf(o, "whoareyou.enr-seq = %d\n", p.RecordSeq) } diff --git a/p2p/discover/v5wire/msg.go b/p2p/discover/v5wire/msg.go index 24411d0b5b5..4e488ddc68e 100644 --- a/p2p/discover/v5wire/msg.go +++ b/p2p/discover/v5wire/msg.go @@ -28,7 +28,6 @@ import ( // Packet is implemented by all message types. type Packet interface { - // ??? Src() enode.ID // Src returns the source node ID of the packet. Name() string // Name returns a string corresponding to the message type. Kind() byte // Kind returns the message type. SetReqID([]byte) // Sets the request ID. @@ -56,17 +55,21 @@ const ( type ( // Unknown represents any packet that can't be decrypted. Unknown struct { - AuthTag Nonce + Nonce Nonce } // WHOAREYOU contains the handshake challenge. Whoareyou struct { - AuthTag Nonce - IDNonce [32]byte // To be signed by recipient. + Header Header + Nonce Nonce // Nonce of request packet + IDNonce [16]byte // Identity proof data RecordSeq uint64 // ENR sequence number of recipient - Node *enode.Node // Locally known node records of recipient. - sent mclock.AbsTime + // Node is the locally known node record of recipient. + // This must be set by the caller of Encode. + Node *enode.Node + + sent mclock.AbsTime // for handshake GC. } // PING is sent during liveness checks. diff --git a/p2p/discover/v5wire/testdata/v5.1-ping-handshake-enr.txt b/p2p/discover/v5wire/testdata/v5.1-ping-handshake-enr.txt index 10ee435affb..4edf4e82f7d 100644 --- a/p2p/discover/v5wire/testdata/v5.1-ping-handshake-enr.txt +++ b/p2p/discover/v5wire/testdata/v5.1-ping-handshake-enr.txt @@ -7,20 +7,22 @@ # # handshake inputs: # +# whoareyou.iv = 0x00000000000000000000000000000000 +# whoareyou.authdata = 0x0102030405060708090a0b0c0102030405060708090a0b0c0d0e0f100000000000000000 # whoareyou.request-nonce = 0x0102030405060708090a0b0c -# whoareyou.id-nonce = 0x0102030405060708090a0b0c0d0e0f1000000000000000000000000000000000 +# whoareyou.id-nonce = 0x0102030405060708090a0b0c0d0e0f10 # whoareyou.enr-seq = 0 # ephemeral-key = 0x0288ef00023598499cb6c940146d050d2b1fb914198c327f76aad590bead68b6 # ephemeral-pubkey = 0x039a003ba6517b473fa0cd74aefe99dadfdb34627f90fec6362df85803908f53a5 00000000000000000000000000000000088b3d4342774649980a4adf72a8fcaa 963f24b27a2f6bb44c7ed5ca10e87de130f94d2390b9853c3dcb402b1e9472d4 -3c9ae48d04689e4bb28c15e69676a8d185b20cea81ece9fe381f5df082418ce9 -a55a707ebeac1cf993f38bdc4bd131089494fe3bcffb9adddc372106aee116f4 -c145d9a715618b37f05796706adff216ab862a9186875f9494150c4ae06fa4d1 +3c9ae48d04689e4bb26d809d2756501313e8e59826da6a446913ef43e027fd05 +b16e15e0b544a240c7c3b86d635aeff54300d3707e4fdfce00e0e0e4b05ff99b +2eee244de9271951f65796706adff216ab862a9186875f9494150c4ae06fa4d1 f0396c93f215fa4ef524e0ed04c3c21e39b1868e1ca8105e585ec17315e755e6 cfc4dd6cb7fd8e1a1f55e49b4b5eb024221482105346f3c82b15fdaae36a3bb1 2a494683b4a3c7f2ae41306252fed84785e2bbff3b022812d0882f06978df84a 80d443972213342d04b9048fc3b1d5fcb1df0f822152eced6da4d3f6df27e70e -4539717307a0208cd27fccc3d4569a69fd694853b50fd0af6d021c161f5139c4 -33 +4539717307a0208cd27fccc3d4569a69fd321469e07a60e8125a9be78518093d +d2 diff --git a/p2p/discover/v5wire/testdata/v5.1-ping-handshake.txt b/p2p/discover/v5wire/testdata/v5.1-ping-handshake.txt index 909bfd7ff9c..1114b363dbe 100644 --- a/p2p/discover/v5wire/testdata/v5.1-ping-handshake.txt +++ b/p2p/discover/v5wire/testdata/v5.1-ping-handshake.txt @@ -7,16 +7,18 @@ # # handshake inputs: # +# whoareyou.iv = 0x00000000000000000000000000000000 +# whoareyou.authdata = 0x0102030405060708090a0b0c0102030405060708090a0b0c0d0e0f100000000000000001 # whoareyou.request-nonce = 0x0102030405060708090a0b0c -# whoareyou.id-nonce = 0x0102030405060708090a0b0c0d0e0f1000000000000000000000000000000000 +# whoareyou.id-nonce = 0x0102030405060708090a0b0c0d0e0f10 # whoareyou.enr-seq = 1 # ephemeral-key = 0x0288ef00023598499cb6c940146d050d2b1fb914198c327f76aad590bead68b6 # ephemeral-pubkey = 0x039a003ba6517b473fa0cd74aefe99dadfdb34627f90fec6362df85803908f53a5 00000000000000000000000000000000088b3d4342774649980a4adf72a8fcaa 963f24b27a2f6bb44c7ed5ca10e87de130f94d2390b9853c3dcbc12b1e9472d4 -3c9ae48d04689e4bb28c15e69676a8d185b20cea81ece9fe381f5df082418ce9 -a55a707ebeac1cf993f38bdc4bd131089494fe3bcffb9adddc372106aee116f4 -c145d9a715618b37f05796706adff216ab862a9186875f9494150c4ae06fa4d1 -f0396c93f215fa4ef5247fccc3d4569a69fd3c0554ce16da107e370a9298d984 -971a +3c9ae48d04689e4bb2fb3b042ea9fdbe25ed8d9e7fe2564f8d1f2cbd61b93eaa +f58b961455335ab91bcf2c2e12656512efaee24a691567ab535b3d447a361e23 +b8f84ca7ba007ff4bd5796706adff216ab862a9186875f9494150c4ae06fa4d1 +f0396c93f215fa4ef5247fccc3d4569a69fd321469e07a60e8125a9be7851809 +3dd2 diff --git a/p2p/discover/v5wire/testdata/v5.1-ping-message.txt b/p2p/discover/v5wire/testdata/v5.1-ping-message.txt index d816d32d9d1..fce68684d60 100644 --- a/p2p/discover/v5wire/testdata/v5.1-ping-message.txt +++ b/p2p/discover/v5wire/testdata/v5.1-ping-message.txt @@ -7,4 +7,4 @@ 00000000000000000000000000000000088b3d4342774649980a4adf72a8fcaa 963f24b27a2f6bb44c7ed5ca10e87de130f94d2390b9853c3fcba22b1e9472d4 -3c9ae48d04689eb84102ed931f66d1d430427642248e344d6e97d20a6b76dd +3c9ae48d04689eb84102ed931f66d1db57c785865ffccae8689057103acb15 diff --git a/p2p/discover/v5wire/testdata/v5.1-whoareyou.txt b/p2p/discover/v5wire/testdata/v5.1-whoareyou.txt index 7bbf2cda13c..98caf7e91ba 100644 --- a/p2p/discover/v5wire/testdata/v5.1-whoareyou.txt +++ b/p2p/discover/v5wire/testdata/v5.1-whoareyou.txt @@ -1,10 +1,11 @@ # src-node-id = 0xaaaa8419e9f49d0083561b48287df592939a8d19947d8c0ef88f2a4856a69fbb # dest-node-id = 0xbbbb9d047f0488c0b5a93c1c3f2d8bafc7c8ff337024a55434a0d0555de64db9 +# whoareyou.iv = 0x00000000000000000000000000000000 +# whoareyou.authdata = 0x0102030405060708090a0b0c0102030405060708090a0b0c0d0e0f100000000000000000 # whoareyou.request-nonce = 0x0102030405060708090a0b0c -# whoareyou.id-nonce = 0x0102030405060708090a0b0c0d0e0f1000000000000000000000000000000000 +# whoareyou.id-nonce = 0x0102030405060708090a0b0c0d0e0f10 # whoareyou.enr-seq = 0 00000000000000000000000000000000088b3d4342774649980a4adf72a8fcaa -963f24b27a2f6bb44c7ed5ca10e87de130f94d2390b9853c3ecb9ad5e368892e -c562137bf19c6d0a9191a5651c4f415117bdfa0c7ab86af62b7a9784eceb2800 -8d03ede83bd1369631f9f3d8da0b45 +963f24b27a2f6bb44c7ed5ca10e87de130f94d2390b9853c3ecb8ad5e368892e +c562137bf19c6d0a9191a5651c4f415117bdfa0c7ab86af62b7a9784eceb28 From 93ea47939da6efdecf0038e27c874472aa158b96 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Wed, 30 Sep 2020 13:20:10 +0200 Subject: [PATCH 30/70] p2p/discover: authTag -> nonce --- p2p/discover/v5_udp.go | 4 ++-- p2p/discover/v5_udp_test.go | 24 ++++++++++++------------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/p2p/discover/v5_udp.go b/p2p/discover/v5_udp.go index d0b8008241e..957ce49579a 100644 --- a/p2p/discover/v5_udp.go +++ b/p2p/discover/v5_udp.go @@ -704,7 +704,7 @@ func (t *UDPv5) handle(p v5wire.Packet, fromID enode.ID, fromAddr *net.UDPAddr) // handleUnknown initiates a handshake by responding with WHOAREYOU. func (t *UDPv5) handleUnknown(p *v5wire.Unknown, fromID enode.ID, fromAddr *net.UDPAddr) { - challenge := &v5wire.Whoareyou{AuthTag: p.AuthTag} + challenge := &v5wire.Whoareyou{Nonce: p.Nonce} crand.Read(challenge.IDNonce[:]) if n := t.getNode(fromID); n != nil { challenge.Node = n @@ -720,7 +720,7 @@ var ( // handleWhoareyou resends the active call as a handshake packet. func (t *UDPv5) handleWhoareyou(p *v5wire.Whoareyou, fromID enode.ID, fromAddr *net.UDPAddr) { - c, err := t.matchWithCall(fromID, p.AuthTag) + c, err := t.matchWithCall(fromID, p.Nonce) if err != nil { t.log.Debug("Invalid "+p.Name(), "id", fromID, "addr", fromAddr, "err", err) return diff --git a/p2p/discover/v5_udp_test.go b/p2p/discover/v5_udp_test.go index fbbc1b413d8..d91a2097db2 100644 --- a/p2p/discover/v5_udp_test.go +++ b/p2p/discover/v5_udp_test.go @@ -123,13 +123,13 @@ func TestUDPv5_unknownPacket(t *testing.T) { test := newUDPV5Test(t) defer test.close() - authTag := v5wire.Nonce{1, 2, 3} + nonce := v5wire.Nonce{1, 2, 3} check := func(p *v5wire.Whoareyou, wantSeq uint64) { t.Helper() - if p.AuthTag != authTag { - t.Error("wrong token in WHOAREYOU:", p.AuthTag, authTag) + if p.Nonce != nonce { + t.Error("wrong nonce in WHOAREYOU:", p.Nonce, nonce) } - if p.IDNonce == ([32]byte{}) { + if p.IDNonce == ([16]byte{}) { t.Error("all zero ID nonce") } if p.RecordSeq != wantSeq { @@ -138,7 +138,7 @@ func TestUDPv5_unknownPacket(t *testing.T) { } // Unknown packet from unknown node. - test.packetIn(&v5wire.Unknown{AuthTag: authTag}) + test.packetIn(&v5wire.Unknown{Nonce: nonce}) test.waitPacketOut(func(p *v5wire.Whoareyou, addr *net.UDPAddr, _ v5wire.Nonce) { check(p, 0) }) @@ -147,7 +147,7 @@ func TestUDPv5_unknownPacket(t *testing.T) { n := test.getNode(test.remotekey, test.remoteaddr).Node() test.table.addSeenNode(wrapNode(n)) - test.packetIn(&v5wire.Unknown{AuthTag: authTag}) + test.packetIn(&v5wire.Unknown{Nonce: nonce}) test.waitPacketOut(func(p *v5wire.Whoareyou, addr *net.UDPAddr, _ v5wire.Nonce) { check(p, n.Seq()) }) @@ -344,8 +344,8 @@ func TestUDPv5_callResend(t *testing.T) { }() // Ping answered by WHOAREYOU. - test.waitPacketOut(func(p *v5wire.Ping, addr *net.UDPAddr, authTag v5wire.Nonce) { - test.packetIn(&v5wire.Whoareyou{AuthTag: authTag}) + test.waitPacketOut(func(p *v5wire.Ping, addr *net.UDPAddr, nonce v5wire.Nonce) { + test.packetIn(&v5wire.Whoareyou{Nonce: nonce}) }) // Ping should be re-sent. test.waitPacketOut(func(p *v5wire.Ping, addr *net.UDPAddr, _ v5wire.Nonce) { @@ -377,12 +377,12 @@ func TestUDPv5_multipleHandshakeRounds(t *testing.T) { }() // Ping answered by WHOAREYOU. - test.waitPacketOut(func(p *v5wire.Ping, addr *net.UDPAddr, authTag v5wire.Nonce) { - test.packetIn(&v5wire.Whoareyou{AuthTag: authTag}) + test.waitPacketOut(func(p *v5wire.Ping, addr *net.UDPAddr, nonce v5wire.Nonce) { + test.packetIn(&v5wire.Whoareyou{Nonce: nonce}) }) // Ping answered by WHOAREYOU again. - test.waitPacketOut(func(p *v5wire.Ping, addr *net.UDPAddr, authTag v5wire.Nonce) { - test.packetIn(&v5wire.Whoareyou{AuthTag: authTag}) + test.waitPacketOut(func(p *v5wire.Ping, addr *net.UDPAddr, nonce v5wire.Nonce) { + test.packetIn(&v5wire.Whoareyou{Nonce: nonce}) }) if err := <-done; err != errTimeout { t.Fatalf("unexpected ping error: %q", err) From bf8c20d7f3386e09f82be9f57914a821f849dd14 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Wed, 30 Sep 2020 13:53:25 +0200 Subject: [PATCH 31/70] p2p/discover/v5wire: avoid logging challenge encoding in test vector test --- p2p/discover/v5wire/encoding_test.go | 46 +++++++++++++++------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/p2p/discover/v5wire/encoding_test.go b/p2p/discover/v5wire/encoding_test.go index aa63e4508ab..9f8134d101f 100644 --- a/p2p/discover/v5wire/encoding_test.go +++ b/p2p/discover/v5wire/encoding_test.go @@ -259,18 +259,22 @@ func TestTestVectorsV5(t *testing.T) { writeKey: hexutil.MustDecode("0x00000000000000000000000000000000"), readKey: hexutil.MustDecode("0x01010101010101010101010101010101"), } - challenge0 = &Whoareyou{ - Nonce: Nonce{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}, - IDNonce: testIDnonce, - RecordSeq: 0, - } - challenge1 = &Whoareyou{ - Nonce: Nonce{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}, - IDNonce: testIDnonce, - RecordSeq: 1, - } + challenge0A, challenge1A, challenge0B Whoareyou ) + // Create challenge packets. + c := Whoareyou{ + Nonce: Nonce{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}, + IDNonce: testIDnonce, + } + challenge0A, challenge1A, challenge0B = c, c, c + challenge1A.RecordSeq = 1 + net := newHandshakeTest() + challenge0A.Node = net.nodeA.n() + challenge1A.Node = net.nodeA.n() + challenge0B.Node = net.nodeB.n() + net.close() + type testVectorTest struct { name string // test vector name packet Packet // the packet to be encoded @@ -280,7 +284,7 @@ func TestTestVectorsV5(t *testing.T) { tests := []testVectorTest{ { name: "v5.1-whoareyou", - packet: challenge0, + packet: &challenge0B, }, { name: "v5.1-ping-message", @@ -294,29 +298,29 @@ func TestTestVectorsV5(t *testing.T) { }, }, { - name: "v5.1-ping-handshake", + name: "v5.1-ping-handshake-enr", packet: &Ping{ ReqID: []byte{0, 0, 0, 1}, ENRSeq: 1, }, - challenge: challenge1, + challenge: &challenge0A, prep: func(net *handshakeTest) { - challenge1.Node = net.nodeA.n() - net.nodeA.encode(t, net.nodeB, challenge1) - net.nodeB.c.sc.storeSentHandshake(idA, addr, challenge1) + // Update challenge.Header.AuthData. + net.nodeA.c.encodeWhoareyou(idB, &challenge0A) + net.nodeB.c.sc.storeSentHandshake(idA, addr, &challenge0A) }, }, { - name: "v5.1-ping-handshake-enr", + name: "v5.1-ping-handshake", packet: &Ping{ ReqID: []byte{0, 0, 0, 1}, ENRSeq: 1, }, - challenge: challenge0, + challenge: &challenge1A, prep: func(net *handshakeTest) { - challenge0.Node = net.nodeA.n() - net.nodeA.encode(t, net.nodeB, challenge0) - net.nodeB.c.sc.storeSentHandshake(idA, addr, challenge0) + // Update challenge.Header.AuthData. + net.nodeA.c.encodeWhoareyou(idB, &challenge1A) + net.nodeB.c.sc.storeSentHandshake(idA, addr, &challenge1A) }, }, } From 3897ca56f44d697d4db2b5bdde12253ce7a1bbac Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Wed, 30 Sep 2020 13:53:59 +0200 Subject: [PATCH 32/70] p2p/discover/v5wire: feed IV into KDF as well This increases the recipient's contribution to session keys back to 32 bytes. --- p2p/discover/v5wire/encoding.go | 20 ++++++++++--------- .../testdata/v5.1-ping-handshake-enr.txt | 6 +++--- .../v5wire/testdata/v5.1-ping-handshake.txt | 6 +++--- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/p2p/discover/v5wire/encoding.go b/p2p/discover/v5wire/encoding.go index 7ccb49c4837..94ac81fed48 100644 --- a/p2p/discover/v5wire/encoding.go +++ b/p2p/discover/v5wire/encoding.go @@ -601,19 +601,21 @@ func (c *Codec) decryptMessage(input []byte, nonce []byte, readKey []byte) (Pack // deriveKeys generates session keys using elliptic-curve Diffie-Hellman key agreement. func (c *Codec) deriveKeys(n1, n2 enode.ID, priv *ecdsa.PrivateKey, pub *ecdsa.PublicKey, challenge *Whoareyou) *session { + var ( + info = []byte("discovery v5 key agreement") + salt = make([]byte, 32) + ) + info = append(info, n1[:]...) + info = append(info, n2[:]...) + copy(salt, challenge.Header.IV[:]) + copy(salt[len(challenge.Header.IV):], challenge.IDNonce[:]) + eph := ecdh(priv, pub) if eph == nil { return nil } - - info := []byte("discovery v5 key agreement") - info = append(info, n1[:]...) - info = append(info, n2[:]...) - kdf := hkdf.New(c.sha256reset, eph, challenge.IDNonce[:], info) - sec := session{ - writeKey: make([]byte, aesKeySize), - readKey: make([]byte, aesKeySize), - } + kdf := hkdf.New(c.sha256reset, eph, salt, info) + sec := session{writeKey: make([]byte, aesKeySize), readKey: make([]byte, aesKeySize)} kdf.Read(sec.writeKey) kdf.Read(sec.readKey) for i := range eph { diff --git a/p2p/discover/v5wire/testdata/v5.1-ping-handshake-enr.txt b/p2p/discover/v5wire/testdata/v5.1-ping-handshake-enr.txt index 4edf4e82f7d..be5d8efb40c 100644 --- a/p2p/discover/v5wire/testdata/v5.1-ping-handshake-enr.txt +++ b/p2p/discover/v5wire/testdata/v5.1-ping-handshake-enr.txt @@ -1,7 +1,7 @@ # src-node-id = 0xaaaa8419e9f49d0083561b48287df592939a8d19947d8c0ef88f2a4856a69fbb # dest-node-id = 0xbbbb9d047f0488c0b5a93c1c3f2d8bafc7c8ff337024a55434a0d0555de64db9 # nonce = 0xffffffffffffffffffffffff -# read-key = 0x4917330b5aeb51650213f90d5f253c45 +# read-key = 0xf901161aebd1298aa813621ad0c05343 # ping.req-id = 0x00000001 # ping.enr-seq = 1 # @@ -24,5 +24,5 @@ f0396c93f215fa4ef524e0ed04c3c21e39b1868e1ca8105e585ec17315e755e6 cfc4dd6cb7fd8e1a1f55e49b4b5eb024221482105346f3c82b15fdaae36a3bb1 2a494683b4a3c7f2ae41306252fed84785e2bbff3b022812d0882f06978df84a 80d443972213342d04b9048fc3b1d5fcb1df0f822152eced6da4d3f6df27e70e -4539717307a0208cd27fccc3d4569a69fd321469e07a60e8125a9be78518093d -d2 +4539717307a0208cd2dc394f221e162100550a011363be21154c6b42e3816f0a +38 diff --git a/p2p/discover/v5wire/testdata/v5.1-ping-handshake.txt b/p2p/discover/v5wire/testdata/v5.1-ping-handshake.txt index 1114b363dbe..01a40b717c9 100644 --- a/p2p/discover/v5wire/testdata/v5.1-ping-handshake.txt +++ b/p2p/discover/v5wire/testdata/v5.1-ping-handshake.txt @@ -1,7 +1,7 @@ # src-node-id = 0xaaaa8419e9f49d0083561b48287df592939a8d19947d8c0ef88f2a4856a69fbb # dest-node-id = 0xbbbb9d047f0488c0b5a93c1c3f2d8bafc7c8ff337024a55434a0d0555de64db9 # nonce = 0xffffffffffffffffffffffff -# read-key = 0x4917330b5aeb51650213f90d5f253c45 +# read-key = 0xf901161aebd1298aa813621ad0c05343 # ping.req-id = 0x00000001 # ping.enr-seq = 1 # @@ -20,5 +20,5 @@ 3c9ae48d04689e4bb2fb3b042ea9fdbe25ed8d9e7fe2564f8d1f2cbd61b93eaa f58b961455335ab91bcf2c2e12656512efaee24a691567ab535b3d447a361e23 b8f84ca7ba007ff4bd5796706adff216ab862a9186875f9494150c4ae06fa4d1 -f0396c93f215fa4ef5247fccc3d4569a69fd321469e07a60e8125a9be7851809 -3dd2 +f0396c93f215fa4ef524dc394f221e162100550a011363be21154c6b42e3816f +0a38 From 15e295e086e58b2733907fa635514ffce78c713a Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Wed, 30 Sep 2020 18:36:34 +0200 Subject: [PATCH 33/70] p2p/discover/v5wire: update id signature text --- p2p/discover/v5wire/crypto.go | 7 +++---- p2p/discover/v5wire/testdata/v5.1-ping-handshake-enr.txt | 6 +++--- p2p/discover/v5wire/testdata/v5.1-ping-handshake.txt | 6 +++--- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/p2p/discover/v5wire/crypto.go b/p2p/discover/v5wire/crypto.go index 0cf1752e1f8..b4ca8ca7ff0 100644 --- a/p2p/discover/v5wire/crypto.go +++ b/p2p/discover/v5wire/crypto.go @@ -16,9 +16,8 @@ import ( const ( // Encryption/authentication parameters. - aesKeySize = 16 - gcmNonceSize = 12 - idNoncePrefix = "discovery-id-nonce" + aesKeySize = 16 + gcmNonceSize = 12 ) // Nonce represents a nonce used for AES/GCM. @@ -50,7 +49,7 @@ func DecodePubkey(curve elliptic.Curve, e []byte) (*ecdsa.PublicKey, error) { // idNonceHash computes the ID signature hash used in the handshake. func idNonceHash(h hash.Hash, destID enode.ID, iv, authdata, ephkey []byte) []byte { h.Reset() - h.Write([]byte(idNoncePrefix)) + h.Write([]byte("discovery v5 identity proof")) h.Write(iv) h.Write(authdata) h.Write(ephkey) diff --git a/p2p/discover/v5wire/testdata/v5.1-ping-handshake-enr.txt b/p2p/discover/v5wire/testdata/v5.1-ping-handshake-enr.txt index be5d8efb40c..e0337391f66 100644 --- a/p2p/discover/v5wire/testdata/v5.1-ping-handshake-enr.txt +++ b/p2p/discover/v5wire/testdata/v5.1-ping-handshake-enr.txt @@ -17,9 +17,9 @@ 00000000000000000000000000000000088b3d4342774649980a4adf72a8fcaa 963f24b27a2f6bb44c7ed5ca10e87de130f94d2390b9853c3dcb402b1e9472d4 -3c9ae48d04689e4bb26d809d2756501313e8e59826da6a446913ef43e027fd05 -b16e15e0b544a240c7c3b86d635aeff54300d3707e4fdfce00e0e0e4b05ff99b -2eee244de9271951f65796706adff216ab862a9186875f9494150c4ae06fa4d1 +3c9ae48d04689e4bb2a1b46f2dd001878934d0ee3dc3126ccff587a3a155e378 +e27ad720d8b6ecb86ab88496376d96283d0d01a1e7a55d36e0e04ecab1947dc2 +758335d19afe0b13e65796706adff216ab862a9186875f9494150c4ae06fa4d1 f0396c93f215fa4ef524e0ed04c3c21e39b1868e1ca8105e585ec17315e755e6 cfc4dd6cb7fd8e1a1f55e49b4b5eb024221482105346f3c82b15fdaae36a3bb1 2a494683b4a3c7f2ae41306252fed84785e2bbff3b022812d0882f06978df84a diff --git a/p2p/discover/v5wire/testdata/v5.1-ping-handshake.txt b/p2p/discover/v5wire/testdata/v5.1-ping-handshake.txt index 01a40b717c9..0e81aaab0df 100644 --- a/p2p/discover/v5wire/testdata/v5.1-ping-handshake.txt +++ b/p2p/discover/v5wire/testdata/v5.1-ping-handshake.txt @@ -17,8 +17,8 @@ 00000000000000000000000000000000088b3d4342774649980a4adf72a8fcaa 963f24b27a2f6bb44c7ed5ca10e87de130f94d2390b9853c3dcbc12b1e9472d4 -3c9ae48d04689e4bb2fb3b042ea9fdbe25ed8d9e7fe2564f8d1f2cbd61b93eaa -f58b961455335ab91bcf2c2e12656512efaee24a691567ab535b3d447a361e23 -b8f84ca7ba007ff4bd5796706adff216ab862a9186875f9494150c4ae06fa4d1 +3c9ae48d04689e4bb21c063afb08d96bb8e3e73d3589170d8371a04e30d35186 +197b8906260a01637fe8d38a4e2dfc699103e34c55dc41744000b461b353aaa2 +a7266e63cedafd918b5796706adff216ab862a9186875f9494150c4ae06fa4d1 f0396c93f215fa4ef524dc394f221e162100550a011363be21154c6b42e3816f 0a38 From 1fdee239f8673e09539e3f0956a488818551a41c Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Wed, 30 Sep 2020 18:51:48 +0200 Subject: [PATCH 34/70] p2p/discover/v5wire: clean up packet version check --- p2p/discover/v5wire/encoding.go | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/p2p/discover/v5wire/encoding.go b/p2p/discover/v5wire/encoding.go index 94ac81fed48..055d7a545ab 100644 --- a/p2p/discover/v5wire/encoding.go +++ b/p2p/discover/v5wire/encoding.go @@ -51,7 +51,8 @@ type Header struct { // HeaderData contains the static fields of a packet header. type HeaderData struct { - ProtocolID [8]byte + ProtocolID [6]byte + Version uint16 SrcID enode.ID Flag byte AuthSize uint16 @@ -86,13 +87,13 @@ const ( // Protocol constants. const ( handshakeTimeout = time.Second - handshakeVersion = 1 + version = 1 minVersion = 1 - protocolID = "discv5" - versionOffset = 7 sizeofMaskingIV = 16 ) +var protocolID = [6]byte{'d', 'i', 's', 'c', 'v', '5'} + // Errors. var ( errTooShort = errors.New("packet too short") @@ -226,8 +227,8 @@ func (c *Codec) makeHeader(toID enode.ID, flag byte, authsizeExtra int) Header { panic(fmt.Errorf("BUG: auth size %d overflows uint16", authsize)) } data.AuthSize = uint16(authsize) - copy(data.ProtocolID[:], protocolID) - data.ProtocolID[versionOffset] = handshakeVersion + data.ProtocolID = protocolID + data.Version = version return Header{HeaderData: data} } @@ -636,12 +637,7 @@ func (c *Codec) maskOutputPacket(destID enode.ID, head *Header, headerBytes []by // isValid returns true if h contains a valid protocol ID and auth size. func (h *HeaderData) isValid(packetLen int) bool { - for i := range protocolID { - if h.ProtocolID[i] != protocolID[i] { - return false - } - } - if h.ProtocolID[versionOffset] < minVersion { + if h.ProtocolID != protocolID || h.Version < minVersion { return false } return int(h.AuthSize) <= packetLen From 07e5435ed4b8f237277b9a447e3b1a96288f88cd Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Wed, 30 Sep 2020 19:40:51 +0200 Subject: [PATCH 35/70] p2p/discover/v5wire: check minimum packet size --- p2p/discover/v5wire/encoding.go | 41 ++++++++++------------------ p2p/discover/v5wire/encoding_test.go | 19 +++++++++++++ p2p/discover/v5wire/session.go | 3 ++ 3 files changed, 37 insertions(+), 26 deletions(-) diff --git a/p2p/discover/v5wire/encoding.go b/p2p/discover/v5wire/encoding.go index 055d7a545ab..94fe482fe80 100644 --- a/p2p/discover/v5wire/encoding.go +++ b/p2p/discover/v5wire/encoding.go @@ -27,7 +27,7 @@ import ( "errors" "fmt" "hash" - "time" + mrand "math/rand" "github.com/ethereum/go-ethereum/common/mclock" "github.com/ethereum/go-ethereum/p2p/enode" @@ -86,10 +86,11 @@ const ( // Protocol constants. const ( - handshakeTimeout = time.Second - version = 1 - minVersion = 1 - sizeofMaskingIV = 16 + version = 1 + minVersion = 1 + sizeofMaskingIV = 16 + minPacketSize = 90 + randomPacketMsgSize = 20 ) var protocolID = [6]byte{'d', 'i', 's', 'c', 'v', '5'} @@ -115,16 +116,6 @@ var ( sizeofMessageAuthData = len(Nonce{}) ) -// func init() { -// var ( -// sizeofGCMTag = 16 -// sizeofWhoareyou = sizeofHeaderData + sizeofWhoareyouAuthData -// sizeofEmptyMsg = sizeofHeaderData + sizeofMessageAuthData + sizeofGCMTag -// ) -// fmt.Println("WHOAREYOU size:", sizeofWhoareyou) -// fmt.Println("EMPTY msg size:", sizeofEmptyMsg) -// } - // Codec encodes and decodes discovery v5 packets. type Codec struct { sha256 hash.Hash @@ -177,16 +168,13 @@ func (c *Codec) Encode(id enode.ID, addr string, packet Packet, challenge *Whoar return nil, Nonce{}, err } enc, err := c.EncodeRaw(id, header, msgdata) - if err != nil { - return nil, Nonce{}, err - } - return enc, header.Nonce, nil + return enc, header.Nonce, err } func (c *Codec) EncodeRaw(id enode.ID, head Header, msgdata []byte) ([]byte, error) { // Generate masking IV. if err := c.sc.maskingIVGen(head.IV[:]); err != nil { - return nil, err + return nil, fmt.Errorf("can't generate masking IV: %v", err) } // Encode the packet. @@ -239,7 +227,8 @@ func (c *Codec) encodeRandom(toID enode.ID) (Header, []byte, error) { return head, nil, fmt.Errorf("can't get random data: %v", err) } head.AuthData = head.Nonce[:] - msgdata := make([]byte, 16) + msgdata := make([]byte, randomPacketMsgSize) + mrand.Read(msgdata) return head, msgdata, nil } @@ -390,12 +379,8 @@ func (c *Codec) encodeMessage(toID enode.ID, s *session, packet Packet) (Header, // Decode decodes a discovery packet. func (c *Codec) Decode(input []byte, addr string) (src enode.ID, n *enode.Node, p Packet, err error) { - // Delete timed-out handshakes. This must happen before decoding to avoid - // processing the same handshake twice. - c.sc.handshakeGC() - // Unmask the static header. - if len(input) < sizeofHeaderData+sizeofMaskingIV { + if len(input) < minPacketSize { return enode.ID{}, nil, nil, errTooShort } var head Header @@ -416,6 +401,10 @@ func (c *Codec) Decode(input []byte, addr string) (src enode.ID, n *enode.Node, copy(head.AuthData, input[sizeofHeaderData:]) mask.XORKeyStream(head.AuthData, head.AuthData) + // Delete timed-out handshakes. This must happen before decoding to avoid + // processing the same handshake twice. + c.sc.handshakeGC() + // Decode auth part and message. msgdata := input[sizeofHeaderData+len(head.AuthData):] switch head.Flag { diff --git a/p2p/discover/v5wire/encoding_test.go b/p2p/discover/v5wire/encoding_test.go index 9f8134d101f..92f397f7e28 100644 --- a/p2p/discover/v5wire/encoding_test.go +++ b/p2p/discover/v5wire/encoding_test.go @@ -73,6 +73,25 @@ func TestDeriveKeysV5(t *testing.T) { } } +// This test checks that the minPacketSize and randomPacketMsgSize constants are well-defined. +func TestMinSizes(t *testing.T) { + var ( + gcmTagSize = 16 + whoareyou = sizeofMaskingIV + sizeofHeaderData + sizeofWhoareyouAuthData + msgHeader = sizeofMaskingIV + sizeofHeaderData + sizeofMessageAuthData + emptyMsg = msgHeader + gcmTagSize + minMessageSize = 3 // should be fine + ) + t.Log("WHOAREYOU size", whoareyou) + t.Log("EMPTY msg size", emptyMsg) + if want := emptyMsg + minMessageSize; minPacketSize != want { + t.Fatalf("wrong minPacketSize %d, want %d", minPacketSize, want) + } + if msgHeader+randomPacketMsgSize < minPacketSize { + t.Fatalf("randomPacketMsgSize %d too small", randomPacketMsgSize) + } +} + // This test checks the basic handshake flow where A talks to B and A has no secrets. func TestHandshakeV5(t *testing.T) { t.Parallel() diff --git a/p2p/discover/v5wire/session.go b/p2p/discover/v5wire/session.go index 36e38cca9f0..d52b5c11810 100644 --- a/p2p/discover/v5wire/session.go +++ b/p2p/discover/v5wire/session.go @@ -20,6 +20,7 @@ import ( "crypto/ecdsa" crand "crypto/rand" "encoding/binary" + "time" "github.com/ethereum/go-ethereum/common/mclock" "github.com/ethereum/go-ethereum/crypto" @@ -27,6 +28,8 @@ import ( "github.com/hashicorp/golang-lru/simplelru" ) +const handshakeTimeout = time.Second + // The SessionCache keeps negotiated encryption keys and // state for in-progress handshakes in the Discovery v5 wire protocol. type SessionCache struct { From cafd22d17487c9522a20a5115e4d41e207939978 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Wed, 30 Sep 2020 22:25:12 +0200 Subject: [PATCH 36/70] p2p/discover/v5wire: move src-id into authdata This reduces WHOAREYOU size by another 32 bytes. It is now much smaller than any message packet. Unfortunately, this also means we no longer have the source ID for invalid packets. --- p2p/discover/v5_udp.go | 4 +- p2p/discover/v5wire/encoding.go | 202 ++++++++++-------- p2p/discover/v5wire/encoding_test.go | 17 +- .../testdata/v5.1-ping-handshake-enr.txt | 12 +- .../v5wire/testdata/v5.1-ping-handshake.txt | 12 +- .../v5wire/testdata/v5.1-ping-message.txt | 6 +- .../v5wire/testdata/v5.1-whoareyou.txt | 7 +- 7 files changed, 142 insertions(+), 118 deletions(-) diff --git a/p2p/discover/v5_udp.go b/p2p/discover/v5_udp.go index 957ce49579a..fe0d8be64df 100644 --- a/p2p/discover/v5_udp.go +++ b/p2p/discover/v5_udp.go @@ -722,7 +722,7 @@ var ( func (t *UDPv5) handleWhoareyou(p *v5wire.Whoareyou, fromID enode.ID, fromAddr *net.UDPAddr) { c, err := t.matchWithCall(fromID, p.Nonce) if err != nil { - t.log.Debug("Invalid "+p.Name(), "id", fromID, "addr", fromAddr, "err", err) + t.log.Debug("Invalid "+p.Name(), "addr", fromAddr, "err", err) return } @@ -737,7 +737,7 @@ func (t *UDPv5) handleWhoareyou(p *v5wire.Whoareyou, fromID enode.ID, fromAddr * // matchWithCall checks whether a handshake attempt matches the active call. func (t *UDPv5) matchWithCall(fromID enode.ID, authTag v5wire.Nonce) (*callV5, error) { c := t.activeCallByAuth[string(authTag[:])] - if c == nil || c.node.ID() != fromID { + if c == nil { return nil, errChallengeNoCall } if c.handshakeCount > 0 { diff --git a/p2p/discover/v5wire/encoding.go b/p2p/discover/v5wire/encoding.go index 94fe482fe80..7cebf655d40 100644 --- a/p2p/discover/v5wire/encoding.go +++ b/p2p/discover/v5wire/encoding.go @@ -27,7 +27,6 @@ import ( "errors" "fmt" "hash" - mrand "math/rand" "github.com/ethereum/go-ethereum/common/mclock" "github.com/ethereum/go-ethereum/p2p/enode" @@ -45,51 +44,56 @@ import ( type Header struct { IV [sizeofMaskingIV]byte HeaderData - Nonce Nonce AuthData []byte + + src enode.ID // used by decoder } // HeaderData contains the static fields of a packet header. type HeaderData struct { ProtocolID [6]byte Version uint16 - SrcID enode.ID Flag byte + Nonce Nonce AuthSize uint16 } // Authdata layouts. type ( whoareyouAuthData struct { - Nonce Nonce // nonce of request packet IDNonce [16]byte // ID proof data RecordSeq uint64 // highest known ENR sequence of requester } handshakeAuthData struct { h struct { - Nonce Nonce // AES-GCM nonce of message - SigSize byte // ignature data - PubkeySize byte // offset of + SrcID enode.ID + SigSize byte // ignature data + PubkeySize byte // offset of } // Trailing variable-size data. signature, pubkey, record []byte } + + messageAuthData struct { + SrcID enode.ID + } ) // Packet header flag values. const ( - flagMessage = 0 - flagWhoareyou = 1 - flagHandshake = 2 + flagMessage = iota + flagWhoareyou + flagHandshake ) // Protocol constants. const ( - version = 1 - minVersion = 1 - sizeofMaskingIV = 16 - minPacketSize = 90 + version = 1 + minVersion = 1 + sizeofMaskingIV = 16 + + minMessageSize = 51 // this refers to data after static headers randomPacketMsgSize = 20 ) @@ -100,6 +104,9 @@ var ( errTooShort = errors.New("packet too short") errInvalidHeader = errors.New("invalid packet header") errInvalidFlag = errors.New("invalid flag value in header") + errMinVersion = errors.New("version of packet header below minimum") + errMsgTooShort = errors.New("message/handshake packet below minimum size") + errAuthSize = errors.New("declared auth size is beyond packet length") errUnexpectedHandshake = errors.New("unexpected auth response, not in handshake") errInvalidAuthKey = errors.New("invalid ephemeral pubkey") errNoRecord = errors.New("expected ENR in handshake but none sent") @@ -113,7 +120,8 @@ var ( sizeofHeaderData = binary.Size(HeaderData{}) sizeofWhoareyouAuthData = binary.Size(whoareyouAuthData{}) sizeofHandshakeAuthData = binary.Size(handshakeAuthData{}.h) - sizeofMessageAuthData = len(Nonce{}) + sizeofMessageAuthData = binary.Size(messageAuthData{}) + sizeofStaticPacketData = sizeofMaskingIV + sizeofHeaderData ) // Codec encodes and decodes discovery v5 packets. @@ -148,7 +156,6 @@ func (c *Codec) Encode(id enode.ID, addr string, packet Packet, challenge *Whoar err error ) if p, ok := packet.(*Whoareyou); ok { - // WHOAREYOU has special encoding. header, err = c.encodeWhoareyou(id, p) if err == nil { c.sc.storeSentHandshake(id, addr, p) @@ -195,10 +202,6 @@ func (c *Codec) EncodeRaw(id enode.ID, head Header, msgdata []byte) ([]byte, err // makeHeader creates a packet header. func (c *Codec) makeHeader(toID enode.ID, flag byte, authsizeExtra int) Header { - data := HeaderData{ - SrcID: c.localnode.ID(), - Flag: flag, - } var authsize int switch flag { case flagMessage: @@ -214,21 +217,29 @@ func (c *Codec) makeHeader(toID enode.ID, flag byte, authsizeExtra int) Header { if authsize > int(^uint16(0)) { panic(fmt.Errorf("BUG: auth size %d overflows uint16", authsize)) } - data.AuthSize = uint16(authsize) - data.ProtocolID = protocolID - data.Version = version - return Header{HeaderData: data} + return Header{ + HeaderData: HeaderData{ + ProtocolID: protocolID, + Version: version, + Flag: flag, + AuthSize: uint16(authsize), + }, + } } // encodeRandom encodes a packet with random content. func (c *Codec) encodeRandom(toID enode.ID) (Header, []byte, error) { head := c.makeHeader(toID, flagMessage, 0) + auth := messageAuthData{SrcID: c.localnode.ID()} if _, err := crand.Read(head.Nonce[:]); err != nil { return head, nil, fmt.Errorf("can't get random data: %v", err) } - head.AuthData = head.Nonce[:] + c.buf.Reset() + binary.Write(&c.buf, binary.BigEndian, auth) + head.AuthData = bytesCopy(&c.buf) + msgdata := make([]byte, randomPacketMsgSize) - mrand.Read(msgdata) + crand.Read(msgdata) return head, msgdata, nil } @@ -241,7 +252,6 @@ func (c *Codec) encodeWhoareyou(toID enode.ID, packet *Whoareyou) (Header, error // Encode auth data. auth := &whoareyouAuthData{ - Nonce: packet.Nonce, IDNonce: packet.IDNonce, RecordSeq: packet.RecordSeq, } @@ -250,8 +260,8 @@ func (c *Codec) encodeWhoareyou(toID enode.ID, packet *Whoareyou) (Header, error // Create header. head := c.makeHeader(toID, flagWhoareyou, 0) - head.AuthData = make([]byte, c.buf.Len()) - copy(head.AuthData, c.buf.Bytes()) + head.AuthData = bytesCopy(&c.buf) + head.Nonce = packet.Nonce // Update header in packet. packet.Header = head @@ -272,6 +282,12 @@ func (c *Codec) encodeHandshakeMessage(toID enode.ID, addr string, packet Packet return Header{}, nil, err } + // Generate nonce for message. + nonce, err := c.sc.nextNonce(session) + if err != nil { + return Header{}, nil, fmt.Errorf("can't generate nonce: %v", err) + } + // TODO: this should happen when the first authenticated message is received c.sc.storeNewSession(toID, addr, session) @@ -285,19 +301,18 @@ func (c *Codec) encodeHandshakeMessage(toID enode.ID, addr string, packet Packet c.buf.Write(auth.signature) c.buf.Write(auth.pubkey) c.buf.Write(auth.record) + head.AuthData = bytesCopy(&c.buf) + head.Nonce = nonce - // Copy auth into packet header. - head.AuthData = make([]byte, c.buf.Len()) - copy(head.AuthData, c.buf.Bytes()) - head.Nonce = auth.h.Nonce - - // Encrypt packet body. + // Encode message plaintext. c.msgbuf.Reset() c.msgbuf.WriteByte(packet.Kind()) if err := rlp.Encode(&c.msgbuf, packet); err != nil { return head, nil, err } messagePT := c.msgbuf.Bytes() + + // Encrypt message data. var msgdata []byte msgdata, err = encryptGCM(msgdata, session.writeKey, head.Nonce[:], messagePT, nil) return head, msgdata, err @@ -305,14 +320,8 @@ func (c *Codec) encodeHandshakeMessage(toID enode.ID, addr string, packet Packet // encodeAuthHeader creates the auth header on a request packet following WHOAREYOU. func (c *Codec) makeHandshakeAuth(toID enode.ID, addr string, challenge *Whoareyou) (*handshakeAuthData, *session, error) { - session := new(session) - nonce, err := c.sc.nextNonce(session) - if err != nil { - return nil, nil, fmt.Errorf("can't generate nonce: %v", err) - } - auth := new(handshakeAuthData) - auth.h.Nonce = nonce + auth.h.SrcID = c.localnode.ID() // Create the ephemeral key. This needs to be first because the // key is part of the ID nonce signature. @@ -355,13 +364,16 @@ func (c *Codec) makeHandshakeAuth(toID enode.ID, addr string, challenge *Whoarey func (c *Codec) encodeMessage(toID enode.ID, s *session, packet Packet) (Header, []byte, error) { head := c.makeHeader(toID, flagMessage, 0) - // Create the nonce. + // Create the header. nonce, err := c.sc.nextNonce(s) if err != nil { return Header{}, nil, fmt.Errorf("can't generate nonce: %v", err) } + auth := messageAuthData{SrcID: c.localnode.ID()} + c.buf.Reset() + binary.Write(&c.buf, binary.BigEndian, &auth) + head.AuthData = bytesCopy(&c.buf) head.Nonce = nonce - head.AuthData = head.Nonce[:] // Encode the message plaintext. c.msgbuf.Reset() @@ -380,7 +392,7 @@ func (c *Codec) encodeMessage(toID enode.ID, s *session, packet Packet) (Header, // Decode decodes a discovery packet. func (c *Codec) Decode(input []byte, addr string) (src enode.ID, n *enode.Node, p Packet, err error) { // Unmask the static header. - if len(input) < minPacketSize { + if len(input) < sizeofStaticPacketData { return enode.ID{}, nil, nil, errTooShort } var head Header @@ -392,10 +404,11 @@ func (c *Codec) Decode(input []byte, addr string) (src enode.ID, n *enode.Node, // Decode and verify the header. c.reader.Reset(input) - err = binary.Read(&c.reader, binary.BigEndian, &head.HeaderData) - if err != nil || !head.isValid(c.reader.Len()) { - return enode.ID{}, nil, nil, errInvalidHeader + binary.Read(&c.reader, binary.BigEndian, &head.HeaderData) + if err := head.checkValid(c.reader.Len()); err != nil { + return enode.ID{}, nil, nil, err } + // Unmask auth data. head.AuthData = make([]byte, head.AuthSize) copy(head.AuthData, input[sizeofHeaderData:]) @@ -417,7 +430,7 @@ func (c *Codec) Decode(input []byte, addr string) (src enode.ID, n *enode.Node, default: err = errInvalidFlag } - return head.SrcID, n, p, err + return head.src, n, p, err } // decodeWhoareyou reads packet data after the header as a WHOAREYOU packet. @@ -431,10 +444,8 @@ func (c *Codec) decodeWhoareyou(head *Header) (Packet, error) { c.reader.Reset(head.AuthData) auth := new(whoareyouAuthData) binary.Read(&c.reader, binary.BigEndian, auth) - head.Nonce = auth.Nonce p := &Whoareyou{ Header: *head, - Nonce: auth.Nonce, IDNonce: auth.IDNonce, RecordSeq: auth.RecordSeq, } @@ -442,55 +453,54 @@ func (c *Codec) decodeWhoareyou(head *Header) (Packet, error) { } func (c *Codec) decodeHandshakeMessage(fromAddr string, head *Header, msgdata []byte) (n *enode.Node, p Packet, err error) { - node, nonce, session, err := c.decodeHandshake(fromAddr, head) + node, auth, session, err := c.decodeHandshake(fromAddr, head) if err != nil { return nil, nil, err } - head.Nonce = nonce // Decrypt the message using the new session keys. - msg, err := c.decryptMessage(msgdata, nonce[:], session.readKey) + msg, err := c.decryptMessage(msgdata, head.Nonce[:], session.readKey) if err != nil { return node, msg, err } // Handshake OK, drop the challenge and store the new session keys. - c.sc.storeNewSession(head.SrcID, fromAddr, session) - c.sc.deleteHandshake(head.SrcID, fromAddr) + c.sc.storeNewSession(auth.h.SrcID, fromAddr, session) + c.sc.deleteHandshake(auth.h.SrcID, fromAddr) return node, msg, nil } -func (c *Codec) decodeHandshake(fromAddr string, head *Header) (*enode.Node, Nonce, *session, error) { +func (c *Codec) decodeHandshake(fromAddr string, head *Header) (*enode.Node, *handshakeAuthData, *session, error) { auth, err := c.decodeHandshakeAuthData(head) if err != nil { - return nil, Nonce{}, nil, err + return nil, nil, nil, err } // Verify against our last WHOAREYOU. - challenge := c.sc.getHandshake(head.SrcID, fromAddr) + challenge := c.sc.getHandshake(auth.h.SrcID, fromAddr) if challenge == nil { - return nil, Nonce{}, nil, errUnexpectedHandshake + return nil, nil, nil, errUnexpectedHandshake } // Get node record. - node, err := c.decodeHandshakeRecord(challenge.Node, head.SrcID, auth.record) + node, err := c.decodeHandshakeRecord(challenge.Node, auth.h.SrcID, auth.record) if err != nil { - return nil, Nonce{}, nil, err + return nil, nil, nil, err } // Verify ephemeral key is on curve. ephkey, err := DecodePubkey(c.privkey.Curve, auth.pubkey) if err != nil { - return nil, Nonce{}, nil, errInvalidAuthKey + return nil, nil, nil, errInvalidAuthKey } // Verify ID nonce signature. sig := auth.signature err = verifyIDSignature(c.sha256, sig, node, c.localnode.ID(), auth.pubkey, &challenge.Header) if err != nil { - return nil, Nonce{}, nil, err + return nil, nil, nil, err } // Derive sesssion keys. - session := c.deriveKeys(head.SrcID, c.localnode.ID(), c.privkey, ephkey, challenge) + session := c.deriveKeys(auth.h.SrcID, c.localnode.ID(), c.privkey, ephkey, challenge) session = session.keysFlipped() - return node, auth.h.Nonce, session, nil + return node, auth, session, nil } // decodeHandshakeAuthData reads the authdata section of a handshake packet. @@ -502,6 +512,7 @@ func (c *Codec) decodeHandshakeAuthData(head *Header) (*handshakeAuthData, error c.reader.Reset(head.AuthData) var auth handshakeAuthData binary.Read(&c.reader, binary.BigEndian, &auth.h) + head.src = auth.h.SrcID // Decode variable-size part. varspace := int(head.AuthSize) - sizeofHandshakeAuthData @@ -524,16 +535,6 @@ func (c *Codec) decodeHandshakeAuthData(head *Header) (*handshakeAuthData, error return &auth, nil } -// readNew reads 'length' bytes from 'r' and stores them into 'data'. -func readNew(data *[]byte, length int, r *bytes.Reader) bool { - if length == 0 { - return true - } - *data = make([]byte, length) - n, _ := r.Read(*data) - return n == length -} - // decodeHandshakeRecord verifies the node record contained in a handshake packet. The // remote node should include the record if we don't have one or if ours is older than the // latest sequence number. @@ -566,10 +567,13 @@ func (c *Codec) decodeMessage(fromAddr string, head *Header, msgdata []byte) (Pa if len(head.AuthData) < sizeofMessageAuthData { return nil, errTooShort } - copy(head.Nonce[:], head.AuthData) + var auth messageAuthData + c.reader.Reset(head.AuthData) + binary.Read(&c.reader, binary.BigEndian, &auth) + head.src = auth.SrcID // Try decrypting the message. - key := c.sc.readKey(head.SrcID, fromAddr) + key := c.sc.readKey(auth.SrcID, fromAddr) msg, err := c.decryptMessage(msgdata, head.Nonce[:], key) if err == errMessageDecrypt { // It didn't work. Start the handshake since this is an ordinary message packet. @@ -578,7 +582,7 @@ func (c *Codec) decodeMessage(fromAddr string, head *Header, msgdata []byte) (Pa return msg, err } -func (c *Codec) decryptMessage(input []byte, nonce []byte, readKey []byte) (Packet, error) { +func (c *Codec) decryptMessage(input, nonce, readKey []byte) (Packet, error) { msgdata, err := decryptGCM(readKey, nonce, input, nil) if err != nil { return nil, errMessageDecrypt @@ -620,16 +624,22 @@ func (c *Codec) sha256reset() hash.Hash { return c.sha256 } -// maskOutputPacket applies protocol header masking to a packet sent to destID. -func (c *Codec) maskOutputPacket(destID enode.ID, head *Header, headerBytes []byte) { -} - -// isValid returns true if h contains a valid protocol ID and auth size. -func (h *HeaderData) isValid(packetLen int) bool { - if h.ProtocolID != protocolID || h.Version < minVersion { - return false +// checkValid performs some basic validity checks on the header. +// The packetLen here is the length remaining after the static header. +func (h *HeaderData) checkValid(packetLen int) error { + if h.ProtocolID != protocolID { + return errInvalidHeader + } + if h.Version < minVersion { + return errMinVersion + } + if h.Flag != flagWhoareyou && packetLen < minMessageSize { + return errMsgTooShort } - return int(h.AuthSize) <= packetLen + if int(h.AuthSize) > packetLen { + return errAuthSize + } + return nil } // headerMask returns a cipher for 'masking' / 'unmasking' packet headers. @@ -640,3 +650,19 @@ func (h *Header) mask(destID enode.ID) cipher.Stream { } return cipher.NewCTR(block, h.IV[:]) } + +func bytesCopy(r *bytes.Buffer) []byte { + b := make([]byte, r.Len()) + copy(b, r.Bytes()) + return b +} + +// readNew reads 'length' bytes from 'r' and stores them into 'data'. +func readNew(data *[]byte, length int, r *bytes.Reader) bool { + if length == 0 { + return true + } + *data = make([]byte, length) + n, _ := r.Read(*data) + return n == length +} diff --git a/p2p/discover/v5wire/encoding_test.go b/p2p/discover/v5wire/encoding_test.go index 92f397f7e28..2bdce6fee67 100644 --- a/p2p/discover/v5wire/encoding_test.go +++ b/p2p/discover/v5wire/encoding_test.go @@ -77,17 +77,16 @@ func TestDeriveKeysV5(t *testing.T) { func TestMinSizes(t *testing.T) { var ( gcmTagSize = 16 - whoareyou = sizeofMaskingIV + sizeofHeaderData + sizeofWhoareyouAuthData - msgHeader = sizeofMaskingIV + sizeofHeaderData + sizeofMessageAuthData - emptyMsg = msgHeader + gcmTagSize - minMessageSize = 3 // should be fine + emptyMsg = sizeofMessageAuthData + gcmTagSize + minMessageData = 3 // OK because message contains RLP ) - t.Log("WHOAREYOU size", whoareyou) - t.Log("EMPTY msg size", emptyMsg) - if want := emptyMsg + minMessageSize; minPacketSize != want { - t.Fatalf("wrong minPacketSize %d, want %d", minPacketSize, want) + t.Log("WHOAREYOU size", sizeofStaticPacketData+sizeofWhoareyouAuthData) + t.Log("EMPTY msg size", sizeofStaticPacketData+emptyMsg) + t.Log("MIN msg packet size", sizeofStaticPacketData+minMessageSize) + if want := emptyMsg + minMessageData; minMessageSize != want { + t.Fatalf("wrong minPacketSize %d, want %d", minMessageData, want) } - if msgHeader+randomPacketMsgSize < minPacketSize { + if sizeofMessageAuthData+randomPacketMsgSize < minMessageSize { t.Fatalf("randomPacketMsgSize %d too small", randomPacketMsgSize) } } diff --git a/p2p/discover/v5wire/testdata/v5.1-ping-handshake-enr.txt b/p2p/discover/v5wire/testdata/v5.1-ping-handshake-enr.txt index e0337391f66..21227ddf5a3 100644 --- a/p2p/discover/v5wire/testdata/v5.1-ping-handshake-enr.txt +++ b/p2p/discover/v5wire/testdata/v5.1-ping-handshake-enr.txt @@ -8,18 +8,18 @@ # handshake inputs: # # whoareyou.iv = 0x00000000000000000000000000000000 -# whoareyou.authdata = 0x0102030405060708090a0b0c0102030405060708090a0b0c0d0e0f100000000000000000 +# whoareyou.authdata = 0x0102030405060708090a0b0c0d0e0f100000000000000000 # whoareyou.request-nonce = 0x0102030405060708090a0b0c # whoareyou.id-nonce = 0x0102030405060708090a0b0c0d0e0f10 # whoareyou.enr-seq = 0 # ephemeral-key = 0x0288ef00023598499cb6c940146d050d2b1fb914198c327f76aad590bead68b6 # ephemeral-pubkey = 0x039a003ba6517b473fa0cd74aefe99dadfdb34627f90fec6362df85803908f53a5 -00000000000000000000000000000000088b3d4342774649980a4adf72a8fcaa -963f24b27a2f6bb44c7ed5ca10e87de130f94d2390b9853c3dcb402b1e9472d4 -3c9ae48d04689e4bb2a1b46f2dd001878934d0ee3dc3126ccff587a3a155e378 -e27ad720d8b6ecb86ab88496376d96283d0d01a1e7a55d36e0e04ecab1947dc2 -758335d19afe0b13e65796706adff216ab862a9186875f9494150c4ae06fa4d1 +00000000000000000000000000000000088b3d4342774649305f313964a39e55 +ea96c005ad539c8c7560413a7008f16c9e6d2f43bbea8814a546b7409ce783d3 +4c4f53245d08da4bb2f6dcb382c5d8d278b57f348cdc967ebf060aca235dd420 +9711826d77482afb37b6106070af606add9d8a8bb052a35eeed1d9cf829d2d73 +821b4b506b5600498c5796706adff216ab862a9186875f9494150c4ae06fa4d1 f0396c93f215fa4ef524e0ed04c3c21e39b1868e1ca8105e585ec17315e755e6 cfc4dd6cb7fd8e1a1f55e49b4b5eb024221482105346f3c82b15fdaae36a3bb1 2a494683b4a3c7f2ae41306252fed84785e2bbff3b022812d0882f06978df84a diff --git a/p2p/discover/v5wire/testdata/v5.1-ping-handshake.txt b/p2p/discover/v5wire/testdata/v5.1-ping-handshake.txt index 0e81aaab0df..6bc946dce0a 100644 --- a/p2p/discover/v5wire/testdata/v5.1-ping-handshake.txt +++ b/p2p/discover/v5wire/testdata/v5.1-ping-handshake.txt @@ -8,17 +8,17 @@ # handshake inputs: # # whoareyou.iv = 0x00000000000000000000000000000000 -# whoareyou.authdata = 0x0102030405060708090a0b0c0102030405060708090a0b0c0d0e0f100000000000000001 +# whoareyou.authdata = 0x0102030405060708090a0b0c0d0e0f100000000000000001 # whoareyou.request-nonce = 0x0102030405060708090a0b0c # whoareyou.id-nonce = 0x0102030405060708090a0b0c0d0e0f10 # whoareyou.enr-seq = 1 # ephemeral-key = 0x0288ef00023598499cb6c940146d050d2b1fb914198c327f76aad590bead68b6 # ephemeral-pubkey = 0x039a003ba6517b473fa0cd74aefe99dadfdb34627f90fec6362df85803908f53a5 -00000000000000000000000000000000088b3d4342774649980a4adf72a8fcaa -963f24b27a2f6bb44c7ed5ca10e87de130f94d2390b9853c3dcbc12b1e9472d4 -3c9ae48d04689e4bb21c063afb08d96bb8e3e73d3589170d8371a04e30d35186 -197b8906260a01637fe8d38a4e2dfc699103e34c55dc41744000b461b353aaa2 -a7266e63cedafd918b5796706adff216ab862a9186875f9494150c4ae06fa4d1 +00000000000000000000000000000000088b3d4342774649305f313964a39e55 +ea96c005ad521d8c7560413a7008f16c9e6d2f43bbea8814a546b7409ce783d3 +4c4f53245d08da4bb265bd5b8f27e00bcb6f1b193f52eb737dbb3033ee890cab +bd2728dfa01b3613a2a3a6edc1e2b4359f45c4823db4e5a91132d68606508845 +772fbdd366664b350f5796706adff216ab862a9186875f9494150c4ae06fa4d1 f0396c93f215fa4ef524dc394f221e162100550a011363be21154c6b42e3816f 0a38 diff --git a/p2p/discover/v5wire/testdata/v5.1-ping-message.txt b/p2p/discover/v5wire/testdata/v5.1-ping-message.txt index fce68684d60..00be64c3ae5 100644 --- a/p2p/discover/v5wire/testdata/v5.1-ping-message.txt +++ b/p2p/discover/v5wire/testdata/v5.1-ping-message.txt @@ -5,6 +5,6 @@ # ping.req-id = 0x00000001 # ping.enr-seq = 2 -00000000000000000000000000000000088b3d4342774649980a4adf72a8fcaa -963f24b27a2f6bb44c7ed5ca10e87de130f94d2390b9853c3fcba22b1e9472d4 -3c9ae48d04689eb84102ed931f66d1db57c785865ffccae8689057103acb15 +00000000000000000000000000000000088b3d4342774649325f313964a39e55 +ea96c005ad52be8c7560413a7008f16c9e6d2f43bbea8814a546b7409ce783d3 +4c4f53245d08dab84102ed931f66d1db57c785865ffccae8689057103acb15 diff --git a/p2p/discover/v5wire/testdata/v5.1-whoareyou.txt b/p2p/discover/v5wire/testdata/v5.1-whoareyou.txt index 98caf7e91ba..fbd32e1fd5d 100644 --- a/p2p/discover/v5wire/testdata/v5.1-whoareyou.txt +++ b/p2p/discover/v5wire/testdata/v5.1-whoareyou.txt @@ -1,11 +1,10 @@ # src-node-id = 0xaaaa8419e9f49d0083561b48287df592939a8d19947d8c0ef88f2a4856a69fbb # dest-node-id = 0xbbbb9d047f0488c0b5a93c1c3f2d8bafc7c8ff337024a55434a0d0555de64db9 # whoareyou.iv = 0x00000000000000000000000000000000 -# whoareyou.authdata = 0x0102030405060708090a0b0c0102030405060708090a0b0c0d0e0f100000000000000000 +# whoareyou.authdata = 0x0102030405060708090a0b0c0d0e0f100000000000000000 # whoareyou.request-nonce = 0x0102030405060708090a0b0c # whoareyou.id-nonce = 0x0102030405060708090a0b0c0d0e0f10 # whoareyou.enr-seq = 0 -00000000000000000000000000000000088b3d4342774649980a4adf72a8fcaa -963f24b27a2f6bb44c7ed5ca10e87de130f94d2390b9853c3ecb8ad5e368892e -c562137bf19c6d0a9191a5651c4f415117bdfa0c7ab86af62b7a9784eceb28 +00000000000000000000000000000000088b3d434277464933a1ccc59f5967ad +1d6035f15e528627dde75cd68292f9e6c27d6b66c8100a873fcbaed4e16b8d From f288f6cac229f2f375efdffae912d785a6fee463 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Wed, 30 Sep 2020 22:41:50 +0200 Subject: [PATCH 37/70] p2p/discover/v5wire: lower min message size No need to play games with assumed RLP size anymore. --- p2p/discover/v5wire/encoding.go | 2 +- p2p/discover/v5wire/encoding_test.go | 15 +++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/p2p/discover/v5wire/encoding.go b/p2p/discover/v5wire/encoding.go index 7cebf655d40..df266716f1f 100644 --- a/p2p/discover/v5wire/encoding.go +++ b/p2p/discover/v5wire/encoding.go @@ -93,7 +93,7 @@ const ( minVersion = 1 sizeofMaskingIV = 16 - minMessageSize = 51 // this refers to data after static headers + minMessageSize = 48 // this refers to data after static headers randomPacketMsgSize = 20 ) diff --git a/p2p/discover/v5wire/encoding_test.go b/p2p/discover/v5wire/encoding_test.go index 2bdce6fee67..01cee2728b5 100644 --- a/p2p/discover/v5wire/encoding_test.go +++ b/p2p/discover/v5wire/encoding_test.go @@ -76,15 +76,14 @@ func TestDeriveKeysV5(t *testing.T) { // This test checks that the minPacketSize and randomPacketMsgSize constants are well-defined. func TestMinSizes(t *testing.T) { var ( - gcmTagSize = 16 - emptyMsg = sizeofMessageAuthData + gcmTagSize - minMessageData = 3 // OK because message contains RLP + gcmTagSize = 16 + emptyMsg = sizeofMessageAuthData + gcmTagSize ) - t.Log("WHOAREYOU size", sizeofStaticPacketData+sizeofWhoareyouAuthData) - t.Log("EMPTY msg size", sizeofStaticPacketData+emptyMsg) - t.Log("MIN msg packet size", sizeofStaticPacketData+minMessageSize) - if want := emptyMsg + minMessageData; minMessageSize != want { - t.Fatalf("wrong minPacketSize %d, want %d", minMessageData, want) + t.Log("static header size", sizeofStaticPacketData) + t.Log("whoareyou size", sizeofStaticPacketData+sizeofWhoareyouAuthData) + t.Log("empty msg size", sizeofStaticPacketData+emptyMsg) + if want := emptyMsg; minMessageSize != want { + t.Fatalf("wrong minMessageSize %d, want %d", minMessageSize, want) } if sizeofMessageAuthData+randomPacketMsgSize < minMessageSize { t.Fatalf("randomPacketMsgSize %d too small", randomPacketMsgSize) From b84d7d3110445382a6b1ed3d7456ac2f0b772af1 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Thu, 1 Oct 2020 00:10:37 +0200 Subject: [PATCH 38/70] p2p/discover/v5wire: add low-level crypto test vector code --- p2p/discover/v5wire/crypto.go | 16 +++++ p2p/discover/v5wire/crypto_test.go | 111 +++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 p2p/discover/v5wire/crypto_test.go diff --git a/p2p/discover/v5wire/crypto.go b/p2p/discover/v5wire/crypto.go index b4ca8ca7ff0..ce91552c283 100644 --- a/p2p/discover/v5wire/crypto.go +++ b/p2p/discover/v5wire/crypto.go @@ -1,3 +1,19 @@ +// Copyright 2020 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + package v5wire import ( diff --git a/p2p/discover/v5wire/crypto_test.go b/p2p/discover/v5wire/crypto_test.go new file mode 100644 index 00000000000..a2e35e6764a --- /dev/null +++ b/p2p/discover/v5wire/crypto_test.go @@ -0,0 +1,111 @@ +// Copyright 2020 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package v5wire + +import ( + "bytes" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/sha256" + "strings" + "testing" + + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/p2p/enode" +) + +func TestVector_ECDH(t *testing.T) { + var ( + staticKey = hexPrivkey("0xfb757dc581730490a1d7a00deea65e9b1936924caaea8f44d476014856b68736") + publicKey = hexPubkey(crypto.S256(), "0x039961e4c2356d61bedb83052c115d311acb3a96f5777296dcf297351130266231") + want = hexutil.MustDecode("0x033b11a2a1f214567e1537ce5e509ffd9b21373247f2a3ff6841f4976f53165e7e") + ) + result := ecdh(staticKey, publicKey) + check(t, "shared-secret", result, want) +} + +func TestVector_KDF(t *testing.T) { + var ( + ephKey = hexPrivkey("0xfb757dc581730490a1d7a00deea65e9b1936924caaea8f44d476014856b68736") + net = newHandshakeTest() + challenge Whoareyou + ) + copy(challenge.Header.IV[:], hexutil.MustDecode("0x01010101010101010101010101010101")) + copy(challenge.IDNonce[:], hexutil.MustDecode("0x02020202020202020202020202020202")) + defer net.close() + + destKey := &net.nodeB.c.privkey.PublicKey + s := net.nodeA.c.deriveKeys(net.nodeA.id(), net.nodeB.id(), ephKey, destKey, &challenge) + t.Logf("ephemeral-key = %#x", ephKey.D) + t.Logf("dest-pubkey = %#x", EncodePubkey(destKey)) + t.Logf("node-id-a = %#x", net.nodeA.id().Bytes()) + t.Logf("node-id-b = %#x", net.nodeB.id().Bytes()) + t.Logf("whoareyou.masking-iv = %#x", challenge.Header.IV[:]) + t.Logf("whoareyou.id-nonce = %#x", challenge.IDNonce[:]) + check(t, "initiator-key", s.writeKey, hexutil.MustDecode("0xb10e94a89b34cfb87b65aa7f8902f40c")) + check(t, "recipient-key", s.readKey, hexutil.MustDecode("0xce8db25ae599c9b2c4a9d60090c9efdd")) +} + +func TestVector_IDSignature(t *testing.T) { + var ( + key = hexPrivkey("0xfb757dc581730490a1d7a00deea65e9b1936924caaea8f44d476014856b68736") + destID = enode.HexID("0xbbbb9d047f0488c0b5a93c1c3f2d8bafc7c8ff337024a55434a0d0555de64db9") + ephkey = hexutil.MustDecode("0x039961e4c2356d61bedb83052c115d311acb3a96f5777296dcf297351130266231") + header = Header{ + AuthData: hexutil.MustDecode("0x0102030405060708090a0b0c0d0e0f100000000000000000"), + } + ) + copy(header.IV[:], hexutil.MustDecode("0x01010101010101010101010101010101")) + + sig, err := makeIDSignature(sha256.New(), key, destID, ephkey, &Header{}) + if err != nil { + t.Fatal(err) + } + t.Logf("static-key = %#x", key.D) + t.Logf("masking-iv = %#x", header.IV[:]) + t.Logf("authdata = %#x", header.AuthData) + t.Logf("ephemeral-pubkey = %#x", ephkey) + t.Logf("node-id-B = %#x", destID.Bytes()) + check(t, "id-signature", sig, hexutil.MustDecode("0xd82364cfffb18101355371de84ee0def3dca31191b9add79b21a14f4442b6df02dc26df6278f71c83d43645da13071881cacdb43b0aea1e256cdec73a73faf01")) +} + +func check(t *testing.T, what string, x, y []byte) { + t.Helper() + + if !bytes.Equal(x, y) { + t.Errorf("wrong %s: %#x != %#x", what, x, y) + } else { + t.Logf("%s = %#x", what, x) + } +} + +func hexPrivkey(input string) *ecdsa.PrivateKey { + key, err := crypto.HexToECDSA(strings.TrimPrefix(input, "0x")) + if err != nil { + panic(err) + } + return key +} + +func hexPubkey(curve elliptic.Curve, input string) *ecdsa.PublicKey { + key, err := DecodePubkey(curve, hexutil.MustDecode(input)) + if err != nil { + panic(err) + } + return key +} From 4eabc4aac2617ca64b6f9a9f8097998e42a5f3a2 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Fri, 2 Oct 2020 14:02:33 +0200 Subject: [PATCH 39/70] p2p/discover: implement simplified challenge data handling --- p2p/discover/v5wire/crypto.go | 38 ++- p2p/discover/v5wire/crypto_test.go | 51 ++-- p2p/discover/v5wire/encoding.go | 241 ++++++++---------- p2p/discover/v5wire/encoding_test.go | 56 ++-- p2p/discover/v5wire/msg.go | 8 +- .../testdata/v5.1-ping-handshake-enr.txt | 15 +- .../v5wire/testdata/v5.1-ping-handshake.txt | 15 +- .../v5wire/testdata/v5.1-ping-message.txt | 2 +- .../v5wire/testdata/v5.1-whoareyou.txt | 3 +- 9 files changed, 210 insertions(+), 219 deletions(-) diff --git a/p2p/discover/v5wire/crypto.go b/p2p/discover/v5wire/crypto.go index ce91552c283..fc0a0edef59 100644 --- a/p2p/discover/v5wire/crypto.go +++ b/p2p/discover/v5wire/crypto.go @@ -28,6 +28,7 @@ import ( "github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/p2p/enode" + "golang.org/x/crypto/hkdf" ) const ( @@ -63,21 +64,20 @@ func DecodePubkey(curve elliptic.Curve, e []byte) (*ecdsa.PublicKey, error) { } // idNonceHash computes the ID signature hash used in the handshake. -func idNonceHash(h hash.Hash, destID enode.ID, iv, authdata, ephkey []byte) []byte { +func idNonceHash(h hash.Hash, challenge, ephkey []byte, destID enode.ID) []byte { h.Reset() h.Write([]byte("discovery v5 identity proof")) - h.Write(iv) - h.Write(authdata) + h.Write(challenge) h.Write(ephkey) h.Write(destID[:]) return h.Sum(nil) } // makeIDSignature creates the ID nonce signature. -func makeIDSignature(hash hash.Hash, key *ecdsa.PrivateKey, destID enode.ID, ephkey []byte, challenge *Header) ([]byte, error) { +func makeIDSignature(hash hash.Hash, key *ecdsa.PrivateKey, challenge, ephkey []byte, destID enode.ID) ([]byte, error) { + input := idNonceHash(hash, challenge, ephkey, destID) switch key.Curve { case crypto.S256(): - input := idNonceHash(hash, destID, challenge.IV[:], challenge.AuthData, ephkey) idsig, err := crypto.Sign(input, key) if err != nil { return nil, err @@ -94,14 +94,14 @@ type s256raw []byte func (s256raw) ENRKey() string { return "secp256k1" } // verifyIDSignature checks that signature over idnonce was made by the given node. -func verifyIDSignature(hash hash.Hash, sig []byte, n *enode.Node, destID enode.ID, ephkey []byte, challenge *Header) error { +func verifyIDSignature(hash hash.Hash, sig []byte, n *enode.Node, challenge, ephkey []byte, destID enode.ID) error { switch idscheme := n.Record().IdentityScheme(); idscheme { case "v4": var pubkey s256raw if n.Load(&pubkey) != nil { return errors.New("no secp256k1 public key in record") } - input := idNonceHash(hash, destID, challenge.IV[:], challenge.AuthData, ephkey) + input := idNonceHash(hash, challenge, ephkey, destID) if !crypto.VerifySignature(pubkey, input, sig) { return errInvalidNonceSig } @@ -111,6 +111,30 @@ func verifyIDSignature(hash hash.Hash, sig []byte, n *enode.Node, destID enode.I } } +type hashFn func() hash.Hash + +// deriveKeys creates the session keys. +func deriveKeys(hash hashFn, priv *ecdsa.PrivateKey, pub *ecdsa.PublicKey, n1, n2 enode.ID, challenge []byte) *session { + const text = "discovery v5 key agreement" + var info = make([]byte, 0, len(text)+len(n1)+len(n2)) + info = append(info, text...) + info = append(info, n1[:]...) + info = append(info, n2[:]...) + + eph := ecdh(priv, pub) + if eph == nil { + return nil + } + kdf := hkdf.New(hash, eph, challenge, info) + sec := session{writeKey: make([]byte, aesKeySize), readKey: make([]byte, aesKeySize)} + kdf.Read(sec.writeKey) + kdf.Read(sec.readKey) + for i := range eph { + eph[i] = 0 + } + return &sec +} + // ecdh creates a shared secret. func ecdh(privkey *ecdsa.PrivateKey, pubkey *ecdsa.PublicKey) []byte { secX, secY := pubkey.ScalarMult(pubkey.X, pubkey.Y, privkey.D.Bytes()) diff --git a/p2p/discover/v5wire/crypto_test.go b/p2p/discover/v5wire/crypto_test.go index a2e35e6764a..72169b43141 100644 --- a/p2p/discover/v5wire/crypto_test.go +++ b/p2p/discover/v5wire/crypto_test.go @@ -21,6 +21,7 @@ import ( "crypto/ecdsa" "crypto/elliptic" "crypto/sha256" + "reflect" "strings" "testing" @@ -41,24 +42,21 @@ func TestVector_ECDH(t *testing.T) { func TestVector_KDF(t *testing.T) { var ( - ephKey = hexPrivkey("0xfb757dc581730490a1d7a00deea65e9b1936924caaea8f44d476014856b68736") - net = newHandshakeTest() - challenge Whoareyou + ephKey = hexPrivkey("0xfb757dc581730490a1d7a00deea65e9b1936924caaea8f44d476014856b68736") + cdata = hexutil.MustDecode("0x000000000000000000000000000000006469736376350001010102030405060708090a0b0c00180102030405060708090a0b0c0d0e0f100000000000000000") + net = newHandshakeTest() ) - copy(challenge.Header.IV[:], hexutil.MustDecode("0x01010101010101010101010101010101")) - copy(challenge.IDNonce[:], hexutil.MustDecode("0x02020202020202020202020202020202")) defer net.close() - destKey := &net.nodeB.c.privkey.PublicKey - s := net.nodeA.c.deriveKeys(net.nodeA.id(), net.nodeB.id(), ephKey, destKey, &challenge) + destKey := &testKeyB.PublicKey + s := deriveKeys(sha256.New, ephKey, destKey, net.nodeA.id(), net.nodeB.id(), cdata) t.Logf("ephemeral-key = %#x", ephKey.D) t.Logf("dest-pubkey = %#x", EncodePubkey(destKey)) t.Logf("node-id-a = %#x", net.nodeA.id().Bytes()) t.Logf("node-id-b = %#x", net.nodeB.id().Bytes()) - t.Logf("whoareyou.masking-iv = %#x", challenge.Header.IV[:]) - t.Logf("whoareyou.id-nonce = %#x", challenge.IDNonce[:]) - check(t, "initiator-key", s.writeKey, hexutil.MustDecode("0xb10e94a89b34cfb87b65aa7f8902f40c")) - check(t, "recipient-key", s.readKey, hexutil.MustDecode("0xce8db25ae599c9b2c4a9d60090c9efdd")) + t.Logf("challenge-data = %#x", cdata) + check(t, "initiator-key", s.writeKey, hexutil.MustDecode("0xdccc82d81bd610f4f76d3ebe97a40571")) + check(t, "recipient-key", s.readKey, hexutil.MustDecode("0xac74bb8773749920b0d3a8881c173ec5")) } func TestVector_IDSignature(t *testing.T) { @@ -66,22 +64,37 @@ func TestVector_IDSignature(t *testing.T) { key = hexPrivkey("0xfb757dc581730490a1d7a00deea65e9b1936924caaea8f44d476014856b68736") destID = enode.HexID("0xbbbb9d047f0488c0b5a93c1c3f2d8bafc7c8ff337024a55434a0d0555de64db9") ephkey = hexutil.MustDecode("0x039961e4c2356d61bedb83052c115d311acb3a96f5777296dcf297351130266231") - header = Header{ - AuthData: hexutil.MustDecode("0x0102030405060708090a0b0c0d0e0f100000000000000000"), - } + cdata = hexutil.MustDecode("0x000000000000000000000000000000006469736376350001010102030405060708090a0b0c00180102030405060708090a0b0c0d0e0f100000000000000000") ) - copy(header.IV[:], hexutil.MustDecode("0x01010101010101010101010101010101")) - sig, err := makeIDSignature(sha256.New(), key, destID, ephkey, &Header{}) + sig, err := makeIDSignature(sha256.New(), key, cdata, ephkey, destID) if err != nil { t.Fatal(err) } t.Logf("static-key = %#x", key.D) - t.Logf("masking-iv = %#x", header.IV[:]) - t.Logf("authdata = %#x", header.AuthData) + t.Logf("challenge-data = %#x", cdata) t.Logf("ephemeral-pubkey = %#x", ephkey) t.Logf("node-id-B = %#x", destID.Bytes()) - check(t, "id-signature", sig, hexutil.MustDecode("0xd82364cfffb18101355371de84ee0def3dca31191b9add79b21a14f4442b6df02dc26df6278f71c83d43645da13071881cacdb43b0aea1e256cdec73a73faf01")) + expected := "0x94852a1e2318c4e5e9d422c98eaf19d1d90d876b29cd06ca7cb7546d0fff7b484fe86c09a064fe72bdbef73ba8e9c34df0cd2b53e9d65528c2c7f336d5dfc6e6" + check(t, "id-signature", sig, hexutil.MustDecode(expected)) +} + +func TestDeriveKeys(t *testing.T) { + t.Parallel() + + var ( + n1 = enode.ID{1} + n2 = enode.ID{2} + cdata = []byte{1, 2, 3, 4} + ) + sec1 := deriveKeys(sha256.New, testKeyA, &testKeyB.PublicKey, n1, n2, cdata) + sec2 := deriveKeys(sha256.New, testKeyB, &testKeyA.PublicKey, n1, n2, cdata) + if sec1 == nil || sec2 == nil { + t.Fatal("key agreement failed") + } + if !reflect.DeepEqual(sec1, sec2) { + t.Fatalf("keys not equal:\n %+v\n %+v", sec1, sec2) + } } func check(t *testing.T, what string, x, y []byte) { diff --git a/p2p/discover/v5wire/encoding.go b/p2p/discover/v5wire/encoding.go index df266716f1f..6f9b83c63e4 100644 --- a/p2p/discover/v5wire/encoding.go +++ b/p2p/discover/v5wire/encoding.go @@ -32,7 +32,6 @@ import ( "github.com/ethereum/go-ethereum/p2p/enode" "github.com/ethereum/go-ethereum/p2p/enr" "github.com/ethereum/go-ethereum/rlp" - "golang.org/x/crypto/hkdf" ) // TODO concurrent WHOAREYOU tie-breaker @@ -43,14 +42,14 @@ import ( // Header represents a packet header. type Header struct { IV [sizeofMaskingIV]byte - HeaderData + StaticHeader AuthData []byte src enode.ID // used by decoder } -// HeaderData contains the static fields of a packet header. -type HeaderData struct { +// StaticHeader contains the static fields of a packet header. +type StaticHeader struct { ProtocolID [6]byte Version uint16 Flag byte @@ -117,11 +116,11 @@ var ( // Packet sizes. var ( - sizeofHeaderData = binary.Size(HeaderData{}) + sizeofStaticHeader = binary.Size(StaticHeader{}) sizeofWhoareyouAuthData = binary.Size(whoareyouAuthData{}) sizeofHandshakeAuthData = binary.Size(handshakeAuthData{}.h) sizeofMessageAuthData = binary.Size(messageAuthData{}) - sizeofStaticPacketData = sizeofMaskingIV + sizeofHeaderData + sizeofStaticPacketData = sizeofMaskingIV + sizeofStaticHeader ) // Codec encodes and decodes discovery v5 packets. @@ -150,45 +149,60 @@ func NewCodec(ln *enode.LocalNode, key *ecdsa.PrivateKey, clock mclock.Clock) *C // 'challenge' parameter should be the most recently received WHOAREYOU packet from that // node. func (c *Codec) Encode(id enode.ID, addr string, packet Packet, challenge *Whoareyou) ([]byte, Nonce, error) { + // Create the packet header. var ( - header Header - msgdata []byte + head Header + session *session + msgData []byte err error ) - if p, ok := packet.(*Whoareyou); ok { - header, err = c.encodeWhoareyou(id, p) - if err == nil { - c.sc.storeSentHandshake(id, addr, p) + switch { + case packet.Kind() == WhoareyouPacket: + head, err = c.encodeWhoareyou(id, packet.(*Whoareyou)) + case challenge != nil: + // We have an unanswered challenge, send handshake. + head, session, err = c.encodeHandshakeHeader(id, addr, challenge) + default: + session = c.sc.session(id, addr) + if session != nil { + // There is a session, use it. + head, err = c.encodeMessageHeader(id, session) + } else { + // No keys, send random data to kick off the handshake. + head, msgData, err = c.encodeRandom(id) } - } else if challenge != nil { - // Remote sent a challenge, answer it with a handshake. - header, msgdata, err = c.encodeHandshakeMessage(id, addr, packet, challenge) - } else if session := c.sc.session(id, addr); session != nil { - // There is a session, use it. - header, msgdata, err = c.encodeMessage(id, session, packet) - } else { - // No keys, no handshake: send random data to kick off the handshake. - header, msgdata, err = c.encodeRandom(id) } - if err != nil { return nil, Nonce{}, err } - enc, err := c.EncodeRaw(id, header, msgdata) - return enc, header.Nonce, err -} -func (c *Codec) EncodeRaw(id enode.ID, head Header, msgdata []byte) ([]byte, error) { // Generate masking IV. if err := c.sc.maskingIVGen(head.IV[:]); err != nil { - return nil, fmt.Errorf("can't generate masking IV: %v", err) + return nil, Nonce{}, fmt.Errorf("can't generate masking IV: %v", err) } - // Encode the packet. - c.buf.Reset() - c.buf.Write(head.IV[:]) - binary.Write(&c.buf, binary.BigEndian, &head.HeaderData) - c.buf.Write(head.AuthData) + // Encode header data. + c.writeHeaders(&head) + + // Store sent WHOAREYOU challenges. + if challenge, ok := packet.(*Whoareyou); ok { + challenge.ChallengeData = bytesCopy(&c.buf) + c.sc.storeSentHandshake(id, addr, challenge) + } else if msgData == nil { + headerData := c.buf.Bytes() + msgData, err = c.encryptMessage(session, packet, &head, headerData) + if err != nil { + return nil, Nonce{}, err + } + } + + enc, err := c.EncodeRaw(id, head, msgData) + return enc, head.Nonce, err +} + +// EncodeRaw encodes a packet with the given header. +func (c *Codec) EncodeRaw(id enode.ID, head Header, msgdata []byte) ([]byte, error) { + c.writeHeaders(&head) // Apply masking. masked := c.buf.Bytes()[sizeofMaskingIV:] @@ -200,6 +214,13 @@ func (c *Codec) EncodeRaw(id enode.ID, head Header, msgdata []byte) ([]byte, err return c.buf.Bytes(), nil } +func (c *Codec) writeHeaders(head *Header) { + c.buf.Reset() + c.buf.Write(head.IV[:]) + binary.Write(&c.buf, binary.BigEndian, &head.StaticHeader) + c.buf.Write(head.AuthData) +} + // makeHeader creates a packet header. func (c *Codec) makeHeader(toID enode.ID, flag byte, authsizeExtra int) Header { var authsize int @@ -218,7 +239,7 @@ func (c *Codec) makeHeader(toID enode.ID, flag byte, authsizeExtra int) Header { panic(fmt.Errorf("BUG: auth size %d overflows uint16", authsize)) } return Header{ - HeaderData: HeaderData{ + StaticHeader: StaticHeader{ ProtocolID: protocolID, Version: version, Flag: flag, @@ -262,15 +283,11 @@ func (c *Codec) encodeWhoareyou(toID enode.ID, packet *Whoareyou) (Header, error head := c.makeHeader(toID, flagWhoareyou, 0) head.AuthData = bytesCopy(&c.buf) head.Nonce = packet.Nonce - - // Update header in packet. - packet.Header = head return head, nil } -// encodeHandshakeMessage encodes an encrypted message with a handshake -// response header. -func (c *Codec) encodeHandshakeMessage(toID enode.ID, addr string, packet Packet, challenge *Whoareyou) (Header, []byte, error) { +// encodeHandshakeMessage encodes the handshake message packet header. +func (c *Codec) encodeHandshakeHeader(toID enode.ID, addr string, challenge *Whoareyou) (Header, *session, error) { // Ensure calling code sets challenge.node. if challenge.Node == nil { panic("BUG: missing challenge.Node in encode") @@ -303,19 +320,7 @@ func (c *Codec) encodeHandshakeMessage(toID enode.ID, addr string, packet Packet c.buf.Write(auth.record) head.AuthData = bytesCopy(&c.buf) head.Nonce = nonce - - // Encode message plaintext. - c.msgbuf.Reset() - c.msgbuf.WriteByte(packet.Kind()) - if err := rlp.Encode(&c.msgbuf, packet); err != nil { - return head, nil, err - } - messagePT := c.msgbuf.Bytes() - - // Encrypt message data. - var msgdata []byte - msgdata, err = encryptGCM(msgdata, session.writeKey, head.Nonce[:], messagePT, nil) - return head, msgdata, err + return head, session, err } // encodeAuthHeader creates the auth header on a request packet following WHOAREYOU. @@ -338,22 +343,22 @@ func (c *Codec) makeHandshakeAuth(toID enode.ID, addr string, challenge *Whoarey auth.h.PubkeySize = byte(len(auth.pubkey)) // Add ID nonce signature to response. - idsig, err := makeIDSignature(c.sha256, c.privkey, toID, ephpubkey[:], &challenge.Header) + cdata := challenge.ChallengeData + idsig, err := makeIDSignature(c.sha256, c.privkey, cdata, ephpubkey[:], toID) if err != nil { return nil, nil, fmt.Errorf("can't sign: %v", err) } auth.signature = idsig auth.h.SigSize = byte(len(auth.signature)) - // Add our record to response if it's newer than what remote - // side has. + // Add our record to response if it's newer than what remote side has. ln := c.localnode.Node() if challenge.RecordSeq < ln.Seq() { auth.record, _ = rlp.EncodeToBytes(ln.Record()) } // Create session keys. - sec := c.deriveKeys(c.localnode.ID(), challenge.Node.ID(), ephkey, remotePubkey, challenge) + sec := deriveKeys(sha256.New, ephkey, remotePubkey, c.localnode.ID(), challenge.Node.ID(), cdata) if sec == nil { return nil, nil, fmt.Errorf("key derivation failed") } @@ -361,32 +366,30 @@ func (c *Codec) makeHandshakeAuth(toID enode.ID, addr string, challenge *Whoarey } // encodeMessage encodes an encrypted message packet. -func (c *Codec) encodeMessage(toID enode.ID, s *session, packet Packet) (Header, []byte, error) { +func (c *Codec) encodeMessageHeader(toID enode.ID, s *session) (Header, error) { head := c.makeHeader(toID, flagMessage, 0) // Create the header. nonce, err := c.sc.nextNonce(s) if err != nil { - return Header{}, nil, fmt.Errorf("can't generate nonce: %v", err) + return Header{}, fmt.Errorf("can't generate nonce: %v", err) } auth := messageAuthData{SrcID: c.localnode.ID()} c.buf.Reset() binary.Write(&c.buf, binary.BigEndian, &auth) head.AuthData = bytesCopy(&c.buf) head.Nonce = nonce + return head, err +} - // Encode the message plaintext. +func (c *Codec) encryptMessage(s *session, p Packet, head *Header, headerData []byte) ([]byte, error) { c.msgbuf.Reset() - c.msgbuf.WriteByte(packet.Kind()) - if err := rlp.Encode(&c.msgbuf, packet); err != nil { - return head, nil, err + c.msgbuf.WriteByte(p.Kind()) + if err := rlp.Encode(&c.msgbuf, p); err != nil { + return nil, err } messagePT := c.msgbuf.Bytes() - - // Encrypt the message. - var msgdata []byte - msgdata, err = encryptGCM(msgdata, s.writeKey, nonce[:], messagePT, nil) - return head, msgdata, err + return encryptGCM(nil, s.writeKey, head.Nonce[:], messagePT, headerData) } // Decode decodes a discovery packet. @@ -398,35 +401,37 @@ func (c *Codec) Decode(input []byte, addr string) (src enode.ID, n *enode.Node, var head Header copy(head.IV[:], input[:sizeofMaskingIV]) mask := head.mask(c.localnode.ID()) - input = input[sizeofMaskingIV:] - headerData := input[:sizeofHeaderData] - mask.XORKeyStream(headerData, headerData) - - // Decode and verify the header. - c.reader.Reset(input) - binary.Read(&c.reader, binary.BigEndian, &head.HeaderData) - if err := head.checkValid(c.reader.Len()); err != nil { + staticHeader := input[sizeofMaskingIV:sizeofStaticPacketData] + mask.XORKeyStream(staticHeader, staticHeader) + + // Decode and verify the static header. + c.reader.Reset(staticHeader) + binary.Read(&c.reader, binary.BigEndian, &head.StaticHeader) + remainingInput := len(input) - sizeofStaticPacketData + if err := head.checkValid(remainingInput); err != nil { return enode.ID{}, nil, nil, err } // Unmask auth data. - head.AuthData = make([]byte, head.AuthSize) - copy(head.AuthData, input[sizeofHeaderData:]) - mask.XORKeyStream(head.AuthData, head.AuthData) + authDataEnd := sizeofStaticPacketData + int(head.AuthSize) + authData := input[sizeofStaticPacketData:authDataEnd] + mask.XORKeyStream(authData, authData) + head.AuthData = authData // Delete timed-out handshakes. This must happen before decoding to avoid // processing the same handshake twice. c.sc.handshakeGC() // Decode auth part and message. - msgdata := input[sizeofHeaderData+len(head.AuthData):] + headerData := input[:authDataEnd] + msgData := input[authDataEnd:] switch head.Flag { case flagWhoareyou: - p, err = c.decodeWhoareyou(&head) + p, err = c.decodeWhoareyou(&head, headerData) case flagHandshake: - n, p, err = c.decodeHandshakeMessage(addr, &head, msgdata) + n, p, err = c.decodeHandshakeMessage(addr, &head, headerData, msgData) case flagMessage: - p, err = c.decodeMessage(addr, &head, msgdata) + p, err = c.decodeMessage(addr, &head, headerData, msgData) default: err = errInvalidFlag } @@ -434,32 +439,31 @@ func (c *Codec) Decode(input []byte, addr string) (src enode.ID, n *enode.Node, } // decodeWhoareyou reads packet data after the header as a WHOAREYOU packet. -func (c *Codec) decodeWhoareyou(head *Header) (Packet, error) { - if c.reader.Len() < sizeofWhoareyouAuthData { - return nil, errTooShort - } - if int(head.AuthSize) != sizeofWhoareyouAuthData { - return nil, fmt.Errorf("invalid auth size for whoareyou") +func (c *Codec) decodeWhoareyou(head *Header, headerData []byte) (Packet, error) { + if len(head.AuthData) != sizeofWhoareyouAuthData { + return nil, fmt.Errorf("invalid auth size %d for WHOAREYOU", len(head.AuthData)) } c.reader.Reset(head.AuthData) auth := new(whoareyouAuthData) binary.Read(&c.reader, binary.BigEndian, auth) p := &Whoareyou{ - Header: *head, - IDNonce: auth.IDNonce, - RecordSeq: auth.RecordSeq, + Nonce: head.Nonce, + IDNonce: auth.IDNonce, + RecordSeq: auth.RecordSeq, + ChallengeData: make([]byte, len(headerData)), } + copy(p.ChallengeData, headerData) return p, nil } -func (c *Codec) decodeHandshakeMessage(fromAddr string, head *Header, msgdata []byte) (n *enode.Node, p Packet, err error) { - node, auth, session, err := c.decodeHandshake(fromAddr, head) +func (c *Codec) decodeHandshakeMessage(fromAddr string, head *Header, headerData, msgData []byte) (n *enode.Node, p Packet, err error) { + node, auth, session, err := c.decodeHandshake(fromAddr, head, headerData) if err != nil { return nil, nil, err } // Decrypt the message using the new session keys. - msg, err := c.decryptMessage(msgdata, head.Nonce[:], session.readKey) + msg, err := c.decryptMessage(msgData, head.Nonce[:], headerData, session.readKey) if err != nil { return node, msg, err } @@ -470,7 +474,7 @@ func (c *Codec) decodeHandshakeMessage(fromAddr string, head *Header, msgdata [] return node, msg, nil } -func (c *Codec) decodeHandshake(fromAddr string, head *Header) (*enode.Node, *handshakeAuthData, *session, error) { +func (c *Codec) decodeHandshake(fromAddr string, head *Header, headerData []byte) (*enode.Node, *handshakeAuthData, *session, error) { auth, err := c.decodeHandshakeAuthData(head) if err != nil { return nil, nil, nil, err @@ -493,12 +497,12 @@ func (c *Codec) decodeHandshake(fromAddr string, head *Header) (*enode.Node, *ha } // Verify ID nonce signature. sig := auth.signature - err = verifyIDSignature(c.sha256, sig, node, c.localnode.ID(), auth.pubkey, &challenge.Header) - if err != nil { + cdata := challenge.ChallengeData + if err = verifyIDSignature(c.sha256, sig, node, cdata, auth.pubkey, c.localnode.ID()); err != nil { return nil, nil, nil, err } // Derive sesssion keys. - session := c.deriveKeys(auth.h.SrcID, c.localnode.ID(), c.privkey, ephkey, challenge) + session := deriveKeys(sha256.New, c.privkey, ephkey, auth.h.SrcID, c.localnode.ID(), cdata) session = session.keysFlipped() return node, auth, session, nil } @@ -506,7 +510,7 @@ func (c *Codec) decodeHandshake(fromAddr string, head *Header) (*enode.Node, *ha // decodeHandshakeAuthData reads the authdata section of a handshake packet. func (c *Codec) decodeHandshakeAuthData(head *Header) (*handshakeAuthData, error) { // Decode fixed size part. - if int(head.AuthSize) < sizeofHandshakeAuthData { + if len(head.AuthData) < sizeofHandshakeAuthData { return nil, fmt.Errorf("header authsize %d too low for handshake", head.AuthSize) } c.reader.Reset(head.AuthData) @@ -563,9 +567,9 @@ func (c *Codec) decodeHandshakeRecord(local *enode.Node, wantID enode.ID, remote } // decodeMessage reads packet data following the header as an ordinary message packet. -func (c *Codec) decodeMessage(fromAddr string, head *Header, msgdata []byte) (Packet, error) { - if len(head.AuthData) < sizeofMessageAuthData { - return nil, errTooShort +func (c *Codec) decodeMessage(fromAddr string, head *Header, headerData, msgData []byte) (Packet, error) { + if len(head.AuthData) != sizeofMessageAuthData { + return nil, fmt.Errorf("invalid auth size %d for message packet", len(head.AuthData)) } var auth messageAuthData c.reader.Reset(head.AuthData) @@ -574,7 +578,7 @@ func (c *Codec) decodeMessage(fromAddr string, head *Header, msgdata []byte) (Pa // Try decrypting the message. key := c.sc.readKey(auth.SrcID, fromAddr) - msg, err := c.decryptMessage(msgdata, head.Nonce[:], key) + msg, err := c.decryptMessage(msgData, head.Nonce[:], headerData, key) if err == errMessageDecrypt { // It didn't work. Start the handshake since this is an ordinary message packet. return &Unknown{Nonce: head.Nonce}, nil @@ -582,8 +586,8 @@ func (c *Codec) decodeMessage(fromAddr string, head *Header, msgdata []byte) (Pa return msg, err } -func (c *Codec) decryptMessage(input, nonce, readKey []byte) (Packet, error) { - msgdata, err := decryptGCM(readKey, nonce, input, nil) +func (c *Codec) decryptMessage(input, nonce, headerData, readKey []byte) (Packet, error) { + msgdata, err := decryptGCM(readKey, nonce, input, headerData) if err != nil { return nil, errMessageDecrypt } @@ -593,31 +597,6 @@ func (c *Codec) decryptMessage(input, nonce, readKey []byte) (Packet, error) { return DecodeMessage(msgdata[0], msgdata[1:]) } -// deriveKeys generates session keys using elliptic-curve Diffie-Hellman key agreement. -func (c *Codec) deriveKeys(n1, n2 enode.ID, priv *ecdsa.PrivateKey, pub *ecdsa.PublicKey, challenge *Whoareyou) *session { - var ( - info = []byte("discovery v5 key agreement") - salt = make([]byte, 32) - ) - info = append(info, n1[:]...) - info = append(info, n2[:]...) - copy(salt, challenge.Header.IV[:]) - copy(salt[len(challenge.Header.IV):], challenge.IDNonce[:]) - - eph := ecdh(priv, pub) - if eph == nil { - return nil - } - kdf := hkdf.New(c.sha256reset, eph, salt, info) - sec := session{writeKey: make([]byte, aesKeySize), readKey: make([]byte, aesKeySize)} - kdf.Read(sec.writeKey) - kdf.Read(sec.readKey) - for i := range eph { - eph[i] = 0 - } - return &sec -} - // sha256 returns the shared hash instance. func (c *Codec) sha256reset() hash.Hash { c.sha256.Reset() @@ -626,7 +605,7 @@ func (c *Codec) sha256reset() hash.Hash { // checkValid performs some basic validity checks on the header. // The packetLen here is the length remaining after the static header. -func (h *HeaderData) checkValid(packetLen int) error { +func (h *StaticHeader) checkValid(packetLen int) error { if h.ProtocolID != protocolID { return errInvalidHeader } diff --git a/p2p/discover/v5wire/encoding_test.go b/p2p/discover/v5wire/encoding_test.go index 01cee2728b5..6ca1c0bafe8 100644 --- a/p2p/discover/v5wire/encoding_test.go +++ b/p2p/discover/v5wire/encoding_test.go @@ -50,29 +50,6 @@ var ( testIDnonce = [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16} ) -func TestDeriveKeysV5(t *testing.T) { - t.Parallel() - - var ( - n1 = enode.ID{1} - n2 = enode.ID{2} - challenge = &Whoareyou{} - db, _ = enode.OpenDB("") - ln = enode.NewLocalNode(db, testKeyA) - c = NewCodec(ln, testKeyA, mclock.System{}) - ) - defer db.Close() - - sec1 := c.deriveKeys(n1, n2, testKeyA, &testKeyB.PublicKey, challenge) - sec2 := c.deriveKeys(n1, n2, testKeyB, &testKeyA.PublicKey, challenge) - if sec1 == nil || sec2 == nil { - t.Fatal("key agreement failed") - } - if !reflect.DeepEqual(sec1, sec2) { - t.Fatalf("keys not equal:\n %+v\n %+v", sec1, sec2) - } -} - // This test checks that the minPacketSize and randomPacketMsgSize constants are well-defined. func TestMinSizes(t *testing.T) { var ( @@ -288,8 +265,8 @@ func TestTestVectorsV5(t *testing.T) { challenge1A.RecordSeq = 1 net := newHandshakeTest() challenge0A.Node = net.nodeA.n() - challenge1A.Node = net.nodeA.n() challenge0B.Node = net.nodeB.n() + challenge1A.Node = net.nodeA.n() net.close() type testVectorTest struct { @@ -323,7 +300,7 @@ func TestTestVectorsV5(t *testing.T) { challenge: &challenge0A, prep: func(net *handshakeTest) { // Update challenge.Header.AuthData. - net.nodeA.c.encodeWhoareyou(idB, &challenge0A) + net.nodeA.c.Encode(idB, "", &challenge0A, nil) net.nodeB.c.sc.storeSentHandshake(idA, addr, &challenge0A) }, }, @@ -335,8 +312,8 @@ func TestTestVectorsV5(t *testing.T) { }, challenge: &challenge1A, prep: func(net *handshakeTest) { - // Update challenge.Header.AuthData. - net.nodeA.c.encodeWhoareyou(idB, &challenge1A) + // Update challenge data. + net.nodeA.c.Encode(idB, "", &challenge1A, nil) net.nodeB.c.sc.storeSentHandshake(idA, addr, &challenge1A) }, }, @@ -348,22 +325,24 @@ func TestTestVectorsV5(t *testing.T) { net := newHandshakeTest() defer net.close() + // Override all random inputs. + net.nodeA.c.sc.nonceGen = func(counter uint32) (Nonce, error) { + return Nonce{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, nil + } + net.nodeA.c.sc.maskingIVGen = func(buf []byte) error { + return nil // all zero + } + net.nodeA.c.sc.ephemeralKeyGen = func() (*ecdsa.PrivateKey, error) { + return testEphKey, nil + } + + // Prime the codec for encoding/decoding. if test.prep != nil { test.prep(net) } file := filepath.Join("testdata", test.name+".txt") if *writeTestVectorsFlag { - // Override all random inputs. - net.nodeA.c.sc.nonceGen = func(counter uint32) (Nonce, error) { - return Nonce{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, nil - } - net.nodeA.c.sc.maskingIVGen = func(buf []byte) error { - return nil // all zero - } - net.nodeA.c.sc.ephemeralKeyGen = func() (*ecdsa.PrivateKey, error) { - return testEphKey, nil - } // Encode the packet. d, nonce := net.nodeA.encodeWithChallenge(t, net.nodeB, test.challenge, test.packet) comment := testVectorComment(net, test.packet, test.challenge, nonce) @@ -379,8 +358,7 @@ func TestTestVectorsV5(t *testing.T) { func testVectorComment(net *handshakeTest, p Packet, challenge *Whoareyou, nonce Nonce) string { o := new(strings.Builder) printWhoareyou := func(p *Whoareyou) { - fmt.Fprintf(o, "whoareyou.iv = %#x\n", p.Header.IV[:]) - fmt.Fprintf(o, "whoareyou.authdata = %#x\n", p.Header.AuthData[:]) + fmt.Fprintf(o, "whoareyou.challenge-data = %#x\n", p.ChallengeData) fmt.Fprintf(o, "whoareyou.request-nonce = %#x\n", p.Nonce[:]) fmt.Fprintf(o, "whoareyou.id-nonce = %#x\n", p.IDNonce[:]) fmt.Fprintf(o, "whoareyou.enr-seq = %d\n", p.RecordSeq) diff --git a/p2p/discover/v5wire/msg.go b/p2p/discover/v5wire/msg.go index 4e488ddc68e..e6e506ca113 100644 --- a/p2p/discover/v5wire/msg.go +++ b/p2p/discover/v5wire/msg.go @@ -60,10 +60,10 @@ type ( // WHOAREYOU contains the handshake challenge. Whoareyou struct { - Header Header - Nonce Nonce // Nonce of request packet - IDNonce [16]byte // Identity proof data - RecordSeq uint64 // ENR sequence number of recipient + ChallengeData []byte // Encoded challenge + Nonce Nonce // Nonce of request packet + IDNonce [16]byte // Identity proof data + RecordSeq uint64 // ENR sequence number of recipient // Node is the locally known node record of recipient. // This must be set by the caller of Encode. diff --git a/p2p/discover/v5wire/testdata/v5.1-ping-handshake-enr.txt b/p2p/discover/v5wire/testdata/v5.1-ping-handshake-enr.txt index 21227ddf5a3..477f9e15a82 100644 --- a/p2p/discover/v5wire/testdata/v5.1-ping-handshake-enr.txt +++ b/p2p/discover/v5wire/testdata/v5.1-ping-handshake-enr.txt @@ -1,14 +1,13 @@ # src-node-id = 0xaaaa8419e9f49d0083561b48287df592939a8d19947d8c0ef88f2a4856a69fbb # dest-node-id = 0xbbbb9d047f0488c0b5a93c1c3f2d8bafc7c8ff337024a55434a0d0555de64db9 # nonce = 0xffffffffffffffffffffffff -# read-key = 0xf901161aebd1298aa813621ad0c05343 +# read-key = 0x53b1c075f41876423154e157470c2f48 # ping.req-id = 0x00000001 # ping.enr-seq = 1 # # handshake inputs: # -# whoareyou.iv = 0x00000000000000000000000000000000 -# whoareyou.authdata = 0x0102030405060708090a0b0c0d0e0f100000000000000000 +# whoareyou.challenge-data = 0x000000000000000000000000000000006469736376350001010102030405060708090a0b0c00180102030405060708090a0b0c0d0e0f100000000000000000 # whoareyou.request-nonce = 0x0102030405060708090a0b0c # whoareyou.id-nonce = 0x0102030405060708090a0b0c0d0e0f10 # whoareyou.enr-seq = 0 @@ -17,12 +16,12 @@ 00000000000000000000000000000000088b3d4342774649305f313964a39e55 ea96c005ad539c8c7560413a7008f16c9e6d2f43bbea8814a546b7409ce783d3 -4c4f53245d08da4bb2f6dcb382c5d8d278b57f348cdc967ebf060aca235dd420 -9711826d77482afb37b6106070af606add9d8a8bb052a35eeed1d9cf829d2d73 -821b4b506b5600498c5796706adff216ab862a9186875f9494150c4ae06fa4d1 +4c4f53245d08da4bb23698868350aaad22e3ab8dd034f548a1c43cd246be9856 +2fafa0a1fa86d8e7a3b95ae78cc2b988ded6a5b59eb83ad58097252188b902b2 +1481e30e5e285f19735796706adff216ab862a9186875f9494150c4ae06fa4d1 f0396c93f215fa4ef524e0ed04c3c21e39b1868e1ca8105e585ec17315e755e6 cfc4dd6cb7fd8e1a1f55e49b4b5eb024221482105346f3c82b15fdaae36a3bb1 2a494683b4a3c7f2ae41306252fed84785e2bbff3b022812d0882f06978df84a 80d443972213342d04b9048fc3b1d5fcb1df0f822152eced6da4d3f6df27e70e -4539717307a0208cd2dc394f221e162100550a011363be21154c6b42e3816f0a -38 +4539717307a0208cd208d65093ccab5aa596a34d7511401987662d8cf62b1394 +71 diff --git a/p2p/discover/v5wire/testdata/v5.1-ping-handshake.txt b/p2p/discover/v5wire/testdata/v5.1-ping-handshake.txt index 6bc946dce0a..b3f304766cc 100644 --- a/p2p/discover/v5wire/testdata/v5.1-ping-handshake.txt +++ b/p2p/discover/v5wire/testdata/v5.1-ping-handshake.txt @@ -1,14 +1,13 @@ # src-node-id = 0xaaaa8419e9f49d0083561b48287df592939a8d19947d8c0ef88f2a4856a69fbb # dest-node-id = 0xbbbb9d047f0488c0b5a93c1c3f2d8bafc7c8ff337024a55434a0d0555de64db9 # nonce = 0xffffffffffffffffffffffff -# read-key = 0xf901161aebd1298aa813621ad0c05343 +# read-key = 0x4f9fac6de7567d1e3b1241dffe90f662 # ping.req-id = 0x00000001 # ping.enr-seq = 1 # # handshake inputs: # -# whoareyou.iv = 0x00000000000000000000000000000000 -# whoareyou.authdata = 0x0102030405060708090a0b0c0d0e0f100000000000000001 +# whoareyou.challenge-data = 0x000000000000000000000000000000006469736376350001010102030405060708090a0b0c00180102030405060708090a0b0c0d0e0f100000000000000001 # whoareyou.request-nonce = 0x0102030405060708090a0b0c # whoareyou.id-nonce = 0x0102030405060708090a0b0c0d0e0f10 # whoareyou.enr-seq = 1 @@ -17,8 +16,8 @@ 00000000000000000000000000000000088b3d4342774649305f313964a39e55 ea96c005ad521d8c7560413a7008f16c9e6d2f43bbea8814a546b7409ce783d3 -4c4f53245d08da4bb265bd5b8f27e00bcb6f1b193f52eb737dbb3033ee890cab -bd2728dfa01b3613a2a3a6edc1e2b4359f45c4823db4e5a91132d68606508845 -772fbdd366664b350f5796706adff216ab862a9186875f9494150c4ae06fa4d1 -f0396c93f215fa4ef524dc394f221e162100550a011363be21154c6b42e3816f -0a38 +4c4f53245d08da4bb252012b2cba3f4f374a90a75cff91f142fa9be3e0a5f3ef +268ccb9065aeecfd67a999e7fdc137e062b2ec4a0eb92947f0d9a74bfbf44dfb +a776b21301f8b65efd5796706adff216ab862a9186875f9494150c4ae06fa4d1 +f0396c93f215fa4ef524f1eadf5f0f4126b79336671cbcf7a885b1f8bd2a5d83 +9cf8 diff --git a/p2p/discover/v5wire/testdata/v5.1-ping-message.txt b/p2p/discover/v5wire/testdata/v5.1-ping-message.txt index 00be64c3ae5..f82b99c3bc7 100644 --- a/p2p/discover/v5wire/testdata/v5.1-ping-message.txt +++ b/p2p/discover/v5wire/testdata/v5.1-ping-message.txt @@ -7,4 +7,4 @@ 00000000000000000000000000000000088b3d4342774649325f313964a39e55 ea96c005ad52be8c7560413a7008f16c9e6d2f43bbea8814a546b7409ce783d3 -4c4f53245d08dab84102ed931f66d1db57c785865ffccae8689057103acb15 +4c4f53245d08dab84102ed931f66d1492acb308fa1c6715b9d139b81acbdcc diff --git a/p2p/discover/v5wire/testdata/v5.1-whoareyou.txt b/p2p/discover/v5wire/testdata/v5.1-whoareyou.txt index fbd32e1fd5d..1a75f525ee9 100644 --- a/p2p/discover/v5wire/testdata/v5.1-whoareyou.txt +++ b/p2p/discover/v5wire/testdata/v5.1-whoareyou.txt @@ -1,7 +1,6 @@ # src-node-id = 0xaaaa8419e9f49d0083561b48287df592939a8d19947d8c0ef88f2a4856a69fbb # dest-node-id = 0xbbbb9d047f0488c0b5a93c1c3f2d8bafc7c8ff337024a55434a0d0555de64db9 -# whoareyou.iv = 0x00000000000000000000000000000000 -# whoareyou.authdata = 0x0102030405060708090a0b0c0d0e0f100000000000000000 +# whoareyou.challenge-data = 0x000000000000000000000000000000006469736376350001010102030405060708090a0b0c00180102030405060708090a0b0c0d0e0f100000000000000000 # whoareyou.request-nonce = 0x0102030405060708090a0b0c # whoareyou.id-nonce = 0x0102030405060708090a0b0c0d0e0f10 # whoareyou.enr-seq = 0 From fbb61f10f868bdb18ade733d30bff2685281473e Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Mon, 5 Oct 2020 12:15:12 +0200 Subject: [PATCH 40/70] p2p/discover/v5wire: add note about concurrency --- p2p/discover/v5wire/encoding.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/p2p/discover/v5wire/encoding.go b/p2p/discover/v5wire/encoding.go index 6f9b83c63e4..edcbdbbb8c1 100644 --- a/p2p/discover/v5wire/encoding.go +++ b/p2p/discover/v5wire/encoding.go @@ -123,7 +123,8 @@ var ( sizeofStaticPacketData = sizeofMaskingIV + sizeofStaticHeader ) -// Codec encodes and decodes discovery v5 packets. +// Codec encodes and decodes Discovery v5 packets. +// This type is not safe for concurrent use. type Codec struct { sha256 hash.Hash localnode *enode.LocalNode From 7a1bcf46cd47b621675c8244bde6a69a3bafe867 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Mon, 5 Oct 2020 12:21:38 +0200 Subject: [PATCH 41/70] p2p/discover/v5wire: avoid copying header data when encoding --- p2p/discover/v5wire/encoding.go | 43 ++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/p2p/discover/v5wire/encoding.go b/p2p/discover/v5wire/encoding.go index edcbdbbb8c1..0b489c7ef0c 100644 --- a/p2p/discover/v5wire/encoding.go +++ b/p2p/discover/v5wire/encoding.go @@ -129,10 +129,14 @@ type Codec struct { sha256 hash.Hash localnode *enode.LocalNode privkey *ecdsa.PrivateKey - buf bytes.Buffer // used for encoding of packets - msgbuf bytes.Buffer // used for encoding of message content - reader bytes.Reader // used for decoding sc *SessionCache + + // encoder buffers + buf bytes.Buffer // whole packet + headbuf bytes.Buffer // packet header + msgbuf bytes.Buffer // message RLP plaintext + // decoder buffer + reader bytes.Reader } // NewCodec creates a wire codec. @@ -256,9 +260,9 @@ func (c *Codec) encodeRandom(toID enode.ID) (Header, []byte, error) { if _, err := crand.Read(head.Nonce[:]); err != nil { return head, nil, fmt.Errorf("can't get random data: %v", err) } - c.buf.Reset() - binary.Write(&c.buf, binary.BigEndian, auth) - head.AuthData = bytesCopy(&c.buf) + c.headbuf.Reset() + binary.Write(&c.headbuf, binary.BigEndian, auth) + head.AuthData = c.headbuf.Bytes() msgdata := make([]byte, randomPacketMsgSize) crand.Read(msgdata) @@ -272,18 +276,19 @@ func (c *Codec) encodeWhoareyou(toID enode.ID, packet *Whoareyou) (Header, error panic("BUG: missing node in whoareyouV5 with non-zero seq") } + // Create header. + head := c.makeHeader(toID, flagWhoareyou, 0) + head.AuthData = bytesCopy(&c.buf) + head.Nonce = packet.Nonce + // Encode auth data. auth := &whoareyouAuthData{ IDNonce: packet.IDNonce, RecordSeq: packet.RecordSeq, } - c.buf.Reset() - binary.Write(&c.buf, binary.BigEndian, auth) - - // Create header. - head := c.makeHeader(toID, flagWhoareyou, 0) - head.AuthData = bytesCopy(&c.buf) - head.Nonce = packet.Nonce + c.headbuf.Reset() + binary.Write(&c.headbuf, binary.BigEndian, auth) + head.AuthData = c.headbuf.Bytes() return head, nil } @@ -314,12 +319,12 @@ func (c *Codec) encodeHandshakeHeader(toID enode.ID, addr string, challenge *Who authsizeExtra = len(auth.pubkey) + len(auth.signature) + len(auth.record) head = c.makeHeader(toID, flagHandshake, authsizeExtra) ) - c.buf.Reset() - binary.Write(&c.buf, binary.BigEndian, &auth.h) - c.buf.Write(auth.signature) - c.buf.Write(auth.pubkey) - c.buf.Write(auth.record) - head.AuthData = bytesCopy(&c.buf) + c.headbuf.Reset() + binary.Write(&c.headbuf, binary.BigEndian, &auth.h) + c.headbuf.Write(auth.signature) + c.headbuf.Write(auth.pubkey) + c.headbuf.Write(auth.record) + head.AuthData = c.headbuf.Bytes() head.Nonce = nonce return head, session, err } From 80e6ee4870c2ce15ec0bca59cb5c4e951aec5c46 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Mon, 5 Oct 2020 12:37:30 +0200 Subject: [PATCH 42/70] p2p/discover/v5wire: add reusable message ciphertext buffer --- p2p/discover/v5wire/encoding.go | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/p2p/discover/v5wire/encoding.go b/p2p/discover/v5wire/encoding.go index 0b489c7ef0c..af6ab7b05c0 100644 --- a/p2p/discover/v5wire/encoding.go +++ b/p2p/discover/v5wire/encoding.go @@ -132,9 +132,11 @@ type Codec struct { sc *SessionCache // encoder buffers - buf bytes.Buffer // whole packet - headbuf bytes.Buffer // packet header - msgbuf bytes.Buffer // message RLP plaintext + buf bytes.Buffer // whole packet + headbuf bytes.Buffer // packet header + msgbuf bytes.Buffer // message RLP plaintext + msgctbuf []byte // message data ciphertext + // decoder buffer reader bytes.Reader } @@ -256,6 +258,8 @@ func (c *Codec) makeHeader(toID enode.ID, flag byte, authsizeExtra int) Header { // encodeRandom encodes a packet with random content. func (c *Codec) encodeRandom(toID enode.ID) (Header, []byte, error) { head := c.makeHeader(toID, flagMessage, 0) + + // Encode auth data. auth := messageAuthData{SrcID: c.localnode.ID()} if _, err := crand.Read(head.Nonce[:]); err != nil { return head, nil, fmt.Errorf("can't get random data: %v", err) @@ -264,9 +268,10 @@ func (c *Codec) encodeRandom(toID enode.ID) (Header, []byte, error) { binary.Write(&c.headbuf, binary.BigEndian, auth) head.AuthData = c.headbuf.Bytes() - msgdata := make([]byte, randomPacketMsgSize) - crand.Read(msgdata) - return head, msgdata, nil + // Fill message ciphertext buffer with random bytes. + c.msgctbuf = append(c.msgctbuf[:0], make([]byte, randomPacketMsgSize)...) + crand.Read(c.msgctbuf) + return head, c.msgctbuf, nil } // encodeWhoareyou encodes a WHOAREYOU packet. @@ -389,13 +394,20 @@ func (c *Codec) encodeMessageHeader(toID enode.ID, s *session) (Header, error) { } func (c *Codec) encryptMessage(s *session, p Packet, head *Header, headerData []byte) ([]byte, error) { + // Encode message plaintext. c.msgbuf.Reset() c.msgbuf.WriteByte(p.Kind()) if err := rlp.Encode(&c.msgbuf, p); err != nil { return nil, err } messagePT := c.msgbuf.Bytes() - return encryptGCM(nil, s.writeKey, head.Nonce[:], messagePT, headerData) + + // Encrypt into message ciphertext buffer. + messageCT, err := encryptGCM(c.msgctbuf[:0], s.writeKey, head.Nonce[:], messagePT, headerData) + if err == nil { + c.msgctbuf = messageCT + } + return messageCT, err } // Decode decodes a discovery packet. From 545cf22575ed92e43121ea20f3d2f645771c0e91 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Mon, 5 Oct 2020 12:37:59 +0200 Subject: [PATCH 43/70] p2p/discover/v5wire: remove some TODOs --- p2p/discover/v5wire/encoding.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/p2p/discover/v5wire/encoding.go b/p2p/discover/v5wire/encoding.go index af6ab7b05c0..1f4588664a4 100644 --- a/p2p/discover/v5wire/encoding.go +++ b/p2p/discover/v5wire/encoding.go @@ -35,8 +35,6 @@ import ( ) // TODO concurrent WHOAREYOU tie-breaker -// TODO deal with WHOAREYOU amplification factor (min packet size?) -// TODO add counter to nonce // TODO rehandshake after X packets // Header represents a packet header. From 2c7ae84053f6edf50df150751fa717010510ed37 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Mon, 5 Oct 2020 13:31:55 +0200 Subject: [PATCH 44/70] p2p/discover: clean up challenge nonce handling --- p2p/discover/v5_udp.go | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/p2p/discover/v5_udp.go b/p2p/discover/v5_udp.go index fe0d8be64df..216f9122dcd 100644 --- a/p2p/discover/v5_udp.go +++ b/p2p/discover/v5_udp.go @@ -86,7 +86,7 @@ type UDPv5 struct { // state of dispatch codec codecV5 activeCallByNode map[enode.ID]*callV5 - activeCallByAuth map[string]*callV5 + activeCallByAuth map[v5wire.Nonce]*callV5 callQueue map[enode.ID][]*callV5 // shutdown stuff @@ -106,7 +106,7 @@ type callV5 struct { err chan error // errors sent here // Valid for active calls only: - authTag v5wire.Nonce // nonce of request packet + nonce v5wire.Nonce // nonce of request packet handshakeCount int // # times we attempted handshake for this call challenge *v5wire.Whoareyou // last sent handshake challenge timeout mclock.Timer @@ -155,7 +155,7 @@ func newUDPv5(conn UDPConn, ln *enode.LocalNode, cfg Config) (*UDPv5, error) { // state of dispatch codec: v5wire.NewCodec(ln, cfg.PrivateKey, cfg.Clock), activeCallByNode: make(map[enode.ID]*callV5), - activeCallByAuth: make(map[string]*callV5), + activeCallByAuth: make(map[v5wire.Nonce]*callV5), callQueue: make(map[enode.ID][]*callV5), // shutdown closeCtx: closeCtx, @@ -497,7 +497,7 @@ func (t *UDPv5) dispatch() { panic("BUG: callDone for inactive call") } c.timeout.Stop() - delete(t.activeCallByAuth, string(c.authTag[:])) + delete(t.activeCallByAuth, c.nonce) delete(t.activeCallByNode, id) t.sendNextCall(id) @@ -517,7 +517,7 @@ func (t *UDPv5) dispatch() { for id, c := range t.activeCallByNode { c.err <- errClosed delete(t.activeCallByNode, id) - delete(t.activeCallByAuth, string(c.authTag[:])) + delete(t.activeCallByAuth, c.nonce) } return } @@ -563,17 +563,16 @@ func (t *UDPv5) sendNextCall(id enode.ID) { // sendCall encodes and sends a request packet to the call's recipient node. // This performs a handshake if needed. func (t *UDPv5) sendCall(c *callV5) { - if len(c.authTag) > 0 { - // The call already has an authTag from a previous handshake attempt. Remove the - // entry for the authTag because we're about to generate a new authTag for this - // call. - delete(t.activeCallByAuth, string(c.authTag[:])) + // The call might have a nonce from a previous handshake attempt. Remove the entry for + // the old nonce because we're about to generate a new nonce for this call. + if c.nonce != (v5wire.Nonce{}) { + delete(t.activeCallByAuth, c.nonce) } addr := &net.UDPAddr{IP: c.node.IP(), Port: c.node.UDP()} - newTag, _ := t.send(c.node.ID(), addr, c.packet, c.challenge) - c.authTag = newTag - t.activeCallByAuth[string(c.authTag[:])] = c + newNonce, _ := t.send(c.node.ID(), addr, c.packet, c.challenge) + c.nonce = newNonce + t.activeCallByAuth[newNonce] = c t.startResponseTimeout(c) } @@ -587,14 +586,14 @@ func (t *UDPv5) sendResponse(toID enode.ID, toAddr *net.UDPAddr, packet v5wire.P // send sends a packet to the given node. func (t *UDPv5) send(toID enode.ID, toAddr *net.UDPAddr, packet v5wire.Packet, c *v5wire.Whoareyou) (v5wire.Nonce, error) { addr := toAddr.String() - enc, authTag, err := t.codec.Encode(toID, addr, packet, c) + enc, nonce, err := t.codec.Encode(toID, addr, packet, c) if err != nil { t.log.Warn(">> "+packet.Name(), "id", toID, "addr", addr, "err", err) - return authTag, err + return nonce, err } _, err = t.conn.WriteToUDP(enc, toAddr) t.log.Trace(">> "+packet.Name(), "id", toID, "addr", addr) - return authTag, err + return nonce, err } // readLoop runs in its own goroutine and reads packets from the network. @@ -735,8 +734,8 @@ func (t *UDPv5) handleWhoareyou(p *v5wire.Whoareyou, fromID enode.ID, fromAddr * } // matchWithCall checks whether a handshake attempt matches the active call. -func (t *UDPv5) matchWithCall(fromID enode.ID, authTag v5wire.Nonce) (*callV5, error) { - c := t.activeCallByAuth[string(authTag[:])] +func (t *UDPv5) matchWithCall(fromID enode.ID, nonce v5wire.Nonce) (*callV5, error) { + c := t.activeCallByAuth[nonce] if c == nil { return nil, errChallengeNoCall } From 354f19ca087233916557e4e71f76681b19e80f45 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Mon, 5 Oct 2020 20:00:51 +0200 Subject: [PATCH 45/70] p2p/discover/v5wire: remove unused argument --- p2p/discover/v5wire/encoding.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/p2p/discover/v5wire/encoding.go b/p2p/discover/v5wire/encoding.go index 1f4588664a4..2248baa72d5 100644 --- a/p2p/discover/v5wire/encoding.go +++ b/p2p/discover/v5wire/encoding.go @@ -473,7 +473,7 @@ func (c *Codec) decodeWhoareyou(head *Header, headerData []byte) (Packet, error) } func (c *Codec) decodeHandshakeMessage(fromAddr string, head *Header, headerData, msgData []byte) (n *enode.Node, p Packet, err error) { - node, auth, session, err := c.decodeHandshake(fromAddr, head, headerData) + node, auth, session, err := c.decodeHandshake(fromAddr, head) if err != nil { return nil, nil, err } @@ -490,7 +490,7 @@ func (c *Codec) decodeHandshakeMessage(fromAddr string, head *Header, headerData return node, msg, nil } -func (c *Codec) decodeHandshake(fromAddr string, head *Header, headerData []byte) (*enode.Node, *handshakeAuthData, *session, error) { +func (c *Codec) decodeHandshake(fromAddr string, head *Header) (*enode.Node, *handshakeAuthData, *session, error) { auth, err := c.decodeHandshakeAuthData(head) if err != nil { return nil, nil, nil, err From 1e3a6506eebbecd60c12ca587cbdd42e5550d4b1 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Mon, 5 Oct 2020 22:29:41 +0200 Subject: [PATCH 46/70] cmd/devp2p: use listening address in local ENR --- cmd/devp2p/discv4cmd.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cmd/devp2p/discv4cmd.go b/cmd/devp2p/discv4cmd.go index 13bc098b92e..467c20deb5b 100644 --- a/cmd/devp2p/discv4cmd.go +++ b/cmd/devp2p/discv4cmd.go @@ -286,7 +286,11 @@ func listen(ln *enode.LocalNode, addr string) *net.UDPConn { } usocket := socket.(*net.UDPConn) uaddr := socket.LocalAddr().(*net.UDPAddr) - ln.SetFallbackIP(net.IP{127, 0, 0, 1}) + if uaddr.IP.IsUnspecified() { + ln.SetFallbackIP(net.IP{127, 0, 0, 1}) + } else { + ln.SetFallbackIP(uaddr.IP) + } ln.SetFallbackUDP(uaddr.Port) return usocket } From 282d1a716619901e0b12b38b6c08133d06865ebe Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Tue, 6 Oct 2020 16:47:17 +0200 Subject: [PATCH 47/70] cmd/devp2p/internal/v5test: rename testenv -> conn --- cmd/devp2p/internal/v5test/discv5tests.go | 42 ++++++------- cmd/devp2p/internal/v5test/framework.go | 73 +++++++++++++---------- 2 files changed, 63 insertions(+), 52 deletions(-) diff --git a/cmd/devp2p/internal/v5test/discv5tests.go b/cmd/devp2p/internal/v5test/discv5tests.go index 6ce1a975dbf..d1fb03c656e 100644 --- a/cmd/devp2p/internal/v5test/discv5tests.go +++ b/cmd/devp2p/internal/v5test/discv5tests.go @@ -31,8 +31,8 @@ type Suite struct { Listen1, Listen2 string // listening addresses } -func (s *Suite) listen() *testenv { - return newTestEnv(s.Dest, s.Listen1, s.Listen2) +func (s *Suite) listen() *conn { + return newConn(s.Dest, s.Listen1, s.Listen2) } func (s *Suite) AllTests() []utesting.Test { @@ -45,21 +45,21 @@ func (s *Suite) AllTests() []utesting.Test { // This test sends PING and expects a PONG response. func (s *Suite) TestPing(t *utesting.T) { - te := s.listen() - defer te.close() + conn := s.listen() + defer conn.close() - id := te.nextReqID() - resp := te.reqresp(te.l1, &v5wire.Ping{ReqID: id}) + id := conn.nextReqID() + resp := conn.reqresp(conn.l1, &v5wire.Ping{ReqID: id}) switch resp := resp.(type) { case *v5wire.Pong: if !bytes.Equal(resp.ReqID, id) { t.Fatalf("wrong request ID %x in PONG, want %x", resp.ReqID, id) } - if !resp.ToIP.Equal(laddr(te.l1).IP) { - t.Fatalf("wrong destination IP %v in PONG, want %v", resp.ToIP, laddr(te.l1).IP) + if !resp.ToIP.Equal(laddr(conn.l1).IP) { + t.Fatalf("wrong destination IP %v in PONG, want %v", resp.ToIP, laddr(conn.l1).IP) } - if int(resp.ToPort) != laddr(te.l1).Port { - t.Fatalf("wrong destination port %v in PONG, want %v", resp.ToPort, laddr(te.l1).Port) + if int(resp.ToPort) != laddr(conn.l1).Port { + t.Fatalf("wrong destination port %v in PONG, want %v", resp.ToPort, laddr(conn.l1).Port) } default: t.Fatal("expected PONG, got", resp.Name()) @@ -68,12 +68,12 @@ func (s *Suite) TestPing(t *utesting.T) { // This test sends TALKREQ and expects an empty TALKRESP response. func (s *Suite) TestTalkRequest(t *utesting.T) { - te := s.listen() - defer te.close() + conn := s.listen() + defer conn.close() // Non-empty request ID. - id := te.nextReqID() - resp := te.reqresp(te.l1, &v5wire.TalkRequest{ReqID: id, Protocol: "test-protocol"}) + id := conn.nextReqID() + resp := conn.reqresp(conn.l1, &v5wire.TalkRequest{ReqID: id, Protocol: "test-protocol"}) switch resp := resp.(type) { case *v5wire.TalkResponse: if !bytes.Equal(resp.ReqID, id) { @@ -87,7 +87,7 @@ func (s *Suite) TestTalkRequest(t *utesting.T) { } // Empty request ID. - resp = te.reqresp(te.l1, &v5wire.TalkRequest{Protocol: "test-protocol"}) + resp = conn.reqresp(conn.l1, &v5wire.TalkRequest{Protocol: "test-protocol"}) switch resp := resp.(type) { case *v5wire.TalkResponse: if len(resp.ReqID) > 0 { @@ -103,11 +103,11 @@ func (s *Suite) TestTalkRequest(t *utesting.T) { // This test checks that the remote node returns itself for FINDNODE with distance zero. func (s *Suite) TestFindnodeZeroDistance(t *utesting.T) { - te := s.listen() - defer te.close() + conn := s.listen() + defer conn.close() - id := te.nextReqID() - resp := te.reqresp(te.l1, &v5wire.Findnode{ReqID: id, Distances: []uint{0}}) + id := conn.nextReqID() + resp := conn.reqresp(conn.l1, &v5wire.Findnode{ReqID: id, Distances: []uint{0}}) switch resp := resp.(type) { case *v5wire.Nodes: if !bytes.Equal(resp.ReqID, id) { @@ -120,8 +120,8 @@ func (s *Suite) TestFindnodeZeroDistance(t *utesting.T) { if err != nil { t.Errorf("invalid node in NODES response: %v", err) } - if nodes[0].ID() != te.remote.ID() { - t.Errorf("ID of response node is %v, want %v", nodes[0].ID(), te.remote.ID()) + if nodes[0].ID() != conn.remote.ID() { + t.Errorf("ID of response node is %v, want %v", nodes[0].ID(), conn.remote.ID()) } default: t.Fatal("expected NODES, got", resp.Name()) diff --git a/cmd/devp2p/internal/v5test/framework.go b/cmd/devp2p/internal/v5test/framework.go index 2ecc0da474a..a70f55778e8 100644 --- a/cmd/devp2p/internal/v5test/framework.go +++ b/cmd/devp2p/internal/v5test/framework.go @@ -29,9 +29,23 @@ import ( "github.com/ethereum/go-ethereum/p2p/enode" ) +// errorPacket represents an error during packet reading. +// This exists to facilitate type-switching on the result of conn.read. +type errorPacket struct { + err error +} + +func (p *errorPacket) Kind() byte { return 99 } +func (p *errorPacket) Name() string { return fmt.Sprintf("error: %v", p.err) } +func (p *errorPacket) SetReqID([]byte) {} +func (p *errorPacket) Error() string { return p.err.Error() } +func (p *errorPacket) Unwrap() error { return p.err } + +// This is the response timeout used in tests. const waitTime = 300 * time.Millisecond -type testenv struct { +// conn is a connection to the node under test. +type conn struct { l1, l2 net.PacketConn localNode *enode.LocalNode localKey *ecdsa.PrivateKey @@ -44,17 +58,8 @@ type testenv struct { idCounter uint32 } -type errorPacket struct { - err error -} - -func (p *errorPacket) Kind() byte { return 99 } -func (p *errorPacket) Name() string { return fmt.Sprintf("error: %v", p.err) } -func (p *errorPacket) SetReqID([]byte) {} -func (p *errorPacket) Error() string { return p.err.Error() } -func (p *errorPacket) Unwrap() error { return p.err } - -func newTestEnv(dest *enode.Node, listen1, listen2 string) *testenv { +// newConn sets up a connection to the given node. +func newConn(dest *enode.Node, listen1, listen2 string) *conn { l1, err := net.ListenPacket("udp", fmt.Sprintf("%v:0", listen1)) if err != nil { panic(err) @@ -75,7 +80,7 @@ func newTestEnv(dest *enode.Node, listen1, listen2 string) *testenv { ln.SetStaticIP(laddr(l1).IP) ln.SetFallbackUDP(laddr(l1).Port) - return &testenv{ + return &conn{ l1: l1, l2: l2, localKey: key, @@ -86,42 +91,48 @@ func newTestEnv(dest *enode.Node, listen1, listen2 string) *testenv { } } -func (te *testenv) close() { - te.l1.Close() - te.l2.Close() - te.localNode.Database().Close() +// close shuts down the listener. +func (tc *conn) close() { + tc.l1.Close() + tc.l2.Close() + tc.localNode.Database().Close() } -func (te *testenv) nextReqID() []byte { +// nextReqID creates a request id. +func (tc *conn) nextReqID() []byte { id := make([]byte, 4) - te.idCounter++ - binary.BigEndian.PutUint32(id, te.idCounter) + tc.idCounter++ + binary.BigEndian.PutUint32(id, tc.idCounter) return id } -func (te *testenv) reqresp(c net.PacketConn, req v5wire.Packet) v5wire.Packet { - te.write(c, req, nil) - resp := te.read(c) +// reqresp performs a request/response interaction on the given connection. +// The request is retried if a handshake is requested. +func (tc *conn) reqresp(c net.PacketConn, req v5wire.Packet) v5wire.Packet { + tc.write(c, req, nil) + resp := tc.read(c) if resp.Kind() == v5wire.WhoareyouPacket { challenge := resp.(*v5wire.Whoareyou) - challenge.Node = te.remote - te.write(c, req, challenge) - return te.read(c) + challenge.Node = tc.remote + tc.write(c, req, challenge) + return tc.read(c) } return resp } -func (te *testenv) write(c net.PacketConn, p v5wire.Packet, challenge *v5wire.Whoareyou) { - packet, _, err := te.codec.Encode(te.remote.ID(), te.remoteAddr.String(), p, challenge) +// write sends a packet on the given connection. +func (tc *conn) write(c net.PacketConn, p v5wire.Packet, challenge *v5wire.Whoareyou) { + packet, _, err := tc.codec.Encode(tc.remote.ID(), tc.remoteAddr.String(), p, challenge) if err != nil { panic(fmt.Errorf("can't encode %v packet: %v", p.Name(), err)) } - if _, err := c.WriteTo(packet, te.remoteAddr); err != nil { + if _, err := c.WriteTo(packet, tc.remoteAddr); err != nil { panic(fmt.Errorf("can't send %v: %v", p.Name(), err)) } } -func (te *testenv) read(c net.PacketConn) v5wire.Packet { +// read waits for an incoming packet on the given connection. +func (tc *conn) read(c net.PacketConn) v5wire.Packet { buf := make([]byte, 1280) if err := c.SetReadDeadline(time.Now().Add(waitTime)); err != nil { return &errorPacket{err} @@ -130,7 +141,7 @@ func (te *testenv) read(c net.PacketConn) v5wire.Packet { if err != nil { return &errorPacket{err} } - _, _, p, err := te.codec.Decode(buf[:n], fromAddr.String()) + _, _, p, err := tc.codec.Decode(buf[:n], fromAddr.String()) if err != nil { return &errorPacket{err} } From 7c099cd8afd762d59d09776120a8c2eadfb7ae65 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Wed, 7 Oct 2020 09:58:50 +0200 Subject: [PATCH 48/70] internal/utesting: print live output This is important for long-running tests. --- internal/utesting/utesting.go | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/internal/utesting/utesting.go b/internal/utesting/utesting.go index 23c748cae9b..4de0ecf99aa 100644 --- a/internal/utesting/utesting.go +++ b/internal/utesting/utesting.go @@ -65,10 +65,17 @@ func MatchTests(tests []Test, expr string) []Test { func RunTests(tests []Test, report io.Writer) []Result { results := make([]Result, len(tests)) for i, test := range tests { + var output io.Writer + buffer := new(bytes.Buffer) + output = buffer + if report != nil { + output = io.MultiWriter(buffer, report) + } start := time.Now() results[i].Name = test.Name - results[i].Failed, results[i].Output = Run(test) + results[i].Failed = run(test, output) results[i].Duration = time.Since(start) + results[i].Output = buffer.String() if report != nil { printResult(results[i], report) } @@ -80,7 +87,6 @@ func printResult(r Result, w io.Writer) { pd := r.Duration.Truncate(100 * time.Microsecond) if r.Failed { fmt.Fprintf(w, "-- FAIL %s (%v)\n", r.Name, pd) - fmt.Fprintln(w, r.Output) } else { fmt.Fprintf(w, "-- OK %s (%v)\n", r.Name, pd) } @@ -99,7 +105,13 @@ func CountFailures(rr []Result) int { // Run executes a single test. func Run(test Test) (bool, string) { - t := new(T) + output := new(bytes.Buffer) + failed := run(test, output) + return failed, output.String() +} + +func run(test Test, output io.Writer) bool { + t := &T{output: output} done := make(chan struct{}) go func() { defer close(done) @@ -114,7 +126,7 @@ func Run(test Test) (bool, string) { test.Fn(t) }() <-done - return t.failed, t.output.String() + return t.failed } // T is the value given to the test function. The test can signal failures @@ -122,7 +134,7 @@ func Run(test Test) (bool, string) { type T struct { mu sync.Mutex failed bool - output bytes.Buffer + output io.Writer } // FailNow marks the test as having failed and stops its execution by calling @@ -151,7 +163,7 @@ func (t *T) Failed() bool { func (t *T) Log(vs ...interface{}) { t.mu.Lock() defer t.mu.Unlock() - fmt.Fprintln(&t.output, vs...) + fmt.Fprintln(t.output, vs...) } // Logf formats its arguments according to the format, analogous to Printf, and records @@ -162,7 +174,7 @@ func (t *T) Logf(format string, vs ...interface{}) { if len(format) == 0 || format[len(format)-1] != '\n' { format += "\n" } - fmt.Fprintf(&t.output, format, vs...) + fmt.Fprintf(t.output, format, vs...) } // Error is equivalent to Log followed by Fail. From 15d1a038f5feedab01e2624cf57d2bc005e433b2 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Wed, 7 Oct 2020 10:02:38 +0200 Subject: [PATCH 49/70] cmd/devp2p/internal/v5test: implement FINDNODE test --- cmd/devp2p/internal/v5test/discv5tests.go | 160 ++++++++++++++++++++-- cmd/devp2p/internal/v5test/framework.go | 88 +++++++++++- 2 files changed, 231 insertions(+), 17 deletions(-) diff --git a/cmd/devp2p/internal/v5test/discv5tests.go b/cmd/devp2p/internal/v5test/discv5tests.go index d1fb03c656e..78edc9d318f 100644 --- a/cmd/devp2p/internal/v5test/discv5tests.go +++ b/cmd/devp2p/internal/v5test/discv5tests.go @@ -18,11 +18,13 @@ package v5test import ( "bytes" + "io" + "sync" + "time" "github.com/ethereum/go-ethereum/internal/utesting" "github.com/ethereum/go-ethereum/p2p/discover/v5wire" "github.com/ethereum/go-ethereum/p2p/enode" - "github.com/ethereum/go-ethereum/p2p/enr" ) // Suite is the discv5 test suite. @@ -31,8 +33,8 @@ type Suite struct { Listen1, Listen2 string // listening addresses } -func (s *Suite) listen() *conn { - return newConn(s.Dest, s.Listen1, s.Listen2) +func (s *Suite) listen(log logger) *conn { + return newConn(s.Dest, s.Listen1, s.Listen2, log) } func (s *Suite) AllTests() []utesting.Test { @@ -40,12 +42,13 @@ func (s *Suite) AllTests() []utesting.Test { {Name: "Ping", Fn: s.TestPing}, {Name: "TalkRequest", Fn: s.TestTalkRequest}, {Name: "FindnodeZeroDistance", Fn: s.TestFindnodeZeroDistance}, + {Name: "FindnodeResults", Fn: s.TestFindnodeResults}, } } // This test sends PING and expects a PONG response. func (s *Suite) TestPing(t *utesting.T) { - conn := s.listen() + conn := s.listen(t) defer conn.close() id := conn.nextReqID() @@ -68,7 +71,7 @@ func (s *Suite) TestPing(t *utesting.T) { // This test sends TALKREQ and expects an empty TALKRESP response. func (s *Suite) TestTalkRequest(t *utesting.T) { - conn := s.listen() + conn := s.listen(t) defer conn.close() // Non-empty request ID. @@ -103,7 +106,7 @@ func (s *Suite) TestTalkRequest(t *utesting.T) { // This test checks that the remote node returns itself for FINDNODE with distance zero. func (s *Suite) TestFindnodeZeroDistance(t *utesting.T) { - conn := s.listen() + conn := s.listen(t) defer conn.close() id := conn.nextReqID() @@ -128,14 +131,143 @@ func (s *Suite) TestFindnodeZeroDistance(t *utesting.T) { } } -func checkRecords(records []*enr.Record) ([]*enode.Node, error) { - nodes := make([]*enode.Node, len(records)) - for i := range records { - n, err := enode.New(enode.ValidSchemes, records[i]) - if err != nil { - return nil, err +// In this test, multiple nodes ping the node under test. After waiting for them to be +// accepted into the remote table, the test checks that they are returned by FINDNODE. +func (s *Suite) TestFindnodeResults(t *utesting.T) { + conn := s.listen(t) + defer conn.close() + + // Create bystanders. + nodes := make([]*bystander, 5) + added := make(chan enode.ID, len(nodes)) + for i := range nodes { + nodes[i] = newBystander(t, s, added) + defer nodes[i].close() + } + + // Get them added to the remote table. + timeout := 60 * time.Second + timeoutCh := time.After(timeout) + for count := 0; count < len(nodes); { + select { + case id := <-added: + t.Logf("bystander node %v added to remote table", id) + count++ + case <-timeoutCh: + t.Errorf("remote added %d bystander nodes in %v, need %d to continue", count, timeout, len(nodes)) + t.Logf("this can happen if the node has a non-empty table from previous runs") + return + } + } + t.Logf("all %d bystander nodes were added", len(nodes)) + + // Collect our nodes by distance. + var dists []uint + expect := make(map[enode.ID]*enode.Node) + for _, bn := range nodes { + n := bn.conn.localNode.Node() + expect[n.ID()] = n + d := uint(enode.LogDist(n.ID(), s.Dest.ID())) + if !containsUint(dists, d) { + dists = append(dists, uint(d)) } - nodes[i] = n } - return nodes, nil + + // Send FINDNODE for all distances. + foundNodes, err := conn.findnode(conn.l1, dists) + if err != nil { + t.Fatal(err) + } + t.Logf("remote returned %d nodes for distance list %v", len(foundNodes), dists) + for _, n := range foundNodes { + delete(expect, n.ID()) + } + if len(expect) > 0 { + t.Errorf("missing %d nodes in FINDNODE result", len(expect)) + t.Logf("this can happen if the test is run multiple times in quick succession") + t.Logf("and the remote node hasn't removed dead nodes from previous runs yet") + } else { + t.Logf("all %d expected nodes were returned", len(nodes)) + } +} + +// A bystander is a node whose only purpose is filling a spot in the remote table. +type bystander struct { + dest *enode.Node + conn *conn + log *utesting.T + + addedCh chan enode.ID + done sync.WaitGroup +} + +func newBystander(t *utesting.T, s *Suite, added chan enode.ID) *bystander { + bn := &bystander{ + conn: s.listen(t), + dest: s.Dest, + addedCh: added, + } + bn.done.Add(1) + go bn.loop() + return bn +} + +// id returns the node ID of the bystander. +func (bn *bystander) id() enode.ID { + return bn.conn.localNode.ID() +} + +// close shuts down loop. +func (bn *bystander) close() { + bn.conn.close() + bn.done.Wait() +} + +// loop answers packets from the remote node until quit. +func (bn *bystander) loop() { + defer bn.done.Done() + + var ( + lastPing time.Time + wasAdded bool + ) + for { + // Ping the remote node. + if !wasAdded && time.Since(lastPing) > 10*time.Second { + bn.conn.reqresp(bn.conn.l1, &v5wire.Ping{ + ReqID: bn.conn.nextReqID(), + ENRSeq: bn.dest.Seq(), + }) + lastPing = time.Now() + } + // Answer packets. + switch p := bn.conn.read(bn.conn.l1).(type) { + case *v5wire.Ping: + bn.conn.write(bn.conn.l1, &v5wire.Pong{ + ReqID: p.ReqID, + ENRSeq: bn.conn.localNode.Seq(), + ToIP: bn.dest.IP(), + ToPort: uint16(bn.dest.UDP()), + }, nil) + wasAdded = true + bn.notifyAdded() + case *v5wire.Findnode: + bn.conn.write(bn.conn.l1, &v5wire.Nodes{ReqID: p.ReqID, Total: 1}, nil) + wasAdded = true + bn.notifyAdded() + case *v5wire.TalkRequest: + bn.conn.write(bn.conn.l1, &v5wire.TalkResponse{ReqID: p.ReqID}, nil) + case *errorPacket: + if p.err == io.EOF { + return + } + } + } +} + +func (bn *bystander) notifyAdded() { + if bn.addedCh != nil { + bn.addedCh <- bn.id() + bn.addedCh = nil + } } diff --git a/cmd/devp2p/internal/v5test/framework.go b/cmd/devp2p/internal/v5test/framework.go index a70f55778e8..3e72db8a2a1 100644 --- a/cmd/devp2p/internal/v5test/framework.go +++ b/cmd/devp2p/internal/v5test/framework.go @@ -17,6 +17,7 @@ package v5test import ( + "bytes" "crypto/ecdsa" "encoding/binary" "fmt" @@ -27,6 +28,7 @@ import ( "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/p2p/discover/v5wire" "github.com/ethereum/go-ethereum/p2p/enode" + "github.com/ethereum/go-ethereum/p2p/enr" ) // errorPacket represents an error during packet reading. @@ -52,14 +54,19 @@ type conn struct { remote *enode.Node remoteAddr *net.UDPAddr + log logger codec *v5wire.Codec lastRequest v5wire.Packet lastChallenge *v5wire.Whoareyou idCounter uint32 } +type logger interface { + Logf(string, ...interface{}) +} + // newConn sets up a connection to the given node. -func newConn(dest *enode.Node, listen1, listen2 string) *conn { +func newConn(dest *enode.Node, listen1, listen2 string, log logger) *conn { l1, err := net.ListenPacket("udp", fmt.Sprintf("%v:0", listen1)) if err != nil { panic(err) @@ -88,6 +95,7 @@ func newConn(dest *enode.Node, listen1, listen2 string) *conn { remote: dest, remoteAddr: &net.UDPAddr{IP: dest.IP(), Port: dest.UDP()}, codec: v5wire.NewCodec(ln, key, mclock.System{}), + log: log, } } @@ -120,15 +128,67 @@ func (tc *conn) reqresp(c net.PacketConn, req v5wire.Packet) v5wire.Packet { return resp } +// findnode sends a FINDNODE request and waits for its responses. +func (tc *conn) findnode(c net.PacketConn, dists []uint) ([]*enode.Node, error) { + var ( + findnode = &v5wire.Findnode{ReqID: tc.nextReqID(), Distances: dists} + reqnonce = tc.write(c, findnode, nil) + first = true + total uint8 + results []*enode.Node + ) + for n := 1; n > 0; { + switch resp := tc.read(c).(type) { + case *v5wire.Whoareyou: + // Handle handshake. + if resp.Nonce == reqnonce { + resp.Node = tc.remote + tc.write(c, findnode, resp) + } else { + return nil, fmt.Errorf("unexpected WHOAREYOU (nonce %x), waiting for NODES", resp.Nonce[:]) + } + case *v5wire.Nodes: + // Check request ID. + if !bytes.Equal(resp.ReqID, findnode.ReqID) { + return nil, fmt.Errorf("NODES response has wrong request id %x", n, resp.ReqID) + } + // Check total count. It should be greater than one + // and needs to be the same across all responses. + if first { + if resp.Total <= 1 || resp.Total > 6 { + return nil, fmt.Errorf("invalid NODES response 'total' %d (want > 1, < 7)", resp.Total) + } + total = resp.Total + n = int(total) - 1 + first = false + } else { + if resp.Total != total { + return nil, fmt.Errorf("invalid NODES response 'total' %d (!= %d)", resp.Total, total) + } + n-- + } + // Check nodes. + nodes, err := checkRecords(resp.Nodes) + if err != nil { + return nil, fmt.Errorf("invalid node in NODES response: %v", err) + } + results = append(results, nodes...) + } + } + return results, nil +} + // write sends a packet on the given connection. -func (tc *conn) write(c net.PacketConn, p v5wire.Packet, challenge *v5wire.Whoareyou) { - packet, _, err := tc.codec.Encode(tc.remote.ID(), tc.remoteAddr.String(), p, challenge) +func (tc *conn) write(c net.PacketConn, p v5wire.Packet, challenge *v5wire.Whoareyou) v5wire.Nonce { + packet, nonce, err := tc.codec.Encode(tc.remote.ID(), tc.remoteAddr.String(), p, challenge) if err != nil { panic(fmt.Errorf("can't encode %v packet: %v", p.Name(), err)) } if _, err := c.WriteTo(packet, tc.remoteAddr); err != nil { panic(fmt.Errorf("can't send %v: %v", p.Name(), err)) } + tc.log.Logf("(%s) >> %s", tc.localNode.ID().TerminalString(), p.Name()) + return nonce } // read waits for an incoming packet on the given connection. @@ -145,9 +205,31 @@ func (tc *conn) read(c net.PacketConn) v5wire.Packet { if err != nil { return &errorPacket{err} } + tc.log.Logf("(%s) << %s", tc.localNode.ID().TerminalString(), p.Name()) return p } func laddr(c net.PacketConn) *net.UDPAddr { return c.LocalAddr().(*net.UDPAddr) } + +func checkRecords(records []*enr.Record) ([]*enode.Node, error) { + nodes := make([]*enode.Node, len(records)) + for i := range records { + n, err := enode.New(enode.ValidSchemes, records[i]) + if err != nil { + return nil, err + } + nodes[i] = n + } + return nodes, nil +} + +func containsUint(ints []uint, x uint) bool { + for i := range ints { + if ints[i] == x { + return true + } + } + return false +} From 104dd5ccb425ab13276fa8fa4ccd8f9014c79afb Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Wed, 7 Oct 2020 10:20:15 +0200 Subject: [PATCH 50/70] cmd/devp2p/internal/v5test: fix shutdown --- cmd/devp2p/internal/v5test/discv5tests.go | 5 ++-- cmd/devp2p/internal/v5test/framework.go | 31 +++++++++++++++-------- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/cmd/devp2p/internal/v5test/discv5tests.go b/cmd/devp2p/internal/v5test/discv5tests.go index 78edc9d318f..eb8d8f5a10a 100644 --- a/cmd/devp2p/internal/v5test/discv5tests.go +++ b/cmd/devp2p/internal/v5test/discv5tests.go @@ -18,13 +18,13 @@ package v5test import ( "bytes" - "io" "sync" "time" "github.com/ethereum/go-ethereum/internal/utesting" "github.com/ethereum/go-ethereum/p2p/discover/v5wire" "github.com/ethereum/go-ethereum/p2p/enode" + "github.com/ethereum/go-ethereum/p2p/netutil" ) // Suite is the discv5 test suite. @@ -258,7 +258,8 @@ func (bn *bystander) loop() { case *v5wire.TalkRequest: bn.conn.write(bn.conn.l1, &v5wire.TalkResponse{ReqID: p.ReqID}, nil) case *errorPacket: - if p.err == io.EOF { + if !netutil.IsTemporaryError(p.err) { + bn.conn.logf("shutting down: %v", p.err) return } } diff --git a/cmd/devp2p/internal/v5test/framework.go b/cmd/devp2p/internal/v5test/framework.go index 3e72db8a2a1..7e88d8d875b 100644 --- a/cmd/devp2p/internal/v5test/framework.go +++ b/cmd/devp2p/internal/v5test/framework.go @@ -117,15 +117,19 @@ func (tc *conn) nextReqID() []byte { // reqresp performs a request/response interaction on the given connection. // The request is retried if a handshake is requested. func (tc *conn) reqresp(c net.PacketConn, req v5wire.Packet) v5wire.Packet { - tc.write(c, req, nil) - resp := tc.read(c) - if resp.Kind() == v5wire.WhoareyouPacket { - challenge := resp.(*v5wire.Whoareyou) - challenge.Node = tc.remote - tc.write(c, req, challenge) + reqnonce := tc.write(c, req, nil) + switch resp := tc.read(c).(type) { + case *v5wire.Whoareyou: + if resp.Nonce != reqnonce { + return &errorPacket{fmt.Errorf("wrong nonce %x in WHOAREYOU (want %x)", resp.Nonce[:], reqnonce[:])} + } + resp.Node = tc.remote + resp.RecordSeq = tc.remote.Seq() + tc.write(c, req, resp) return tc.read(c) + default: + return resp } - return resp } // findnode sends a FINDNODE request and waits for its responses. @@ -185,9 +189,10 @@ func (tc *conn) write(c net.PacketConn, p v5wire.Packet, challenge *v5wire.Whoar panic(fmt.Errorf("can't encode %v packet: %v", p.Name(), err)) } if _, err := c.WriteTo(packet, tc.remoteAddr); err != nil { - panic(fmt.Errorf("can't send %v: %v", p.Name(), err)) + tc.logf("Can't send %s: %v", p.Name(), err) + } else { + tc.logf(">> %s", p.Name()) } - tc.log.Logf("(%s) >> %s", tc.localNode.ID().TerminalString(), p.Name()) return nonce } @@ -205,10 +210,16 @@ func (tc *conn) read(c net.PacketConn) v5wire.Packet { if err != nil { return &errorPacket{err} } - tc.log.Logf("(%s) << %s", tc.localNode.ID().TerminalString(), p.Name()) + tc.logf("<< %s", p.Name()) return p } +func (tc *conn) logf(format string, args ...interface{}) { + if tc.log != nil { + tc.log.Logf("(%s) %s", tc.localNode.ID().TerminalString(), fmt.Sprintf(format, args...)) + } +} + func laddr(c net.PacketConn) *net.UDPAddr { return c.LocalAddr().(*net.UDPAddr) } From 91d22af012bc0b23a48158db22507b3d67bad768 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Wed, 7 Oct 2020 11:42:11 +0200 Subject: [PATCH 51/70] cmd/devp2p/internal/v5test: fix various things --- cmd/devp2p/internal/v5test/discv5tests.go | 2 +- cmd/devp2p/internal/v5test/framework.go | 40 +++++++++++++++-------- 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/cmd/devp2p/internal/v5test/discv5tests.go b/cmd/devp2p/internal/v5test/discv5tests.go index eb8d8f5a10a..6132153db8e 100644 --- a/cmd/devp2p/internal/v5test/discv5tests.go +++ b/cmd/devp2p/internal/v5test/discv5tests.go @@ -257,7 +257,7 @@ func (bn *bystander) loop() { bn.notifyAdded() case *v5wire.TalkRequest: bn.conn.write(bn.conn.l1, &v5wire.TalkResponse{ReqID: p.ReqID}, nil) - case *errorPacket: + case *readError: if !netutil.IsTemporaryError(p.err) { bn.conn.logf("shutting down: %v", p.err) return diff --git a/cmd/devp2p/internal/v5test/framework.go b/cmd/devp2p/internal/v5test/framework.go index 7e88d8d875b..e134e329bb1 100644 --- a/cmd/devp2p/internal/v5test/framework.go +++ b/cmd/devp2p/internal/v5test/framework.go @@ -31,17 +31,22 @@ import ( "github.com/ethereum/go-ethereum/p2p/enr" ) -// errorPacket represents an error during packet reading. +// readError represents an error during packet reading. // This exists to facilitate type-switching on the result of conn.read. -type errorPacket struct { +type readError struct { err error } -func (p *errorPacket) Kind() byte { return 99 } -func (p *errorPacket) Name() string { return fmt.Sprintf("error: %v", p.err) } -func (p *errorPacket) SetReqID([]byte) {} -func (p *errorPacket) Error() string { return p.err.Error() } -func (p *errorPacket) Unwrap() error { return p.err } +func (p *readError) Kind() byte { return 99 } +func (p *readError) Name() string { return fmt.Sprintf("error: %v", p.err) } +func (p *readError) SetReqID([]byte) {} +func (p *readError) Error() string { return p.err.Error() } +func (p *readError) Unwrap() error { return p.err } + +// readErrorf creates a readError with the given text. +func readErrorf(format string, args ...interface{}) *readError { + return &readError{fmt.Errorf(format, args...)} +} // This is the response timeout used in tests. const waitTime = 300 * time.Millisecond @@ -121,7 +126,7 @@ func (tc *conn) reqresp(c net.PacketConn, req v5wire.Packet) v5wire.Packet { switch resp := tc.read(c).(type) { case *v5wire.Whoareyou: if resp.Nonce != reqnonce { - return &errorPacket{fmt.Errorf("wrong nonce %x in WHOAREYOU (want %x)", resp.Nonce[:], reqnonce[:])} + return readErrorf("wrong nonce %x in WHOAREYOU (want %x)", resp.Nonce[:], reqnonce[:]) } resp.Node = tc.remote resp.RecordSeq = tc.remote.Seq() @@ -151,8 +156,14 @@ func (tc *conn) findnode(c net.PacketConn, dists []uint) ([]*enode.Node, error) } else { return nil, fmt.Errorf("unexpected WHOAREYOU (nonce %x), waiting for NODES", resp.Nonce[:]) } + case *v5wire.Ping: + // Handle ping from remote. + tc.write(c, &v5wire.Pong{ + ReqID: resp.ReqID, + ENRSeq: tc.localNode.Seq(), + }, nil) case *v5wire.Nodes: - // Check request ID. + // Got NODES! Check request ID. if !bytes.Equal(resp.ReqID, findnode.ReqID) { return nil, fmt.Errorf("NODES response has wrong request id %x", n, resp.ReqID) } @@ -166,10 +177,10 @@ func (tc *conn) findnode(c net.PacketConn, dists []uint) ([]*enode.Node, error) n = int(total) - 1 first = false } else { + n-- if resp.Total != total { return nil, fmt.Errorf("invalid NODES response 'total' %d (!= %d)", resp.Total, total) } - n-- } // Check nodes. nodes, err := checkRecords(resp.Nodes) @@ -177,6 +188,8 @@ func (tc *conn) findnode(c net.PacketConn, dists []uint) ([]*enode.Node, error) return nil, fmt.Errorf("invalid node in NODES response: %v", err) } results = append(results, nodes...) + default: + return nil, fmt.Errorf("expected NODES, got %v", resp) } } return results, nil @@ -200,20 +213,21 @@ func (tc *conn) write(c net.PacketConn, p v5wire.Packet, challenge *v5wire.Whoar func (tc *conn) read(c net.PacketConn) v5wire.Packet { buf := make([]byte, 1280) if err := c.SetReadDeadline(time.Now().Add(waitTime)); err != nil { - return &errorPacket{err} + return &readError{err} } n, fromAddr, err := c.ReadFrom(buf) if err != nil { - return &errorPacket{err} + return &readError{err} } _, _, p, err := tc.codec.Decode(buf[:n], fromAddr.String()) if err != nil { - return &errorPacket{err} + return &readError{err} } tc.logf("<< %s", p.Name()) return p } +// logf prints to the test log. func (tc *conn) logf(format string, args ...interface{}) { if tc.log != nil { tc.log.Logf("(%s) %s", tc.localNode.ID().TerminalString(), fmt.Sprintf(format, args...)) From 08e27fe13ba532a18f0d2477cb4a22946c70b198 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Wed, 7 Oct 2020 12:03:44 +0200 Subject: [PATCH 52/70] cmd/devp2p/internal/v5test: fix handshake issue --- cmd/devp2p/internal/v5test/framework.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cmd/devp2p/internal/v5test/framework.go b/cmd/devp2p/internal/v5test/framework.go index e134e329bb1..dde3d11bbfb 100644 --- a/cmd/devp2p/internal/v5test/framework.go +++ b/cmd/devp2p/internal/v5test/framework.go @@ -129,7 +129,6 @@ func (tc *conn) reqresp(c net.PacketConn, req v5wire.Packet) v5wire.Packet { return readErrorf("wrong nonce %x in WHOAREYOU (want %x)", resp.Nonce[:], reqnonce[:]) } resp.Node = tc.remote - resp.RecordSeq = tc.remote.Seq() tc.write(c, req, resp) return tc.read(c) default: From 30510f3a87d6b73b93d8a6d600240a4ba328106f Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Wed, 7 Oct 2020 12:09:37 +0200 Subject: [PATCH 53/70] p2p/netutil: add IsTimeout --- p2p/netutil/error.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/p2p/netutil/error.go b/p2p/netutil/error.go index cb21b9cd4ce..5d3d9bfd653 100644 --- a/p2p/netutil/error.go +++ b/p2p/netutil/error.go @@ -23,3 +23,11 @@ func IsTemporaryError(err error) bool { }) return ok && tempErr.Temporary() || isPacketTooBig(err) } + +// IsTimeout checks whether the given error is a timeout. +func IsTimeout(err error) bool { + timeoutErr, ok := err.(interface { + Timeout() bool + }) + return ok && timeoutErr.Timeout() +} From 32f0cacc66bcdd2e28483616c01660a6a485c016 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Wed, 7 Oct 2020 12:09:57 +0200 Subject: [PATCH 54/70] cmd/devp2p/internal/v5test: add TestPingLargeRequestID --- cmd/devp2p/internal/v5test/discv5tests.go | 27 ++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/cmd/devp2p/internal/v5test/discv5tests.go b/cmd/devp2p/internal/v5test/discv5tests.go index 6132153db8e..b9d05519946 100644 --- a/cmd/devp2p/internal/v5test/discv5tests.go +++ b/cmd/devp2p/internal/v5test/discv5tests.go @@ -40,6 +40,7 @@ func (s *Suite) listen(log logger) *conn { func (s *Suite) AllTests() []utesting.Test { return []utesting.Test{ {Name: "Ping", Fn: s.TestPing}, + {Name: "PingLargeRequestID", Fn: s.TestPingLargeRequestID}, {Name: "TalkRequest", Fn: s.TestTalkRequest}, {Name: "FindnodeZeroDistance", Fn: s.TestFindnodeZeroDistance}, {Name: "FindnodeResults", Fn: s.TestFindnodeResults}, @@ -51,12 +52,11 @@ func (s *Suite) TestPing(t *utesting.T) { conn := s.listen(t) defer conn.close() - id := conn.nextReqID() - resp := conn.reqresp(conn.l1, &v5wire.Ping{ReqID: id}) - switch resp := resp.(type) { + ping := &v5wire.Ping{ReqID: conn.nextReqID()} + switch resp := conn.reqresp(conn.l1, ping).(type) { case *v5wire.Pong: - if !bytes.Equal(resp.ReqID, id) { - t.Fatalf("wrong request ID %x in PONG, want %x", resp.ReqID, id) + if !bytes.Equal(resp.ReqID, ping.ReqID) { + t.Fatalf("wrong request ID %x in PONG, want %x", resp.ReqID, ping.ReqID) } if !resp.ToIP.Equal(laddr(conn.l1).IP) { t.Fatalf("wrong destination IP %v in PONG, want %v", resp.ToIP, laddr(conn.l1).IP) @@ -69,6 +69,23 @@ func (s *Suite) TestPing(t *utesting.T) { } } +// This test sends PING with a 9-byte request ID, which isn't allowed by the spec. +// The remote node should not respond. +func (s *Suite) TestPingLargeRequestID(t *utesting.T) { + conn := s.listen(t) + defer conn.close() + + ping := &v5wire.Ping{ReqID: make([]byte, 9)} + switch resp := conn.reqresp(conn.l1, ping).(type) { + case *v5wire.Pong: + t.Errorf("remote responded to PING with 9-byte request ID %x", resp.ReqID) + case *readError: + if !netutil.IsTimeout(resp.err) { + t.Error(resp) + } + } +} + // This test sends TALKREQ and expects an empty TALKRESP response. func (s *Suite) TestTalkRequest(t *utesting.T) { conn := s.listen(t) From 922ffa815ddfd41d8f0ff5c6cc052743e19e6bea Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Wed, 7 Oct 2020 12:14:59 +0200 Subject: [PATCH 55/70] cmd/devp2p/internal/v5test: use conn.findnode in TestFindnodeZeroDistance --- cmd/devp2p/internal/v5test/discv5tests.go | 28 ++++++++--------------- cmd/devp2p/internal/v5test/framework.go | 4 ++-- 2 files changed, 11 insertions(+), 21 deletions(-) diff --git a/cmd/devp2p/internal/v5test/discv5tests.go b/cmd/devp2p/internal/v5test/discv5tests.go index b9d05519946..0ab6f117cc7 100644 --- a/cmd/devp2p/internal/v5test/discv5tests.go +++ b/cmd/devp2p/internal/v5test/discv5tests.go @@ -126,25 +126,15 @@ func (s *Suite) TestFindnodeZeroDistance(t *utesting.T) { conn := s.listen(t) defer conn.close() - id := conn.nextReqID() - resp := conn.reqresp(conn.l1, &v5wire.Findnode{ReqID: id, Distances: []uint{0}}) - switch resp := resp.(type) { - case *v5wire.Nodes: - if !bytes.Equal(resp.ReqID, id) { - t.Fatalf("wrong request ID %x in NODES, want %x", resp.ReqID, id) - } - if len(resp.Nodes) != 1 { - t.Error("invalid number of entries in NODES response") - } - nodes, err := checkRecords(resp.Nodes) - if err != nil { - t.Errorf("invalid node in NODES response: %v", err) - } - if nodes[0].ID() != conn.remote.ID() { - t.Errorf("ID of response node is %v, want %v", nodes[0].ID(), conn.remote.ID()) - } - default: - t.Fatal("expected NODES, got", resp.Name()) + nodes, err := conn.findnode(conn.l1, []uint{0}) + if err != nil { + t.Fatal(err) + } + if len(nodes) != 1 { + t.Fatalf("remote returned more than one node for FINDNODE [0]") + } + if nodes[0].ID() != conn.remote.ID() { + t.Errorf("ID of response node is %v, want %v", nodes[0].ID(), conn.remote.ID()) } } diff --git a/cmd/devp2p/internal/v5test/framework.go b/cmd/devp2p/internal/v5test/framework.go index dde3d11bbfb..0c10b93db86 100644 --- a/cmd/devp2p/internal/v5test/framework.go +++ b/cmd/devp2p/internal/v5test/framework.go @@ -169,8 +169,8 @@ func (tc *conn) findnode(c net.PacketConn, dists []uint) ([]*enode.Node, error) // Check total count. It should be greater than one // and needs to be the same across all responses. if first { - if resp.Total <= 1 || resp.Total > 6 { - return nil, fmt.Errorf("invalid NODES response 'total' %d (want > 1, < 7)", resp.Total) + if resp.Total == 0 || resp.Total > 6 { + return nil, fmt.Errorf("invalid NODES response 'total' %d (not in (0,7))", resp.Total) } total = resp.Total n = int(total) - 1 From 2f6d2733ae8c20365333d3c0b15e8549cf2988ec Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Wed, 7 Oct 2020 12:47:07 +0200 Subject: [PATCH 56/70] p2p/discover/v5wire: add check for oversized request ID --- p2p/discover/v5_udp.go | 22 +++--- p2p/discover/v5wire/encoding.go | 1 + p2p/discover/v5wire/msg.go | 125 ++++++++++++++++++-------------- 3 files changed, 84 insertions(+), 64 deletions(-) diff --git a/p2p/discover/v5_udp.go b/p2p/discover/v5_udp.go index 216f9122dcd..c95317a0055 100644 --- a/p2p/discover/v5_udp.go +++ b/p2p/discover/v5_udp.go @@ -442,7 +442,7 @@ func (t *UDPv5) call(node *enode.Node, responseType byte, packet v5wire.Packet) } // Assign request ID. crand.Read(c.reqid) - packet.SetReqID(c.reqid) + packet.SetRequestID(c.reqid) // Send call to dispatch. select { case t.callCh <- c: @@ -649,22 +649,23 @@ func (t *UDPv5) handlePacket(rawpacket []byte, fromAddr *net.UDPAddr) error { } // handleCallResponse dispatches a response packet to the call waiting for it. -func (t *UDPv5) handleCallResponse(fromID enode.ID, fromAddr *net.UDPAddr, reqid []byte, p v5wire.Packet) { +func (t *UDPv5) handleCallResponse(fromID enode.ID, fromAddr *net.UDPAddr, p v5wire.Packet) bool { ac := t.activeCallByNode[fromID] - if ac == nil || !bytes.Equal(reqid, ac.reqid) { + if ac == nil || !bytes.Equal(p.RequestID(), ac.reqid) { t.log.Debug(fmt.Sprintf("Unsolicited/late %s response", p.Name()), "id", fromID, "addr", fromAddr) - return + return false } if !fromAddr.IP.Equal(ac.node.IP()) || fromAddr.Port != ac.node.UDP() { t.log.Debug(fmt.Sprintf("%s from wrong endpoint", p.Name()), "id", fromID, "addr", fromAddr) - return + return false } if p.Kind() != ac.responseType { t.log.Debug(fmt.Sprintf("Wrong discv5 response type %s", p.Name()), "id", fromID, "addr", fromAddr) - return + return false } t.startResponseTimeout(ac) ac.ch <- p + return true } // getNode looks for a node record in table and database. @@ -688,16 +689,17 @@ func (t *UDPv5) handle(p v5wire.Packet, fromID enode.ID, fromAddr *net.UDPAddr) case *v5wire.Ping: t.handlePing(p, fromID, fromAddr) case *v5wire.Pong: - t.localNode.UDPEndpointStatement(fromAddr, &net.UDPAddr{IP: p.ToIP, Port: int(p.ToPort)}) - t.handleCallResponse(fromID, fromAddr, p.ReqID, p) + if t.handleCallResponse(fromID, fromAddr, p) { + t.localNode.UDPEndpointStatement(fromAddr, &net.UDPAddr{IP: p.ToIP, Port: int(p.ToPort)}) + } case *v5wire.Findnode: t.handleFindnode(p, fromID, fromAddr) case *v5wire.Nodes: - t.handleCallResponse(fromID, fromAddr, p.ReqID, p) + t.handleCallResponse(fromID, fromAddr, p) case *v5wire.TalkRequest: t.handleTalkRequest(p, fromID, fromAddr) case *v5wire.TalkResponse: - t.handleCallResponse(fromID, fromAddr, p.ReqID, p) + t.handleCallResponse(fromID, fromAddr, p) } } diff --git a/p2p/discover/v5wire/encoding.go b/p2p/discover/v5wire/encoding.go index 2248baa72d5..16982251c94 100644 --- a/p2p/discover/v5wire/encoding.go +++ b/p2p/discover/v5wire/encoding.go @@ -110,6 +110,7 @@ var ( errInvalidNonceSig = errors.New("invalid ID nonce signature") errMessageTooShort = errors.New("message contains no data") errMessageDecrypt = errors.New("cannot decrypt message") + errInvalidReqID = errors.New("request ID larger than 8 bytes") ) // Packet sizes. diff --git a/p2p/discover/v5wire/msg.go b/p2p/discover/v5wire/msg.go index e6e506ca113..023b1a8c62b 100644 --- a/p2p/discover/v5wire/msg.go +++ b/p2p/discover/v5wire/msg.go @@ -28,9 +28,10 @@ import ( // Packet is implemented by all message types. type Packet interface { - Name() string // Name returns a string corresponding to the message type. - Kind() byte // Kind returns the message type. - SetReqID([]byte) // Sets the request ID. + Name() string // Name returns a string corresponding to the message type. + Kind() byte // Kind returns the message type. + RequestID() []byte // Returns the request ID. + SetRequestID([]byte) // Sets the request ID. } // Message types. @@ -176,57 +177,73 @@ func DecodeMessage(ptype byte, body []byte) (Packet, error) { if err := rlp.DecodeBytes(body, dec); err != nil { return nil, err } + if dec.RequestID() != nil && len(dec.RequestID()) > 8 { + return nil, errInvalidReqID + } return dec, nil } -func (*Whoareyou) Name() string { return "WHOAREYOU/v5" } -func (*Whoareyou) Kind() byte { return WhoareyouPacket } -func (*Whoareyou) SetReqID([]byte) {} - -func (*Unknown) Name() string { return "UNKNOWN/v5" } -func (*Unknown) Kind() byte { return UnknownPacket } -func (*Unknown) SetReqID([]byte) {} - -func (*Ping) Name() string { return "PING/v5" } -func (*Ping) Kind() byte { return PingMsg } -func (p *Ping) SetReqID(id []byte) { p.ReqID = id } - -func (*Pong) Name() string { return "PONG/v5" } -func (*Pong) Kind() byte { return PongMsg } -func (p *Pong) SetReqID(id []byte) { p.ReqID = id } - -func (*Findnode) Name() string { return "FINDNODE/v5" } -func (*Findnode) Kind() byte { return FindnodeMsg } -func (p *Findnode) SetReqID(id []byte) { p.ReqID = id } - -func (*Nodes) Name() string { return "NODES/v5" } -func (*Nodes) Kind() byte { return NodesMsg } -func (p *Nodes) SetReqID(id []byte) { p.ReqID = id } - -func (*TalkRequest) Name() string { return "TALKREQ/v5" } -func (*TalkRequest) Kind() byte { return TalkRequestMsg } -func (p *TalkRequest) SetReqID(id []byte) { p.ReqID = id } - -func (*TalkResponse) Name() string { return "TALKRESP/v5" } -func (*TalkResponse) Kind() byte { return TalkResponseMsg } -func (p *TalkResponse) SetReqID(id []byte) { p.ReqID = id } - -func (*RequestTicket) Name() string { return "REQTICKET/v5" } -func (*RequestTicket) Kind() byte { return RequestTicketMsg } -func (p *RequestTicket) SetReqID(id []byte) { p.ReqID = id } - -func (*Regtopic) Name() string { return "REGTOPIC/v5" } -func (*Regtopic) Kind() byte { return RegtopicMsg } -func (p *Regtopic) SetReqID(id []byte) { p.ReqID = id } - -func (*Ticket) Name() string { return "TICKET/v5" } -func (*Ticket) Kind() byte { return TicketMsg } -func (p *Ticket) SetReqID(id []byte) { p.ReqID = id } - -func (*Regconfirmation) Name() string { return "REGCONFIRMATION/v5" } -func (*Regconfirmation) Kind() byte { return RegconfirmationMsg } -func (p *Regconfirmation) SetReqID(id []byte) { p.ReqID = id } - -func (*TopicQuery) Name() string { return "TOPICQUERY/v5" } -func (*TopicQuery) Kind() byte { return TopicQueryMsg } -func (p *TopicQuery) SetReqID(id []byte) { p.ReqID = id } +func (*Whoareyou) Name() string { return "WHOAREYOU/v5" } +func (*Whoareyou) Kind() byte { return WhoareyouPacket } +func (*Whoareyou) RequestID() []byte { return nil } +func (*Whoareyou) SetRequestID([]byte) {} + +func (*Unknown) Name() string { return "UNKNOWN/v5" } +func (*Unknown) Kind() byte { return UnknownPacket } +func (*Unknown) RequestID() []byte { return nil } +func (*Unknown) SetRequestID([]byte) {} + +func (*Ping) Name() string { return "PING/v5" } +func (*Ping) Kind() byte { return PingMsg } +func (p *Ping) RequestID() []byte { return p.ReqID } +func (p *Ping) SetRequestID(id []byte) { p.ReqID = id } + +func (*Pong) Name() string { return "PONG/v5" } +func (*Pong) Kind() byte { return PongMsg } +func (p *Pong) RequestID() []byte { return p.ReqID } +func (p *Pong) SetRequestID(id []byte) { p.ReqID = id } + +func (*Findnode) Name() string { return "FINDNODE/v5" } +func (*Findnode) Kind() byte { return FindnodeMsg } +func (p *Findnode) RequestID() []byte { return p.ReqID } +func (p *Findnode) SetRequestID(id []byte) { p.ReqID = id } + +func (*Nodes) Name() string { return "NODES/v5" } +func (*Nodes) Kind() byte { return NodesMsg } +func (p *Nodes) RequestID() []byte { return p.ReqID } +func (p *Nodes) SetRequestID(id []byte) { p.ReqID = id } + +func (*TalkRequest) Name() string { return "TALKREQ/v5" } +func (*TalkRequest) Kind() byte { return TalkRequestMsg } +func (p *TalkRequest) RequestID() []byte { return p.ReqID } +func (p *TalkRequest) SetRequestID(id []byte) { p.ReqID = id } + +func (*TalkResponse) Name() string { return "TALKRESP/v5" } +func (*TalkResponse) Kind() byte { return TalkResponseMsg } +func (p *TalkResponse) RequestID() []byte { return p.ReqID } +func (p *TalkResponse) SetRequestID(id []byte) { p.ReqID = id } + +func (*RequestTicket) Name() string { return "REQTICKET/v5" } +func (*RequestTicket) Kind() byte { return RequestTicketMsg } +func (p *RequestTicket) RequestID() []byte { return p.ReqID } +func (p *RequestTicket) SetRequestID(id []byte) { p.ReqID = id } + +func (*Regtopic) Name() string { return "REGTOPIC/v5" } +func (*Regtopic) Kind() byte { return RegtopicMsg } +func (p *Regtopic) RequestID() []byte { return p.ReqID } +func (p *Regtopic) SetRequestID(id []byte) { p.ReqID = id } + +func (*Ticket) Name() string { return "TICKET/v5" } +func (*Ticket) Kind() byte { return TicketMsg } +func (p *Ticket) RequestID() []byte { return p.ReqID } +func (p *Ticket) SetRequestID(id []byte) { p.ReqID = id } + +func (*Regconfirmation) Name() string { return "REGCONFIRMATION/v5" } +func (*Regconfirmation) Kind() byte { return RegconfirmationMsg } +func (p *Regconfirmation) RequestID() []byte { return p.ReqID } +func (p *Regconfirmation) SetRequestID(id []byte) { p.ReqID = id } + +func (*TopicQuery) Name() string { return "TOPICQUERY/v5" } +func (*TopicQuery) Kind() byte { return TopicQueryMsg } +func (p *TopicQuery) RequestID() []byte { return p.ReqID } +func (p *TopicQuery) SetRequestID(id []byte) { p.ReqID = id } From cfdd78ee33bf693552411c2dc3ec76d92ad21d33 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Wed, 7 Oct 2020 12:57:28 +0200 Subject: [PATCH 57/70] p2p/discover/v5wire: export request ID error --- p2p/discover/v5wire/encoding.go | 6 +++++- p2p/discover/v5wire/msg.go | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/p2p/discover/v5wire/encoding.go b/p2p/discover/v5wire/encoding.go index 16982251c94..c0431808dd5 100644 --- a/p2p/discover/v5wire/encoding.go +++ b/p2p/discover/v5wire/encoding.go @@ -110,7 +110,11 @@ var ( errInvalidNonceSig = errors.New("invalid ID nonce signature") errMessageTooShort = errors.New("message contains no data") errMessageDecrypt = errors.New("cannot decrypt message") - errInvalidReqID = errors.New("request ID larger than 8 bytes") +) + +// Public errors. +var ( + ErrInvalidReqID = errors.New("request ID larger than 8 bytes") ) // Packet sizes. diff --git a/p2p/discover/v5wire/msg.go b/p2p/discover/v5wire/msg.go index 023b1a8c62b..7c3686111b7 100644 --- a/p2p/discover/v5wire/msg.go +++ b/p2p/discover/v5wire/msg.go @@ -178,7 +178,7 @@ func DecodeMessage(ptype byte, body []byte) (Packet, error) { return nil, err } if dec.RequestID() != nil && len(dec.RequestID()) > 8 { - return nil, errInvalidReqID + return nil, ErrInvalidReqID } return dec, nil } From 9e2214a632823d1a5489ae14b51d34716416314a Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Wed, 7 Oct 2020 12:58:16 +0200 Subject: [PATCH 58/70] cmd/devp2p/internal/v5test: fix new test --- cmd/devp2p/internal/v5test/discv5tests.go | 6 ++++-- cmd/devp2p/internal/v5test/framework.go | 11 ++++++----- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/cmd/devp2p/internal/v5test/discv5tests.go b/cmd/devp2p/internal/v5test/discv5tests.go index 0ab6f117cc7..d4cac26ce35 100644 --- a/cmd/devp2p/internal/v5test/discv5tests.go +++ b/cmd/devp2p/internal/v5test/discv5tests.go @@ -78,9 +78,11 @@ func (s *Suite) TestPingLargeRequestID(t *utesting.T) { ping := &v5wire.Ping{ReqID: make([]byte, 9)} switch resp := conn.reqresp(conn.l1, ping).(type) { case *v5wire.Pong: - t.Errorf("remote responded to PING with 9-byte request ID %x", resp.ReqID) + t.Errorf("PONG response with unknown request ID %x", resp.ReqID) case *readError: - if !netutil.IsTimeout(resp.err) { + if resp.err == v5wire.ErrInvalidReqID { + t.Error("response with oversized request ID") + } else if !netutil.IsTimeout(resp.err) { t.Error(resp) } } diff --git a/cmd/devp2p/internal/v5test/framework.go b/cmd/devp2p/internal/v5test/framework.go index 0c10b93db86..6bf637f5a8f 100644 --- a/cmd/devp2p/internal/v5test/framework.go +++ b/cmd/devp2p/internal/v5test/framework.go @@ -37,11 +37,12 @@ type readError struct { err error } -func (p *readError) Kind() byte { return 99 } -func (p *readError) Name() string { return fmt.Sprintf("error: %v", p.err) } -func (p *readError) SetReqID([]byte) {} -func (p *readError) Error() string { return p.err.Error() } -func (p *readError) Unwrap() error { return p.err } +func (p *readError) Kind() byte { return 99 } +func (p *readError) Name() string { return fmt.Sprintf("error: %v", p.err) } +func (p *readError) Error() string { return p.err.Error() } +func (p *readError) Unwrap() error { return p.err } +func (p *readError) RequestID() []byte { return nil } +func (p *readError) SetRequestID([]byte) {} // readErrorf creates a readError with the given text. func readErrorf(format string, args ...interface{}) *readError { From 80571fca13fd1a7b49308393d2a9a75f91243f15 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Wed, 7 Oct 2020 13:31:02 +0200 Subject: [PATCH 59/70] cmd/devp2p/internal/v5test: add multi IP test --- cmd/devp2p/internal/v5test/discv5tests.go | 40 +++++++++++++++++++++++ cmd/devp2p/internal/v5test/framework.go | 2 +- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/cmd/devp2p/internal/v5test/discv5tests.go b/cmd/devp2p/internal/v5test/discv5tests.go index d4cac26ce35..599239c95a9 100644 --- a/cmd/devp2p/internal/v5test/discv5tests.go +++ b/cmd/devp2p/internal/v5test/discv5tests.go @@ -41,6 +41,7 @@ func (s *Suite) AllTests() []utesting.Test { return []utesting.Test{ {Name: "Ping", Fn: s.TestPing}, {Name: "PingLargeRequestID", Fn: s.TestPingLargeRequestID}, + {Name: "PingMultiIP", Fn: s.TestPingMultiIP}, {Name: "TalkRequest", Fn: s.TestTalkRequest}, {Name: "FindnodeZeroDistance", Fn: s.TestFindnodeZeroDistance}, {Name: "FindnodeResults", Fn: s.TestFindnodeResults}, @@ -88,6 +89,45 @@ func (s *Suite) TestPingLargeRequestID(t *utesting.T) { } } +// In this test, a session is established from one IP as usual. The session is then reused +// on another IP, which shouldn't work. The remote node should respond with WHOAREYOU for +// the attempt from a different IP. +func (s *Suite) TestPingMultiIP(t *utesting.T) { + conn := s.listen(t) + defer conn.close() + + // Create the session on l1. + ping := &v5wire.Ping{ReqID: conn.nextReqID()} + resp := conn.reqresp(conn.l1, ping) + if resp.Kind() != v5wire.PongMsg { + t.Fatal("expected PONG, got", resp) + } + + // Send on l2. This reuses the session because there is only one codec. + ping2 := &v5wire.Ping{ReqID: conn.nextReqID()} + conn.write(conn.l2, ping2, nil) + switch resp := conn.read(conn.l2).(type) { + case *v5wire.Pong: + t.Fatalf("remote responded to PING from %v for session on IP %v", laddr(conn.l2).IP, laddr(conn.l1).IP) + case *v5wire.Whoareyou: + t.Logf("got WHOAREYOU for new session as expected") + default: + t.Fatal("expected WHOAREYOU, got", resp) + } + + // Try on l1 again. + ping3 := &v5wire.Ping{ReqID: conn.nextReqID()} + conn.write(conn.l1, ping3, nil) + switch resp := conn.read(conn.l2).(type) { + case *v5wire.Pong: + t.Fatalf("remote responded to PING from %v for session on IP %v", laddr(conn.l1).IP, laddr(conn.l2).IP) + case *v5wire.Whoareyou: + t.Logf("got WHOAREYOU for new session as expected") + default: + t.Fatal("expected WHOAREYOU, got", resp) + } +} + // This test sends TALKREQ and expects an empty TALKRESP response. func (s *Suite) TestTalkRequest(t *utesting.T) { conn := s.listen(t) diff --git a/cmd/devp2p/internal/v5test/framework.go b/cmd/devp2p/internal/v5test/framework.go index 6bf637f5a8f..7beb1420511 100644 --- a/cmd/devp2p/internal/v5test/framework.go +++ b/cmd/devp2p/internal/v5test/framework.go @@ -165,7 +165,7 @@ func (tc *conn) findnode(c net.PacketConn, dists []uint) ([]*enode.Node, error) case *v5wire.Nodes: // Got NODES! Check request ID. if !bytes.Equal(resp.ReqID, findnode.ReqID) { - return nil, fmt.Errorf("NODES response has wrong request id %x", n, resp.ReqID) + return nil, fmt.Errorf("NODES response has wrong request id %x", resp.ReqID) } // Check total count. It should be greater than one // and needs to be the same across all responses. From 456cb2a73d8929e66d8c302c403b7af9af594a7a Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Wed, 7 Oct 2020 13:44:49 +0200 Subject: [PATCH 60/70] cmd/devp2p/internal/v5test: fix multi-IP test --- cmd/devp2p/internal/v5test/discv5tests.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cmd/devp2p/internal/v5test/discv5tests.go b/cmd/devp2p/internal/v5test/discv5tests.go index 599239c95a9..cc527dae9da 100644 --- a/cmd/devp2p/internal/v5test/discv5tests.go +++ b/cmd/devp2p/internal/v5test/discv5tests.go @@ -111,6 +111,8 @@ func (s *Suite) TestPingMultiIP(t *utesting.T) { t.Fatalf("remote responded to PING from %v for session on IP %v", laddr(conn.l2).IP, laddr(conn.l1).IP) case *v5wire.Whoareyou: t.Logf("got WHOAREYOU for new session as expected") + resp.Node = s.Dest + conn.write(conn.l2, ping2, resp) default: t.Fatal("expected WHOAREYOU, got", resp) } From 4c97a48e76838ae67400d6afce3e392873dfaaf2 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Wed, 7 Oct 2020 13:48:58 +0200 Subject: [PATCH 61/70] cmd/devp2p/internal/v5test: fix multi-IP test --- cmd/devp2p/internal/v5test/discv5tests.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/cmd/devp2p/internal/v5test/discv5tests.go b/cmd/devp2p/internal/v5test/discv5tests.go index cc527dae9da..9aadac5bab1 100644 --- a/cmd/devp2p/internal/v5test/discv5tests.go +++ b/cmd/devp2p/internal/v5test/discv5tests.go @@ -117,10 +117,17 @@ func (s *Suite) TestPingMultiIP(t *utesting.T) { t.Fatal("expected WHOAREYOU, got", resp) } + // Catch the PONG on l2. + switch resp := conn.read(conn.l2).(type) { + case *v5wire.Pong: + default: + t.Fatal("expected PONG, got", resp) + } + // Try on l1 again. ping3 := &v5wire.Ping{ReqID: conn.nextReqID()} conn.write(conn.l1, ping3, nil) - switch resp := conn.read(conn.l2).(type) { + switch resp := conn.read(conn.l1).(type) { case *v5wire.Pong: t.Fatalf("remote responded to PING from %v for session on IP %v", laddr(conn.l1).IP, laddr(conn.l2).IP) case *v5wire.Whoareyou: From 1f40ffc8788bd723344226b1f4c5fa9c6c724c74 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Wed, 7 Oct 2020 15:45:13 +0200 Subject: [PATCH 62/70] cmd/devp2p/internal/v5test: add test for interrupted handshake --- cmd/devp2p/internal/v5test/discv5tests.go | 53 +++++++++++++++++++---- 1 file changed, 44 insertions(+), 9 deletions(-) diff --git a/cmd/devp2p/internal/v5test/discv5tests.go b/cmd/devp2p/internal/v5test/discv5tests.go index 9aadac5bab1..4df55be320d 100644 --- a/cmd/devp2p/internal/v5test/discv5tests.go +++ b/cmd/devp2p/internal/v5test/discv5tests.go @@ -18,6 +18,7 @@ package v5test import ( "bytes" + "net" "sync" "time" @@ -42,6 +43,7 @@ func (s *Suite) AllTests() []utesting.Test { {Name: "Ping", Fn: s.TestPing}, {Name: "PingLargeRequestID", Fn: s.TestPingLargeRequestID}, {Name: "PingMultiIP", Fn: s.TestPingMultiIP}, + {Name: "PingHandshakeInterrupted", Fn: s.TestPingHandshakeInterrupted}, {Name: "TalkRequest", Fn: s.TestTalkRequest}, {Name: "FindnodeZeroDistance", Fn: s.TestFindnodeZeroDistance}, {Name: "FindnodeResults", Fn: s.TestFindnodeResults}, @@ -56,20 +58,24 @@ func (s *Suite) TestPing(t *utesting.T) { ping := &v5wire.Ping{ReqID: conn.nextReqID()} switch resp := conn.reqresp(conn.l1, ping).(type) { case *v5wire.Pong: - if !bytes.Equal(resp.ReqID, ping.ReqID) { - t.Fatalf("wrong request ID %x in PONG, want %x", resp.ReqID, ping.ReqID) - } - if !resp.ToIP.Equal(laddr(conn.l1).IP) { - t.Fatalf("wrong destination IP %v in PONG, want %v", resp.ToIP, laddr(conn.l1).IP) - } - if int(resp.ToPort) != laddr(conn.l1).Port { - t.Fatalf("wrong destination port %v in PONG, want %v", resp.ToPort, laddr(conn.l1).Port) - } + checkPong(t, resp, ping, conn.l1) default: t.Fatal("expected PONG, got", resp.Name()) } } +func checkPong(t *utesting.T, pong *v5wire.Pong, ping *v5wire.Ping, c net.PacketConn) { + if !bytes.Equal(pong.ReqID, ping.ReqID) { + t.Fatalf("wrong request ID %x in PONG, want %x", pong.ReqID, ping.ReqID) + } + if !pong.ToIP.Equal(laddr(c).IP) { + t.Fatalf("wrong destination IP %v in PONG, want %v", pong.ToIP, laddr(c).IP) + } + if int(pong.ToPort) != laddr(c).Port { + t.Fatalf("wrong destination port %v in PONG, want %v", pong.ToPort, laddr(c).Port) + } +} + // This test sends PING with a 9-byte request ID, which isn't allowed by the spec. // The remote node should not respond. func (s *Suite) TestPingLargeRequestID(t *utesting.T) { @@ -102,6 +108,7 @@ func (s *Suite) TestPingMultiIP(t *utesting.T) { if resp.Kind() != v5wire.PongMsg { t.Fatal("expected PONG, got", resp) } + checkPong(t, resp.(*v5wire.Pong), ping, conn.l1) // Send on l2. This reuses the session because there is only one codec. ping2 := &v5wire.Ping{ReqID: conn.nextReqID()} @@ -120,6 +127,7 @@ func (s *Suite) TestPingMultiIP(t *utesting.T) { // Catch the PONG on l2. switch resp := conn.read(conn.l2).(type) { case *v5wire.Pong: + checkPong(t, resp, ping2, conn.l2) default: t.Fatal("expected PONG, got", resp) } @@ -137,6 +145,33 @@ func (s *Suite) TestPingMultiIP(t *utesting.T) { } } +// This test starts a handshake, but doesn't finish it and sends a second ordinary message +// packet instead of a handshake message packet. The remote node should respond with +// another WHOAREYOU challenge for the second packet. +func (s *Suite) TestPingHandshakeInterrupted(t *utesting.T) { + conn := s.listen(t) + defer conn.close() + + // First PING triggers challenge. + ping := &v5wire.Ping{ReqID: conn.nextReqID()} + conn.write(conn.l1, ping, nil) + switch resp := conn.read(conn.l1).(type) { + case *v5wire.Whoareyou: + t.Logf("got WHOAREYOU for PING") + default: + t.Fatal("expected WHOAREYOU, got", resp) + } + + // Send second PING. + ping2 := &v5wire.Ping{ReqID: conn.nextReqID()} + switch resp := conn.reqresp(conn.l1, ping2).(type) { + case *v5wire.Pong: + checkPong(t, resp, ping2, conn.l1) + default: + t.Fatal("expected WHOAREYOU, got", resp) + } +} + // This test sends TALKREQ and expects an empty TALKRESP response. func (s *Suite) TestTalkRequest(t *utesting.T) { conn := s.listen(t) From 9f97dd02ebe26dead229aa09e0ff6207fff3dc52 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Thu, 8 Oct 2020 14:45:14 +0200 Subject: [PATCH 63/70] p2p/discover/v5wire: decode ephkey after signature check --- p2p/discover/v5wire/encoding.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/p2p/discover/v5wire/encoding.go b/p2p/discover/v5wire/encoding.go index c0431808dd5..4ec95558fa5 100644 --- a/p2p/discover/v5wire/encoding.go +++ b/p2p/discover/v5wire/encoding.go @@ -511,17 +511,17 @@ func (c *Codec) decodeHandshake(fromAddr string, head *Header) (*enode.Node, *ha if err != nil { return nil, nil, nil, err } - // Verify ephemeral key is on curve. - ephkey, err := DecodePubkey(c.privkey.Curve, auth.pubkey) - if err != nil { - return nil, nil, nil, errInvalidAuthKey - } // Verify ID nonce signature. sig := auth.signature cdata := challenge.ChallengeData if err = verifyIDSignature(c.sha256, sig, node, cdata, auth.pubkey, c.localnode.ID()); err != nil { return nil, nil, nil, err } + // Verify ephemeral key is on curve. + ephkey, err := DecodePubkey(c.privkey.Curve, auth.pubkey) + if err != nil { + return nil, nil, nil, errInvalidAuthKey + } // Derive sesssion keys. session := deriveKeys(sha256.New, c.privkey, ephkey, auth.h.SrcID, c.localnode.ID(), cdata) session = session.keysFlipped() From 05c03ab1da019e224b9eb0e51287a5ca89dcc6b1 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Thu, 8 Oct 2020 14:47:01 +0200 Subject: [PATCH 64/70] p2p/discover/v5wire: remove sha256reset --- p2p/discover/v5wire/encoding.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/p2p/discover/v5wire/encoding.go b/p2p/discover/v5wire/encoding.go index 4ec95558fa5..e7426486d42 100644 --- a/p2p/discover/v5wire/encoding.go +++ b/p2p/discover/v5wire/encoding.go @@ -618,12 +618,6 @@ func (c *Codec) decryptMessage(input, nonce, headerData, readKey []byte) (Packet return DecodeMessage(msgdata[0], msgdata[1:]) } -// sha256 returns the shared hash instance. -func (c *Codec) sha256reset() hash.Hash { - c.sha256.Reset() - return c.sha256 -} - // checkValid performs some basic validity checks on the header. // The packetLen here is the length remaining after the static header. func (h *StaticHeader) checkValid(packetLen int) error { From 409d1217413728e346a12e7427b912bd63664a87 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Thu, 8 Oct 2020 15:32:32 +0200 Subject: [PATCH 65/70] p2p/discover/v5wire: reduce allocations in handshake decoder There is no need to copy the handshake fields because the header struct is thrown away after decoding. --- p2p/discover/v5wire/encoding.go | 74 +++++++++++++-------------------- 1 file changed, 29 insertions(+), 45 deletions(-) diff --git a/p2p/discover/v5wire/encoding.go b/p2p/discover/v5wire/encoding.go index e7426486d42..8a783678acd 100644 --- a/p2p/discover/v5wire/encoding.go +++ b/p2p/discover/v5wire/encoding.go @@ -495,76 +495,70 @@ func (c *Codec) decodeHandshakeMessage(fromAddr string, head *Header, headerData return node, msg, nil } -func (c *Codec) decodeHandshake(fromAddr string, head *Header) (*enode.Node, *handshakeAuthData, *session, error) { - auth, err := c.decodeHandshakeAuthData(head) - if err != nil { - return nil, nil, nil, err +func (c *Codec) decodeHandshake(fromAddr string, head *Header) (n *enode.Node, auth handshakeAuthData, s *session, err error) { + if auth, err = c.decodeHandshakeAuthData(head); err != nil { + return nil, auth, nil, err } // Verify against our last WHOAREYOU. challenge := c.sc.getHandshake(auth.h.SrcID, fromAddr) if challenge == nil { - return nil, nil, nil, errUnexpectedHandshake + return nil, auth, nil, errUnexpectedHandshake } // Get node record. - node, err := c.decodeHandshakeRecord(challenge.Node, auth.h.SrcID, auth.record) + n, err = c.decodeHandshakeRecord(challenge.Node, auth.h.SrcID, auth.record) if err != nil { - return nil, nil, nil, err + return nil, auth, nil, err } // Verify ID nonce signature. sig := auth.signature cdata := challenge.ChallengeData - if err = verifyIDSignature(c.sha256, sig, node, cdata, auth.pubkey, c.localnode.ID()); err != nil { - return nil, nil, nil, err + err = verifyIDSignature(c.sha256, sig, n, cdata, auth.pubkey, c.localnode.ID()) + if err != nil { + return nil, auth, nil, err } // Verify ephemeral key is on curve. ephkey, err := DecodePubkey(c.privkey.Curve, auth.pubkey) if err != nil { - return nil, nil, nil, errInvalidAuthKey + return nil, auth, nil, errInvalidAuthKey } // Derive sesssion keys. session := deriveKeys(sha256.New, c.privkey, ephkey, auth.h.SrcID, c.localnode.ID(), cdata) session = session.keysFlipped() - return node, auth, session, nil + return n, auth, session, nil } // decodeHandshakeAuthData reads the authdata section of a handshake packet. -func (c *Codec) decodeHandshakeAuthData(head *Header) (*handshakeAuthData, error) { +func (c *Codec) decodeHandshakeAuthData(head *Header) (auth handshakeAuthData, err error) { // Decode fixed size part. if len(head.AuthData) < sizeofHandshakeAuthData { - return nil, fmt.Errorf("header authsize %d too low for handshake", head.AuthSize) + return auth, fmt.Errorf("header authsize %d too low for handshake", head.AuthSize) } c.reader.Reset(head.AuthData) - var auth handshakeAuthData binary.Read(&c.reader, binary.BigEndian, &auth.h) head.src = auth.h.SrcID // Decode variable-size part. - varspace := int(head.AuthSize) - sizeofHandshakeAuthData - if c.reader.Len() < varspace { - return nil, errTooShort - } - if int(auth.h.SigSize)+int(auth.h.PubkeySize) > varspace { - return nil, fmt.Errorf("invalid handshake data sizes (%d+%d > %d)", auth.h.SigSize, auth.h.PubkeySize, varspace) - } - if !readNew(&auth.signature, int(auth.h.SigSize), &c.reader) { - return nil, fmt.Errorf("can't read auth signature") - } - if !readNew(&auth.pubkey, int(auth.h.PubkeySize), &c.reader) { - return nil, fmt.Errorf("can't read auth pubkey") - } - recordsize := varspace - int(auth.h.SigSize) - int(auth.h.PubkeySize) - if !readNew(&auth.record, recordsize, &c.reader) { - return nil, fmt.Errorf("can't read auth node record") + var ( + vardata = head.AuthData[sizeofHandshakeAuthData:] + sigAndKeySize = int(auth.h.SigSize) + int(auth.h.PubkeySize) + keyOffset = int(auth.h.SigSize) + recOffset = keyOffset + int(auth.h.PubkeySize) + ) + if len(vardata) < sigAndKeySize { + return auth, errTooShort } - return &auth, nil + auth.signature = vardata[:keyOffset] + auth.pubkey = vardata[keyOffset:recOffset] + auth.record = vardata[recOffset:] + return auth, nil } // decodeHandshakeRecord verifies the node record contained in a handshake packet. The // remote node should include the record if we don't have one or if ours is older than the // latest sequence number. -func (c *Codec) decodeHandshakeRecord(local *enode.Node, wantID enode.ID, remote []byte) (node *enode.Node, err error) { - node = local +func (c *Codec) decodeHandshakeRecord(local *enode.Node, wantID enode.ID, remote []byte) (*enode.Node, error) { + node := local if len(remote) > 0 { var record enr.Record if err := rlp.DecodeBytes(remote, &record); err != nil { @@ -582,9 +576,9 @@ func (c *Codec) decodeHandshakeRecord(local *enode.Node, wantID enode.ID, remote } } if node == nil { - err = errNoRecord + return nil, errNoRecord } - return node, err + return node, nil } // decodeMessage reads packet data following the header as an ordinary message packet. @@ -650,13 +644,3 @@ func bytesCopy(r *bytes.Buffer) []byte { copy(b, r.Bytes()) return b } - -// readNew reads 'length' bytes from 'r' and stores them into 'data'. -func readNew(data *[]byte, length int, r *bytes.Reader) bool { - if length == 0 { - return true - } - *data = make([]byte, length) - n, _ := r.Read(*data) - return n == length -} From 1eb2e24ef5b6d36e6b4726ecb620f45b0383fcf2 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Thu, 8 Oct 2020 16:05:14 +0200 Subject: [PATCH 66/70] cmd/devp2p/internal/v5test: improve listener handling There is no need to listen on both IPs for most tests. This change also ensures the node record of most tests does not contain IP/port and thus can't be added to the remote table. --- cmd/devp2p/internal/v5test/discv5tests.go | 90 +++++++++++++---------- cmd/devp2p/internal/v5test/framework.go | 37 +++++----- 2 files changed, 70 insertions(+), 57 deletions(-) diff --git a/cmd/devp2p/internal/v5test/discv5tests.go b/cmd/devp2p/internal/v5test/discv5tests.go index 4df55be320d..20d30e5e170 100644 --- a/cmd/devp2p/internal/v5test/discv5tests.go +++ b/cmd/devp2p/internal/v5test/discv5tests.go @@ -34,8 +34,16 @@ type Suite struct { Listen1, Listen2 string // listening addresses } -func (s *Suite) listen(log logger) *conn { - return newConn(s.Dest, s.Listen1, s.Listen2, log) +func (s *Suite) listen1(log logger) (*conn, net.PacketConn) { + c := newConn(s.Dest, log) + l := c.listen(s.Listen1) + return c, l +} + +func (s *Suite) listen2(log logger) (*conn, net.PacketConn, net.PacketConn) { + c := newConn(s.Dest, log) + l1, l2 := c.listen(s.Listen1), c.listen(s.Listen2) + return c, l1, l2 } func (s *Suite) AllTests() []utesting.Test { @@ -52,13 +60,13 @@ func (s *Suite) AllTests() []utesting.Test { // This test sends PING and expects a PONG response. func (s *Suite) TestPing(t *utesting.T) { - conn := s.listen(t) + conn, l1 := s.listen1(t) defer conn.close() ping := &v5wire.Ping{ReqID: conn.nextReqID()} - switch resp := conn.reqresp(conn.l1, ping).(type) { + switch resp := conn.reqresp(l1, ping).(type) { case *v5wire.Pong: - checkPong(t, resp, ping, conn.l1) + checkPong(t, resp, ping, l1) default: t.Fatal("expected PONG, got", resp.Name()) } @@ -79,11 +87,11 @@ func checkPong(t *utesting.T, pong *v5wire.Pong, ping *v5wire.Ping, c net.Packet // This test sends PING with a 9-byte request ID, which isn't allowed by the spec. // The remote node should not respond. func (s *Suite) TestPingLargeRequestID(t *utesting.T) { - conn := s.listen(t) + conn, l1 := s.listen1(t) defer conn.close() ping := &v5wire.Ping{ReqID: make([]byte, 9)} - switch resp := conn.reqresp(conn.l1, ping).(type) { + switch resp := conn.reqresp(l1, ping).(type) { case *v5wire.Pong: t.Errorf("PONG response with unknown request ID %x", resp.ReqID) case *readError: @@ -99,45 +107,45 @@ func (s *Suite) TestPingLargeRequestID(t *utesting.T) { // on another IP, which shouldn't work. The remote node should respond with WHOAREYOU for // the attempt from a different IP. func (s *Suite) TestPingMultiIP(t *utesting.T) { - conn := s.listen(t) + conn, l1, l2 := s.listen2(t) defer conn.close() // Create the session on l1. ping := &v5wire.Ping{ReqID: conn.nextReqID()} - resp := conn.reqresp(conn.l1, ping) + resp := conn.reqresp(l1, ping) if resp.Kind() != v5wire.PongMsg { t.Fatal("expected PONG, got", resp) } - checkPong(t, resp.(*v5wire.Pong), ping, conn.l1) + checkPong(t, resp.(*v5wire.Pong), ping, l1) // Send on l2. This reuses the session because there is only one codec. ping2 := &v5wire.Ping{ReqID: conn.nextReqID()} - conn.write(conn.l2, ping2, nil) - switch resp := conn.read(conn.l2).(type) { + conn.write(l2, ping2, nil) + switch resp := conn.read(l2).(type) { case *v5wire.Pong: - t.Fatalf("remote responded to PING from %v for session on IP %v", laddr(conn.l2).IP, laddr(conn.l1).IP) + t.Fatalf("remote responded to PING from %v for session on IP %v", laddr(l2).IP, laddr(l1).IP) case *v5wire.Whoareyou: t.Logf("got WHOAREYOU for new session as expected") resp.Node = s.Dest - conn.write(conn.l2, ping2, resp) + conn.write(l2, ping2, resp) default: t.Fatal("expected WHOAREYOU, got", resp) } // Catch the PONG on l2. - switch resp := conn.read(conn.l2).(type) { + switch resp := conn.read(l2).(type) { case *v5wire.Pong: - checkPong(t, resp, ping2, conn.l2) + checkPong(t, resp, ping2, l2) default: t.Fatal("expected PONG, got", resp) } // Try on l1 again. ping3 := &v5wire.Ping{ReqID: conn.nextReqID()} - conn.write(conn.l1, ping3, nil) - switch resp := conn.read(conn.l1).(type) { + conn.write(l1, ping3, nil) + switch resp := conn.read(l1).(type) { case *v5wire.Pong: - t.Fatalf("remote responded to PING from %v for session on IP %v", laddr(conn.l1).IP, laddr(conn.l2).IP) + t.Fatalf("remote responded to PING from %v for session on IP %v", laddr(l1).IP, laddr(l2).IP) case *v5wire.Whoareyou: t.Logf("got WHOAREYOU for new session as expected") default: @@ -149,13 +157,13 @@ func (s *Suite) TestPingMultiIP(t *utesting.T) { // packet instead of a handshake message packet. The remote node should respond with // another WHOAREYOU challenge for the second packet. func (s *Suite) TestPingHandshakeInterrupted(t *utesting.T) { - conn := s.listen(t) + conn, l1 := s.listen1(t) defer conn.close() // First PING triggers challenge. ping := &v5wire.Ping{ReqID: conn.nextReqID()} - conn.write(conn.l1, ping, nil) - switch resp := conn.read(conn.l1).(type) { + conn.write(l1, ping, nil) + switch resp := conn.read(l1).(type) { case *v5wire.Whoareyou: t.Logf("got WHOAREYOU for PING") default: @@ -164,9 +172,9 @@ func (s *Suite) TestPingHandshakeInterrupted(t *utesting.T) { // Send second PING. ping2 := &v5wire.Ping{ReqID: conn.nextReqID()} - switch resp := conn.reqresp(conn.l1, ping2).(type) { + switch resp := conn.reqresp(l1, ping2).(type) { case *v5wire.Pong: - checkPong(t, resp, ping2, conn.l1) + checkPong(t, resp, ping2, l1) default: t.Fatal("expected WHOAREYOU, got", resp) } @@ -174,12 +182,12 @@ func (s *Suite) TestPingHandshakeInterrupted(t *utesting.T) { // This test sends TALKREQ and expects an empty TALKRESP response. func (s *Suite) TestTalkRequest(t *utesting.T) { - conn := s.listen(t) + conn, l1 := s.listen1(t) defer conn.close() // Non-empty request ID. id := conn.nextReqID() - resp := conn.reqresp(conn.l1, &v5wire.TalkRequest{ReqID: id, Protocol: "test-protocol"}) + resp := conn.reqresp(l1, &v5wire.TalkRequest{ReqID: id, Protocol: "test-protocol"}) switch resp := resp.(type) { case *v5wire.TalkResponse: if !bytes.Equal(resp.ReqID, id) { @@ -193,7 +201,7 @@ func (s *Suite) TestTalkRequest(t *utesting.T) { } // Empty request ID. - resp = conn.reqresp(conn.l1, &v5wire.TalkRequest{Protocol: "test-protocol"}) + resp = conn.reqresp(l1, &v5wire.TalkRequest{Protocol: "test-protocol"}) switch resp := resp.(type) { case *v5wire.TalkResponse: if len(resp.ReqID) > 0 { @@ -209,10 +217,10 @@ func (s *Suite) TestTalkRequest(t *utesting.T) { // This test checks that the remote node returns itself for FINDNODE with distance zero. func (s *Suite) TestFindnodeZeroDistance(t *utesting.T) { - conn := s.listen(t) + conn, l1 := s.listen1(t) defer conn.close() - nodes, err := conn.findnode(conn.l1, []uint{0}) + nodes, err := conn.findnode(l1, []uint{0}) if err != nil { t.Fatal(err) } @@ -227,9 +235,6 @@ func (s *Suite) TestFindnodeZeroDistance(t *utesting.T) { // In this test, multiple nodes ping the node under test. After waiting for them to be // accepted into the remote table, the test checks that they are returned by FINDNODE. func (s *Suite) TestFindnodeResults(t *utesting.T) { - conn := s.listen(t) - defer conn.close() - // Create bystanders. nodes := make([]*bystander, 5) added := make(chan enode.ID, len(nodes)) @@ -267,7 +272,9 @@ func (s *Suite) TestFindnodeResults(t *utesting.T) { } // Send FINDNODE for all distances. - foundNodes, err := conn.findnode(conn.l1, dists) + conn, l1 := s.listen1(t) + defer conn.close() + foundNodes, err := conn.findnode(l1, dists) if err != nil { t.Fatal(err) } @@ -288,15 +295,18 @@ func (s *Suite) TestFindnodeResults(t *utesting.T) { type bystander struct { dest *enode.Node conn *conn - log *utesting.T + l net.PacketConn addedCh chan enode.ID done sync.WaitGroup } func newBystander(t *utesting.T, s *Suite, added chan enode.ID) *bystander { + conn, l := s.listen1(t) + conn.setEndpoint(l) // bystander nodes need IP/port to get pinged bn := &bystander{ - conn: s.listen(t), + conn: conn, + l: l, dest: s.Dest, addedCh: added, } @@ -327,16 +337,16 @@ func (bn *bystander) loop() { for { // Ping the remote node. if !wasAdded && time.Since(lastPing) > 10*time.Second { - bn.conn.reqresp(bn.conn.l1, &v5wire.Ping{ + bn.conn.reqresp(bn.l, &v5wire.Ping{ ReqID: bn.conn.nextReqID(), ENRSeq: bn.dest.Seq(), }) lastPing = time.Now() } // Answer packets. - switch p := bn.conn.read(bn.conn.l1).(type) { + switch p := bn.conn.read(bn.l).(type) { case *v5wire.Ping: - bn.conn.write(bn.conn.l1, &v5wire.Pong{ + bn.conn.write(bn.l, &v5wire.Pong{ ReqID: p.ReqID, ENRSeq: bn.conn.localNode.Seq(), ToIP: bn.dest.IP(), @@ -345,11 +355,11 @@ func (bn *bystander) loop() { wasAdded = true bn.notifyAdded() case *v5wire.Findnode: - bn.conn.write(bn.conn.l1, &v5wire.Nodes{ReqID: p.ReqID, Total: 1}, nil) + bn.conn.write(bn.l, &v5wire.Nodes{ReqID: p.ReqID, Total: 1}, nil) wasAdded = true bn.notifyAdded() case *v5wire.TalkRequest: - bn.conn.write(bn.conn.l1, &v5wire.TalkResponse{ReqID: p.ReqID}, nil) + bn.conn.write(bn.l, &v5wire.TalkResponse{ReqID: p.ReqID}, nil) case *readError: if !netutil.IsTemporaryError(p.err) { bn.conn.logf("shutting down: %v", p.err) diff --git a/cmd/devp2p/internal/v5test/framework.go b/cmd/devp2p/internal/v5test/framework.go index 7beb1420511..9eac37520f7 100644 --- a/cmd/devp2p/internal/v5test/framework.go +++ b/cmd/devp2p/internal/v5test/framework.go @@ -54,11 +54,11 @@ const waitTime = 300 * time.Millisecond // conn is a connection to the node under test. type conn struct { - l1, l2 net.PacketConn localNode *enode.LocalNode localKey *ecdsa.PrivateKey remote *enode.Node remoteAddr *net.UDPAddr + listeners []net.PacketConn log logger codec *v5wire.Codec @@ -72,15 +72,7 @@ type logger interface { } // newConn sets up a connection to the given node. -func newConn(dest *enode.Node, listen1, listen2 string, log logger) *conn { - l1, err := net.ListenPacket("udp", fmt.Sprintf("%v:0", listen1)) - if err != nil { - panic(err) - } - l2, err := net.ListenPacket("udp", fmt.Sprintf("%v:0", listen2)) - if err != nil { - panic(err) - } +func newConn(dest *enode.Node, log logger) *conn { key, err := crypto.GenerateKey() if err != nil { panic(err) @@ -90,12 +82,8 @@ func newConn(dest *enode.Node, listen1, listen2 string, log logger) *conn { panic(err) } ln := enode.NewLocalNode(db, key) - ln.SetStaticIP(laddr(l1).IP) - ln.SetFallbackUDP(laddr(l1).Port) return &conn{ - l1: l1, - l2: l2, localKey: key, localNode: ln, remote: dest, @@ -105,10 +93,25 @@ func newConn(dest *enode.Node, listen1, listen2 string, log logger) *conn { } } -// close shuts down the listener. +func (tc *conn) setEndpoint(c net.PacketConn) { + tc.localNode.SetStaticIP(laddr(c).IP) + tc.localNode.SetFallbackUDP(laddr(c).Port) +} + +func (tc *conn) listen(ip string) net.PacketConn { + l, err := net.ListenPacket("udp", fmt.Sprintf("%v:0", ip)) + if err != nil { + panic(err) + } + tc.listeners = append(tc.listeners, l) + return l +} + +// close shuts down all listeners and the local node. func (tc *conn) close() { - tc.l1.Close() - tc.l2.Close() + for _, l := range tc.listeners { + l.Close() + } tc.localNode.Database().Close() } From b7d8c57ad3726e90219fabd8622c1d553d5da93a Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Thu, 8 Oct 2020 16:46:10 +0200 Subject: [PATCH 67/70] p2p/discover/v5wire: fix style thing --- p2p/discover/v5wire/encoding.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/p2p/discover/v5wire/encoding.go b/p2p/discover/v5wire/encoding.go index 8a783678acd..27c4e6ad7d6 100644 --- a/p2p/discover/v5wire/encoding.go +++ b/p2p/discover/v5wire/encoding.go @@ -464,9 +464,9 @@ func (c *Codec) decodeWhoareyou(head *Header, headerData []byte) (Packet, error) if len(head.AuthData) != sizeofWhoareyouAuthData { return nil, fmt.Errorf("invalid auth size %d for WHOAREYOU", len(head.AuthData)) } + var auth whoareyouAuthData c.reader.Reset(head.AuthData) - auth := new(whoareyouAuthData) - binary.Read(&c.reader, binary.BigEndian, auth) + binary.Read(&c.reader, binary.BigEndian, &auth) p := &Whoareyou{ Nonce: head.Nonce, IDNonce: auth.IDNonce, From 59b11fa55e228a3ec299a75590a04252a48b43e0 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Wed, 14 Oct 2020 11:18:02 +0200 Subject: [PATCH 68/70] p2p/discover/v5wire: add bad handshake test and fix --- p2p/discover/v5wire/encoding.go | 2 ++ p2p/discover/v5wire/encoding_test.go | 39 ++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/p2p/discover/v5wire/encoding.go b/p2p/discover/v5wire/encoding.go index 27c4e6ad7d6..5b4ade69d79 100644 --- a/p2p/discover/v5wire/encoding.go +++ b/p2p/discover/v5wire/encoding.go @@ -480,12 +480,14 @@ func (c *Codec) decodeWhoareyou(head *Header, headerData []byte) (Packet, error) func (c *Codec) decodeHandshakeMessage(fromAddr string, head *Header, headerData, msgData []byte) (n *enode.Node, p Packet, err error) { node, auth, session, err := c.decodeHandshake(fromAddr, head) if err != nil { + c.sc.deleteHandshake(auth.h.SrcID, fromAddr) return nil, nil, err } // Decrypt the message using the new session keys. msg, err := c.decryptMessage(msgData, head.Nonce[:], headerData, session.readKey) if err != nil { + c.sc.deleteHandshake(auth.h.SrcID, fromAddr) return node, msg, err } diff --git a/p2p/discover/v5wire/encoding_test.go b/p2p/discover/v5wire/encoding_test.go index 6ca1c0bafe8..e772ae70be6 100644 --- a/p2p/discover/v5wire/encoding_test.go +++ b/p2p/discover/v5wire/encoding_test.go @@ -231,6 +231,45 @@ func TestHandshakeV5_rekey2(t *testing.T) { net.nodeA.expectDecode(t, NodesMsg, nodes) } +func TestHandshakeV5_BadHandshakeAttack(t *testing.T) { + t.Parallel() + net := newHandshakeTest() + defer net.close() + + // A -> B RANDOM PACKET + packet, _ := net.nodeA.encode(t, net.nodeB, &Findnode{}) + resp := net.nodeB.expectDecode(t, UnknownPacket, packet) + + // A <- B WHOAREYOU + challenge := &Whoareyou{ + Nonce: resp.(*Unknown).Nonce, + IDNonce: testIDnonce, + RecordSeq: 0, + } + whoareyou, _ := net.nodeB.encode(t, net.nodeA, challenge) + net.nodeA.expectDecode(t, WhoareyouPacket, whoareyou) + + // A -> B FINDNODE + incorrect_challenge := &Whoareyou{ + IDNonce: [16]byte{5, 6, 7, 8, 9, 6, 11, 12}, + RecordSeq: challenge.RecordSeq, + Node: challenge.Node, + sent: challenge.sent, + } + incorrect_findnode, _ := net.nodeA.encodeWithChallenge(t, net.nodeB, incorrect_challenge, &Findnode{}) + incorrect_findnode2 := make([]byte, len(incorrect_findnode)) + copy(incorrect_findnode2, incorrect_findnode) + + net.nodeB.expectDecodeErr(t, errInvalidNonceSig, incorrect_findnode) + + // Reject new findnode as previous handshake is now deleted. + net.nodeB.expectDecodeErr(t, errUnexpectedHandshake, incorrect_findnode2) + + // The findnode packet is again rejected even with a valid challenge this time. + findnode, _ := net.nodeA.encodeWithChallenge(t, net.nodeB, challenge, &Findnode{}) + net.nodeB.expectDecodeErr(t, errUnexpectedHandshake, findnode) +} + // This test checks some malformed packets. func TestDecodeErrorsV5(t *testing.T) { t.Parallel() From a44e2f9b3455c32fd94e19d51a3a1156e7f40d7b Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Wed, 14 Oct 2020 11:28:47 +0200 Subject: [PATCH 69/70] p2p/discover/v5wire: remove remaining 'v5' in identifiers --- p2p/discover/v5wire/encoding.go | 2 +- p2p/discover/v5wire/encoding_test.go | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/p2p/discover/v5wire/encoding.go b/p2p/discover/v5wire/encoding.go index 5b4ade69d79..f502339e1e5 100644 --- a/p2p/discover/v5wire/encoding.go +++ b/p2p/discover/v5wire/encoding.go @@ -281,7 +281,7 @@ func (c *Codec) encodeRandom(toID enode.ID) (Header, []byte, error) { func (c *Codec) encodeWhoareyou(toID enode.ID, packet *Whoareyou) (Header, error) { // Sanity check node field to catch misbehaving callers. if packet.RecordSeq > 0 && packet.Node == nil { - panic("BUG: missing node in whoareyouV5 with non-zero seq") + panic("BUG: missing node in whoareyou with non-zero seq") } // Create header. diff --git a/p2p/discover/v5wire/encoding_test.go b/p2p/discover/v5wire/encoding_test.go index e772ae70be6..d9c807e0a8d 100644 --- a/p2p/discover/v5wire/encoding_test.go +++ b/p2p/discover/v5wire/encoding_test.go @@ -68,7 +68,7 @@ func TestMinSizes(t *testing.T) { } // This test checks the basic handshake flow where A talks to B and A has no secrets. -func TestHandshakeV5(t *testing.T) { +func TestHandshake(t *testing.T) { t.Parallel() net := newHandshakeTest() defer net.close() @@ -99,7 +99,7 @@ func TestHandshakeV5(t *testing.T) { } // This test checks that handshake attempts are removed within the timeout. -func TestHandshakeV5_timeout(t *testing.T) { +func TestHandshake_timeout(t *testing.T) { t.Parallel() net := newHandshakeTest() defer net.close() @@ -124,7 +124,7 @@ func TestHandshakeV5_timeout(t *testing.T) { } // This test checks handshake behavior when no record is sent in the auth response. -func TestHandshakeV5_norecord(t *testing.T) { +func TestHandshake_norecord(t *testing.T) { t.Parallel() net := newHandshakeTest() defer net.close() @@ -158,7 +158,7 @@ func TestHandshakeV5_norecord(t *testing.T) { // In this test, A tries to send FINDNODE with existing secrets but B doesn't know // anything about A. -func TestHandshakeV5_rekey(t *testing.T) { +func TestHandshake_rekey(t *testing.T) { t.Parallel() net := newHandshakeTest() defer net.close() @@ -197,7 +197,7 @@ func TestHandshakeV5_rekey(t *testing.T) { } // In this test A and B have different keys before the handshake. -func TestHandshakeV5_rekey2(t *testing.T) { +func TestHandshake_rekey2(t *testing.T) { t.Parallel() net := newHandshakeTest() defer net.close() @@ -231,7 +231,7 @@ func TestHandshakeV5_rekey2(t *testing.T) { net.nodeA.expectDecode(t, NodesMsg, nodes) } -func TestHandshakeV5_BadHandshakeAttack(t *testing.T) { +func TestHandshake_BadHandshakeAttack(t *testing.T) { t.Parallel() net := newHandshakeTest() defer net.close() From fd5507dbdc2610ad0a67a5eae73a6910f86b64f8 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Wed, 14 Oct 2020 11:39:25 +0200 Subject: [PATCH 70/70] cmd/devp2p/internal/v5test: fix lint issue --- cmd/devp2p/internal/v5test/discv5tests.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/devp2p/internal/v5test/discv5tests.go b/cmd/devp2p/internal/v5test/discv5tests.go index 20d30e5e170..7866498f737 100644 --- a/cmd/devp2p/internal/v5test/discv5tests.go +++ b/cmd/devp2p/internal/v5test/discv5tests.go @@ -267,7 +267,7 @@ func (s *Suite) TestFindnodeResults(t *utesting.T) { expect[n.ID()] = n d := uint(enode.LogDist(n.ID(), s.Dest.ID())) if !containsUint(dists, d) { - dists = append(dists, uint(d)) + dists = append(dists, d) } }