1
0
mirror of https://github.com/ipfs/kubo.git synced 2025-06-29 01:12:24 +08:00

namesys: verify signature in ipns validator

License: MIT
Signed-off-by: Dirk McCormick <dirkmdev@gmail.com>
This commit is contained in:
Dirk McCormick
2018-01-30 22:32:12 -05:00
parent 3106135dd5
commit 5e8c9481ee
6 changed files with 343 additions and 255 deletions

View File

@ -949,14 +949,14 @@ func startListening(ctx context.Context, host p2phost.Host, cfg *config.Config)
func constructDHTRouting(ctx context.Context, host p2phost.Host, dstore repo.Datastore) (routing.IpfsRouting, error) {
dhtRouting := dht.NewDHT(ctx, host, dstore)
dhtRouting.Validator[IpnsValidatorTag] = namesys.IpnsRecordValidator
dhtRouting.Validator[IpnsValidatorTag] = namesys.NewIpnsRecordValidator(host.Peerstore())
dhtRouting.Selector[IpnsValidatorTag] = namesys.IpnsSelectorFunc
return dhtRouting, nil
}
func constructClientDHTRouting(ctx context.Context, host p2phost.Host, dstore repo.Datastore) (routing.IpfsRouting, error) {
dhtRouting := dht.NewDHTClient(ctx, host, dstore)
dhtRouting.Validator[IpnsValidatorTag] = namesys.IpnsRecordValidator
dhtRouting.Validator[IpnsValidatorTag] = namesys.NewIpnsRecordValidator(host.Peerstore())
dhtRouting.Selector[IpnsValidatorTag] = namesys.IpnsSelectorFunc
return dhtRouting, nil
}

View File

@ -1,134 +1,215 @@
package namesys
import (
"io"
"context"
"testing"
"time"
path "github.com/ipfs/go-ipfs/path"
mockrouting "github.com/ipfs/go-ipfs/routing/mock"
u "gx/ipfs/QmNiJuT8Ja3hMVpBHXv3Q6dwmperaQ6JjLtpMQgMCD7xvx/go-ipfs-util"
ds "gx/ipfs/QmPpegoMqhAEqjncrzArm7KVWAkCm78rqL2DPuNjhPrshg/go-datastore"
dssync "gx/ipfs/QmPpegoMqhAEqjncrzArm7KVWAkCm78rqL2DPuNjhPrshg/go-datastore/sync"
routing "gx/ipfs/QmTiWLZ6Fo5j4KcTVutZJ5KWRRJrbxzmxA4td8NfEdrPh7/go-libp2p-routing"
record "gx/ipfs/QmUpttFinNDmNPgFwKN8sZK6BUtBmA68Y4KdSBDXa8t9sJ/go-libp2p-record"
recordpb "gx/ipfs/QmUpttFinNDmNPgFwKN8sZK6BUtBmA68Y4KdSBDXa8t9sJ/go-libp2p-record/pb"
testutil "gx/ipfs/QmVvkK7s5imCiq3JVbL3pGfnhcCnf3LrFJPF4GE2sAoGZf/go-testutil"
pstore "gx/ipfs/QmXauCuJzmzapetmC6W4TuDJLL1yFFrVzSHoWv8YdbmnxH/go-libp2p-peerstore"
proto "gx/ipfs/QmZ4Qi3GaRbjcx28Sme5eMH7RQjGkt8wHxt2a65oLaeFEV/gogo-protobuf/proto"
peer "gx/ipfs/QmZoWKhxUmZ2seW4BzX6fJkNR8hh9PsGModr7q171yq2SS/go-libp2p-peer"
ci "gx/ipfs/QmaPbCnUMBohSGo3KnxEa2bHqyJVVeEEcwtqJAYxerieBo/go-libp2p-crypto"
)
func TestValidation(t *testing.T) {
// Create a record validator
validator := make(record.Validator)
validator["ipns"] = &record.ValidChecker{Func: ValidateIpnsRecord, Sign: true}
ctx := context.Background()
rid := testutil.RandIdentityOrFatal(t)
dstore := dssync.MutexWrap(ds.NewMapDatastore())
peerstore := pstore.NewPeerstore()
// Generate a key for signing the records
r := u.NewSeededRand(15) // generate deterministic keypair
priv, ipnsPath := genKeys(t, r)
vstore := newMockValueStore(rid, dstore, peerstore)
vstore.Validator["ipns"] = NewIpnsRecordValidator(peerstore)
vstore.Validator["pk"] = &record.ValidChecker{
Func: func(r *record.ValidationRecord) error {
return nil
},
Sign: false,
}
resolver := NewRoutingResolver(vstore, 0)
// Create entry with expiry in one hour
priv, id, _, ipnsDHTPath := genKeys(t)
ts := time.Now()
entry, err := CreateRoutingEntryData(priv, path.Path("foo"), 1, ts.Add(time.Hour))
p := path.Path("/ipfs/QmfM2r8seH2GiRaC4esTjeraXEachRt8ZsSeGaWTPLyMoG")
entry, err := CreateRoutingEntryData(priv, p, 1, ts.Add(time.Hour))
if err != nil {
t.Fatal(err)
}
val, err := proto.Marshal(entry)
// Make peer's public key available in peer store
err = peerstore.AddPubKey(id, priv.GetPublic())
if err != nil {
t.Fatal(err)
}
// Create the record
rec, err := record.MakePutRecord(priv, ipnsPath, val, true)
// Publish entry
err = PublishEntry(ctx, vstore, ipnsDHTPath, entry)
if err != nil {
t.Fatal(err)
}
// Validate the record
err = validator.VerifyRecord(rec)
// Resolve entry
resp, err := resolver.resolveOnce(ctx, id.Pretty())
if err != nil {
t.Fatal(err)
}
/* TODO(#4613)
// Create IPNS record path with a different private key
_, ipnsWrongAuthor := genKeys(t, r)
wrongAuthorRec, err := record.MakePutRecord(priv, ipnsWrongAuthor, val, true)
if err != nil {
t.Fatal(err)
if resp != p {
t.Fatal("Mismatch between published path %s and resolved path %s", p, resp)
}
// Record should fail validation because path doesn't match author
err = validator.VerifyRecord(wrongAuthorRec)
if err != ErrInvalidAuthor {
t.Fatal("ValidateIpnsRecord should have returned ErrInvalidAuthor")
}
// Create IPNS record path with extra path components after author
extraPath := ipnsPath + "/some/path"
extraPathRec, err := record.MakePutRecord(priv, extraPath, val, true)
if err != nil {
t.Fatal(err)
}
// Record should fail validation because path has extra components after author
err = validator.VerifyRecord(extraPathRec)
if err != ErrInvalidAuthor {
t.Fatal("ValidateIpnsRecord should have returned ErrInvalidAuthor")
}
// Create unsigned IPNS record
unsignedRec, err := record.MakePutRecord(priv, ipnsPath, val, false)
if err != nil {
t.Fatal(err)
}
// Record should fail validation because IPNS records require signature
err = validator.VerifyRecord(unsignedRec)
if err != ErrInvalidAuthor {
t.Fatal("ValidateIpnsRecord should have returned ErrInvalidAuthor")
}
// Create unsigned IPNS record with no author
unsignedRecNoAuthor, err := record.MakePutRecord(priv, ipnsPath, val, false)
if err != nil {
t.Fatal(err)
}
noAuth := ""
unsignedRecNoAuthor.Author = &noAuth
// Record should fail validation because IPNS records require author
err = validator.VerifyRecord(unsignedRecNoAuthor)
if err != ErrInvalidAuthor {
t.Fatal("ValidateIpnsRecord should have returned ErrInvalidAuthor")
}
*/
// Create expired entry
expiredEntry, err := CreateRoutingEntryData(priv, path.Path("foo"), 1, ts.Add(-1*time.Hour))
if err != nil {
t.Fatal(err)
}
valExp, err := proto.Marshal(expiredEntry)
expiredEntry, err := CreateRoutingEntryData(priv, p, 1, ts.Add(-1*time.Hour))
if err != nil {
t.Fatal(err)
}
// Create record with the expired entry
expiredRec, err := record.MakePutRecord(priv, ipnsPath, valExp, true)
// Publish entry
err = PublishEntry(ctx, vstore, ipnsDHTPath, expiredEntry)
if err != nil {
t.Fatal(err)
}
// Record should fail validation because entry is expired
err = validator.VerifyRecord(expiredRec)
_, err = resolver.resolveOnce(ctx, id.Pretty())
if err != ErrExpiredRecord {
t.Fatal("ValidateIpnsRecord should have returned ErrExpiredRecord")
}
// Create IPNS record path with a different private key
priv2, id2, _, ipnsDHTPath2 := genKeys(t)
// Make peer's public key available in peer store
err = peerstore.AddPubKey(id2, priv2.GetPublic())
if err != nil {
t.Fatal(err)
}
// Publish entry
err = PublishEntry(ctx, vstore, ipnsDHTPath2, entry)
if err != nil {
t.Fatal(err)
}
// Record should fail validation because public key defined by
// ipns path doesn't match record signature
_, err = resolver.resolveOnce(ctx, id2.Pretty())
if err != ErrSignature {
t.Fatal("ValidateIpnsRecord should have failed signature verification")
}
// Publish entry without making public key available in peer store
priv3, id3, pubkDHTPath3, ipnsDHTPath3 := genKeys(t)
entry3, err := CreateRoutingEntryData(priv3, p, 1, ts.Add(time.Hour))
if err != nil {
t.Fatal(err)
}
err = PublishEntry(ctx, vstore, ipnsDHTPath3, entry3)
if err != nil {
t.Fatal(err)
}
// Record should fail validation because public key is not available
// in peer store or on network
_, err = resolver.resolveOnce(ctx, id3.Pretty())
if err == nil {
t.Fatal("ValidateIpnsRecord should have failed because public key was not found")
}
// Publish public key to the network
err = PublishPublicKey(ctx, vstore, pubkDHTPath3, priv3.GetPublic())
if err != nil {
t.Fatal(err)
}
// Record should now pass validation because resolver will ensure
// public key is available in the peer store by looking it up in
// the DHT, which causes the DHT to fetch it and cache it in the
// peer store
_, err = resolver.resolveOnce(ctx, id3.Pretty())
if err != nil {
t.Fatal(err)
}
}
func genKeys(t *testing.T, r io.Reader) (ci.PrivKey, string) {
priv, _, err := ci.GenerateKeyPairWithReader(ci.RSA, 1024, r)
func genKeys(t *testing.T) (ci.PrivKey, peer.ID, string, string) {
sr := u.NewTimeSeededRand()
priv, _, err := ci.GenerateKeyPairWithReader(ci.RSA, 1024, sr)
if err != nil {
t.Fatal(err)
}
id, err := peer.IDFromPrivateKey(priv)
// Create entry with expiry in one hour
pid, err := peer.IDFromPrivateKey(priv)
if err != nil {
t.Fatal(err)
}
_, ipnsKey := IpnsKeysForID(id)
return priv, ipnsKey
pubkDHTPath, ipnsDHTPath := IpnsKeysForID(pid)
return priv, pid, pubkDHTPath, ipnsDHTPath
}
type mockValueStore struct {
r routing.ValueStore
kbook pstore.KeyBook
Validator record.Validator
}
func newMockValueStore(id testutil.Identity, dstore ds.Datastore, kbook pstore.KeyBook) *mockValueStore {
serv := mockrouting.NewServer()
r := serv.ClientWithDatastore(context.Background(), id, dstore)
return &mockValueStore{r, kbook, make(record.Validator)}
}
func (m *mockValueStore) GetValue(ctx context.Context, k string) ([]byte, error) {
data, err := m.r.GetValue(ctx, k)
if err != nil {
return data, err
}
rec := new(recordpb.Record)
rec.Key = proto.String(k)
rec.Value = data
if err = m.Validator.VerifyRecord(rec); err != nil {
return nil, err
}
return data, err
}
func (m *mockValueStore) GetPublicKey(ctx context.Context, p peer.ID) (ci.PubKey, error) {
pk := m.kbook.PubKey(p)
if pk != nil {
return pk, nil
}
pkkey := routing.KeyForPublicKey(p)
val, err := m.GetValue(ctx, pkkey)
if err != nil {
return nil, err
}
pk, err = ci.UnmarshalPublicKey(val)
if err != nil {
return nil, err
}
return pk, m.kbook.AddPubKey(p, pk)
}
func (m *mockValueStore) GetValues(ctx context.Context, k string, count int) ([]routing.RecvdVal, error) {
return m.r.GetValues(ctx, k, count)
}
func (m *mockValueStore) PutValue(ctx context.Context, k string, d []byte) error {
return m.r.PutValue(ctx, k, d)
}

View File

@ -3,7 +3,6 @@ package namesys
import (
"bytes"
"context"
"errors"
"fmt"
"time"
@ -16,25 +15,12 @@ import (
u "gx/ipfs/QmNiJuT8Ja3hMVpBHXv3Q6dwmperaQ6JjLtpMQgMCD7xvx/go-ipfs-util"
ds "gx/ipfs/QmPpegoMqhAEqjncrzArm7KVWAkCm78rqL2DPuNjhPrshg/go-datastore"
routing "gx/ipfs/QmTiWLZ6Fo5j4KcTVutZJ5KWRRJrbxzmxA4td8NfEdrPh7/go-libp2p-routing"
record "gx/ipfs/QmUpttFinNDmNPgFwKN8sZK6BUtBmA68Y4KdSBDXa8t9sJ/go-libp2p-record"
dhtpb "gx/ipfs/QmUpttFinNDmNPgFwKN8sZK6BUtBmA68Y4KdSBDXa8t9sJ/go-libp2p-record/pb"
proto "gx/ipfs/QmZ4Qi3GaRbjcx28Sme5eMH7RQjGkt8wHxt2a65oLaeFEV/gogo-protobuf/proto"
peer "gx/ipfs/QmZoWKhxUmZ2seW4BzX6fJkNR8hh9PsGModr7q171yq2SS/go-libp2p-peer"
ci "gx/ipfs/QmaPbCnUMBohSGo3KnxEa2bHqyJVVeEEcwtqJAYxerieBo/go-libp2p-crypto"
)
// ErrExpiredRecord should be returned when an ipns record is
// invalid due to being too old
var ErrExpiredRecord = errors.New("expired record")
// ErrUnrecognizedValidity is returned when an IpnsRecord has an
// unknown validity type.
var ErrUnrecognizedValidity = errors.New("unrecognized validity type")
// ErrInvalidPath should be returned when an ipns record path
// is not in a valid format
var ErrInvalidPath = errors.New("record path invalid")
const PublishPutValTimeout = time.Minute
const DefaultRecordTTL = 24 * time.Hour
@ -208,7 +194,7 @@ func PublishEntry(ctx context.Context, r routing.ValueStore, ipnskey string, rec
}
log.Debugf("Storing ipns entry at: %s", ipnskey)
// Store ipns entry at "/ipns/"+b58(h(pubkey))
// Store ipns entry at "/ipns/"+h(pubkey)
return r.PutValue(timectx, ipnskey, data)
}
@ -238,114 +224,6 @@ func ipnsEntryDataForSig(e *pb.IpnsEntry) []byte {
[]byte{})
}
var IpnsRecordValidator = &record.ValidChecker{
Func: ValidateIpnsRecord,
Sign: true,
}
func IpnsSelectorFunc(k string, vals [][]byte) (int, error) {
var recs []*pb.IpnsEntry
for _, v := range vals {
e := new(pb.IpnsEntry)
err := proto.Unmarshal(v, e)
if err == nil {
recs = append(recs, e)
} else {
recs = append(recs, nil)
}
}
return selectRecord(recs, vals)
}
func selectRecord(recs []*pb.IpnsEntry, vals [][]byte) (int, error) {
var best_seq uint64
best_i := -1
for i, r := range recs {
if r == nil || r.GetSequence() < best_seq {
continue
}
if best_i == -1 || r.GetSequence() > best_seq {
best_seq = r.GetSequence()
best_i = i
} else if r.GetSequence() == best_seq {
rt, err := u.ParseRFC3339(string(r.GetValidity()))
if err != nil {
continue
}
bestt, err := u.ParseRFC3339(string(recs[best_i].GetValidity()))
if err != nil {
continue
}
if rt.After(bestt) {
best_i = i
} else if rt == bestt {
if bytes.Compare(vals[i], vals[best_i]) > 0 {
best_i = i
}
}
}
}
if best_i == -1 {
return 0, errors.New("no usable records in given set")
}
return best_i, nil
}
// ValidateIpnsRecord implements ValidatorFunc and verifies that the
// given 'val' is an IpnsEntry and that that entry is valid.
func ValidateIpnsRecord(r *record.ValidationRecord) error {
if r.Namespace != "ipns" {
return ErrInvalidPath
}
entry := new(pb.IpnsEntry)
err := proto.Unmarshal(r.Value, entry)
if err != nil {
return err
}
// NOTE/FIXME(#4613): We're not checking the DHT signature/author here.
// We're going to remove them in a followup commit and then check the
// *IPNS* signature. However, to do that, we need to ensure we *have*
// the public key and:
//
// 1. Don't want to fetch it from the network when handling PUTs.
// 2. Do want to fetch it from the network when handling GETs.
//
// Therefore, we'll need to either:
//
// 1. Pass some for of offline hint to the validator (e.g., using a context).
// 2. Ensure we pre-fetch the key when performing gets.
//
// This PR is already *way* too large so we're punting that fix to a new
// PR.
//
// This is not a regression, it just restores the current (bad)
// behavior.
// Check that record has not expired
switch entry.GetValidityType() {
case pb.IpnsEntry_EOL:
t, err := u.ParseRFC3339(string(entry.GetValidity()))
if err != nil {
log.Debug("failed parsing time for ipns record EOL")
return err
}
if time.Now().After(t) {
return ErrExpiredRecord
}
default:
return ErrUnrecognizedValidity
}
return nil
}
// InitializeKeyspace sets the ipns record for the given key to
// point to an empty directory.
// TODO: this doesnt feel like it belongs here

View File

@ -2,7 +2,6 @@ package namesys
import (
"context"
"fmt"
"strings"
"time"
@ -15,7 +14,7 @@ import (
lru "gx/ipfs/QmVYxfoJQiZijTgPNHCHgHELvQpbsJNTg6Crmc3dQkj3yy/golang-lru"
proto "gx/ipfs/QmZ4Qi3GaRbjcx28Sme5eMH7RQjGkt8wHxt2a65oLaeFEV/gogo-protobuf/proto"
mh "gx/ipfs/QmZyZDi491cCNTLfAhwcaDii2Kg4pwKRkhqQzURGDvY6ua/go-multihash"
ci "gx/ipfs/QmaPbCnUMBohSGo3KnxEa2bHqyJVVeEEcwtqJAYxerieBo/go-libp2p-crypto"
peer "gx/ipfs/QmZoWKhxUmZ2seW4BzX6fJkNR8hh9PsGModr7q171yq2SS/go-libp2p-peer"
cid "gx/ipfs/QmcZfnkapfECQGcLZaf9B79NRg7cRa9EnZh4LSbkCzwNvY/go-cid"
)
@ -131,59 +130,38 @@ func (r *routingResolver) resolveOnce(ctx context.Context, name string) (path.Pa
return "", err
}
// Name should be the hash of a public key retrievable from ipfs.
// We retrieve the public key here to make certain that it's in the peer
// store before calling GetValue() on the DHT - the DHT will call the
// ipns validator, which in turn will get the public key from the peer
// store to verify the record signature
_, err = routing.GetPublicKey(r.routing, ctx, hash)
if err != nil {
log.Debugf("RoutingResolver: could not retrieve public key %s: %s\n", name, err)
return "", err
}
pid, err := peer.IDFromBytes(hash)
if err != nil {
log.Debugf("RoutingResolver: could not convert public key hash %s to peer ID: %s\n", name, err)
return "", err
}
// use the routing system to get the name.
// /ipns/<name>
h := []byte("/ipns/" + string(hash))
var entry *pb.IpnsEntry
var pubkey ci.PubKey
resp := make(chan error, 2)
go func() {
ipnsKey := string(h)
val, err := r.routing.GetValue(ctx, ipnsKey)
if err != nil {
log.Debugf("RoutingResolver: dht get failed: %s", err)
resp <- err
return
}
entry = new(pb.IpnsEntry)
err = proto.Unmarshal(val, entry)
if err != nil {
resp <- err
return
}
resp <- nil
}()
go func() {
// name should be a public key retrievable from ipfs
pubk, err := routing.GetPublicKey(r.routing, ctx, hash)
if err != nil {
resp <- err
return
}
pubkey = pubk
resp <- nil
}()
for i := 0; i < 2; i++ {
err = <-resp
if err != nil {
return "", err
}
_, ipnsKey := IpnsKeysForID(pid)
val, err := r.routing.GetValue(ctx, ipnsKey)
if err != nil {
log.Debugf("RoutingResolver: dht get for name %s failed: %s", name, err)
return "", err
}
// check sig with pk
if ok, err := pubkey.Verify(ipnsEntryDataForSig(entry), entry.GetSignature()); err != nil || !ok {
return "", fmt.Errorf("ipns entry for %s has invalid signature", h)
entry := new(pb.IpnsEntry)
err = proto.Unmarshal(val, entry)
if err != nil {
log.Debugf("RoutingResolver: could not unmarshal value for name %s: %s", name, err)
return "", err
}
// ok sig checks out. this is a valid name.
// check for old style record:
valh, err := mh.Cast(entry.GetValue())
if err != nil {
@ -197,7 +175,7 @@ func (r *routingResolver) resolveOnce(ctx context.Context, name string) (path.Pa
return p, nil
} else {
// Its an old style multihash record
log.Debugf("encountered CIDv0 ipns entry: %s", h)
log.Debugf("encountered CIDv0 ipns entry: %s", valh)
p := path.FromCid(cid.NewCidV0(valh))
r.cacheSet(name, p, entry)
return p, nil

65
namesys/selector.go Normal file
View File

@ -0,0 +1,65 @@
package namesys
import (
"bytes"
"errors"
pb "github.com/ipfs/go-ipfs/namesys/pb"
u "gx/ipfs/QmNiJuT8Ja3hMVpBHXv3Q6dwmperaQ6JjLtpMQgMCD7xvx/go-ipfs-util"
proto "gx/ipfs/QmZ4Qi3GaRbjcx28Sme5eMH7RQjGkt8wHxt2a65oLaeFEV/gogo-protobuf/proto"
)
func IpnsSelectorFunc(k string, vals [][]byte) (int, error) {
var recs []*pb.IpnsEntry
for _, v := range vals {
e := new(pb.IpnsEntry)
err := proto.Unmarshal(v, e)
if err == nil {
recs = append(recs, e)
} else {
recs = append(recs, nil)
}
}
return selectRecord(recs, vals)
}
func selectRecord(recs []*pb.IpnsEntry, vals [][]byte) (int, error) {
var best_seq uint64
best_i := -1
for i, r := range recs {
if r == nil || r.GetSequence() < best_seq {
continue
}
if best_i == -1 || r.GetSequence() > best_seq {
best_seq = r.GetSequence()
best_i = i
} else if r.GetSequence() == best_seq {
rt, err := u.ParseRFC3339(string(r.GetValidity()))
if err != nil {
continue
}
bestt, err := u.ParseRFC3339(string(recs[best_i].GetValidity()))
if err != nil {
continue
}
if rt.After(bestt) {
best_i = i
} else if rt == bestt {
if bytes.Compare(vals[i], vals[best_i]) > 0 {
best_i = i
}
}
}
}
if best_i == -1 {
return 0, errors.New("no usable records in given set")
}
return best_i, nil
}

86
namesys/validator.go Normal file
View File

@ -0,0 +1,86 @@
package namesys
import (
"errors"
"time"
pb "github.com/ipfs/go-ipfs/namesys/pb"
peer "gx/ipfs/QmZoWKhxUmZ2seW4BzX6fJkNR8hh9PsGModr7q171yq2SS/go-libp2p-peer"
pstore "gx/ipfs/QmXauCuJzmzapetmC6W4TuDJLL1yFFrVzSHoWv8YdbmnxH/go-libp2p-peerstore"
u "gx/ipfs/QmNiJuT8Ja3hMVpBHXv3Q6dwmperaQ6JjLtpMQgMCD7xvx/go-ipfs-util"
proto "gx/ipfs/QmZ4Qi3GaRbjcx28Sme5eMH7RQjGkt8wHxt2a65oLaeFEV/gogo-protobuf/proto"
record "gx/ipfs/QmUpttFinNDmNPgFwKN8sZK6BUtBmA68Y4KdSBDXa8t9sJ/go-libp2p-record"
)
// ErrExpiredRecord should be returned when an ipns record is
// invalid due to being too old
var ErrExpiredRecord = errors.New("expired record")
// ErrUnrecognizedValidity is returned when an IpnsRecord has an
// unknown validity type.
var ErrUnrecognizedValidity = errors.New("unrecognized validity type")
// ErrInvalidPath should be returned when an ipns record path
// is not in a valid format
var ErrInvalidPath = errors.New("record path invalid")
// ErrSignature should be returned when an ipns record fails
// signature verification
var ErrSignature = errors.New("record signature verification failed")
func NewIpnsRecordValidator(kbook pstore.KeyBook) *record.ValidChecker {
// ValidateIpnsRecord implements ValidatorFunc and verifies that the
// given 'val' is an IpnsEntry and that that entry is valid.
ValidateIpnsRecord := func(r *record.ValidationRecord) error {
if r.Namespace != "ipns" {
return ErrInvalidPath
}
// Parse the value into an IpnsEntry
entry := new(pb.IpnsEntry)
err := proto.Unmarshal(r.Value, entry)
if err != nil {
return err
}
// Get the public key defined by the ipns path
pid, err := peer.IDFromString(r.Key)
if err != nil {
log.Debugf("failed to parse ipns record key %s into public key hash", r.Key)
return ErrSignature
}
pubk := kbook.PubKey(pid)
if pubk == nil {
log.Debugf("public key with hash %s not found in peer store", pid)
return ErrSignature
}
// Check the ipns record signature with the public key
if ok, err := pubk.Verify(ipnsEntryDataForSig(entry), entry.GetSignature()); err != nil || !ok {
log.Debugf("failed to verify signature for ipns record %s", r.Key)
return ErrSignature
}
// Check that record has not expired
switch entry.GetValidityType() {
case pb.IpnsEntry_EOL:
t, err := u.ParseRFC3339(string(entry.GetValidity()))
if err != nil {
log.Debugf("failed parsing time for ipns record EOL in record %s", r.Key)
return err
}
if time.Now().After(t) {
return ErrExpiredRecord
}
default:
return ErrUnrecognizedValidity
}
return nil
}
return &record.ValidChecker{
Func: ValidateIpnsRecord,
Sign: false,
}
}