mirror of https://github.com/ethereum/go-ethereum
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.pull/21710/head
parent
4eb01b21c8
commit
524aaf5ec6
@ -0,0 +1,86 @@ |
|||||||
|
# The devp2p command |
||||||
|
|
||||||
|
The devp2p command line tool is a utility for low-level peer-to-peer debugging and |
||||||
|
protocol development purposes. It can do many things. |
||||||
|
|
||||||
|
### ENR Decoding |
||||||
|
|
||||||
|
Use `devp2p enrdump <base64>` to verify and display an Ethereum Node Record. |
||||||
|
|
||||||
|
### Node Key Management |
||||||
|
|
||||||
|
The `devp2p key ...` command family deals with node key files. |
||||||
|
|
||||||
|
Run `devp2p key generate mynode.key` to create a new node key in the `mynode.key` file. |
||||||
|
|
||||||
|
Run `devp2p key to-enode mynode.key -ip 127.0.0.1 -tcp 30303` to create an enode:// URL |
||||||
|
corresponding to the given node key and address information. |
||||||
|
|
||||||
|
### Maintaining DNS Discovery Node Lists |
||||||
|
|
||||||
|
The devp2p command can create and publish DNS discovery node lists. |
||||||
|
|
||||||
|
Run `devp2p dns sign <directory>` to update the signature of a DNS discovery tree. |
||||||
|
|
||||||
|
Run `devp2p dns sync <enrtree-URL>` to download a complete DNS discovery tree. |
||||||
|
|
||||||
|
Run `devp2p dns to-cloudflare <directory>` to publish a tree to CloudFlare DNS. |
||||||
|
|
||||||
|
Run `devp2p dns to-route53 <directory>` to publish a tree to Amazon Route53. |
||||||
|
|
||||||
|
You can find more information about these commands in the [DNS Discovery Setup Guide][dns-tutorial]. |
||||||
|
|
||||||
|
### Discovery v4 Utilities |
||||||
|
|
||||||
|
The `devp2p discv4 ...` command family deals with the [Node Discovery v4][discv4] |
||||||
|
protocol. |
||||||
|
|
||||||
|
Run `devp2p discv4 ping <enode/ENR>` to ping a node. |
||||||
|
|
||||||
|
Run `devp2p discv4 resolve <enode/ENR>` to find the most recent node record of a node in |
||||||
|
the DHT. |
||||||
|
|
||||||
|
Run `devp2p discv4 crawl <nodes.json path>` to create or update a JSON node set. |
||||||
|
|
||||||
|
### Discovery v5 Utilities |
||||||
|
|
||||||
|
The `devp2p discv5 ...` command family deals with the [Node Discovery v5][discv5] |
||||||
|
protocol. This protocol is currently under active development. |
||||||
|
|
||||||
|
Run `devp2p discv5 ping <ENR>` to ping a node. |
||||||
|
|
||||||
|
Run `devp2p discv5 resolve <ENR>` to find the most recent node record of a node in |
||||||
|
the discv5 DHT. |
||||||
|
|
||||||
|
Run `devp2p discv5 listen` to run a Discovery v5 node. |
||||||
|
|
||||||
|
Run `devp2p discv5 crawl <nodes.json path>` to create or update a JSON node set containing |
||||||
|
discv5 nodes. |
||||||
|
|
||||||
|
### Discovery Test Suites |
||||||
|
|
||||||
|
The devp2p command also contains interactive test suites for Discovery v4 and Discovery |
||||||
|
v5. |
||||||
|
|
||||||
|
To run these tests against your implementation, you need to set up a networking |
||||||
|
environment where two separate UDP listening addresses are available on the same machine. |
||||||
|
The two listening addresses must also be routed such that they are able to reach the node |
||||||
|
you want to test. |
||||||
|
|
||||||
|
For example, if you want to run the test on your local host, and the node under test is |
||||||
|
also on the local host, you need to assign two IP addresses (or a larger range) to your |
||||||
|
loopback interface. On macOS, this can be done by executing the following command: |
||||||
|
|
||||||
|
sudo ifconfig lo0 add 127.0.0.2 |
||||||
|
|
||||||
|
You can now run either test suite as follows: Start the node under test first, ensuring |
||||||
|
that it won't talk to the Internet (i.e. disable bootstrapping). An easy way to prevent |
||||||
|
unintended connections to the global DHT is listening on `127.0.0.1`. |
||||||
|
|
||||||
|
Now get the ENR of your node and store it in the `NODE` environment variable. |
||||||
|
|
||||||
|
Start the test by running `devp2p discv5 test -listen1 127.0.0.1 -listen2 127.0.0.2 $NODE`. |
||||||
|
|
||||||
|
[dns-tutorial]: https://geth.ethereum.org/docs/developers/dns-discovery-setup |
||||||
|
[discv4]: https://github.com/ethereum/devp2p/tree/master/discv4.md |
||||||
|
[discv5]: https://github.com/ethereum/devp2p/tree/master/discv5/discv5.md |
@ -0,0 +1,377 @@ |
|||||||
|
// Copyright 2020 The go-ethereum Authors
|
||||||
|
// This file is part of go-ethereum.
|
||||||
|
//
|
||||||
|
// go-ethereum is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// go-ethereum is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package v5test |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"net" |
||||||
|
"sync" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/internal/utesting" |
||||||
|
"github.com/ethereum/go-ethereum/p2p/discover/v5wire" |
||||||
|
"github.com/ethereum/go-ethereum/p2p/enode" |
||||||
|
"github.com/ethereum/go-ethereum/p2p/netutil" |
||||||
|
) |
||||||
|
|
||||||
|
// Suite is the discv5 test suite.
|
||||||
|
type Suite struct { |
||||||
|
Dest *enode.Node |
||||||
|
Listen1, Listen2 string // listening addresses
|
||||||
|
} |
||||||
|
|
||||||
|
func (s *Suite) listen1(log logger) (*conn, net.PacketConn) { |
||||||
|
c := newConn(s.Dest, log) |
||||||
|
l := c.listen(s.Listen1) |
||||||
|
return c, l |
||||||
|
} |
||||||
|
|
||||||
|
func (s *Suite) listen2(log logger) (*conn, net.PacketConn, net.PacketConn) { |
||||||
|
c := newConn(s.Dest, log) |
||||||
|
l1, l2 := c.listen(s.Listen1), c.listen(s.Listen2) |
||||||
|
return c, l1, l2 |
||||||
|
} |
||||||
|
|
||||||
|
func (s *Suite) AllTests() []utesting.Test { |
||||||
|
return []utesting.Test{ |
||||||
|
{Name: "Ping", Fn: s.TestPing}, |
||||||
|
{Name: "PingLargeRequestID", Fn: s.TestPingLargeRequestID}, |
||||||
|
{Name: "PingMultiIP", Fn: s.TestPingMultiIP}, |
||||||
|
{Name: "PingHandshakeInterrupted", Fn: s.TestPingHandshakeInterrupted}, |
||||||
|
{Name: "TalkRequest", Fn: s.TestTalkRequest}, |
||||||
|
{Name: "FindnodeZeroDistance", Fn: s.TestFindnodeZeroDistance}, |
||||||
|
{Name: "FindnodeResults", Fn: s.TestFindnodeResults}, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// This test sends PING and expects a PONG response.
|
||||||
|
func (s *Suite) TestPing(t *utesting.T) { |
||||||
|
conn, l1 := s.listen1(t) |
||||||
|
defer conn.close() |
||||||
|
|
||||||
|
ping := &v5wire.Ping{ReqID: conn.nextReqID()} |
||||||
|
switch resp := conn.reqresp(l1, ping).(type) { |
||||||
|
case *v5wire.Pong: |
||||||
|
checkPong(t, resp, ping, l1) |
||||||
|
default: |
||||||
|
t.Fatal("expected PONG, got", resp.Name()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func checkPong(t *utesting.T, pong *v5wire.Pong, ping *v5wire.Ping, c net.PacketConn) { |
||||||
|
if !bytes.Equal(pong.ReqID, ping.ReqID) { |
||||||
|
t.Fatalf("wrong request ID %x in PONG, want %x", pong.ReqID, ping.ReqID) |
||||||
|
} |
||||||
|
if !pong.ToIP.Equal(laddr(c).IP) { |
||||||
|
t.Fatalf("wrong destination IP %v in PONG, want %v", pong.ToIP, laddr(c).IP) |
||||||
|
} |
||||||
|
if int(pong.ToPort) != laddr(c).Port { |
||||||
|
t.Fatalf("wrong destination port %v in PONG, want %v", pong.ToPort, laddr(c).Port) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// This test sends PING with a 9-byte request ID, which isn't allowed by the spec.
|
||||||
|
// The remote node should not respond.
|
||||||
|
func (s *Suite) TestPingLargeRequestID(t *utesting.T) { |
||||||
|
conn, l1 := s.listen1(t) |
||||||
|
defer conn.close() |
||||||
|
|
||||||
|
ping := &v5wire.Ping{ReqID: make([]byte, 9)} |
||||||
|
switch resp := conn.reqresp(l1, ping).(type) { |
||||||
|
case *v5wire.Pong: |
||||||
|
t.Errorf("PONG response with unknown request ID %x", resp.ReqID) |
||||||
|
case *readError: |
||||||
|
if resp.err == v5wire.ErrInvalidReqID { |
||||||
|
t.Error("response with oversized request ID") |
||||||
|
} else if !netutil.IsTimeout(resp.err) { |
||||||
|
t.Error(resp) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// In this test, a session is established from one IP as usual. The session is then reused
|
||||||
|
// on another IP, which shouldn't work. The remote node should respond with WHOAREYOU for
|
||||||
|
// the attempt from a different IP.
|
||||||
|
func (s *Suite) TestPingMultiIP(t *utesting.T) { |
||||||
|
conn, l1, l2 := s.listen2(t) |
||||||
|
defer conn.close() |
||||||
|
|
||||||
|
// Create the session on l1.
|
||||||
|
ping := &v5wire.Ping{ReqID: conn.nextReqID()} |
||||||
|
resp := conn.reqresp(l1, ping) |
||||||
|
if resp.Kind() != v5wire.PongMsg { |
||||||
|
t.Fatal("expected PONG, got", resp) |
||||||
|
} |
||||||
|
checkPong(t, resp.(*v5wire.Pong), ping, l1) |
||||||
|
|
||||||
|
// Send on l2. This reuses the session because there is only one codec.
|
||||||
|
ping2 := &v5wire.Ping{ReqID: conn.nextReqID()} |
||||||
|
conn.write(l2, ping2, nil) |
||||||
|
switch resp := conn.read(l2).(type) { |
||||||
|
case *v5wire.Pong: |
||||||
|
t.Fatalf("remote responded to PING from %v for session on IP %v", laddr(l2).IP, laddr(l1).IP) |
||||||
|
case *v5wire.Whoareyou: |
||||||
|
t.Logf("got WHOAREYOU for new session as expected") |
||||||
|
resp.Node = s.Dest |
||||||
|
conn.write(l2, ping2, resp) |
||||||
|
default: |
||||||
|
t.Fatal("expected WHOAREYOU, got", resp) |
||||||
|
} |
||||||
|
|
||||||
|
// Catch the PONG on l2.
|
||||||
|
switch resp := conn.read(l2).(type) { |
||||||
|
case *v5wire.Pong: |
||||||
|
checkPong(t, resp, ping2, l2) |
||||||
|
default: |
||||||
|
t.Fatal("expected PONG, got", resp) |
||||||
|
} |
||||||
|
|
||||||
|
// Try on l1 again.
|
||||||
|
ping3 := &v5wire.Ping{ReqID: conn.nextReqID()} |
||||||
|
conn.write(l1, ping3, nil) |
||||||
|
switch resp := conn.read(l1).(type) { |
||||||
|
case *v5wire.Pong: |
||||||
|
t.Fatalf("remote responded to PING from %v for session on IP %v", laddr(l1).IP, laddr(l2).IP) |
||||||
|
case *v5wire.Whoareyou: |
||||||
|
t.Logf("got WHOAREYOU for new session as expected") |
||||||
|
default: |
||||||
|
t.Fatal("expected WHOAREYOU, got", resp) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// This test starts a handshake, but doesn't finish it and sends a second ordinary message
|
||||||
|
// packet instead of a handshake message packet. The remote node should respond with
|
||||||
|
// another WHOAREYOU challenge for the second packet.
|
||||||
|
func (s *Suite) TestPingHandshakeInterrupted(t *utesting.T) { |
||||||
|
conn, l1 := s.listen1(t) |
||||||
|
defer conn.close() |
||||||
|
|
||||||
|
// First PING triggers challenge.
|
||||||
|
ping := &v5wire.Ping{ReqID: conn.nextReqID()} |
||||||
|
conn.write(l1, ping, nil) |
||||||
|
switch resp := conn.read(l1).(type) { |
||||||
|
case *v5wire.Whoareyou: |
||||||
|
t.Logf("got WHOAREYOU for PING") |
||||||
|
default: |
||||||
|
t.Fatal("expected WHOAREYOU, got", resp) |
||||||
|
} |
||||||
|
|
||||||
|
// Send second PING.
|
||||||
|
ping2 := &v5wire.Ping{ReqID: conn.nextReqID()} |
||||||
|
switch resp := conn.reqresp(l1, ping2).(type) { |
||||||
|
case *v5wire.Pong: |
||||||
|
checkPong(t, resp, ping2, l1) |
||||||
|
default: |
||||||
|
t.Fatal("expected WHOAREYOU, got", resp) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// This test sends TALKREQ and expects an empty TALKRESP response.
|
||||||
|
func (s *Suite) TestTalkRequest(t *utesting.T) { |
||||||
|
conn, l1 := s.listen1(t) |
||||||
|
defer conn.close() |
||||||
|
|
||||||
|
// Non-empty request ID.
|
||||||
|
id := conn.nextReqID() |
||||||
|
resp := conn.reqresp(l1, &v5wire.TalkRequest{ReqID: id, Protocol: "test-protocol"}) |
||||||
|
switch resp := resp.(type) { |
||||||
|
case *v5wire.TalkResponse: |
||||||
|
if !bytes.Equal(resp.ReqID, id) { |
||||||
|
t.Fatalf("wrong request ID %x in TALKRESP, want %x", resp.ReqID, id) |
||||||
|
} |
||||||
|
if len(resp.Message) > 0 { |
||||||
|
t.Fatalf("non-empty message %x in TALKRESP", resp.Message) |
||||||
|
} |
||||||
|
default: |
||||||
|
t.Fatal("expected TALKRESP, got", resp.Name()) |
||||||
|
} |
||||||
|
|
||||||
|
// Empty request ID.
|
||||||
|
resp = conn.reqresp(l1, &v5wire.TalkRequest{Protocol: "test-protocol"}) |
||||||
|
switch resp := resp.(type) { |
||||||
|
case *v5wire.TalkResponse: |
||||||
|
if len(resp.ReqID) > 0 { |
||||||
|
t.Fatalf("wrong request ID %x in TALKRESP, want empty byte array", resp.ReqID) |
||||||
|
} |
||||||
|
if len(resp.Message) > 0 { |
||||||
|
t.Fatalf("non-empty message %x in TALKRESP", resp.Message) |
||||||
|
} |
||||||
|
default: |
||||||
|
t.Fatal("expected TALKRESP, got", resp.Name()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// This test checks that the remote node returns itself for FINDNODE with distance zero.
|
||||||
|
func (s *Suite) TestFindnodeZeroDistance(t *utesting.T) { |
||||||
|
conn, l1 := s.listen1(t) |
||||||
|
defer conn.close() |
||||||
|
|
||||||
|
nodes, err := conn.findnode(l1, []uint{0}) |
||||||
|
if err != nil { |
||||||
|
t.Fatal(err) |
||||||
|
} |
||||||
|
if len(nodes) != 1 { |
||||||
|
t.Fatalf("remote returned more than one node for FINDNODE [0]") |
||||||
|
} |
||||||
|
if nodes[0].ID() != conn.remote.ID() { |
||||||
|
t.Errorf("ID of response node is %v, want %v", nodes[0].ID(), conn.remote.ID()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// In this test, multiple nodes ping the node under test. After waiting for them to be
|
||||||
|
// accepted into the remote table, the test checks that they are returned by FINDNODE.
|
||||||
|
func (s *Suite) TestFindnodeResults(t *utesting.T) { |
||||||
|
// Create bystanders.
|
||||||
|
nodes := make([]*bystander, 5) |
||||||
|
added := make(chan enode.ID, len(nodes)) |
||||||
|
for i := range nodes { |
||||||
|
nodes[i] = newBystander(t, s, added) |
||||||
|
defer nodes[i].close() |
||||||
|
} |
||||||
|
|
||||||
|
// Get them added to the remote table.
|
||||||
|
timeout := 60 * time.Second |
||||||
|
timeoutCh := time.After(timeout) |
||||||
|
for count := 0; count < len(nodes); { |
||||||
|
select { |
||||||
|
case id := <-added: |
||||||
|
t.Logf("bystander node %v added to remote table", id) |
||||||
|
count++ |
||||||
|
case <-timeoutCh: |
||||||
|
t.Errorf("remote added %d bystander nodes in %v, need %d to continue", count, timeout, len(nodes)) |
||||||
|
t.Logf("this can happen if the node has a non-empty table from previous runs") |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
t.Logf("all %d bystander nodes were added", len(nodes)) |
||||||
|
|
||||||
|
// Collect our nodes by distance.
|
||||||
|
var dists []uint |
||||||
|
expect := make(map[enode.ID]*enode.Node) |
||||||
|
for _, bn := range nodes { |
||||||
|
n := bn.conn.localNode.Node() |
||||||
|
expect[n.ID()] = n |
||||||
|
d := uint(enode.LogDist(n.ID(), s.Dest.ID())) |
||||||
|
if !containsUint(dists, d) { |
||||||
|
dists = append(dists, d) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Send FINDNODE for all distances.
|
||||||
|
conn, l1 := s.listen1(t) |
||||||
|
defer conn.close() |
||||||
|
foundNodes, err := conn.findnode(l1, dists) |
||||||
|
if err != nil { |
||||||
|
t.Fatal(err) |
||||||
|
} |
||||||
|
t.Logf("remote returned %d nodes for distance list %v", len(foundNodes), dists) |
||||||
|
for _, n := range foundNodes { |
||||||
|
delete(expect, n.ID()) |
||||||
|
} |
||||||
|
if len(expect) > 0 { |
||||||
|
t.Errorf("missing %d nodes in FINDNODE result", len(expect)) |
||||||
|
t.Logf("this can happen if the test is run multiple times in quick succession") |
||||||
|
t.Logf("and the remote node hasn't removed dead nodes from previous runs yet") |
||||||
|
} else { |
||||||
|
t.Logf("all %d expected nodes were returned", len(nodes)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// A bystander is a node whose only purpose is filling a spot in the remote table.
|
||||||
|
type bystander struct { |
||||||
|
dest *enode.Node |
||||||
|
conn *conn |
||||||
|
l net.PacketConn |
||||||
|
|
||||||
|
addedCh chan enode.ID |
||||||
|
done sync.WaitGroup |
||||||
|
} |
||||||
|
|
||||||
|
func newBystander(t *utesting.T, s *Suite, added chan enode.ID) *bystander { |
||||||
|
conn, l := s.listen1(t) |
||||||
|
conn.setEndpoint(l) // bystander nodes need IP/port to get pinged
|
||||||
|
bn := &bystander{ |
||||||
|
conn: conn, |
||||||
|
l: l, |
||||||
|
dest: s.Dest, |
||||||
|
addedCh: added, |
||||||
|
} |
||||||
|
bn.done.Add(1) |
||||||
|
go bn.loop() |
||||||
|
return bn |
||||||
|
} |
||||||
|
|
||||||
|
// id returns the node ID of the bystander.
|
||||||
|
func (bn *bystander) id() enode.ID { |
||||||
|
return bn.conn.localNode.ID() |
||||||
|
} |
||||||
|
|
||||||
|
// close shuts down loop.
|
||||||
|
func (bn *bystander) close() { |
||||||
|
bn.conn.close() |
||||||
|
bn.done.Wait() |
||||||
|
} |
||||||
|
|
||||||
|
// loop answers packets from the remote node until quit.
|
||||||
|
func (bn *bystander) loop() { |
||||||
|
defer bn.done.Done() |
||||||
|
|
||||||
|
var ( |
||||||
|
lastPing time.Time |
||||||
|
wasAdded bool |
||||||
|
) |
||||||
|
for { |
||||||
|
// Ping the remote node.
|
||||||
|
if !wasAdded && time.Since(lastPing) > 10*time.Second { |
||||||
|
bn.conn.reqresp(bn.l, &v5wire.Ping{ |
||||||
|
ReqID: bn.conn.nextReqID(), |
||||||
|
ENRSeq: bn.dest.Seq(), |
||||||
|
}) |
||||||
|
lastPing = time.Now() |
||||||
|
} |
||||||
|
// Answer packets.
|
||||||
|
switch p := bn.conn.read(bn.l).(type) { |
||||||
|
case *v5wire.Ping: |
||||||
|
bn.conn.write(bn.l, &v5wire.Pong{ |
||||||
|
ReqID: p.ReqID, |
||||||
|
ENRSeq: bn.conn.localNode.Seq(), |
||||||
|
ToIP: bn.dest.IP(), |
||||||
|
ToPort: uint16(bn.dest.UDP()), |
||||||
|
}, nil) |
||||||
|
wasAdded = true |
||||||
|
bn.notifyAdded() |
||||||
|
case *v5wire.Findnode: |
||||||
|
bn.conn.write(bn.l, &v5wire.Nodes{ReqID: p.ReqID, Total: 1}, nil) |
||||||
|
wasAdded = true |
||||||
|
bn.notifyAdded() |
||||||
|
case *v5wire.TalkRequest: |
||||||
|
bn.conn.write(bn.l, &v5wire.TalkResponse{ReqID: p.ReqID}, nil) |
||||||
|
case *readError: |
||||||
|
if !netutil.IsTemporaryError(p.err) { |
||||||
|
bn.conn.logf("shutting down: %v", p.err) |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (bn *bystander) notifyAdded() { |
||||||
|
if bn.addedCh != nil { |
||||||
|
bn.addedCh <- bn.id() |
||||||
|
bn.addedCh = nil |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,263 @@ |
|||||||
|
// Copyright 2020 The go-ethereum Authors
|
||||||
|
// This file is part of go-ethereum.
|
||||||
|
//
|
||||||
|
// go-ethereum is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// go-ethereum is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package v5test |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"crypto/ecdsa" |
||||||
|
"encoding/binary" |
||||||
|
"fmt" |
||||||
|
"net" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common/mclock" |
||||||
|
"github.com/ethereum/go-ethereum/crypto" |
||||||
|
"github.com/ethereum/go-ethereum/p2p/discover/v5wire" |
||||||
|
"github.com/ethereum/go-ethereum/p2p/enode" |
||||||
|
"github.com/ethereum/go-ethereum/p2p/enr" |
||||||
|
) |
||||||
|
|
||||||
|
// readError represents an error during packet reading.
|
||||||
|
// This exists to facilitate type-switching on the result of conn.read.
|
||||||
|
type readError struct { |
||||||
|
err error |
||||||
|
} |
||||||
|
|
||||||
|
func (p *readError) Kind() byte { return 99 } |
||||||
|
func (p *readError) Name() string { return fmt.Sprintf("error: %v", p.err) } |
||||||
|
func (p *readError) Error() string { return p.err.Error() } |
||||||
|
func (p *readError) Unwrap() error { return p.err } |
||||||
|
func (p *readError) RequestID() []byte { return nil } |
||||||
|
func (p *readError) SetRequestID([]byte) {} |
||||||
|
|
||||||
|
// readErrorf creates a readError with the given text.
|
||||||
|
func readErrorf(format string, args ...interface{}) *readError { |
||||||
|
return &readError{fmt.Errorf(format, args...)} |
||||||
|
} |
||||||
|
|
||||||
|
// This is the response timeout used in tests.
|
||||||
|
const waitTime = 300 * time.Millisecond |
||||||
|
|
||||||
|
// conn is a connection to the node under test.
|
||||||
|
type conn struct { |
||||||
|
localNode *enode.LocalNode |
||||||
|
localKey *ecdsa.PrivateKey |
||||||
|
remote *enode.Node |
||||||
|
remoteAddr *net.UDPAddr |
||||||
|
listeners []net.PacketConn |
||||||
|
|
||||||
|
log logger |
||||||
|
codec *v5wire.Codec |
||||||
|
lastRequest v5wire.Packet |
||||||
|
lastChallenge *v5wire.Whoareyou |
||||||
|
idCounter uint32 |
||||||
|
} |
||||||
|
|
||||||
|
type logger interface { |
||||||
|
Logf(string, ...interface{}) |
||||||
|
} |
||||||
|
|
||||||
|
// newConn sets up a connection to the given node.
|
||||||
|
func newConn(dest *enode.Node, log logger) *conn { |
||||||
|
key, err := crypto.GenerateKey() |
||||||
|
if err != nil { |
||||||
|
panic(err) |
||||||
|
} |
||||||
|
db, err := enode.OpenDB("") |
||||||
|
if err != nil { |
||||||
|
panic(err) |
||||||
|
} |
||||||
|
ln := enode.NewLocalNode(db, key) |
||||||
|
|
||||||
|
return &conn{ |
||||||
|
localKey: key, |
||||||
|
localNode: ln, |
||||||
|
remote: dest, |
||||||
|
remoteAddr: &net.UDPAddr{IP: dest.IP(), Port: dest.UDP()}, |
||||||
|
codec: v5wire.NewCodec(ln, key, mclock.System{}), |
||||||
|
log: log, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (tc *conn) setEndpoint(c net.PacketConn) { |
||||||
|
tc.localNode.SetStaticIP(laddr(c).IP) |
||||||
|
tc.localNode.SetFallbackUDP(laddr(c).Port) |
||||||
|
} |
||||||
|
|
||||||
|
func (tc *conn) listen(ip string) net.PacketConn { |
||||||
|
l, err := net.ListenPacket("udp", fmt.Sprintf("%v:0", ip)) |
||||||
|
if err != nil { |
||||||
|
panic(err) |
||||||
|
} |
||||||
|
tc.listeners = append(tc.listeners, l) |
||||||
|
return l |
||||||
|
} |
||||||
|
|
||||||
|
// close shuts down all listeners and the local node.
|
||||||
|
func (tc *conn) close() { |
||||||
|
for _, l := range tc.listeners { |
||||||
|
l.Close() |
||||||
|
} |
||||||
|
tc.localNode.Database().Close() |
||||||
|
} |
||||||
|
|
||||||
|
// nextReqID creates a request id.
|
||||||
|
func (tc *conn) nextReqID() []byte { |
||||||
|
id := make([]byte, 4) |
||||||
|
tc.idCounter++ |
||||||
|
binary.BigEndian.PutUint32(id, tc.idCounter) |
||||||
|
return id |
||||||
|
} |
||||||
|
|
||||||
|
// reqresp performs a request/response interaction on the given connection.
|
||||||
|
// The request is retried if a handshake is requested.
|
||||||
|
func (tc *conn) reqresp(c net.PacketConn, req v5wire.Packet) v5wire.Packet { |
||||||
|
reqnonce := tc.write(c, req, nil) |
||||||
|
switch resp := tc.read(c).(type) { |
||||||
|
case *v5wire.Whoareyou: |
||||||
|
if resp.Nonce != reqnonce { |
||||||
|
return readErrorf("wrong nonce %x in WHOAREYOU (want %x)", resp.Nonce[:], reqnonce[:]) |
||||||
|
} |
||||||
|
resp.Node = tc.remote |
||||||
|
tc.write(c, req, resp) |
||||||
|
return tc.read(c) |
||||||
|
default: |
||||||
|
return resp |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// findnode sends a FINDNODE request and waits for its responses.
|
||||||
|
func (tc *conn) findnode(c net.PacketConn, dists []uint) ([]*enode.Node, error) { |
||||||
|
var ( |
||||||
|
findnode = &v5wire.Findnode{ReqID: tc.nextReqID(), Distances: dists} |
||||||
|
reqnonce = tc.write(c, findnode, nil) |
||||||
|
first = true |
||||||
|
total uint8 |
||||||
|
results []*enode.Node |
||||||
|
) |
||||||
|
for n := 1; n > 0; { |
||||||
|
switch resp := tc.read(c).(type) { |
||||||
|
case *v5wire.Whoareyou: |
||||||
|
// Handle handshake.
|
||||||
|
if resp.Nonce == reqnonce { |
||||||
|
resp.Node = tc.remote |
||||||
|
tc.write(c, findnode, resp) |
||||||
|
} else { |
||||||
|
return nil, fmt.Errorf("unexpected WHOAREYOU (nonce %x), waiting for NODES", resp.Nonce[:]) |
||||||
|
} |
||||||
|
case *v5wire.Ping: |
||||||
|
// Handle ping from remote.
|
||||||
|
tc.write(c, &v5wire.Pong{ |
||||||
|
ReqID: resp.ReqID, |
||||||
|
ENRSeq: tc.localNode.Seq(), |
||||||
|
}, nil) |
||||||
|
case *v5wire.Nodes: |
||||||
|
// Got NODES! Check request ID.
|
||||||
|
if !bytes.Equal(resp.ReqID, findnode.ReqID) { |
||||||
|
return nil, fmt.Errorf("NODES response has wrong request id %x", resp.ReqID) |
||||||
|
} |
||||||
|
// Check total count. It should be greater than one
|
||||||
|
// and needs to be the same across all responses.
|
||||||
|
if first { |
||||||
|
if resp.Total == 0 || resp.Total > 6 { |
||||||
|
return nil, fmt.Errorf("invalid NODES response 'total' %d (not in (0,7))", resp.Total) |
||||||
|
} |
||||||
|
total = resp.Total |
||||||
|
n = int(total) - 1 |
||||||
|
first = false |
||||||
|
} else { |
||||||
|
n-- |
||||||
|
if resp.Total != total { |
||||||
|
return nil, fmt.Errorf("invalid NODES response 'total' %d (!= %d)", resp.Total, total) |
||||||
|
} |
||||||
|
} |
||||||
|
// Check nodes.
|
||||||
|
nodes, err := checkRecords(resp.Nodes) |
||||||
|
if err != nil { |
||||||
|
return nil, fmt.Errorf("invalid node in NODES response: %v", err) |
||||||
|
} |
||||||
|
results = append(results, nodes...) |
||||||
|
default: |
||||||
|
return nil, fmt.Errorf("expected NODES, got %v", resp) |
||||||
|
} |
||||||
|
} |
||||||
|
return results, nil |
||||||
|
} |
||||||
|
|
||||||
|
// write sends a packet on the given connection.
|
||||||
|
func (tc *conn) write(c net.PacketConn, p v5wire.Packet, challenge *v5wire.Whoareyou) v5wire.Nonce { |
||||||
|
packet, nonce, err := tc.codec.Encode(tc.remote.ID(), tc.remoteAddr.String(), p, challenge) |
||||||
|
if err != nil { |
||||||
|
panic(fmt.Errorf("can't encode %v packet: %v", p.Name(), err)) |
||||||
|
} |
||||||
|
if _, err := c.WriteTo(packet, tc.remoteAddr); err != nil { |
||||||
|
tc.logf("Can't send %s: %v", p.Name(), err) |
||||||
|
} else { |
||||||
|
tc.logf(">> %s", p.Name()) |
||||||
|
} |
||||||
|
return nonce |
||||||
|
} |
||||||
|
|
||||||
|
// read waits for an incoming packet on the given connection.
|
||||||
|
func (tc *conn) read(c net.PacketConn) v5wire.Packet { |
||||||
|
buf := make([]byte, 1280) |
||||||
|
if err := c.SetReadDeadline(time.Now().Add(waitTime)); err != nil { |
||||||
|
return &readError{err} |
||||||
|
} |
||||||
|
n, fromAddr, err := c.ReadFrom(buf) |
||||||
|
if err != nil { |
||||||
|
return &readError{err} |
||||||
|
} |
||||||
|
_, _, p, err := tc.codec.Decode(buf[:n], fromAddr.String()) |
||||||
|
if err != nil { |
||||||
|
return &readError{err} |
||||||
|
} |
||||||
|
tc.logf("<< %s", p.Name()) |
||||||
|
return p |
||||||
|
} |
||||||
|
|
||||||
|
// logf prints to the test log.
|
||||||
|
func (tc *conn) logf(format string, args ...interface{}) { |
||||||
|
if tc.log != nil { |
||||||
|
tc.log.Logf("(%s) %s", tc.localNode.ID().TerminalString(), fmt.Sprintf(format, args...)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func laddr(c net.PacketConn) *net.UDPAddr { |
||||||
|
return c.LocalAddr().(*net.UDPAddr) |
||||||
|
} |
||||||
|
|
||||||
|
func checkRecords(records []*enr.Record) ([]*enode.Node, error) { |
||||||
|
nodes := make([]*enode.Node, len(records)) |
||||||
|
for i := range records { |
||||||
|
n, err := enode.New(enode.ValidSchemes, records[i]) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
nodes[i] = n |
||||||
|
} |
||||||
|
return nodes, nil |
||||||
|
} |
||||||
|
|
||||||
|
func containsUint(ints []uint, x uint) bool { |
||||||
|
for i := range ints { |
||||||
|
if ints[i] == x { |
||||||
|
return true |
||||||
|
} |
||||||
|
} |
||||||
|
return false |
||||||
|
} |
@ -1,659 +0,0 @@ |
|||||||
// 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 discover |
|
||||||
|
|
||||||
import ( |
|
||||||
"bytes" |
|
||||||
"crypto/aes" |
|
||||||
"crypto/cipher" |
|
||||||
"crypto/ecdsa" |
|
||||||
"crypto/elliptic" |
|
||||||
crand "crypto/rand" |
|
||||||
"crypto/sha256" |
|
||||||
"errors" |
|
||||||
"fmt" |
|
||||||
"hash" |
|
||||||
"net" |
|
||||||
"time" |
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common/math" |
|
||||||
"github.com/ethereum/go-ethereum/common/mclock" |
|
||||||
"github.com/ethereum/go-ethereum/crypto" |
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode" |
|
||||||
"github.com/ethereum/go-ethereum/p2p/enr" |
|
||||||
"github.com/ethereum/go-ethereum/rlp" |
|
||||||
"golang.org/x/crypto/hkdf" |
|
||||||
) |
|
||||||
|
|
||||||
// TODO concurrent WHOAREYOU tie-breaker
|
|
||||||
// TODO deal with WHOAREYOU amplification factor (min packet size?)
|
|
||||||
// TODO add counter to nonce
|
|
||||||
// TODO rehandshake after X packets
|
|
||||||
|
|
||||||
// Discovery v5 packet types.
|
|
||||||
const ( |
|
||||||
p_pingV5 byte = iota + 1 |
|
||||||
p_pongV5 |
|
||||||
p_findnodeV5 |
|
||||||
p_nodesV5 |
|
||||||
p_requestTicketV5 |
|
||||||
p_ticketV5 |
|
||||||
p_regtopicV5 |
|
||||||
p_regconfirmationV5 |
|
||||||
p_topicqueryV5 |
|
||||||
p_unknownV5 = byte(255) // any non-decryptable packet
|
|
||||||
p_whoareyouV5 = byte(254) // the WHOAREYOU packet
|
|
||||||
) |
|
||||||
|
|
||||||
// Discovery v5 packet structures.
|
|
||||||
type ( |
|
||||||
// unknownV5 represents any packet that can't be decrypted.
|
|
||||||
unknownV5 struct { |
|
||||||
AuthTag []byte |
|
||||||
} |
|
||||||
|
|
||||||
// WHOAREYOU contains the handshake challenge.
|
|
||||||
whoareyouV5 struct { |
|
||||||
AuthTag []byte |
|
||||||
IDNonce [32]byte // To be signed by recipient.
|
|
||||||
RecordSeq uint64 // ENR sequence number of recipient
|
|
||||||
|
|
||||||
node *enode.Node |
|
||||||
sent mclock.AbsTime |
|
||||||
} |
|
||||||
|
|
||||||
// PING is sent during liveness checks.
|
|
||||||
pingV5 struct { |
|
||||||
ReqID []byte |
|
||||||
ENRSeq uint64 |
|
||||||
} |
|
||||||
|
|
||||||
// PONG is the reply to PING.
|
|
||||||
pongV5 struct { |
|
||||||
ReqID []byte |
|
||||||
ENRSeq uint64 |
|
||||||
ToIP net.IP // These fields should mirror the UDP envelope address of the ping
|
|
||||||
ToPort uint16 // packet, which provides a way to discover the the external address (after NAT).
|
|
||||||
} |
|
||||||
|
|
||||||
// FINDNODE is a query for nodes in the given bucket.
|
|
||||||
findnodeV5 struct { |
|
||||||
ReqID []byte |
|
||||||
Distance uint |
|
||||||
} |
|
||||||
|
|
||||||
// NODES is the reply to FINDNODE and TOPICQUERY.
|
|
||||||
nodesV5 struct { |
|
||||||
ReqID []byte |
|
||||||
Total uint8 |
|
||||||
Nodes []*enr.Record |
|
||||||
} |
|
||||||
|
|
||||||
// REQUESTTICKET requests a ticket for a topic queue.
|
|
||||||
requestTicketV5 struct { |
|
||||||
ReqID []byte |
|
||||||
Topic []byte |
|
||||||
} |
|
||||||
|
|
||||||
// TICKET is the response to REQUESTTICKET.
|
|
||||||
ticketV5 struct { |
|
||||||
ReqID []byte |
|
||||||
Ticket []byte |
|
||||||
} |
|
||||||
|
|
||||||
// REGTOPIC registers the sender in a topic queue using a ticket.
|
|
||||||
regtopicV5 struct { |
|
||||||
ReqID []byte |
|
||||||
Ticket []byte |
|
||||||
ENR *enr.Record |
|
||||||
} |
|
||||||
|
|
||||||
// REGCONFIRMATION is the reply to REGTOPIC.
|
|
||||||
regconfirmationV5 struct { |
|
||||||
ReqID []byte |
|
||||||
Registered bool |
|
||||||
} |
|
||||||
|
|
||||||
// TOPICQUERY asks for nodes with the given topic.
|
|
||||||
topicqueryV5 struct { |
|
||||||
ReqID []byte |
|
||||||
Topic []byte |
|
||||||
} |
|
||||||
) |
|
||||||
|
|
||||||
const ( |
|
||||||
// Encryption/authentication parameters.
|
|
||||||
authSchemeName = "gcm" |
|
||||||
aesKeySize = 16 |
|
||||||
gcmNonceSize = 12 |
|
||||||
idNoncePrefix = "discovery-id-nonce" |
|
||||||
handshakeTimeout = time.Second |
|
||||||
) |
|
||||||
|
|
||||||
var ( |
|
||||||
errTooShort = errors.New("packet too short") |
|
||||||
errUnexpectedHandshake = errors.New("unexpected auth response, not in handshake") |
|
||||||
errHandshakeNonceMismatch = errors.New("wrong nonce in auth response") |
|
||||||
errInvalidAuthKey = errors.New("invalid ephemeral pubkey") |
|
||||||
errUnknownAuthScheme = errors.New("unknown auth scheme in handshake") |
|
||||||
errNoRecord = errors.New("expected ENR in handshake but none sent") |
|
||||||
errInvalidNonceSig = errors.New("invalid ID nonce signature") |
|
||||||
zeroNonce = make([]byte, gcmNonceSize) |
|
||||||
) |
|
||||||
|
|
||||||
// wireCodec encodes and decodes discovery v5 packets.
|
|
||||||
type wireCodec struct { |
|
||||||
sha256 hash.Hash |
|
||||||
localnode *enode.LocalNode |
|
||||||
privkey *ecdsa.PrivateKey |
|
||||||
myChtagHash enode.ID |
|
||||||
myWhoareyouMagic []byte |
|
||||||
|
|
||||||
sc *sessionCache |
|
||||||
} |
|
||||||
|
|
||||||
type handshakeSecrets struct { |
|
||||||
writeKey, readKey, authRespKey []byte |
|
||||||
} |
|
||||||
|
|
||||||
type authHeader struct { |
|
||||||
authHeaderList |
|
||||||
isHandshake bool |
|
||||||
} |
|
||||||
|
|
||||||
type authHeaderList struct { |
|
||||||
Auth []byte // authentication info of packet
|
|
||||||
IDNonce [32]byte // IDNonce of WHOAREYOU
|
|
||||||
Scheme string // name of encryption/authentication scheme
|
|
||||||
EphemeralKey []byte // ephemeral public key
|
|
||||||
Response []byte // encrypted authResponse
|
|
||||||
} |
|
||||||
|
|
||||||
type authResponse struct { |
|
||||||
Version uint |
|
||||||
Signature []byte |
|
||||||
Record *enr.Record `rlp:"nil"` // sender's record
|
|
||||||
} |
|
||||||
|
|
||||||
func (h *authHeader) DecodeRLP(r *rlp.Stream) error { |
|
||||||
k, _, err := r.Kind() |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
if k == rlp.Byte || k == rlp.String { |
|
||||||
return r.Decode(&h.Auth) |
|
||||||
} |
|
||||||
h.isHandshake = true |
|
||||||
return r.Decode(&h.authHeaderList) |
|
||||||
} |
|
||||||
|
|
||||||
// ephemeralKey decodes the ephemeral public key in the header.
|
|
||||||
func (h *authHeaderList) ephemeralKey(curve elliptic.Curve) *ecdsa.PublicKey { |
|
||||||
var key encPubkey |
|
||||||
copy(key[:], h.EphemeralKey) |
|
||||||
pubkey, _ := decodePubkey(curve, key) |
|
||||||
return pubkey |
|
||||||
} |
|
||||||
|
|
||||||
// newWireCodec creates a wire codec.
|
|
||||||
func newWireCodec(ln *enode.LocalNode, key *ecdsa.PrivateKey, clock mclock.Clock) *wireCodec { |
|
||||||
c := &wireCodec{ |
|
||||||
sha256: sha256.New(), |
|
||||||
localnode: ln, |
|
||||||
privkey: key, |
|
||||||
sc: newSessionCache(1024, clock), |
|
||||||
} |
|
||||||
// Create magic strings for packet matching.
|
|
||||||
self := ln.ID() |
|
||||||
c.myWhoareyouMagic = c.sha256sum(self[:], []byte("WHOAREYOU")) |
|
||||||
copy(c.myChtagHash[:], c.sha256sum(self[:])) |
|
||||||
return c |
|
||||||
} |
|
||||||
|
|
||||||
// encode encodes a packet to a node. 'id' and 'addr' specify the destination node. The
|
|
||||||
// 'challenge' parameter should be the most recently received WHOAREYOU packet from that
|
|
||||||
// node.
|
|
||||||
func (c *wireCodec) encode(id enode.ID, addr string, packet packetV5, challenge *whoareyouV5) ([]byte, []byte, error) { |
|
||||||
if packet.kind() == p_whoareyouV5 { |
|
||||||
p := packet.(*whoareyouV5) |
|
||||||
enc, err := c.encodeWhoareyou(id, p) |
|
||||||
if err == nil { |
|
||||||
c.sc.storeSentHandshake(id, addr, p) |
|
||||||
} |
|
||||||
return enc, nil, err |
|
||||||
} |
|
||||||
// Ensure calling code sets node if needed.
|
|
||||||
if challenge != nil && challenge.node == nil { |
|
||||||
panic("BUG: missing challenge.node in encode") |
|
||||||
} |
|
||||||
writeKey := c.sc.writeKey(id, addr) |
|
||||||
if writeKey != nil || challenge != nil { |
|
||||||
return c.encodeEncrypted(id, addr, packet, writeKey, challenge) |
|
||||||
} |
|
||||||
return c.encodeRandom(id) |
|
||||||
} |
|
||||||
|
|
||||||
// encodeRandom encodes a random packet.
|
|
||||||
func (c *wireCodec) encodeRandom(toID enode.ID) ([]byte, []byte, error) { |
|
||||||
tag := xorTag(c.sha256sum(toID[:]), c.localnode.ID()) |
|
||||||
r := make([]byte, 44) // TODO randomize size
|
|
||||||
if _, err := crand.Read(r); err != nil { |
|
||||||
return nil, nil, err |
|
||||||
} |
|
||||||
nonce := make([]byte, gcmNonceSize) |
|
||||||
if _, err := crand.Read(nonce); err != nil { |
|
||||||
return nil, nil, fmt.Errorf("can't get random data: %v", err) |
|
||||||
} |
|
||||||
b := new(bytes.Buffer) |
|
||||||
b.Write(tag[:]) |
|
||||||
rlp.Encode(b, nonce) |
|
||||||
b.Write(r) |
|
||||||
return b.Bytes(), nonce, nil |
|
||||||
} |
|
||||||
|
|
||||||
// encodeWhoareyou encodes WHOAREYOU.
|
|
||||||
func (c *wireCodec) encodeWhoareyou(toID enode.ID, packet *whoareyouV5) ([]byte, error) { |
|
||||||
// Sanity check node field to catch misbehaving callers.
|
|
||||||
if packet.RecordSeq > 0 && packet.node == nil { |
|
||||||
panic("BUG: missing node in whoareyouV5 with non-zero seq") |
|
||||||
} |
|
||||||
b := new(bytes.Buffer) |
|
||||||
b.Write(c.sha256sum(toID[:], []byte("WHOAREYOU"))) |
|
||||||
err := rlp.Encode(b, packet) |
|
||||||
return b.Bytes(), err |
|
||||||
} |
|
||||||
|
|
||||||
// encodeEncrypted encodes an encrypted packet.
|
|
||||||
func (c *wireCodec) encodeEncrypted(toID enode.ID, toAddr string, packet packetV5, writeKey []byte, challenge *whoareyouV5) (enc []byte, authTag []byte, err error) { |
|
||||||
nonce := make([]byte, gcmNonceSize) |
|
||||||
if _, err := crand.Read(nonce); err != nil { |
|
||||||
return nil, nil, fmt.Errorf("can't get random data: %v", err) |
|
||||||
} |
|
||||||
|
|
||||||
var headEnc []byte |
|
||||||
if challenge == nil { |
|
||||||
// Regular packet, use existing key and simply encode nonce.
|
|
||||||
headEnc, _ = rlp.EncodeToBytes(nonce) |
|
||||||
} else { |
|
||||||
// We're answering WHOAREYOU, generate new keys and encrypt with those.
|
|
||||||
header, sec, err := c.makeAuthHeader(nonce, challenge) |
|
||||||
if err != nil { |
|
||||||
return nil, nil, err |
|
||||||
} |
|
||||||
if headEnc, err = rlp.EncodeToBytes(header); err != nil { |
|
||||||
return nil, nil, err |
|
||||||
} |
|
||||||
c.sc.storeNewSession(toID, toAddr, sec.readKey, sec.writeKey) |
|
||||||
writeKey = sec.writeKey |
|
||||||
} |
|
||||||
|
|
||||||
// Encode the packet.
|
|
||||||
body := new(bytes.Buffer) |
|
||||||
body.WriteByte(packet.kind()) |
|
||||||
if err := rlp.Encode(body, packet); err != nil { |
|
||||||
return nil, nil, err |
|
||||||
} |
|
||||||
tag := xorTag(c.sha256sum(toID[:]), c.localnode.ID()) |
|
||||||
headsize := len(tag) + len(headEnc) |
|
||||||
headbuf := make([]byte, headsize) |
|
||||||
copy(headbuf[:], tag[:]) |
|
||||||
copy(headbuf[len(tag):], headEnc) |
|
||||||
|
|
||||||
// Encrypt the body.
|
|
||||||
enc, err = encryptGCM(headbuf, writeKey, nonce, body.Bytes(), tag[:]) |
|
||||||
return enc, nonce, err |
|
||||||
} |
|
||||||
|
|
||||||
// encodeAuthHeader creates the auth header on a call packet following WHOAREYOU.
|
|
||||||
func (c *wireCodec) makeAuthHeader(nonce []byte, challenge *whoareyouV5) (*authHeaderList, *handshakeSecrets, error) { |
|
||||||
resp := &authResponse{Version: 5} |
|
||||||
|
|
||||||
// Add our record to response if it's newer than what remote
|
|
||||||
// side has.
|
|
||||||
ln := c.localnode.Node() |
|
||||||
if challenge.RecordSeq < ln.Seq() { |
|
||||||
resp.Record = ln.Record() |
|
||||||
} |
|
||||||
|
|
||||||
// Create the ephemeral key. This needs to be first because the
|
|
||||||
// key is part of the ID nonce signature.
|
|
||||||
var remotePubkey = new(ecdsa.PublicKey) |
|
||||||
if err := challenge.node.Load((*enode.Secp256k1)(remotePubkey)); err != nil { |
|
||||||
return nil, nil, fmt.Errorf("can't find secp256k1 key for recipient") |
|
||||||
} |
|
||||||
ephkey, err := crypto.GenerateKey() |
|
||||||
if err != nil { |
|
||||||
return nil, nil, fmt.Errorf("can't generate ephemeral key") |
|
||||||
} |
|
||||||
ephpubkey := encodePubkey(&ephkey.PublicKey) |
|
||||||
|
|
||||||
// Add ID nonce signature to response.
|
|
||||||
idsig, err := c.signIDNonce(challenge.IDNonce[:], ephpubkey[:]) |
|
||||||
if err != nil { |
|
||||||
return nil, nil, fmt.Errorf("can't sign: %v", err) |
|
||||||
} |
|
||||||
resp.Signature = idsig |
|
||||||
|
|
||||||
// Create session keys.
|
|
||||||
sec := c.deriveKeys(c.localnode.ID(), challenge.node.ID(), ephkey, remotePubkey, challenge) |
|
||||||
if sec == nil { |
|
||||||
return nil, nil, fmt.Errorf("key derivation failed") |
|
||||||
} |
|
||||||
|
|
||||||
// Encrypt the authentication response and assemble the auth header.
|
|
||||||
respRLP, err := rlp.EncodeToBytes(resp) |
|
||||||
if err != nil { |
|
||||||
return nil, nil, fmt.Errorf("can't encode auth response: %v", err) |
|
||||||
} |
|
||||||
respEnc, err := encryptGCM(nil, sec.authRespKey, zeroNonce, respRLP, nil) |
|
||||||
if err != nil { |
|
||||||
return nil, nil, fmt.Errorf("can't encrypt auth response: %v", err) |
|
||||||
} |
|
||||||
head := &authHeaderList{ |
|
||||||
Auth: nonce, |
|
||||||
Scheme: authSchemeName, |
|
||||||
IDNonce: challenge.IDNonce, |
|
||||||
EphemeralKey: ephpubkey[:], |
|
||||||
Response: respEnc, |
|
||||||
} |
|
||||||
return head, sec, err |
|
||||||
} |
|
||||||
|
|
||||||
// deriveKeys generates session keys using elliptic-curve Diffie-Hellman key agreement.
|
|
||||||
func (c *wireCodec) deriveKeys(n1, n2 enode.ID, priv *ecdsa.PrivateKey, pub *ecdsa.PublicKey, challenge *whoareyouV5) *handshakeSecrets { |
|
||||||
eph := ecdh(priv, pub) |
|
||||||
if eph == nil { |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
info := []byte("discovery v5 key agreement") |
|
||||||
info = append(info, n1[:]...) |
|
||||||
info = append(info, n2[:]...) |
|
||||||
kdf := hkdf.New(sha256.New, eph, challenge.IDNonce[:], info) |
|
||||||
sec := handshakeSecrets{ |
|
||||||
writeKey: make([]byte, aesKeySize), |
|
||||||
readKey: make([]byte, aesKeySize), |
|
||||||
authRespKey: make([]byte, aesKeySize), |
|
||||||
} |
|
||||||
kdf.Read(sec.writeKey) |
|
||||||
kdf.Read(sec.readKey) |
|
||||||
kdf.Read(sec.authRespKey) |
|
||||||
for i := range eph { |
|
||||||
eph[i] = 0 |
|
||||||
} |
|
||||||
return &sec |
|
||||||
} |
|
||||||
|
|
||||||
// signIDNonce creates the ID nonce signature.
|
|
||||||
func (c *wireCodec) signIDNonce(nonce, ephkey []byte) ([]byte, error) { |
|
||||||
idsig, err := crypto.Sign(c.idNonceHash(nonce, ephkey), c.privkey) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("can't sign: %v", err) |
|
||||||
} |
|
||||||
return idsig[:len(idsig)-1], nil // remove recovery ID
|
|
||||||
} |
|
||||||
|
|
||||||
// idNonceHash computes the hash of id nonce with prefix.
|
|
||||||
func (c *wireCodec) idNonceHash(nonce, ephkey []byte) []byte { |
|
||||||
h := c.sha256reset() |
|
||||||
h.Write([]byte(idNoncePrefix)) |
|
||||||
h.Write(nonce) |
|
||||||
h.Write(ephkey) |
|
||||||
return h.Sum(nil) |
|
||||||
} |
|
||||||
|
|
||||||
// decode decodes a discovery packet.
|
|
||||||
func (c *wireCodec) decode(input []byte, addr string) (enode.ID, *enode.Node, packetV5, error) { |
|
||||||
// Delete timed-out handshakes. This must happen before decoding to avoid
|
|
||||||
// processing the same handshake twice.
|
|
||||||
c.sc.handshakeGC() |
|
||||||
|
|
||||||
if len(input) < 32 { |
|
||||||
return enode.ID{}, nil, nil, errTooShort |
|
||||||
} |
|
||||||
if bytes.HasPrefix(input, c.myWhoareyouMagic) { |
|
||||||
p, err := c.decodeWhoareyou(input) |
|
||||||
return enode.ID{}, nil, p, err |
|
||||||
} |
|
||||||
sender := xorTag(input[:32], c.myChtagHash) |
|
||||||
p, n, err := c.decodeEncrypted(sender, addr, input) |
|
||||||
return sender, n, p, err |
|
||||||
} |
|
||||||
|
|
||||||
// decodeWhoareyou decode a WHOAREYOU packet.
|
|
||||||
func (c *wireCodec) decodeWhoareyou(input []byte) (packetV5, error) { |
|
||||||
packet := new(whoareyouV5) |
|
||||||
err := rlp.DecodeBytes(input[32:], packet) |
|
||||||
return packet, err |
|
||||||
} |
|
||||||
|
|
||||||
// decodeEncrypted decodes an encrypted discovery packet.
|
|
||||||
func (c *wireCodec) decodeEncrypted(fromID enode.ID, fromAddr string, input []byte) (packetV5, *enode.Node, error) { |
|
||||||
// Decode packet header.
|
|
||||||
var head authHeader |
|
||||||
r := bytes.NewReader(input[32:]) |
|
||||||
err := rlp.Decode(r, &head) |
|
||||||
if err != nil { |
|
||||||
return nil, nil, err |
|
||||||
} |
|
||||||
|
|
||||||
// Decrypt and process auth response.
|
|
||||||
readKey, node, err := c.decodeAuth(fromID, fromAddr, &head) |
|
||||||
if err != nil { |
|
||||||
return nil, nil, err |
|
||||||
} |
|
||||||
|
|
||||||
// Decrypt and decode the packet body.
|
|
||||||
headsize := len(input) - r.Len() |
|
||||||
bodyEnc := input[headsize:] |
|
||||||
body, err := decryptGCM(readKey, head.Auth, bodyEnc, input[:32]) |
|
||||||
if err != nil { |
|
||||||
if !head.isHandshake { |
|
||||||
// Can't decrypt, start handshake.
|
|
||||||
return &unknownV5{AuthTag: head.Auth}, nil, nil |
|
||||||
} |
|
||||||
return nil, nil, fmt.Errorf("handshake failed: %v", err) |
|
||||||
} |
|
||||||
if len(body) == 0 { |
|
||||||
return nil, nil, errTooShort |
|
||||||
} |
|
||||||
p, err := decodePacketBodyV5(body[0], body[1:]) |
|
||||||
return p, node, err |
|
||||||
} |
|
||||||
|
|
||||||
// decodeAuth processes an auth header.
|
|
||||||
func (c *wireCodec) decodeAuth(fromID enode.ID, fromAddr string, head *authHeader) ([]byte, *enode.Node, error) { |
|
||||||
if !head.isHandshake { |
|
||||||
return c.sc.readKey(fromID, fromAddr), nil, nil |
|
||||||
} |
|
||||||
|
|
||||||
// Remote is attempting handshake. Verify against our last WHOAREYOU.
|
|
||||||
challenge := c.sc.getHandshake(fromID, fromAddr) |
|
||||||
if challenge == nil { |
|
||||||
return nil, nil, errUnexpectedHandshake |
|
||||||
} |
|
||||||
if head.IDNonce != challenge.IDNonce { |
|
||||||
return nil, nil, errHandshakeNonceMismatch |
|
||||||
} |
|
||||||
sec, n, err := c.decodeAuthResp(fromID, fromAddr, &head.authHeaderList, challenge) |
|
||||||
if err != nil { |
|
||||||
return nil, n, err |
|
||||||
} |
|
||||||
// Swap keys to match remote.
|
|
||||||
sec.readKey, sec.writeKey = sec.writeKey, sec.readKey |
|
||||||
c.sc.storeNewSession(fromID, fromAddr, sec.readKey, sec.writeKey) |
|
||||||
c.sc.deleteHandshake(fromID, fromAddr) |
|
||||||
return sec.readKey, n, err |
|
||||||
} |
|
||||||
|
|
||||||
// decodeAuthResp decodes and verifies an authentication response.
|
|
||||||
func (c *wireCodec) decodeAuthResp(fromID enode.ID, fromAddr string, head *authHeaderList, challenge *whoareyouV5) (*handshakeSecrets, *enode.Node, error) { |
|
||||||
// Decrypt / decode the response.
|
|
||||||
if head.Scheme != authSchemeName { |
|
||||||
return nil, nil, errUnknownAuthScheme |
|
||||||
} |
|
||||||
ephkey := head.ephemeralKey(c.privkey.Curve) |
|
||||||
if ephkey == nil { |
|
||||||
return nil, nil, errInvalidAuthKey |
|
||||||
} |
|
||||||
sec := c.deriveKeys(fromID, c.localnode.ID(), c.privkey, ephkey, challenge) |
|
||||||
respPT, err := decryptGCM(sec.authRespKey, zeroNonce, head.Response, nil) |
|
||||||
if err != nil { |
|
||||||
return nil, nil, fmt.Errorf("can't decrypt auth response header: %v", err) |
|
||||||
} |
|
||||||
var resp authResponse |
|
||||||
if err := rlp.DecodeBytes(respPT, &resp); err != nil { |
|
||||||
return nil, nil, fmt.Errorf("invalid auth response: %v", err) |
|
||||||
} |
|
||||||
|
|
||||||
// Verify response node record. The remote node should include the record
|
|
||||||
// if we don't have one or if ours is older than the latest version.
|
|
||||||
node := challenge.node |
|
||||||
if resp.Record != nil { |
|
||||||
if node == nil || node.Seq() < resp.Record.Seq() { |
|
||||||
n, err := enode.New(enode.ValidSchemes, resp.Record) |
|
||||||
if err != nil { |
|
||||||
return nil, nil, fmt.Errorf("invalid node record: %v", err) |
|
||||||
} |
|
||||||
if n.ID() != fromID { |
|
||||||
return nil, nil, fmt.Errorf("record in auth respose has wrong ID: %v", n.ID()) |
|
||||||
} |
|
||||||
node = n |
|
||||||
} |
|
||||||
} |
|
||||||
if node == nil { |
|
||||||
return nil, nil, errNoRecord |
|
||||||
} |
|
||||||
|
|
||||||
// Verify ID nonce signature.
|
|
||||||
err = c.verifyIDSignature(challenge.IDNonce[:], head.EphemeralKey, resp.Signature, node) |
|
||||||
if err != nil { |
|
||||||
return nil, nil, err |
|
||||||
} |
|
||||||
return sec, node, nil |
|
||||||
} |
|
||||||
|
|
||||||
// verifyIDSignature checks that signature over idnonce was made by the node with given record.
|
|
||||||
func (c *wireCodec) verifyIDSignature(nonce, ephkey, sig []byte, n *enode.Node) error { |
|
||||||
switch idscheme := n.Record().IdentityScheme(); idscheme { |
|
||||||
case "v4": |
|
||||||
var pk ecdsa.PublicKey |
|
||||||
n.Load((*enode.Secp256k1)(&pk)) // cannot fail because record is valid
|
|
||||||
if !crypto.VerifySignature(crypto.FromECDSAPub(&pk), c.idNonceHash(nonce, ephkey), sig) { |
|
||||||
return errInvalidNonceSig |
|
||||||
} |
|
||||||
return nil |
|
||||||
default: |
|
||||||
return fmt.Errorf("can't verify ID nonce signature against scheme %q", idscheme) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// decodePacketBody decodes the body of an encrypted discovery packet.
|
|
||||||
func decodePacketBodyV5(ptype byte, body []byte) (packetV5, error) { |
|
||||||
var dec packetV5 |
|
||||||
switch ptype { |
|
||||||
case p_pingV5: |
|
||||||
dec = new(pingV5) |
|
||||||
case p_pongV5: |
|
||||||
dec = new(pongV5) |
|
||||||
case p_findnodeV5: |
|
||||||
dec = new(findnodeV5) |
|
||||||
case p_nodesV5: |
|
||||||
dec = new(nodesV5) |
|
||||||
case p_requestTicketV5: |
|
||||||
dec = new(requestTicketV5) |
|
||||||
case p_ticketV5: |
|
||||||
dec = new(ticketV5) |
|
||||||
case p_regtopicV5: |
|
||||||
dec = new(regtopicV5) |
|
||||||
case p_regconfirmationV5: |
|
||||||
dec = new(regconfirmationV5) |
|
||||||
case p_topicqueryV5: |
|
||||||
dec = new(topicqueryV5) |
|
||||||
default: |
|
||||||
return nil, fmt.Errorf("unknown packet type %d", ptype) |
|
||||||
} |
|
||||||
if err := rlp.DecodeBytes(body, dec); err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
return dec, nil |
|
||||||
} |
|
||||||
|
|
||||||
// sha256reset returns the shared hash instance.
|
|
||||||
func (c *wireCodec) sha256reset() hash.Hash { |
|
||||||
c.sha256.Reset() |
|
||||||
return c.sha256 |
|
||||||
} |
|
||||||
|
|
||||||
// sha256sum computes sha256 on the concatenation of inputs.
|
|
||||||
func (c *wireCodec) sha256sum(inputs ...[]byte) []byte { |
|
||||||
c.sha256.Reset() |
|
||||||
for _, b := range inputs { |
|
||||||
c.sha256.Write(b) |
|
||||||
} |
|
||||||
return c.sha256.Sum(nil) |
|
||||||
} |
|
||||||
|
|
||||||
func xorTag(a []byte, b enode.ID) enode.ID { |
|
||||||
var r enode.ID |
|
||||||
for i := range r { |
|
||||||
r[i] = a[i] ^ b[i] |
|
||||||
} |
|
||||||
return r |
|
||||||
} |
|
||||||
|
|
||||||
// ecdh creates a shared secret.
|
|
||||||
func ecdh(privkey *ecdsa.PrivateKey, pubkey *ecdsa.PublicKey) []byte { |
|
||||||
secX, secY := pubkey.ScalarMult(pubkey.X, pubkey.Y, privkey.D.Bytes()) |
|
||||||
if secX == nil { |
|
||||||
return nil |
|
||||||
} |
|
||||||
sec := make([]byte, 33) |
|
||||||
sec[0] = 0x02 | byte(secY.Bit(0)) |
|
||||||
math.ReadBits(secX, sec[1:]) |
|
||||||
return sec |
|
||||||
} |
|
||||||
|
|
||||||
// encryptGCM encrypts pt using AES-GCM with the given key and nonce.
|
|
||||||
func encryptGCM(dest, key, nonce, pt, authData []byte) ([]byte, error) { |
|
||||||
block, err := aes.NewCipher(key) |
|
||||||
if err != nil { |
|
||||||
panic(fmt.Errorf("can't create block cipher: %v", err)) |
|
||||||
} |
|
||||||
aesgcm, err := cipher.NewGCMWithNonceSize(block, gcmNonceSize) |
|
||||||
if err != nil { |
|
||||||
panic(fmt.Errorf("can't create GCM: %v", err)) |
|
||||||
} |
|
||||||
return aesgcm.Seal(dest, nonce, pt, authData), nil |
|
||||||
} |
|
||||||
|
|
||||||
// decryptGCM decrypts ct using AES-GCM with the given key and nonce.
|
|
||||||
func decryptGCM(key, nonce, ct, authData []byte) ([]byte, error) { |
|
||||||
block, err := aes.NewCipher(key) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("can't create block cipher: %v", err) |
|
||||||
} |
|
||||||
if len(nonce) != gcmNonceSize { |
|
||||||
return nil, fmt.Errorf("invalid GCM nonce size: %d", len(nonce)) |
|
||||||
} |
|
||||||
aesgcm, err := cipher.NewGCMWithNonceSize(block, gcmNonceSize) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("can't create GCM: %v", err) |
|
||||||
} |
|
||||||
pt := make([]byte, 0, len(ct)) |
|
||||||
return aesgcm.Open(pt, nonce, ct, authData) |
|
||||||
} |
|
@ -1,373 +0,0 @@ |
|||||||
// 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 discover |
|
||||||
|
|
||||||
import ( |
|
||||||
"bytes" |
|
||||||
"crypto/ecdsa" |
|
||||||
"encoding/hex" |
|
||||||
"fmt" |
|
||||||
"net" |
|
||||||
"reflect" |
|
||||||
"testing" |
|
||||||
|
|
||||||
"github.com/davecgh/go-spew/spew" |
|
||||||
"github.com/ethereum/go-ethereum/common/mclock" |
|
||||||
"github.com/ethereum/go-ethereum/crypto" |
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode" |
|
||||||
) |
|
||||||
|
|
||||||
var ( |
|
||||||
testKeyA, _ = crypto.HexToECDSA("eef77acb6c6a6eebc5b363a475ac583ec7eccdb42b6481424c60f59aa326547f") |
|
||||||
testKeyB, _ = crypto.HexToECDSA("66fb62bfbd66b9177a138c1e5cddbe4f7c30c343e94e68df8769459cb1cde628") |
|
||||||
testIDnonce = [32]byte{5, 6, 7, 8, 9, 10, 11, 12} |
|
||||||
) |
|
||||||
|
|
||||||
func TestDeriveKeysV5(t *testing.T) { |
|
||||||
t.Parallel() |
|
||||||
|
|
||||||
var ( |
|
||||||
n1 = enode.ID{1} |
|
||||||
n2 = enode.ID{2} |
|
||||||
challenge = &whoareyouV5{} |
|
||||||
db, _ = enode.OpenDB("") |
|
||||||
ln = enode.NewLocalNode(db, testKeyA) |
|
||||||
c = newWireCodec(ln, testKeyA, mclock.System{}) |
|
||||||
) |
|
||||||
defer db.Close() |
|
||||||
|
|
||||||
sec1 := c.deriveKeys(n1, n2, testKeyA, &testKeyB.PublicKey, challenge) |
|
||||||
sec2 := c.deriveKeys(n1, n2, testKeyB, &testKeyA.PublicKey, challenge) |
|
||||||
if sec1 == nil || sec2 == nil { |
|
||||||
t.Fatal("key agreement failed") |
|
||||||
} |
|
||||||
if !reflect.DeepEqual(sec1, sec2) { |
|
||||||
t.Fatalf("keys not equal:\n %+v\n %+v", sec1, sec2) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// This test checks the basic handshake flow where A talks to B and A has no secrets.
|
|
||||||
func TestHandshakeV5(t *testing.T) { |
|
||||||
t.Parallel() |
|
||||||
net := newHandshakeTest() |
|
||||||
defer net.close() |
|
||||||
|
|
||||||
// A -> B RANDOM PACKET
|
|
||||||
packet, _ := net.nodeA.encode(t, net.nodeB, &findnodeV5{}) |
|
||||||
resp := net.nodeB.expectDecode(t, p_unknownV5, packet) |
|
||||||
|
|
||||||
// A <- B WHOAREYOU
|
|
||||||
challenge := &whoareyouV5{ |
|
||||||
AuthTag: resp.(*unknownV5).AuthTag, |
|
||||||
IDNonce: testIDnonce, |
|
||||||
RecordSeq: 0, |
|
||||||
} |
|
||||||
whoareyou, _ := net.nodeB.encode(t, net.nodeA, challenge) |
|
||||||
net.nodeA.expectDecode(t, p_whoareyouV5, whoareyou) |
|
||||||
|
|
||||||
// A -> B FINDNODE
|
|
||||||
findnode, _ := net.nodeA.encodeWithChallenge(t, net.nodeB, challenge, &findnodeV5{}) |
|
||||||
net.nodeB.expectDecode(t, p_findnodeV5, findnode) |
|
||||||
if len(net.nodeB.c.sc.handshakes) > 0 { |
|
||||||
t.Fatalf("node B didn't remove handshake from challenge map") |
|
||||||
} |
|
||||||
|
|
||||||
// A <- B NODES
|
|
||||||
nodes, _ := net.nodeB.encode(t, net.nodeA, &nodesV5{Total: 1}) |
|
||||||
net.nodeA.expectDecode(t, p_nodesV5, nodes) |
|
||||||
} |
|
||||||
|
|
||||||
// This test checks that handshake attempts are removed within the timeout.
|
|
||||||
func TestHandshakeV5_timeout(t *testing.T) { |
|
||||||
t.Parallel() |
|
||||||
net := newHandshakeTest() |
|
||||||
defer net.close() |
|
||||||
|
|
||||||
// A -> B RANDOM PACKET
|
|
||||||
packet, _ := net.nodeA.encode(t, net.nodeB, &findnodeV5{}) |
|
||||||
resp := net.nodeB.expectDecode(t, p_unknownV5, packet) |
|
||||||
|
|
||||||
// A <- B WHOAREYOU
|
|
||||||
challenge := &whoareyouV5{ |
|
||||||
AuthTag: resp.(*unknownV5).AuthTag, |
|
||||||
IDNonce: testIDnonce, |
|
||||||
RecordSeq: 0, |
|
||||||
} |
|
||||||
whoareyou, _ := net.nodeB.encode(t, net.nodeA, challenge) |
|
||||||
net.nodeA.expectDecode(t, p_whoareyouV5, whoareyou) |
|
||||||
|
|
||||||
// A -> B FINDNODE after timeout
|
|
||||||
net.clock.Run(handshakeTimeout + 1) |
|
||||||
findnode, _ := net.nodeA.encodeWithChallenge(t, net.nodeB, challenge, &findnodeV5{}) |
|
||||||
net.nodeB.expectDecodeErr(t, errUnexpectedHandshake, findnode) |
|
||||||
} |
|
||||||
|
|
||||||
// This test checks handshake behavior when no record is sent in the auth response.
|
|
||||||
func TestHandshakeV5_norecord(t *testing.T) { |
|
||||||
t.Parallel() |
|
||||||
net := newHandshakeTest() |
|
||||||
defer net.close() |
|
||||||
|
|
||||||
// A -> B RANDOM PACKET
|
|
||||||
packet, _ := net.nodeA.encode(t, net.nodeB, &findnodeV5{}) |
|
||||||
resp := net.nodeB.expectDecode(t, p_unknownV5, packet) |
|
||||||
|
|
||||||
// A <- B WHOAREYOU
|
|
||||||
nodeA := net.nodeA.n() |
|
||||||
if nodeA.Seq() == 0 { |
|
||||||
t.Fatal("need non-zero sequence number") |
|
||||||
} |
|
||||||
challenge := &whoareyouV5{ |
|
||||||
AuthTag: resp.(*unknownV5).AuthTag, |
|
||||||
IDNonce: testIDnonce, |
|
||||||
RecordSeq: nodeA.Seq(), |
|
||||||
node: nodeA, |
|
||||||
} |
|
||||||
whoareyou, _ := net.nodeB.encode(t, net.nodeA, challenge) |
|
||||||
net.nodeA.expectDecode(t, p_whoareyouV5, whoareyou) |
|
||||||
|
|
||||||
// A -> B FINDNODE
|
|
||||||
findnode, _ := net.nodeA.encodeWithChallenge(t, net.nodeB, challenge, &findnodeV5{}) |
|
||||||
net.nodeB.expectDecode(t, p_findnodeV5, findnode) |
|
||||||
|
|
||||||
// A <- B NODES
|
|
||||||
nodes, _ := net.nodeB.encode(t, net.nodeA, &nodesV5{Total: 1}) |
|
||||||
net.nodeA.expectDecode(t, p_nodesV5, nodes) |
|
||||||
} |
|
||||||
|
|
||||||
// In this test, A tries to send FINDNODE with existing secrets but B doesn't know
|
|
||||||
// anything about A.
|
|
||||||
func TestHandshakeV5_rekey(t *testing.T) { |
|
||||||
t.Parallel() |
|
||||||
net := newHandshakeTest() |
|
||||||
defer net.close() |
|
||||||
|
|
||||||
initKeys := &handshakeSecrets{ |
|
||||||
readKey: []byte("BBBBBBBBBBBBBBBB"), |
|
||||||
writeKey: []byte("AAAAAAAAAAAAAAAA"), |
|
||||||
} |
|
||||||
net.nodeA.c.sc.storeNewSession(net.nodeB.id(), net.nodeB.addr(), initKeys.readKey, initKeys.writeKey) |
|
||||||
|
|
||||||
// A -> B FINDNODE (encrypted with zero keys)
|
|
||||||
findnode, authTag := net.nodeA.encode(t, net.nodeB, &findnodeV5{}) |
|
||||||
net.nodeB.expectDecode(t, p_unknownV5, findnode) |
|
||||||
|
|
||||||
// A <- B WHOAREYOU
|
|
||||||
challenge := &whoareyouV5{AuthTag: authTag, IDNonce: testIDnonce} |
|
||||||
whoareyou, _ := net.nodeB.encode(t, net.nodeA, challenge) |
|
||||||
net.nodeA.expectDecode(t, p_whoareyouV5, whoareyou) |
|
||||||
|
|
||||||
// Check that new keys haven't been stored yet.
|
|
||||||
if s := net.nodeA.c.sc.session(net.nodeB.id(), net.nodeB.addr()); !bytes.Equal(s.writeKey, initKeys.writeKey) || !bytes.Equal(s.readKey, initKeys.readKey) { |
|
||||||
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, &findnodeV5{}) |
|
||||||
net.nodeB.expectDecode(t, p_findnodeV5, findnode) |
|
||||||
|
|
||||||
// A <- B NODES
|
|
||||||
nodes, _ := net.nodeB.encode(t, net.nodeA, &nodesV5{Total: 1}) |
|
||||||
net.nodeA.expectDecode(t, p_nodesV5, nodes) |
|
||||||
} |
|
||||||
|
|
||||||
// In this test A and B have different keys before the handshake.
|
|
||||||
func TestHandshakeV5_rekey2(t *testing.T) { |
|
||||||
t.Parallel() |
|
||||||
net := newHandshakeTest() |
|
||||||
defer net.close() |
|
||||||
|
|
||||||
initKeysA := &handshakeSecrets{ |
|
||||||
readKey: []byte("BBBBBBBBBBBBBBBB"), |
|
||||||
writeKey: []byte("AAAAAAAAAAAAAAAA"), |
|
||||||
} |
|
||||||
initKeysB := &handshakeSecrets{ |
|
||||||
readKey: []byte("CCCCCCCCCCCCCCCC"), |
|
||||||
writeKey: []byte("DDDDDDDDDDDDDDDD"), |
|
||||||
} |
|
||||||
net.nodeA.c.sc.storeNewSession(net.nodeB.id(), net.nodeB.addr(), initKeysA.readKey, initKeysA.writeKey) |
|
||||||
net.nodeB.c.sc.storeNewSession(net.nodeA.id(), net.nodeA.addr(), initKeysB.readKey, initKeysA.writeKey) |
|
||||||
|
|
||||||
// A -> B FINDNODE encrypted with initKeysA
|
|
||||||
findnode, authTag := net.nodeA.encode(t, net.nodeB, &findnodeV5{Distance: 3}) |
|
||||||
net.nodeB.expectDecode(t, p_unknownV5, findnode) |
|
||||||
|
|
||||||
// A <- B WHOAREYOU
|
|
||||||
challenge := &whoareyouV5{AuthTag: authTag, IDNonce: testIDnonce} |
|
||||||
whoareyou, _ := net.nodeB.encode(t, net.nodeA, challenge) |
|
||||||
net.nodeA.expectDecode(t, p_whoareyouV5, whoareyou) |
|
||||||
|
|
||||||
// A -> B FINDNODE encrypted with new keys
|
|
||||||
findnode, _ = net.nodeA.encodeWithChallenge(t, net.nodeB, challenge, &findnodeV5{}) |
|
||||||
net.nodeB.expectDecode(t, p_findnodeV5, findnode) |
|
||||||
|
|
||||||
// A <- B NODES
|
|
||||||
nodes, _ := net.nodeB.encode(t, net.nodeA, &nodesV5{Total: 1}) |
|
||||||
net.nodeA.expectDecode(t, p_nodesV5, nodes) |
|
||||||
} |
|
||||||
|
|
||||||
// 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 :)
|
|
||||||
} |
|
||||||
|
|
||||||
// This benchmark checks performance of authHeader decoding, verification and key derivation.
|
|
||||||
func BenchmarkV5_DecodeAuthSecp256k1(b *testing.B) { |
|
||||||
net := newHandshakeTest() |
|
||||||
defer net.close() |
|
||||||
|
|
||||||
var ( |
|
||||||
idA = net.nodeA.id() |
|
||||||
addrA = net.nodeA.addr() |
|
||||||
challenge = &whoareyouV5{AuthTag: []byte("authresp"), RecordSeq: 0, node: net.nodeB.n()} |
|
||||||
nonce = make([]byte, gcmNonceSize) |
|
||||||
) |
|
||||||
header, _, _ := net.nodeA.c.makeAuthHeader(nonce, challenge) |
|
||||||
challenge.node = nil // force ENR signature verification in decoder
|
|
||||||
b.ResetTimer() |
|
||||||
|
|
||||||
for i := 0; i < b.N; i++ { |
|
||||||
_, _, err := net.nodeB.c.decodeAuthResp(idA, addrA, header, challenge) |
|
||||||
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() |
|
||||||
|
|
||||||
r := []byte{233, 203, 93, 195, 86, 47, 177, 186, 227, 43, 2, 141, 244, 230, 120, 17} |
|
||||||
w := []byte{79, 145, 252, 171, 167, 216, 252, 161, 208, 190, 176, 106, 214, 39, 178, 134} |
|
||||||
net.nodeA.c.sc.storeNewSession(net.nodeB.id(), net.nodeB.addr(), r, w) |
|
||||||
net.nodeB.c.sc.storeNewSession(net.nodeA.id(), net.nodeA.addr(), w, r) |
|
||||||
addrB := net.nodeA.addr() |
|
||||||
ping := &pingV5{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() |
|
||||||
|
|
||||||
for i := 0; i < b.N; i++ { |
|
||||||
_, _, p, _ := net.nodeB.c.decode(enc, addrB) |
|
||||||
if _, ok := p.(*pingV5); !ok { |
|
||||||
b.Fatalf("wrong packet type %T", p) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
var pp = spew.NewDefaultConfig() |
|
||||||
|
|
||||||
type handshakeTest struct { |
|
||||||
nodeA, nodeB handshakeTestNode |
|
||||||
clock mclock.Simulated |
|
||||||
} |
|
||||||
|
|
||||||
type handshakeTestNode struct { |
|
||||||
ln *enode.LocalNode |
|
||||||
c *wireCodec |
|
||||||
} |
|
||||||
|
|
||||||
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) |
|
||||||
n.c = newWireCodec(n.ln, key, clock) |
|
||||||
} |
|
||||||
|
|
||||||
func (n *handshakeTestNode) encode(t testing.TB, to handshakeTestNode, p packetV5) ([]byte, []byte) { |
|
||||||
t.Helper() |
|
||||||
return n.encodeWithChallenge(t, to, nil, p) |
|
||||||
} |
|
||||||
|
|
||||||
func (n *handshakeTestNode) encodeWithChallenge(t testing.TB, to handshakeTestNode, c *whoareyouV5, p packetV5) ([]byte, []byte) { |
|
||||||
t.Helper() |
|
||||||
// Copy challenge and add destination node. This avoids sharing 'c' among the two codecs.
|
|
||||||
var challenge *whoareyouV5 |
|
||||||
if c != nil { |
|
||||||
challengeCopy := *c |
|
||||||
challenge = &challengeCopy |
|
||||||
challenge.node = to.n() |
|
||||||
} |
|
||||||
// Encode to destination.
|
|
||||||
enc, authTag, err := n.c.encode(to.id(), to.addr(), p, challenge) |
|
||||||
if err != nil { |
|
||||||
t.Fatal(fmt.Errorf("(%s) %v", n.ln.ID().TerminalString(), err)) |
|
||||||
} |
|
||||||
t.Logf("(%s) -> (%s) %s\n%s", n.ln.ID().TerminalString(), to.id().TerminalString(), p.name(), hex.Dump(enc)) |
|
||||||
return enc, authTag |
|
||||||
} |
|
||||||
|
|
||||||
func (n *handshakeTestNode) expectDecode(t *testing.T, ptype byte, p []byte) packetV5 { |
|
||||||
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) (packetV5, 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() |
|
||||||
} |
|
@ -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) |
||||||
|
} |
@ -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 |
||||||
|
} |
@ -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 |
||||||
|
} |
@ -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) |
||||||
|
} |
||||||
|
} |
@ -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 } |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
Loading…
Reference in new issue