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:
Felix Lange
2020-10-14 12:28:17 +02:00
committed by GitHub
parent 4eb01b21c8
commit 524aaf5ec6
24 changed files with 3236 additions and 1427 deletions

View 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)
}

View 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
}

View 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
}

View 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
View 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 }

View 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)
}
}
}

View 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

View 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

View 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

View 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