From 5e8c9481eea1c49e815e903824a6d31f32c8860e Mon Sep 17 00:00:00 2001 From: Dirk McCormick Date: Tue, 30 Jan 2018 22:32:12 -0500 Subject: [PATCH] namesys: verify signature in ipns validator License: MIT Signed-off-by: Dirk McCormick --- core/core.go | 4 +- namesys/ipns_validate_test.go | 239 +++++++++++++++++++++++----------- namesys/publisher.go | 124 +----------------- namesys/routing.go | 80 +++++------- namesys/selector.go | 65 +++++++++ namesys/validator.go | 86 ++++++++++++ 6 files changed, 343 insertions(+), 255 deletions(-) create mode 100644 namesys/selector.go create mode 100644 namesys/validator.go diff --git a/core/core.go b/core/core.go index e87cf598b..5f240243b 100644 --- a/core/core.go +++ b/core/core.go @@ -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 } diff --git a/namesys/ipns_validate_test.go b/namesys/ipns_validate_test.go index 430381cbe..cb7d80954 100644 --- a/namesys/ipns_validate_test.go +++ b/namesys/ipns_validate_test.go @@ -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) } diff --git a/namesys/publisher.go b/namesys/publisher.go index c7159036a..79e3168e8 100644 --- a/namesys/publisher.go +++ b/namesys/publisher.go @@ -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 diff --git a/namesys/routing.go b/namesys/routing.go index 4ec68e7b6..0c17ca4ff 100644 --- a/namesys/routing.go +++ b/namesys/routing.go @@ -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/ - 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 diff --git a/namesys/selector.go b/namesys/selector.go new file mode 100644 index 000000000..6114bfcce --- /dev/null +++ b/namesys/selector.go @@ -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 +} diff --git a/namesys/validator.go b/namesys/validator.go new file mode 100644 index 000000000..3eebce7f9 --- /dev/null +++ b/namesys/validator.go @@ -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, + } +}