// 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 discover import ( "bytes" "crypto/aes" "crypto/cipher" "crypto/ecdsa" "crypto/elliptic" crand "crypto/rand" "crypto/sha256" "errors" "fmt" "hash" "net" "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" "golang.org/x/crypto/hkdf" ) // TODO concurrent WHOAREYOU tie-breaker // TODO deal with WHOAREYOU amplification factor (min packet size?) // 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_requestTicketV5 p_ticketV5 p_regtopicV5 p_regconfirmationV5 p_topicqueryV5 p_unknownV5 = byte(255) // any non-decryptable packet p_whoareyouV5 = byte(254) // the WHOAREYOU packet ) // Discovery v5 packet structures. type ( // unknownV5 represents any packet that can't be decrypted. unknownV5 struct { AuthTag []byte } // WHOAREYOU contains the handshake challenge. whoareyouV5 struct { AuthTag []byte 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 Distance uint } // NODES is the reply to FINDNODE and TOPICQUERY. nodesV5 struct { ReqID []byte Total uint8 Nodes []*enr.Record } // 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. authSchemeName = "gcm" aesKeySize = 16 gcmNonceSize = 12 idNoncePrefix = "discovery-id-nonce" handshakeTimeout = time.Second ) var ( errTooShort = errors.New("packet too short") 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) ) // 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 } 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 } // newWireCodec creates a wire codec. func newWireCodec(ln *enode.LocalNode, key *ecdsa.PrivateKey, clock mclock.Clock) *wireCodec { c := &wireCodec{ sha256: sha256.New(), localnode: ln, privkey: key, 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[:])) return c } // 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) { 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 } // Ensure calling code sets node if needed. if challenge != nil && challenge.node == nil { panic("BUG: missing challenge.node in encode") } writeKey := c.sc.writeKey(id, addr) if writeKey != nil || challenge != nil { return c.encodeEncrypted(id, addr, packet, writeKey, challenge) } 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 } nonce := make([]byte, gcmNonceSize) if _, err := crand.Read(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 } // encodeWhoareyou encodes WHOAREYOU. 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 } // 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) } 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 } // Encode the packet. body := new(bytes.Buffer) body.WriteByte(packet.kind()) if err := rlp.Encode(body, 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 } // 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() } // 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 { return nil, nil, fmt.Errorf("can't find secp256k1 key for recipient") } ephkey, err := crypto.GenerateKey() if err != nil { return nil, nil, fmt.Errorf("can't generate ephemeral key") } ephpubkey := encodePubkey(&ephkey.PublicKey) // 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 // 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(c.sha256reset, 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 } // 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) } // decode decodes a discovery packet. func (c *wireCodec) decode(input []byte, addr string) (enode.ID, *enode.Node, packetV5, error) { // Delete timed-out handshakes. This must happen before decoding to avoid // processing the same handshake twice. c.sc.handshakeGC() if len(input) < 32 { return enode.ID{}, nil, nil, errTooShort } if bytes.HasPrefix(input, c.myWhoareyouMagic) { p, err := c.decodeWhoareyou(input) return enode.ID{}, nil, p, err } sender := xorTag(input[:32], c.myChtagHash) p, n, err := c.decodeEncrypted(sender, addr, input) return sender, n, p, err } // 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 } // 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 } // Decrypt and process auth response. readKey, node, err := c.decodeAuth(fromID, 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 } // 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 } // Remote is attempting handshake. Verify against our last WHOAREYOU. challenge := c.sc.getHandshake(fromID, fromAddr) if challenge == nil { return nil, nil, errUnexpectedHandshake } if head.IDNonce != challenge.IDNonce { return nil, nil, errHandshakeNonceMismatch } sec, n, err := c.decodeAuthResp(fromID, fromAddr, &head.authHeaderList, challenge) if err != nil { return nil, n, err } // 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 } // 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 } ephkey := head.ephemeralKey(c.privkey.Curve) if ephkey == nil { return nil, nil, errInvalidAuthKey } 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) } var resp authResponse if err := rlp.DecodeBytes(respPT, &resp); err != nil { return nil, nil, fmt.Errorf("invalid auth response: %v", err) } // 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) if err != nil { return nil, 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()) } node = n } } if node == nil { return nil, nil, errNoRecord } // Verify ID nonce signature. err = c.verifyIDSignature(challenge.IDNonce[:], head.EphemeralKey, resp.Signature, node) if err != nil { return nil, nil, err } return sec, node, 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) } } // 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_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 } // sha256reset returns the shared hash instance. func (c *wireCodec) sha256reset() hash.Hash { c.sha256.Reset() return c.sha256 } // sha256sum computes sha256 on the concatenation of inputs. func (c *wireCodec) sha256sum(inputs ...[]byte) []byte { c.sha256.Reset() for _, b := range inputs { c.sha256.Write(b) } 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()) 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) }