Skip to content

Commit

Permalink
Merge pull request #5079 from ipfs/feat/ipns-pubkey-record
Browse files Browse the repository at this point in the history
embed public keys inside ipns records, use for validation
  • Loading branch information
whyrusleeping committed Jun 5, 2018
2 parents f7a9809 + cc37903 commit f2645c1
Show file tree
Hide file tree
Showing 5 changed files with 189 additions and 30 deletions.
127 changes: 110 additions & 17 deletions namesys/ipns_validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ package namesys
import (
"context"
"fmt"
"math/rand"
"strings"
"testing"
"time"

opts "github.com/ipfs/go-ipfs/namesys/opts"
pb "github.com/ipfs/go-ipfs/namesys/pb"
path "github.com/ipfs/go-ipfs/path"

u "gx/ipfs/QmNiJuT8Ja3hMVpBHXv3Q6dwmperaQ6JjLtpMQgMCD7xvx/go-ipfs-util"
Expand All @@ -26,32 +29,42 @@ import (
func testValidatorCase(t *testing.T, priv ci.PrivKey, kbook pstore.KeyBook, key string, val []byte, eol time.Time, exp error) {
t.Helper()

validator := IpnsValidator{kbook}

p := path.Path("/ipfs/QmfM2r8seH2GiRaC4esTjeraXEachRt8ZsSeGaWTPLyMoG")
entry, err := CreateRoutingEntryData(priv, p, 1, eol)
if err != nil {
t.Fatal(err)
match := func(t *testing.T, err error) {
t.Helper()
if err != exp {
params := fmt.Sprintf("key: %s\neol: %s\n", key, eol)
if exp == nil {
t.Fatalf("Unexpected error %s for params %s", err, params)
} else if err == nil {
t.Fatalf("Expected error %s but there was no error for params %s", exp, params)
} else {
t.Fatalf("Expected error %s but got %s for params %s", exp, err, params)
}
}
}

testValidatorCaseMatchFunc(t, priv, kbook, key, val, eol, match)
}

func testValidatorCaseMatchFunc(t *testing.T, priv ci.PrivKey, kbook pstore.KeyBook, key string, val []byte, eol time.Time, matchf func(*testing.T, error)) {
t.Helper()
validator := IpnsValidator{kbook}

data := val
if data == nil {
data, err = proto.Marshal(entry)
p := path.Path("/ipfs/QmfM2r8seH2GiRaC4esTjeraXEachRt8ZsSeGaWTPLyMoG")
entry, err := CreateRoutingEntryData(priv, p, 1, eol)
if err != nil {
t.Fatal(err)
}
}
err = validator.Validate(key, data)
if err != exp {
params := fmt.Sprintf("key: %s\neol: %s\n", key, eol)
if exp == nil {
t.Fatalf("Unexpected error %s for params %s", err, params)
} else if err == nil {
t.Fatalf("Expected error %s but there was no error for params %s", exp, params)
} else {
t.Fatalf("Expected error %s but got %s for params %s", exp, err, params)

data, err = proto.Marshal(entry)
if err != nil {
t.Fatal(err)
}
}

matchf(t, validator.Validate(key, data))
}

func TestValidator(t *testing.T) {
Expand All @@ -74,6 +87,86 @@ func TestValidator(t *testing.T) {
testValidatorCase(t, priv, kbook, "/wrong/"+string(id), nil, ts.Add(time.Hour), ErrInvalidPath)
}

func mustMarshal(t *testing.T, entry *pb.IpnsEntry) []byte {
t.Helper()
data, err := proto.Marshal(entry)
if err != nil {
t.Fatal(err)
}
return data
}

func TestEmbeddedPubKeyValidate(t *testing.T) {
goodeol := time.Now().Add(time.Hour)
kbook := pstore.NewPeerstore()

pth := path.Path("/ipfs/QmfM2r8seH2GiRaC4esTjeraXEachRt8ZsSeGaWTPLyMoG")

priv, _, _, ipnsk := genKeys(t)

entry, err := CreateRoutingEntryData(priv, pth, 1, goodeol)
if err != nil {
t.Fatal(err)
}

testValidatorCase(t, priv, kbook, ipnsk, mustMarshal(t, entry), goodeol, ErrPublicKeyNotFound)

pubkb, err := priv.GetPublic().Bytes()
if err != nil {
t.Fatal(err)
}

entry.PubKey = pubkb
testValidatorCase(t, priv, kbook, ipnsk, mustMarshal(t, entry), goodeol, nil)

entry.PubKey = []byte("probably not a public key")
testValidatorCaseMatchFunc(t, priv, kbook, ipnsk, mustMarshal(t, entry), goodeol, func(t *testing.T, err error) {
if !strings.Contains(err.Error(), "unmarshaling pubkey in record:") {
t.Fatal("expected pubkey unmarshaling to fail")
}
})

opriv, _, _, _ := genKeys(t)
wrongkeydata, err := opriv.GetPublic().Bytes()
if err != nil {
t.Fatal(err)
}

entry.PubKey = wrongkeydata
testValidatorCase(t, priv, kbook, ipnsk, mustMarshal(t, entry), goodeol, ErrPublicKeyMismatch)
}

func TestPeerIDPubKeyValidate(t *testing.T) {
goodeol := time.Now().Add(time.Hour)
kbook := pstore.NewPeerstore()

pth := path.Path("/ipfs/QmfM2r8seH2GiRaC4esTjeraXEachRt8ZsSeGaWTPLyMoG")

sk, pk, err := ci.GenerateEd25519Key(rand.New(rand.NewSource(42)))
if err != nil {
t.Fatal(err)
}

pid, err := peer.IDFromPublicKey(pk)
if err != nil {
t.Fatal(err)
}

ipnsk := "/ipns/" + string(pid)

entry, err := CreateRoutingEntryData(sk, pth, 1, goodeol)
if err != nil {
t.Fatal(err)
}

dataNoKey, err := proto.Marshal(entry)
if err != nil {
t.Fatal(err)
}

testValidatorCase(t, sk, kbook, ipnsk, dataNoKey, goodeol, nil)
}

func TestResolverValidation(t *testing.T) {
ctx := context.Background()
rid := testutil.RandIdentityOrFatal(t)
Expand Down
33 changes: 24 additions & 9 deletions namesys/pb/namesys.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions namesys/pb/namesys.proto
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,10 @@ message IpnsEntry {
optional uint64 sequence = 5;

optional uint64 ttl = 6;

// in order for nodes to properly validate a record upon receipt, they need the public
// key associated with it. For old RSA keys, its easiest if we just send this as part of
// the record itself. For newer ed25519 keys, the public key can be embedded in the
// peerID, making this field unnecessary.
optional bytes pubKey = 7;
}
13 changes: 13 additions & 0 deletions namesys/publisher.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,13 +240,26 @@ func PutRecordToRouting(ctx context.Context, r routing.ValueStore, k ci.PubKey,
return err
}

// if we can't derive the public key from the peerID, embed the entire pubkey in
// the record to make the verifiers job easier
if extractedPublicKey == nil {
pubkeyBytes, err := k.Bytes()
if err != nil {
return err
}

entry.PubKey = pubkeyBytes
}

namekey, ipnskey := IpnsKeysForID(id)

go func() {
errs <- PublishEntry(ctx, r, ipnskey, entry)
}()

// Publish the public key if a public key cannot be extracted from the ID
// TODO: once v0.4.16 is widespread enough, we can stop doing this
// and at that point we can even deprecate the /pk/ namespace in the dht
if extractedPublicKey == nil {
go func() {
errs <- PublishPublicKey(ctx, r, namekey, k)
Expand Down
40 changes: 36 additions & 4 deletions namesys/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ package namesys
import (
"bytes"
"errors"
"fmt"
"time"

pb "github.com/ipfs/go-ipfs/namesys/pb"
peer "gx/ipfs/QmcJukH2sAFjY3HdBKq35WDzWoL3UUu2gt9wdfqZTUyM74/go-libp2p-peer"
pstore "gx/ipfs/QmdeiKhUy1TVGBaKxt7y1QmBDLBdisSrLJ1x58Eoj4PXUh/go-libp2p-peerstore"
ic "gx/ipfs/Qme1knMqwt1hKZbc1BmQFmnm9f36nyQGwXxPGVpVJ9rMK5/go-libp2p-crypto"

u "gx/ipfs/QmNiJuT8Ja3hMVpBHXv3Q6dwmperaQ6JjLtpMQgMCD7xvx/go-ipfs-util"
record "gx/ipfs/QmTUyK82BVPA6LmSzEJpfEunk9uBaQzWtMsNP917tVj4sT/go-libp2p-record"
Expand Down Expand Up @@ -42,6 +44,8 @@ var ErrKeyFormat = errors.New("record key could not be parsed into peer ID")
// from the peer store
var ErrPublicKeyNotFound = errors.New("public key not found in peer store")

var ErrPublicKeyMismatch = errors.New("public key in record did not match expected pubkey")

type IpnsValidator struct {
KeyBook pstore.KeyBook
}
Expand All @@ -65,10 +69,10 @@ func (v IpnsValidator) Validate(key string, value []byte) error {
log.Debugf("failed to parse ipns record key %s into peer ID", pidString)
return ErrKeyFormat
}
pubk := v.KeyBook.PubKey(pid)
if pubk == nil {
log.Debugf("public key with hash %s not found in peer store", pid)
return ErrPublicKeyNotFound

pubk, err := v.getPublicKey(pid, entry)
if err != nil {
return err
}

// Check the ipns record signature with the public key
Expand All @@ -94,6 +98,34 @@ func (v IpnsValidator) Validate(key string, value []byte) error {
return nil
}

func (v IpnsValidator) getPublicKey(pid peer.ID, entry *pb.IpnsEntry) (ic.PubKey, error) {
if entry.PubKey != nil {
pk, err := ic.UnmarshalPublicKey(entry.PubKey)
if err != nil {
log.Debugf("public key in ipns record failed to parse: ", err)
return nil, fmt.Errorf("unmarshaling pubkey in record: %s", err)
}

expPid, err := peer.IDFromPublicKey(pk)
if err != nil {
return nil, fmt.Errorf("could not regenerate peerID from pubkey: %s", err)
}

if pid != expPid {
return nil, ErrPublicKeyMismatch
}

return pk, nil
}

pubk := v.KeyBook.PubKey(pid)
if pubk == nil {
log.Debugf("public key with hash %s not found in peer store", pid)
return nil, ErrPublicKeyNotFound
}
return pubk, nil
}

// IpnsSelectorFunc selects the best record by checking which has the highest
// sequence number and latest EOL
func (v IpnsValidator) Select(k string, vals [][]byte) (int, error) {
Expand Down

0 comments on commit f2645c1

Please sign in to comment.