p2p/discover: implement v5.1 wire protocol (#21647)
This change implements the Discovery v5.1 wire protocol and also adds an interactive test suite for this protocol.
This commit is contained in:
180
p2p/discover/v5wire/crypto.go
Normal file
180
p2p/discover/v5wire/crypto.go
Normal file
@@ -0,0 +1,180 @@
|
||||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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"
|
||||
"golang.org/x/crypto/hkdf"
|
||||
)
|
||||
|
||||
const (
|
||||
// Encryption/authentication parameters.
|
||||
aesKeySize = 16
|
||||
gcmNonceSize = 12
|
||||
)
|
||||
|
||||
// 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, challenge, ephkey []byte, destID enode.ID) []byte {
|
||||
h.Reset()
|
||||
h.Write([]byte("discovery v5 identity proof"))
|
||||
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, challenge, ephkey []byte, destID enode.ID) ([]byte, error) {
|
||||
input := idNonceHash(hash, challenge, ephkey, destID)
|
||||
switch key.Curve {
|
||||
case crypto.S256():
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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, 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, challenge, ephkey, destID)
|
||||
if !crypto.VerifySignature(pubkey, input, sig) {
|
||||
return errInvalidNonceSig
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("can't verify ID nonce signature against scheme %q", idscheme)
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
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. 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))
|
||||
}
|
||||
aesgcm, err := cipher.NewGCMWithNonceSize(block, gcmNonceSize)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("can't create GCM: %v", err))
|
||||
}
|
||||
return aesgcm.Seal(dest, nonce, plaintext, 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)
|
||||
}
|
124
p2p/discover/v5wire/crypto_test.go
Normal file
124
p2p/discover/v5wire/crypto_test.go
Normal file
@@ -0,0 +1,124 @@
|
||||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
package v5wire
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/sha256"
|
||||
"reflect"
|
||||
"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")
|
||||
cdata = hexutil.MustDecode("0x000000000000000000000000000000006469736376350001010102030405060708090a0b0c00180102030405060708090a0b0c0d0e0f100000000000000000")
|
||||
net = newHandshakeTest()
|
||||
)
|
||||
defer net.close()
|
||||
|
||||
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("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) {
|
||||
var (
|
||||
key = hexPrivkey("0xfb757dc581730490a1d7a00deea65e9b1936924caaea8f44d476014856b68736")
|
||||
destID = enode.HexID("0xbbbb9d047f0488c0b5a93c1c3f2d8bafc7c8ff337024a55434a0d0555de64db9")
|
||||
ephkey = hexutil.MustDecode("0x039961e4c2356d61bedb83052c115d311acb3a96f5777296dcf297351130266231")
|
||||
cdata = hexutil.MustDecode("0x000000000000000000000000000000006469736376350001010102030405060708090a0b0c00180102030405060708090a0b0c0d0e0f100000000000000000")
|
||||
)
|
||||
|
||||
sig, err := makeIDSignature(sha256.New(), key, cdata, ephkey, destID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("static-key = %#x", key.D)
|
||||
t.Logf("challenge-data = %#x", cdata)
|
||||
t.Logf("ephemeral-pubkey = %#x", ephkey)
|
||||
t.Logf("node-id-B = %#x", destID.Bytes())
|
||||
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) {
|
||||
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
|
||||
}
|
648
p2p/discover/v5wire/encoding.go
Normal file
648
p2p/discover/v5wire/encoding.go
Normal file
@@ -0,0 +1,648 @@
|
||||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
package v5wire
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/ecdsa"
|
||||
crand "crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
// TODO concurrent WHOAREYOU tie-breaker
|
||||
// TODO rehandshake after X packets
|
||||
|
||||
// Header represents a packet header.
|
||||
type Header struct {
|
||||
IV [sizeofMaskingIV]byte
|
||||
StaticHeader
|
||||
AuthData []byte
|
||||
|
||||
src enode.ID // used by decoder
|
||||
}
|
||||
|
||||
// StaticHeader contains the static fields of a packet header.
|
||||
type StaticHeader struct {
|
||||
ProtocolID [6]byte
|
||||
Version uint16
|
||||
Flag byte
|
||||
Nonce Nonce
|
||||
AuthSize uint16
|
||||
}
|
||||
|
||||
// Authdata layouts.
|
||||
type (
|
||||
whoareyouAuthData struct {
|
||||
IDNonce [16]byte // ID proof data
|
||||
RecordSeq uint64 // highest known ENR sequence of requester
|
||||
}
|
||||
|
||||
handshakeAuthData struct {
|
||||
h struct {
|
||||
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 = iota
|
||||
flagWhoareyou
|
||||
flagHandshake
|
||||
)
|
||||
|
||||
// Protocol constants.
|
||||
const (
|
||||
version = 1
|
||||
minVersion = 1
|
||||
sizeofMaskingIV = 16
|
||||
|
||||
minMessageSize = 48 // this refers to data after static headers
|
||||
randomPacketMsgSize = 20
|
||||
)
|
||||
|
||||
var protocolID = [6]byte{'d', 'i', 's', 'c', 'v', '5'}
|
||||
|
||||
// Errors.
|
||||
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")
|
||||
errInvalidNonceSig = errors.New("invalid ID nonce signature")
|
||||
errMessageTooShort = errors.New("message contains no data")
|
||||
errMessageDecrypt = errors.New("cannot decrypt message")
|
||||
)
|
||||
|
||||
// Public errors.
|
||||
var (
|
||||
ErrInvalidReqID = errors.New("request ID larger than 8 bytes")
|
||||
)
|
||||
|
||||
// Packet sizes.
|
||||
var (
|
||||
sizeofStaticHeader = binary.Size(StaticHeader{})
|
||||
sizeofWhoareyouAuthData = binary.Size(whoareyouAuthData{})
|
||||
sizeofHandshakeAuthData = binary.Size(handshakeAuthData{}.h)
|
||||
sizeofMessageAuthData = binary.Size(messageAuthData{})
|
||||
sizeofStaticPacketData = sizeofMaskingIV + sizeofStaticHeader
|
||||
)
|
||||
|
||||
// Codec encodes and decodes Discovery v5 packets.
|
||||
// This type is not safe for concurrent use.
|
||||
type Codec struct {
|
||||
sha256 hash.Hash
|
||||
localnode *enode.LocalNode
|
||||
privkey *ecdsa.PrivateKey
|
||||
sc *SessionCache
|
||||
|
||||
// encoder buffers
|
||||
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
|
||||
}
|
||||
|
||||
// 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),
|
||||
}
|
||||
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 *Codec) Encode(id enode.ID, addr string, packet Packet, challenge *Whoareyou) ([]byte, Nonce, error) {
|
||||
// Create the packet header.
|
||||
var (
|
||||
head Header
|
||||
session *session
|
||||
msgData []byte
|
||||
err error
|
||||
)
|
||||
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)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return nil, Nonce{}, err
|
||||
}
|
||||
|
||||
// Generate masking IV.
|
||||
if err := c.sc.maskingIVGen(head.IV[:]); err != nil {
|
||||
return nil, Nonce{}, fmt.Errorf("can't generate masking IV: %v", err)
|
||||
}
|
||||
|
||||
// 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:]
|
||||
mask := head.mask(id)
|
||||
mask.XORKeyStream(masked[:], masked[:])
|
||||
|
||||
// Write message data.
|
||||
c.buf.Write(msgdata)
|
||||
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
|
||||
switch flag {
|
||||
case flagMessage:
|
||||
authsize = sizeofMessageAuthData
|
||||
case flagWhoareyou:
|
||||
authsize = sizeofWhoareyouAuthData
|
||||
case flagHandshake:
|
||||
authsize = sizeofHandshakeAuthData
|
||||
default:
|
||||
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 Header{
|
||||
StaticHeader: StaticHeader{
|
||||
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)
|
||||
|
||||
// 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)
|
||||
}
|
||||
c.headbuf.Reset()
|
||||
binary.Write(&c.headbuf, binary.BigEndian, auth)
|
||||
head.AuthData = c.headbuf.Bytes()
|
||||
|
||||
// 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.
|
||||
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 whoareyou 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.headbuf.Reset()
|
||||
binary.Write(&c.headbuf, binary.BigEndian, auth)
|
||||
head.AuthData = c.headbuf.Bytes()
|
||||
return head, nil
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
// Generate new secrets.
|
||||
auth, session, err := c.makeHandshakeAuth(toID, addr, challenge)
|
||||
if err != nil {
|
||||
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)
|
||||
|
||||
// Encode the auth header.
|
||||
var (
|
||||
authsizeExtra = len(auth.pubkey) + len(auth.signature) + len(auth.record)
|
||||
head = c.makeHeader(toID, flagHandshake, authsizeExtra)
|
||||
)
|
||||
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
|
||||
}
|
||||
|
||||
// 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) {
|
||||
auth := new(handshakeAuthData)
|
||||
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.
|
||||
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 := c.sc.ephemeralKeyGen()
|
||||
if err != nil {
|
||||
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.
|
||||
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.
|
||||
ln := c.localnode.Node()
|
||||
if challenge.RecordSeq < ln.Seq() {
|
||||
auth.record, _ = rlp.EncodeToBytes(ln.Record())
|
||||
}
|
||||
|
||||
// Create session keys.
|
||||
sec := deriveKeys(sha256.New, ephkey, remotePubkey, c.localnode.ID(), challenge.Node.ID(), cdata)
|
||||
if sec == nil {
|
||||
return nil, nil, fmt.Errorf("key derivation failed")
|
||||
}
|
||||
return auth, sec, err
|
||||
}
|
||||
|
||||
// encodeMessage encodes an encrypted message packet.
|
||||
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{}, 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
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
// 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.
|
||||
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) < sizeofStaticPacketData {
|
||||
return enode.ID{}, nil, nil, errTooShort
|
||||
}
|
||||
var head Header
|
||||
copy(head.IV[:], input[:sizeofMaskingIV])
|
||||
mask := head.mask(c.localnode.ID())
|
||||
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.
|
||||
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.
|
||||
headerData := input[:authDataEnd]
|
||||
msgData := input[authDataEnd:]
|
||||
switch head.Flag {
|
||||
case flagWhoareyou:
|
||||
p, err = c.decodeWhoareyou(&head, headerData)
|
||||
case flagHandshake:
|
||||
n, p, err = c.decodeHandshakeMessage(addr, &head, headerData, msgData)
|
||||
case flagMessage:
|
||||
p, err = c.decodeMessage(addr, &head, headerData, msgData)
|
||||
default:
|
||||
err = errInvalidFlag
|
||||
}
|
||||
return head.src, n, p, err
|
||||
}
|
||||
|
||||
// decodeWhoareyou reads packet data after the header as a WHOAREYOU packet.
|
||||
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)
|
||||
binary.Read(&c.reader, binary.BigEndian, &auth)
|
||||
p := &Whoareyou{
|
||||
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, 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
|
||||
}
|
||||
|
||||
// Handshake OK, drop the challenge and store the new session keys.
|
||||
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) (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, auth, nil, errUnexpectedHandshake
|
||||
}
|
||||
// Get node record.
|
||||
n, err = c.decodeHandshakeRecord(challenge.Node, auth.h.SrcID, auth.record)
|
||||
if err != nil {
|
||||
return nil, auth, nil, err
|
||||
}
|
||||
// Verify ID nonce signature.
|
||||
sig := auth.signature
|
||||
cdata := challenge.ChallengeData
|
||||
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, auth, nil, errInvalidAuthKey
|
||||
}
|
||||
// Derive sesssion keys.
|
||||
session := deriveKeys(sha256.New, c.privkey, ephkey, auth.h.SrcID, c.localnode.ID(), cdata)
|
||||
session = session.keysFlipped()
|
||||
return n, auth, session, nil
|
||||
}
|
||||
|
||||
// decodeHandshakeAuthData reads the authdata section of a handshake packet.
|
||||
func (c *Codec) decodeHandshakeAuthData(head *Header) (auth handshakeAuthData, err error) {
|
||||
// Decode fixed size part.
|
||||
if len(head.AuthData) < sizeofHandshakeAuthData {
|
||||
return auth, fmt.Errorf("header authsize %d too low for handshake", head.AuthSize)
|
||||
}
|
||||
c.reader.Reset(head.AuthData)
|
||||
binary.Read(&c.reader, binary.BigEndian, &auth.h)
|
||||
head.src = auth.h.SrcID
|
||||
|
||||
// Decode variable-size part.
|
||||
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
|
||||
}
|
||||
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) (*enode.Node, 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, fmt.Errorf("invalid node record: %v", err)
|
||||
}
|
||||
if n.ID() != wantID {
|
||||
return nil, fmt.Errorf("record in handshake has wrong ID: %v", n.ID())
|
||||
}
|
||||
node = n
|
||||
}
|
||||
}
|
||||
if node == nil {
|
||||
return nil, errNoRecord
|
||||
}
|
||||
return node, nil
|
||||
}
|
||||
|
||||
// decodeMessage reads packet data following the header as an ordinary message packet.
|
||||
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)
|
||||
binary.Read(&c.reader, binary.BigEndian, &auth)
|
||||
head.src = auth.SrcID
|
||||
|
||||
// Try decrypting the message.
|
||||
key := c.sc.readKey(auth.SrcID, fromAddr)
|
||||
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
|
||||
}
|
||||
return msg, err
|
||||
}
|
||||
|
||||
func (c *Codec) decryptMessage(input, nonce, headerData, readKey []byte) (Packet, error) {
|
||||
msgdata, err := decryptGCM(readKey, nonce, input, headerData)
|
||||
if err != nil {
|
||||
return nil, errMessageDecrypt
|
||||
}
|
||||
if len(msgdata) == 0 {
|
||||
return nil, errMessageTooShort
|
||||
}
|
||||
return DecodeMessage(msgdata[0], msgdata[1:])
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if h.ProtocolID != protocolID {
|
||||
return errInvalidHeader
|
||||
}
|
||||
if h.Version < minVersion {
|
||||
return errMinVersion
|
||||
}
|
||||
if h.Flag != flagWhoareyou && packetLen < minMessageSize {
|
||||
return errMsgTooShort
|
||||
}
|
||||
if int(h.AuthSize) > packetLen {
|
||||
return errAuthSize
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// headerMask returns a cipher for 'masking' / 'unmasking' packet headers.
|
||||
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, h.IV[:])
|
||||
}
|
||||
|
||||
func bytesCopy(r *bytes.Buffer) []byte {
|
||||
b := make([]byte, r.Len())
|
||||
copy(b, r.Bytes())
|
||||
return b
|
||||
}
|
636
p2p/discover/v5wire/encoding_test.go
Normal file
636
p2p/discover/v5wire/encoding_test.go
Normal file
@@ -0,0 +1,636 @@
|
||||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
package v5wire
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ecdsa"
|
||||
"encoding/hex"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"os"
|
||||
"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")
|
||||
testEphKey, _ = crypto.HexToECDSA("0288ef00023598499cb6c940146d050d2b1fb914198c327f76aad590bead68b6")
|
||||
testIDnonce = [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}
|
||||
)
|
||||
|
||||
// This test checks that the minPacketSize and randomPacketMsgSize constants are well-defined.
|
||||
func TestMinSizes(t *testing.T) {
|
||||
var (
|
||||
gcmTagSize = 16
|
||||
emptyMsg = sizeofMessageAuthData + gcmTagSize
|
||||
)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// This test checks the basic handshake flow where A talks to B and A has no secrets.
|
||||
func TestHandshake(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 (handshake packet)
|
||||
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, &Nodes{Total: 1})
|
||||
net.nodeA.expectDecode(t, NodesMsg, nodes)
|
||||
}
|
||||
|
||||
// This test checks that handshake attempts are removed within the timeout.
|
||||
func TestHandshake_timeout(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 (handshake packet) after timeout
|
||||
net.clock.Run(handshakeTimeout + 1)
|
||||
findnode, _ := net.nodeA.encodeWithChallenge(t, net.nodeB, challenge, &Findnode{})
|
||||
net.nodeB.expectDecodeErr(t, errUnexpectedHandshake, findnode)
|
||||
}
|
||||
|
||||
// This test checks handshake behavior when no record is sent in the auth response.
|
||||
func TestHandshake_norecord(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
|
||||
nodeA := net.nodeA.n()
|
||||
if nodeA.Seq() == 0 {
|
||||
t.Fatal("need non-zero sequence number")
|
||||
}
|
||||
challenge := &Whoareyou{
|
||||
Nonce: resp.(*Unknown).Nonce,
|
||||
IDNonce: testIDnonce,
|
||||
RecordSeq: nodeA.Seq(),
|
||||
Node: nodeA,
|
||||
}
|
||||
whoareyou, _ := net.nodeB.encode(t, net.nodeA, challenge)
|
||||
net.nodeA.expectDecode(t, WhoareyouPacket, whoareyou)
|
||||
|
||||
// A -> B 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, &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
|
||||
// anything about A.
|
||||
func TestHandshake_rekey(t *testing.T) {
|
||||
t.Parallel()
|
||||
net := newHandshakeTest()
|
||||
defer net.close()
|
||||
|
||||
session := &session{
|
||||
readKey: []byte("BBBBBBBBBBBBBBBB"),
|
||||
writeKey: []byte("AAAAAAAAAAAAAAAA"),
|
||||
}
|
||||
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, &Findnode{})
|
||||
net.nodeB.expectDecode(t, UnknownPacket, findnode)
|
||||
|
||||
// A <- B WHOAREYOU
|
||||
challenge := &Whoareyou{Nonce: authTag, IDNonce: testIDnonce}
|
||||
whoareyou, _ := net.nodeB.encode(t, net.nodeA, challenge)
|
||||
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())
|
||||
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 {
|
||||
t.Fatal("node B stored keys too early")
|
||||
}
|
||||
|
||||
// A -> B FINDNODE encrypted with new keys
|
||||
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, &Nodes{Total: 1})
|
||||
net.nodeA.expectDecode(t, NodesMsg, nodes)
|
||||
}
|
||||
|
||||
// In this test A and B have different keys before the handshake.
|
||||
func TestHandshake_rekey2(t *testing.T) {
|
||||
t.Parallel()
|
||||
net := newHandshakeTest()
|
||||
defer net.close()
|
||||
|
||||
initKeysA := &session{
|
||||
readKey: []byte("BBBBBBBBBBBBBBBB"),
|
||||
writeKey: []byte("AAAAAAAAAAAAAAAA"),
|
||||
}
|
||||
initKeysB := &session{
|
||||
readKey: []byte("CCCCCCCCCCCCCCCC"),
|
||||
writeKey: []byte("DDDDDDDDDDDDDDDD"),
|
||||
}
|
||||
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, &Findnode{Distances: []uint{3}})
|
||||
net.nodeB.expectDecode(t, UnknownPacket, findnode)
|
||||
|
||||
// A <- B WHOAREYOU
|
||||
challenge := &Whoareyou{Nonce: authTag, IDNonce: testIDnonce}
|
||||
whoareyou, _ := net.nodeB.encode(t, net.nodeA, challenge)
|
||||
net.nodeA.expectDecode(t, WhoareyouPacket, whoareyou)
|
||||
|
||||
// A -> B FINDNODE (handshake packet)
|
||||
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, &Nodes{Total: 1})
|
||||
net.nodeA.expectDecode(t, NodesMsg, nodes)
|
||||
}
|
||||
|
||||
func TestHandshake_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()
|
||||
net := newHandshakeTest()
|
||||
defer net.close()
|
||||
|
||||
net.nodeA.expectDecodeErr(t, errTooShort, []byte{})
|
||||
// TODO some more tests would be nice :)
|
||||
// - check invalid authdata sizes
|
||||
// - 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"),
|
||||
}
|
||||
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()
|
||||
challenge0B.Node = net.nodeB.n()
|
||||
challenge1A.Node = net.nodeA.n()
|
||||
net.close()
|
||||
|
||||
type testVectorTest struct {
|
||||
name string // test vector name
|
||||
packet Packet // the packet to be encoded
|
||||
challenge *Whoareyou // handshake challenge passed to encoder
|
||||
prep func(*handshakeTest) // called before encode/decode
|
||||
}
|
||||
tests := []testVectorTest{
|
||||
{
|
||||
name: "v5.1-whoareyou",
|
||||
packet: &challenge0B,
|
||||
},
|
||||
{
|
||||
name: "v5.1-ping-message",
|
||||
packet: &Ping{
|
||||
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-enr",
|
||||
packet: &Ping{
|
||||
ReqID: []byte{0, 0, 0, 1},
|
||||
ENRSeq: 1,
|
||||
},
|
||||
challenge: &challenge0A,
|
||||
prep: func(net *handshakeTest) {
|
||||
// Update challenge.Header.AuthData.
|
||||
net.nodeA.c.Encode(idB, "", &challenge0A, nil)
|
||||
net.nodeB.c.sc.storeSentHandshake(idA, addr, &challenge0A)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "v5.1-ping-handshake",
|
||||
packet: &Ping{
|
||||
ReqID: []byte{0, 0, 0, 1},
|
||||
ENRSeq: 1,
|
||||
},
|
||||
challenge: &challenge1A,
|
||||
prep: func(net *handshakeTest) {
|
||||
// Update challenge data.
|
||||
net.nodeA.c.Encode(idB, "", &challenge1A, nil)
|
||||
net.nodeB.c.sc.storeSentHandshake(idA, addr, &challenge1A)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
test := test
|
||||
t.Run(test.name, func(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 {
|
||||
// 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)
|
||||
}
|
||||
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 Packet, challenge *Whoareyou, nonce Nonce) string {
|
||||
o := new(strings.Builder)
|
||||
printWhoareyou := func(p *Whoareyou) {
|
||||
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)
|
||||
}
|
||||
|
||||
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 *Whoareyou:
|
||||
// WHOAREYOU packet.
|
||||
printWhoareyou(p)
|
||||
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)
|
||||
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)
|
||||
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))
|
||||
}
|
||||
return o.String()
|
||||
}
|
||||
|
||||
// This benchmark checks performance of handshake packet decoding.
|
||||
func BenchmarkV5_DecodeHandshakePingSecp256k1(b *testing.B) {
|
||||
net := newHandshakeTest()
|
||||
defer net.close()
|
||||
|
||||
var (
|
||||
idA = net.nodeA.id()
|
||||
challenge = &Whoareyou{Node: net.nodeB.n()}
|
||||
message = &Ping{ReqID: []byte("reqid")}
|
||||
)
|
||||
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(input, "")
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This benchmark checks how long it takes to decode an encrypted ping packet.
|
||||
func BenchmarkV5_DecodePing(b *testing.B) {
|
||||
net := newHandshakeTest()
|
||||
defer net.close()
|
||||
|
||||
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 := &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)
|
||||
}
|
||||
b.ResetTimer()
|
||||
|
||||
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.(*Ping); !ok {
|
||||
b.Fatalf("wrong packet type %T", packet)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var pp = spew.NewDefaultConfig()
|
||||
|
||||
type handshakeTest struct {
|
||||
nodeA, nodeB handshakeTestNode
|
||||
clock mclock.Simulated
|
||||
}
|
||||
|
||||
type handshakeTestNode struct {
|
||||
ln *enode.LocalNode
|
||||
c *Codec
|
||||
}
|
||||
|
||||
func newHandshakeTest() *handshakeTest {
|
||||
t := new(handshakeTest)
|
||||
t.nodeA.init(testKeyA, net.IP{127, 0, 0, 1}, &t.clock)
|
||||
t.nodeB.init(testKeyB, net.IP{127, 0, 0, 1}, &t.clock)
|
||||
return t
|
||||
}
|
||||
|
||||
func (t *handshakeTest) close() {
|
||||
t.nodeA.ln.Database().Close()
|
||||
t.nodeB.ln.Database().Close()
|
||||
}
|
||||
|
||||
func (n *handshakeTestNode) init(key *ecdsa.PrivateKey, ip net.IP, clock mclock.Clock) {
|
||||
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 = NewCodec(n.ln, key, clock)
|
||||
}
|
||||
|
||||
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 *Whoareyou, p Packet) ([]byte, Nonce) {
|
||||
t.Helper()
|
||||
|
||||
// Copy challenge and add destination node. This avoids sharing 'c' among the two codecs.
|
||||
var challenge *Whoareyou
|
||||
if c != nil {
|
||||
challengeCopy := *c
|
||||
challenge = &challengeCopy
|
||||
challenge.Node = to.n()
|
||||
}
|
||||
// Encode to destination.
|
||||
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, nonce
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
return dec
|
||||
}
|
||||
|
||||
func (n *handshakeTestNode) expectDecodeErr(t *testing.T, wantErr error, p []byte) {
|
||||
t.Helper()
|
||||
if _, err := n.decode(p); !reflect.DeepEqual(err, wantErr) {
|
||||
t.Fatal(fmt.Errorf("(%s) got err %q, want %q", n.ln.ID().TerminalString(), err, wantErr))
|
||||
}
|
||||
}
|
||||
|
||||
func (n *handshakeTestNode) decode(input []byte) (Packet, error) {
|
||||
_, _, p, err := n.c.Decode(input, "127.0.0.1")
|
||||
return p, err
|
||||
}
|
||||
|
||||
func (n *handshakeTestNode) n() *enode.Node {
|
||||
return n.ln.Node()
|
||||
}
|
||||
|
||||
func (n *handshakeTestNode) addr() string {
|
||||
return n.ln.Node().IP().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)
|
||||
}
|
||||
}
|
249
p2p/discover/v5wire/msg.go
Normal file
249
p2p/discover/v5wire/msg.go
Normal file
@@ -0,0 +1,249 @@
|
||||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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 {
|
||||
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.
|
||||
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 {
|
||||
Nonce Nonce
|
||||
}
|
||||
|
||||
// WHOAREYOU contains the handshake challenge.
|
||||
Whoareyou struct {
|
||||
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.
|
||||
Node *enode.Node
|
||||
|
||||
sent mclock.AbsTime // for handshake GC.
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
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) 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 }
|
142
p2p/discover/v5wire/session.go
Normal file
142
p2p/discover/v5wire/session.go
Normal file
@@ -0,0 +1,142 @@
|
||||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
package v5wire
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
crand "crypto/rand"
|
||||
"encoding/binary"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
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 {
|
||||
sessions *simplelru.LRU
|
||||
handshakes map[sessionID]*Whoareyou
|
||||
clock mclock.Clock
|
||||
|
||||
// hooks for overriding randomness.
|
||||
nonceGen func(uint32) (Nonce, error)
|
||||
maskingIVGen func([]byte) error
|
||||
ephemeralKeyGen func() (*ecdsa.PrivateKey, error)
|
||||
}
|
||||
|
||||
// sessionID identifies a session or handshake.
|
||||
type sessionID struct {
|
||||
id enode.ID
|
||||
addr string
|
||||
}
|
||||
|
||||
// session contains session information
|
||||
type session struct {
|
||||
writeKey []byte
|
||||
readKey []byte
|
||||
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 {
|
||||
panic("can't create session cache")
|
||||
}
|
||||
return &SessionCache{
|
||||
sessions: cache,
|
||||
handshakes: make(map[sessionID]*Whoareyou),
|
||||
clock: clock,
|
||||
nonceGen: generateNonce,
|
||||
maskingIVGen: generateMaskingIV,
|
||||
ephemeralKeyGen: crypto.GenerateKey,
|
||||
}
|
||||
}
|
||||
|
||||
func generateNonce(counter uint32) (n Nonce, 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) (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 {
|
||||
item, ok := sc.sessions.Get(sessionID{id, addr})
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return item.(*session)
|
||||
}
|
||||
|
||||
// readKey returns the current read key for the given node.
|
||||
func (sc *SessionCache) readKey(id enode.ID, addr string) []byte {
|
||||
if s := sc.session(id, addr); s != nil {
|
||||
return s.readKey
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// storeNewSession stores new encryption keys in the cache.
|
||||
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) *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 *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) {
|
||||
delete(sc.handshakes, sessionID{id, addr})
|
||||
}
|
||||
|
||||
// handshakeGC deletes timed-out handshakes.
|
||||
func (sc *SessionCache) handshakeGC() {
|
||||
deadline := sc.clock.Now().Add(-handshakeTimeout)
|
||||
for key, challenge := range sc.handshakes {
|
||||
if challenge.sent < deadline {
|
||||
delete(sc.handshakes, key)
|
||||
}
|
||||
}
|
||||
}
|
27
p2p/discover/v5wire/testdata/v5.1-ping-handshake-enr.txt
vendored
Normal file
27
p2p/discover/v5wire/testdata/v5.1-ping-handshake-enr.txt
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
# src-node-id = 0xaaaa8419e9f49d0083561b48287df592939a8d19947d8c0ef88f2a4856a69fbb
|
||||
# dest-node-id = 0xbbbb9d047f0488c0b5a93c1c3f2d8bafc7c8ff337024a55434a0d0555de64db9
|
||||
# nonce = 0xffffffffffffffffffffffff
|
||||
# read-key = 0x53b1c075f41876423154e157470c2f48
|
||||
# ping.req-id = 0x00000001
|
||||
# ping.enr-seq = 1
|
||||
#
|
||||
# handshake inputs:
|
||||
#
|
||||
# whoareyou.challenge-data = 0x000000000000000000000000000000006469736376350001010102030405060708090a0b0c00180102030405060708090a0b0c0d0e0f100000000000000000
|
||||
# whoareyou.request-nonce = 0x0102030405060708090a0b0c
|
||||
# whoareyou.id-nonce = 0x0102030405060708090a0b0c0d0e0f10
|
||||
# whoareyou.enr-seq = 0
|
||||
# ephemeral-key = 0x0288ef00023598499cb6c940146d050d2b1fb914198c327f76aad590bead68b6
|
||||
# ephemeral-pubkey = 0x039a003ba6517b473fa0cd74aefe99dadfdb34627f90fec6362df85803908f53a5
|
||||
|
||||
00000000000000000000000000000000088b3d4342774649305f313964a39e55
|
||||
ea96c005ad539c8c7560413a7008f16c9e6d2f43bbea8814a546b7409ce783d3
|
||||
4c4f53245d08da4bb23698868350aaad22e3ab8dd034f548a1c43cd246be9856
|
||||
2fafa0a1fa86d8e7a3b95ae78cc2b988ded6a5b59eb83ad58097252188b902b2
|
||||
1481e30e5e285f19735796706adff216ab862a9186875f9494150c4ae06fa4d1
|
||||
f0396c93f215fa4ef524e0ed04c3c21e39b1868e1ca8105e585ec17315e755e6
|
||||
cfc4dd6cb7fd8e1a1f55e49b4b5eb024221482105346f3c82b15fdaae36a3bb1
|
||||
2a494683b4a3c7f2ae41306252fed84785e2bbff3b022812d0882f06978df84a
|
||||
80d443972213342d04b9048fc3b1d5fcb1df0f822152eced6da4d3f6df27e70e
|
||||
4539717307a0208cd208d65093ccab5aa596a34d7511401987662d8cf62b1394
|
||||
71
|
23
p2p/discover/v5wire/testdata/v5.1-ping-handshake.txt
vendored
Normal file
23
p2p/discover/v5wire/testdata/v5.1-ping-handshake.txt
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
# src-node-id = 0xaaaa8419e9f49d0083561b48287df592939a8d19947d8c0ef88f2a4856a69fbb
|
||||
# dest-node-id = 0xbbbb9d047f0488c0b5a93c1c3f2d8bafc7c8ff337024a55434a0d0555de64db9
|
||||
# nonce = 0xffffffffffffffffffffffff
|
||||
# read-key = 0x4f9fac6de7567d1e3b1241dffe90f662
|
||||
# ping.req-id = 0x00000001
|
||||
# ping.enr-seq = 1
|
||||
#
|
||||
# handshake inputs:
|
||||
#
|
||||
# whoareyou.challenge-data = 0x000000000000000000000000000000006469736376350001010102030405060708090a0b0c00180102030405060708090a0b0c0d0e0f100000000000000001
|
||||
# whoareyou.request-nonce = 0x0102030405060708090a0b0c
|
||||
# whoareyou.id-nonce = 0x0102030405060708090a0b0c0d0e0f10
|
||||
# whoareyou.enr-seq = 1
|
||||
# ephemeral-key = 0x0288ef00023598499cb6c940146d050d2b1fb914198c327f76aad590bead68b6
|
||||
# ephemeral-pubkey = 0x039a003ba6517b473fa0cd74aefe99dadfdb34627f90fec6362df85803908f53a5
|
||||
|
||||
00000000000000000000000000000000088b3d4342774649305f313964a39e55
|
||||
ea96c005ad521d8c7560413a7008f16c9e6d2f43bbea8814a546b7409ce783d3
|
||||
4c4f53245d08da4bb252012b2cba3f4f374a90a75cff91f142fa9be3e0a5f3ef
|
||||
268ccb9065aeecfd67a999e7fdc137e062b2ec4a0eb92947f0d9a74bfbf44dfb
|
||||
a776b21301f8b65efd5796706adff216ab862a9186875f9494150c4ae06fa4d1
|
||||
f0396c93f215fa4ef524f1eadf5f0f4126b79336671cbcf7a885b1f8bd2a5d83
|
||||
9cf8
|
10
p2p/discover/v5wire/testdata/v5.1-ping-message.txt
vendored
Normal file
10
p2p/discover/v5wire/testdata/v5.1-ping-message.txt
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
# src-node-id = 0xaaaa8419e9f49d0083561b48287df592939a8d19947d8c0ef88f2a4856a69fbb
|
||||
# dest-node-id = 0xbbbb9d047f0488c0b5a93c1c3f2d8bafc7c8ff337024a55434a0d0555de64db9
|
||||
# nonce = 0xffffffffffffffffffffffff
|
||||
# read-key = 0x00000000000000000000000000000000
|
||||
# ping.req-id = 0x00000001
|
||||
# ping.enr-seq = 2
|
||||
|
||||
00000000000000000000000000000000088b3d4342774649325f313964a39e55
|
||||
ea96c005ad52be8c7560413a7008f16c9e6d2f43bbea8814a546b7409ce783d3
|
||||
4c4f53245d08dab84102ed931f66d1492acb308fa1c6715b9d139b81acbdcc
|
9
p2p/discover/v5wire/testdata/v5.1-whoareyou.txt
vendored
Normal file
9
p2p/discover/v5wire/testdata/v5.1-whoareyou.txt
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
# src-node-id = 0xaaaa8419e9f49d0083561b48287df592939a8d19947d8c0ef88f2a4856a69fbb
|
||||
# dest-node-id = 0xbbbb9d047f0488c0b5a93c1c3f2d8bafc7c8ff337024a55434a0d0555de64db9
|
||||
# whoareyou.challenge-data = 0x000000000000000000000000000000006469736376350001010102030405060708090a0b0c00180102030405060708090a0b0c0d0e0f100000000000000000
|
||||
# whoareyou.request-nonce = 0x0102030405060708090a0b0c
|
||||
# whoareyou.id-nonce = 0x0102030405060708090a0b0c0d0e0f10
|
||||
# whoareyou.enr-seq = 0
|
||||
|
||||
00000000000000000000000000000000088b3d434277464933a1ccc59f5967ad
|
||||
1d6035f15e528627dde75cd68292f9e6c27d6b66c8100a873fcbaed4e16b8d
|
Reference in New Issue
Block a user