p2p/enr: updates for discovery v4 compatibility (#16679)

This applies spec changes from ethereum/EIPs#1049 and adds support for
pluggable identity schemes.

Some care has been taken to make the "v4" scheme standalone. It uses
public APIs only and could be moved out of package enr at any time.

A couple of minor changes were needed to make identity schemes work:

- The sequence number is now updated in Set instead of when signing.
- Record is now copy-safe, i.e. calling Set on a shallow copy doesn't
  modify the record it was copied from.
This commit is contained in:
Felix Lange 2018-05-17 15:11:27 +02:00 committed by GitHub
parent f6bc65fc68
commit 6286c255f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 277 additions and 154 deletions

View File

@ -29,21 +29,16 @@ package enr
import (
"bytes"
"crypto/ecdsa"
"errors"
"fmt"
"io"
"sort"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/crypto/sha3"
"github.com/ethereum/go-ethereum/rlp"
)
const SizeLimit = 300 // maximum encoded size of a node record in bytes
const ID_SECP256k1_KECCAK = ID("secp256k1-keccak") // the default identity scheme
var (
errNoID = errors.New("unknown or unspecified identity scheme")
errInvalidSig = errors.New("invalid signature")
@ -80,8 +75,8 @@ func (r *Record) Seq() uint64 {
}
// SetSeq updates the record sequence number. This invalidates any signature on the record.
// Calling SetSeq is usually not required because signing the redord increments the
// sequence number.
// Calling SetSeq is usually not required because setting any key in a signed record
// increments the sequence number.
func (r *Record) SetSeq(s uint64) {
r.signature = nil
r.raw = nil
@ -104,33 +99,42 @@ func (r *Record) Load(e Entry) error {
return &KeyError{Key: e.ENRKey(), Err: errNotFound}
}
// Set adds or updates the given entry in the record.
// It panics if the value can't be encoded.
// Set adds or updates the given entry in the record. It panics if the value can't be
// encoded. If the record is signed, Set increments the sequence number and invalidates
// the sequence number.
func (r *Record) Set(e Entry) {
r.signature = nil
r.raw = nil
blob, err := rlp.EncodeToBytes(e)
if err != nil {
panic(fmt.Errorf("enr: can't encode %s: %v", e.ENRKey(), err))
}
r.invalidate()
i := sort.Search(len(r.pairs), func(i int) bool { return r.pairs[i].k >= e.ENRKey() })
if i < len(r.pairs) && r.pairs[i].k == e.ENRKey() {
pairs := make([]pair, len(r.pairs))
copy(pairs, r.pairs)
i := sort.Search(len(pairs), func(i int) bool { return pairs[i].k >= e.ENRKey() })
switch {
case i < len(pairs) && pairs[i].k == e.ENRKey():
// element is present at r.pairs[i]
r.pairs[i].v = blob
return
} else if i < len(r.pairs) {
pairs[i].v = blob
case i < len(r.pairs):
// insert pair before i-th elem
el := pair{e.ENRKey(), blob}
r.pairs = append(r.pairs, pair{})
copy(r.pairs[i+1:], r.pairs[i:])
r.pairs[i] = el
return
pairs = append(pairs, pair{})
copy(pairs[i+1:], pairs[i:])
pairs[i] = el
default:
// element should be placed at the end of r.pairs
pairs = append(pairs, pair{e.ENRKey(), blob})
}
r.pairs = pairs
}
// element should be placed at the end of r.pairs
r.pairs = append(r.pairs, pair{e.ENRKey(), blob})
func (r *Record) invalidate() {
if r.signature == nil {
r.seq++
}
r.signature = nil
r.raw = nil
}
// EncodeRLP implements rlp.Encoder. Encoding fails if
@ -196,39 +200,55 @@ func (r *Record) DecodeRLP(s *rlp.Stream) error {
return err
}
// Verify signature.
if err = dec.verifySignature(); err != nil {
_, scheme := dec.idScheme()
if scheme == nil {
return errNoID
}
if err := scheme.Verify(&dec, dec.signature); err != nil {
return err
}
*r = dec
return nil
}
type s256raw []byte
func (s256raw) ENRKey() string { return "secp256k1" }
// NodeAddr returns the node address. The return value will be nil if the record is
// unsigned.
// unsigned or uses an unknown identity scheme.
func (r *Record) NodeAddr() []byte {
var entry s256raw
if r.Load(&entry) != nil {
_, scheme := r.idScheme()
if scheme == nil {
return nil
}
return crypto.Keccak256(entry)
return scheme.NodeAddr(r)
}
// Sign signs the record with the given private key. It updates the record's identity
// scheme, public key and increments the sequence number. Sign returns an error if the
// encoded record is larger than the size limit.
func (r *Record) Sign(privkey *ecdsa.PrivateKey) error {
r.seq = r.seq + 1
r.Set(ID_SECP256k1_KECCAK)
r.Set(Secp256k1(privkey.PublicKey))
return r.signAndEncode(privkey)
// SetSig sets the record signature. It returns an error if the encoded record is larger
// than the size limit or if the signature is invalid according to the passed scheme.
func (r *Record) SetSig(idscheme string, sig []byte) error {
// Check that "id" is set and matches the given scheme. This panics because
// inconsitencies here are always implementation bugs in the signing function calling
// this method.
id, s := r.idScheme()
if s == nil {
panic(errNoID)
}
if id != idscheme {
panic(fmt.Errorf("identity scheme mismatch in Sign: record has %s, want %s", id, idscheme))
}
// Verify against the scheme.
if err := s.Verify(r, sig); err != nil {
return err
}
raw, err := r.encode(sig)
if err != nil {
return err
}
r.signature, r.raw = sig, raw
return nil
}
func (r *Record) appendPairs(list []interface{}) []interface{} {
// AppendElements appends the sequence number and entries to the given slice.
func (r *Record) AppendElements(list []interface{}) []interface{} {
list = append(list, r.seq)
for _, p := range r.pairs {
list = append(list, p.k, p.v)
@ -236,54 +256,23 @@ func (r *Record) appendPairs(list []interface{}) []interface{} {
return list
}
func (r *Record) signAndEncode(privkey *ecdsa.PrivateKey) error {
// Put record elements into a flat list. Leave room for the signature.
list := make([]interface{}, 1, len(r.pairs)*2+2)
list = r.appendPairs(list)
// Sign the tail of the list.
h := sha3.NewKeccak256()
rlp.Encode(h, list[1:])
sig, err := crypto.Sign(h.Sum(nil), privkey)
if err != nil {
return err
func (r *Record) encode(sig []byte) (raw []byte, err error) {
list := make([]interface{}, 1, 2*len(r.pairs)+1)
list[0] = sig
list = r.AppendElements(list)
if raw, err = rlp.EncodeToBytes(list); err != nil {
return nil, err
}
sig = sig[:len(sig)-1] // remove v
// Put signature in front.
r.signature, list[0] = sig, sig
r.raw, err = rlp.EncodeToBytes(list)
if err != nil {
return err
if len(raw) > SizeLimit {
return nil, errTooBig
}
if len(r.raw) > SizeLimit {
return errTooBig
}
return nil
return raw, nil
}
func (r *Record) verifySignature() error {
// Get identity scheme, public key, signature.
func (r *Record) idScheme() (string, IdentityScheme) {
var id ID
var entry s256raw
if err := r.Load(&id); err != nil {
return err
} else if id != ID_SECP256k1_KECCAK {
return errNoID
return "", nil
}
if err := r.Load(&entry); err != nil {
return err
} else if len(entry) != 33 {
return fmt.Errorf("invalid public key")
}
// Verify the signature.
list := make([]interface{}, 0, len(r.pairs)*2+1)
list = r.appendPairs(list)
h := sha3.NewKeccak256()
rlp.Encode(h, list)
if !crypto.VerifySignature(entry, h.Sum(nil), r.signature) {
return errInvalidSig
}
return nil
return string(id), FindIdentityScheme(string(id))
}

View File

@ -54,35 +54,35 @@ func TestGetSetID(t *testing.T) {
assert.Equal(t, id, id2)
}
// TestGetSetIP4 tests encoding/decoding and setting/getting of the IP4 key.
// TestGetSetIP4 tests encoding/decoding and setting/getting of the IP key.
func TestGetSetIP4(t *testing.T) {
ip := IP4{192, 168, 0, 3}
ip := IP{192, 168, 0, 3}
var r Record
r.Set(ip)
var ip2 IP4
var ip2 IP
require.NoError(t, r.Load(&ip2))
assert.Equal(t, ip, ip2)
}
// TestGetSetIP6 tests encoding/decoding and setting/getting of the IP6 key.
// TestGetSetIP6 tests encoding/decoding and setting/getting of the IP key.
func TestGetSetIP6(t *testing.T) {
ip := IP6{0x20, 0x01, 0x48, 0x60, 0, 0, 0x20, 0x01, 0, 0, 0, 0, 0, 0, 0x00, 0x68}
ip := IP{0x20, 0x01, 0x48, 0x60, 0, 0, 0x20, 0x01, 0, 0, 0, 0, 0, 0, 0x00, 0x68}
var r Record
r.Set(ip)
var ip2 IP6
var ip2 IP
require.NoError(t, r.Load(&ip2))
assert.Equal(t, ip, ip2)
}
// TestGetSetDiscPort tests encoding/decoding and setting/getting of the DiscPort key.
func TestGetSetDiscPort(t *testing.T) {
port := DiscPort(30309)
func TestGetSetUDP(t *testing.T) {
port := UDP(30309)
var r Record
r.Set(port)
var port2 DiscPort
var port2 UDP
require.NoError(t, r.Load(&port2))
assert.Equal(t, port, port2)
}
@ -90,7 +90,7 @@ func TestGetSetDiscPort(t *testing.T) {
// TestGetSetSecp256k1 tests encoding/decoding and setting/getting of the Secp256k1 key.
func TestGetSetSecp256k1(t *testing.T) {
var r Record
if err := r.Sign(privkey); err != nil {
if err := SignV4(&r, privkey); err != nil {
t.Fatal(err)
}
@ -101,16 +101,16 @@ func TestGetSetSecp256k1(t *testing.T) {
func TestLoadErrors(t *testing.T) {
var r Record
ip4 := IP4{127, 0, 0, 1}
ip4 := IP{127, 0, 0, 1}
r.Set(ip4)
// Check error for missing keys.
var ip6 IP6
err := r.Load(&ip6)
var udp UDP
err := r.Load(&udp)
if !IsNotFound(err) {
t.Error("IsNotFound should return true for missing key")
}
assert.Equal(t, &KeyError{Key: ip6.ENRKey(), Err: errNotFound}, err)
assert.Equal(t, &KeyError{Key: udp.ENRKey(), Err: errNotFound}, err)
// Check error for invalid keys.
var list []uint
@ -174,7 +174,7 @@ func TestDirty(t *testing.T) {
t.Errorf("expected errEncodeUnsigned, got %#v", err)
}
require.NoError(t, r.Sign(privkey))
require.NoError(t, SignV4(&r, privkey))
if !r.Signed() {
t.Error("Signed return false for signed record")
}
@ -194,13 +194,13 @@ func TestDirty(t *testing.T) {
func TestGetSetOverwrite(t *testing.T) {
var r Record
ip := IP4{192, 168, 0, 3}
ip := IP{192, 168, 0, 3}
r.Set(ip)
ip2 := IP4{192, 168, 0, 4}
ip2 := IP{192, 168, 0, 4}
r.Set(ip2)
var ip3 IP4
var ip3 IP
require.NoError(t, r.Load(&ip3))
assert.Equal(t, ip2, ip3)
}
@ -208,9 +208,9 @@ func TestGetSetOverwrite(t *testing.T) {
// TestSignEncodeAndDecode tests signing, RLP encoding and RLP decoding of a record.
func TestSignEncodeAndDecode(t *testing.T) {
var r Record
r.Set(DiscPort(30303))
r.Set(IP4{127, 0, 0, 1})
require.NoError(t, r.Sign(privkey))
r.Set(UDP(30303))
r.Set(IP{127, 0, 0, 1})
require.NoError(t, SignV4(&r, privkey))
blob, err := rlp.EncodeToBytes(r)
require.NoError(t, err)
@ -230,12 +230,12 @@ func TestNodeAddr(t *testing.T) {
t.Errorf("wrong address on empty record: got %v, want %v", addr, nil)
}
require.NoError(t, r.Sign(privkey))
expected := "caaa1485d83b18b32ed9ad666026151bf0cae8a0a88c857ae2d4c5be2daa6726"
require.NoError(t, SignV4(&r, privkey))
expected := "a448f24c6d18e575453db13171562b71999873db5b286df957af199ec94617f7"
assert.Equal(t, expected, hex.EncodeToString(r.NodeAddr()))
}
var pyRecord, _ = hex.DecodeString("f896b840954dc36583c1f4b69ab59b1375f362f06ee99f3723cd77e64b6de6d211c27d7870642a79d4516997f94091325d2a7ca6215376971455fb221d34f35b277149a1018664697363763582765f82696490736563703235366b312d6b656363616b83697034847f00000189736563703235366b31a103ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd3138")
var pyRecord, _ = hex.DecodeString("f884b8407098ad865b00a582051940cb9cf36836572411a47278783077011599ed5cd16b76f2635f4e234738f30813a89eb9137e3e3df5266e3a1f11df72ecf1145ccb9c01826964827634826970847f00000189736563703235366b31a103ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd31388375647082765f")
// TestPythonInterop checks that we can decode and verify a record produced by the Python
// implementation.
@ -246,10 +246,10 @@ func TestPythonInterop(t *testing.T) {
}
var (
wantAddr, _ = hex.DecodeString("caaa1485d83b18b32ed9ad666026151bf0cae8a0a88c857ae2d4c5be2daa6726")
wantSeq = uint64(1)
wantIP = IP4{127, 0, 0, 1}
wantDiscport = DiscPort(30303)
wantAddr, _ = hex.DecodeString("a448f24c6d18e575453db13171562b71999873db5b286df957af199ec94617f7")
wantSeq = uint64(1)
wantIP = IP{127, 0, 0, 1}
wantUDP = UDP(30303)
)
if r.Seq() != wantSeq {
t.Errorf("wrong seq: got %d, want %d", r.Seq(), wantSeq)
@ -257,7 +257,7 @@ func TestPythonInterop(t *testing.T) {
if addr := r.NodeAddr(); !bytes.Equal(addr, wantAddr) {
t.Errorf("wrong addr: got %x, want %x", addr, wantAddr)
}
want := map[Entry]interface{}{new(IP4): &wantIP, new(DiscPort): &wantDiscport}
want := map[Entry]interface{}{new(IP): &wantIP, new(UDP): &wantUDP}
for k, v := range want {
desc := fmt.Sprintf("loading key %q", k.ENRKey())
if assert.NoError(t, r.Load(k), desc) {
@ -272,14 +272,14 @@ func TestRecordTooBig(t *testing.T) {
key := randomString(10)
// set a big value for random key, expect error
r.Set(WithEntry(key, randomString(300)))
if err := r.Sign(privkey); err != errTooBig {
r.Set(WithEntry(key, randomString(SizeLimit)))
if err := SignV4(&r, privkey); err != errTooBig {
t.Fatalf("expected to get errTooBig, got %#v", err)
}
// set an acceptable value for random key, expect no error
r.Set(WithEntry(key, randomString(100)))
require.NoError(t, r.Sign(privkey))
require.NoError(t, SignV4(&r, privkey))
}
// TestSignEncodeAndDecodeRandom tests encoding/decoding of records containing random key/value pairs.
@ -295,7 +295,7 @@ func TestSignEncodeAndDecodeRandom(t *testing.T) {
r.Set(WithEntry(key, &value))
}
require.NoError(t, r.Sign(privkey))
require.NoError(t, SignV4(&r, privkey))
_, err := rlp.EncodeToBytes(r)
require.NoError(t, err)

View File

@ -57,59 +57,43 @@ func WithEntry(k string, v interface{}) Entry {
return &generic{key: k, value: v}
}
// DiscPort is the "discv5" key, which holds the UDP port for discovery v5.
type DiscPort uint16
// TCP is the "tcp" key, which holds the TCP port of the node.
type TCP uint16
func (v DiscPort) ENRKey() string { return "discv5" }
func (v TCP) ENRKey() string { return "tcp" }
// UDP is the "udp" key, which holds the UDP port of the node.
type UDP uint16
func (v UDP) ENRKey() string { return "udp" }
// ID is the "id" key, which holds the name of the identity scheme.
type ID string
const IDv4 = ID("v4") // the default identity scheme
func (v ID) ENRKey() string { return "id" }
// IP4 is the "ip4" key, which holds a 4-byte IPv4 address.
type IP4 net.IP
// IP is the "ip" key, which holds the IP address of the node.
type IP net.IP
func (v IP4) ENRKey() string { return "ip4" }
func (v IP) ENRKey() string { return "ip" }
// EncodeRLP implements rlp.Encoder.
func (v IP4) EncodeRLP(w io.Writer) error {
ip4 := net.IP(v).To4()
if ip4 == nil {
return fmt.Errorf("invalid IPv4 address: %v", v)
func (v IP) EncodeRLP(w io.Writer) error {
if ip4 := net.IP(v).To4(); ip4 != nil {
return rlp.Encode(w, ip4)
}
return rlp.Encode(w, ip4)
return rlp.Encode(w, net.IP(v))
}
// DecodeRLP implements rlp.Decoder.
func (v *IP4) DecodeRLP(s *rlp.Stream) error {
func (v *IP) DecodeRLP(s *rlp.Stream) error {
if err := s.Decode((*net.IP)(v)); err != nil {
return err
}
if len(*v) != 4 {
return fmt.Errorf("invalid IPv4 address, want 4 bytes: %v", *v)
}
return nil
}
// IP6 is the "ip6" key, which holds a 16-byte IPv6 address.
type IP6 net.IP
func (v IP6) ENRKey() string { return "ip6" }
// EncodeRLP implements rlp.Encoder.
func (v IP6) EncodeRLP(w io.Writer) error {
ip6 := net.IP(v)
return rlp.Encode(w, ip6)
}
// DecodeRLP implements rlp.Decoder.
func (v *IP6) DecodeRLP(s *rlp.Stream) error {
if err := s.Decode((*net.IP)(v)); err != nil {
return err
}
if len(*v) != 16 {
return fmt.Errorf("invalid IPv6 address, want 16 bytes: %v", *v)
if len(*v) != 4 && len(*v) != 16 {
return fmt.Errorf("invalid IP address, want 4 or 16 bytes: %v", *v)
}
return nil
}

114
p2p/enr/idscheme.go Normal file
View File

@ -0,0 +1,114 @@
// Copyright 2018 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 enr
import (
"crypto/ecdsa"
"fmt"
"sync"
"github.com/ethereum/go-ethereum/common/math"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/crypto/sha3"
"github.com/ethereum/go-ethereum/rlp"
)
// Registry of known identity schemes.
var schemes sync.Map
// An IdentityScheme is capable of verifying record signatures and
// deriving node addresses.
type IdentityScheme interface {
Verify(r *Record, sig []byte) error
NodeAddr(r *Record) []byte
}
// RegisterIdentityScheme adds an identity scheme to the global registry.
func RegisterIdentityScheme(name string, scheme IdentityScheme) {
if _, loaded := schemes.LoadOrStore(name, scheme); loaded {
panic("identity scheme " + name + " already registered")
}
}
// FindIdentityScheme resolves name to an identity scheme in the global registry.
func FindIdentityScheme(name string) IdentityScheme {
s, ok := schemes.Load(name)
if !ok {
return nil
}
return s.(IdentityScheme)
}
// v4ID is the "v4" identity scheme.
type v4ID struct{}
func init() {
RegisterIdentityScheme("v4", v4ID{})
}
// SignV4 signs a record using the v4 scheme.
func SignV4(r *Record, privkey *ecdsa.PrivateKey) error {
// Copy r to avoid modifying it if signing fails.
cpy := *r
cpy.Set(ID("v4"))
cpy.Set(Secp256k1(privkey.PublicKey))
h := sha3.NewKeccak256()
rlp.Encode(h, cpy.AppendElements(nil))
sig, err := crypto.Sign(h.Sum(nil), privkey)
if err != nil {
return err
}
sig = sig[:len(sig)-1] // remove v
if err = cpy.SetSig("v4", sig); err == nil {
*r = cpy
}
return err
}
// s256raw is an unparsed secp256k1 public key entry.
type s256raw []byte
func (s256raw) ENRKey() string { return "secp256k1" }
func (v4ID) Verify(r *Record, sig []byte) error {
var entry s256raw
if err := r.Load(&entry); err != nil {
return err
} else if len(entry) != 33 {
return fmt.Errorf("invalid public key")
}
h := sha3.NewKeccak256()
rlp.Encode(h, r.AppendElements(nil))
if !crypto.VerifySignature(entry, h.Sum(nil), sig) {
return errInvalidSig
}
return nil
}
func (v4ID) NodeAddr(r *Record) []byte {
var pubkey Secp256k1
err := r.Load(&pubkey)
if err != nil {
return nil
}
buf := make([]byte, 64)
math.ReadBits(pubkey.X, buf[:32])
math.ReadBits(pubkey.Y, buf[32:])
return crypto.Keccak256(buf)
}

36
p2p/enr/idscheme_test.go Normal file
View File

@ -0,0 +1,36 @@
// Copyright 2018 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 enr
import (
"crypto/ecdsa"
"math/big"
"testing"
)
// Checks that failure to sign leaves the record unmodified.
func TestSignError(t *testing.T) {
invalidKey := &ecdsa.PrivateKey{D: new(big.Int), PublicKey: *pubkey}
var r Record
if err := SignV4(&r, invalidKey); err == nil {
t.Fatal("expected error from SignV4")
}
if len(r.pairs) > 0 {
t.Fatal("expected empty record, have", r.pairs)
}
}