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:
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
65
namesys/selector.go
Normal 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
86
namesys/validator.go
Normal 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,
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user