mirror of
https://github.com/ipfs/kubo.git
synced 2025-06-29 09:34:03 +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) {
|
func constructDHTRouting(ctx context.Context, host p2phost.Host, dstore repo.Datastore) (routing.IpfsRouting, error) {
|
||||||
dhtRouting := dht.NewDHT(ctx, host, dstore)
|
dhtRouting := dht.NewDHT(ctx, host, dstore)
|
||||||
dhtRouting.Validator[IpnsValidatorTag] = namesys.IpnsRecordValidator
|
dhtRouting.Validator[IpnsValidatorTag] = namesys.NewIpnsRecordValidator(host.Peerstore())
|
||||||
dhtRouting.Selector[IpnsValidatorTag] = namesys.IpnsSelectorFunc
|
dhtRouting.Selector[IpnsValidatorTag] = namesys.IpnsSelectorFunc
|
||||||
return dhtRouting, nil
|
return dhtRouting, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func constructClientDHTRouting(ctx context.Context, host p2phost.Host, dstore repo.Datastore) (routing.IpfsRouting, error) {
|
func constructClientDHTRouting(ctx context.Context, host p2phost.Host, dstore repo.Datastore) (routing.IpfsRouting, error) {
|
||||||
dhtRouting := dht.NewDHTClient(ctx, host, dstore)
|
dhtRouting := dht.NewDHTClient(ctx, host, dstore)
|
||||||
dhtRouting.Validator[IpnsValidatorTag] = namesys.IpnsRecordValidator
|
dhtRouting.Validator[IpnsValidatorTag] = namesys.NewIpnsRecordValidator(host.Peerstore())
|
||||||
dhtRouting.Selector[IpnsValidatorTag] = namesys.IpnsSelectorFunc
|
dhtRouting.Selector[IpnsValidatorTag] = namesys.IpnsSelectorFunc
|
||||||
return dhtRouting, nil
|
return dhtRouting, nil
|
||||||
}
|
}
|
||||||
|
@ -1,134 +1,215 @@
|
|||||||
package namesys
|
package namesys
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
path "github.com/ipfs/go-ipfs/path"
|
path "github.com/ipfs/go-ipfs/path"
|
||||||
|
mockrouting "github.com/ipfs/go-ipfs/routing/mock"
|
||||||
|
|
||||||
u "gx/ipfs/QmNiJuT8Ja3hMVpBHXv3Q6dwmperaQ6JjLtpMQgMCD7xvx/go-ipfs-util"
|
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"
|
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"
|
proto "gx/ipfs/QmZ4Qi3GaRbjcx28Sme5eMH7RQjGkt8wHxt2a65oLaeFEV/gogo-protobuf/proto"
|
||||||
peer "gx/ipfs/QmZoWKhxUmZ2seW4BzX6fJkNR8hh9PsGModr7q171yq2SS/go-libp2p-peer"
|
peer "gx/ipfs/QmZoWKhxUmZ2seW4BzX6fJkNR8hh9PsGModr7q171yq2SS/go-libp2p-peer"
|
||||||
ci "gx/ipfs/QmaPbCnUMBohSGo3KnxEa2bHqyJVVeEEcwtqJAYxerieBo/go-libp2p-crypto"
|
ci "gx/ipfs/QmaPbCnUMBohSGo3KnxEa2bHqyJVVeEEcwtqJAYxerieBo/go-libp2p-crypto"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestValidation(t *testing.T) {
|
func TestValidation(t *testing.T) {
|
||||||
// Create a record validator
|
ctx := context.Background()
|
||||||
validator := make(record.Validator)
|
rid := testutil.RandIdentityOrFatal(t)
|
||||||
validator["ipns"] = &record.ValidChecker{Func: ValidateIpnsRecord, Sign: true}
|
dstore := dssync.MutexWrap(ds.NewMapDatastore())
|
||||||
|
peerstore := pstore.NewPeerstore()
|
||||||
|
|
||||||
// Generate a key for signing the records
|
vstore := newMockValueStore(rid, dstore, peerstore)
|
||||||
r := u.NewSeededRand(15) // generate deterministic keypair
|
vstore.Validator["ipns"] = NewIpnsRecordValidator(peerstore)
|
||||||
priv, ipnsPath := genKeys(t, r)
|
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
|
// Create entry with expiry in one hour
|
||||||
|
priv, id, _, ipnsDHTPath := genKeys(t)
|
||||||
ts := time.Now()
|
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 {
|
if err != nil {
|
||||||
t.Fatal(err)
|
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 {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the record
|
// Publish entry
|
||||||
rec, err := record.MakePutRecord(priv, ipnsPath, val, true)
|
err = PublishEntry(ctx, vstore, ipnsDHTPath, entry)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate the record
|
// Resolve entry
|
||||||
err = validator.VerifyRecord(rec)
|
resp, err := resolver.resolveOnce(ctx, id.Pretty())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
if resp != p {
|
||||||
/* TODO(#4613)
|
t.Fatal("Mismatch between published path %s and resolved path %s", p, resp)
|
||||||
// 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
// Create expired entry
|
||||||
expiredEntry, err := CreateRoutingEntryData(priv, path.Path("foo"), 1, ts.Add(-1*time.Hour))
|
expiredEntry, err := CreateRoutingEntryData(priv, p, 1, ts.Add(-1*time.Hour))
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
valExp, err := proto.Marshal(expiredEntry)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create record with the expired entry
|
// Publish entry
|
||||||
expiredRec, err := record.MakePutRecord(priv, ipnsPath, valExp, true)
|
err = PublishEntry(ctx, vstore, ipnsDHTPath, expiredEntry)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
// Record should fail validation because entry is expired
|
// Record should fail validation because entry is expired
|
||||||
err = validator.VerifyRecord(expiredRec)
|
_, err = resolver.resolveOnce(ctx, id.Pretty())
|
||||||
if err != ErrExpiredRecord {
|
if err != ErrExpiredRecord {
|
||||||
t.Fatal("ValidateIpnsRecord should have returned 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) {
|
func genKeys(t *testing.T) (ci.PrivKey, peer.ID, string, string) {
|
||||||
priv, _, err := ci.GenerateKeyPairWithReader(ci.RSA, 1024, r)
|
sr := u.NewTimeSeededRand()
|
||||||
|
priv, _, err := ci.GenerateKeyPairWithReader(ci.RSA, 1024, sr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
id, err := peer.IDFromPrivateKey(priv)
|
|
||||||
|
// Create entry with expiry in one hour
|
||||||
|
pid, err := peer.IDFromPrivateKey(priv)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
_, ipnsKey := IpnsKeysForID(id)
|
pubkDHTPath, ipnsDHTPath := IpnsKeysForID(pid)
|
||||||
return priv, ipnsKey
|
|
||||||
|
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 (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -16,25 +15,12 @@ import (
|
|||||||
u "gx/ipfs/QmNiJuT8Ja3hMVpBHXv3Q6dwmperaQ6JjLtpMQgMCD7xvx/go-ipfs-util"
|
u "gx/ipfs/QmNiJuT8Ja3hMVpBHXv3Q6dwmperaQ6JjLtpMQgMCD7xvx/go-ipfs-util"
|
||||||
ds "gx/ipfs/QmPpegoMqhAEqjncrzArm7KVWAkCm78rqL2DPuNjhPrshg/go-datastore"
|
ds "gx/ipfs/QmPpegoMqhAEqjncrzArm7KVWAkCm78rqL2DPuNjhPrshg/go-datastore"
|
||||||
routing "gx/ipfs/QmTiWLZ6Fo5j4KcTVutZJ5KWRRJrbxzmxA4td8NfEdrPh7/go-libp2p-routing"
|
routing "gx/ipfs/QmTiWLZ6Fo5j4KcTVutZJ5KWRRJrbxzmxA4td8NfEdrPh7/go-libp2p-routing"
|
||||||
record "gx/ipfs/QmUpttFinNDmNPgFwKN8sZK6BUtBmA68Y4KdSBDXa8t9sJ/go-libp2p-record"
|
|
||||||
dhtpb "gx/ipfs/QmUpttFinNDmNPgFwKN8sZK6BUtBmA68Y4KdSBDXa8t9sJ/go-libp2p-record/pb"
|
dhtpb "gx/ipfs/QmUpttFinNDmNPgFwKN8sZK6BUtBmA68Y4KdSBDXa8t9sJ/go-libp2p-record/pb"
|
||||||
proto "gx/ipfs/QmZ4Qi3GaRbjcx28Sme5eMH7RQjGkt8wHxt2a65oLaeFEV/gogo-protobuf/proto"
|
proto "gx/ipfs/QmZ4Qi3GaRbjcx28Sme5eMH7RQjGkt8wHxt2a65oLaeFEV/gogo-protobuf/proto"
|
||||||
peer "gx/ipfs/QmZoWKhxUmZ2seW4BzX6fJkNR8hh9PsGModr7q171yq2SS/go-libp2p-peer"
|
peer "gx/ipfs/QmZoWKhxUmZ2seW4BzX6fJkNR8hh9PsGModr7q171yq2SS/go-libp2p-peer"
|
||||||
ci "gx/ipfs/QmaPbCnUMBohSGo3KnxEa2bHqyJVVeEEcwtqJAYxerieBo/go-libp2p-crypto"
|
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 PublishPutValTimeout = time.Minute
|
||||||
const DefaultRecordTTL = 24 * time.Hour
|
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)
|
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)
|
return r.PutValue(timectx, ipnskey, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -238,114 +224,6 @@ func ipnsEntryDataForSig(e *pb.IpnsEntry) []byte {
|
|||||||
[]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
|
// InitializeKeyspace sets the ipns record for the given key to
|
||||||
// point to an empty directory.
|
// point to an empty directory.
|
||||||
// TODO: this doesnt feel like it belongs here
|
// TODO: this doesnt feel like it belongs here
|
||||||
|
@ -2,7 +2,6 @@ package namesys
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -15,7 +14,7 @@ import (
|
|||||||
lru "gx/ipfs/QmVYxfoJQiZijTgPNHCHgHELvQpbsJNTg6Crmc3dQkj3yy/golang-lru"
|
lru "gx/ipfs/QmVYxfoJQiZijTgPNHCHgHELvQpbsJNTg6Crmc3dQkj3yy/golang-lru"
|
||||||
proto "gx/ipfs/QmZ4Qi3GaRbjcx28Sme5eMH7RQjGkt8wHxt2a65oLaeFEV/gogo-protobuf/proto"
|
proto "gx/ipfs/QmZ4Qi3GaRbjcx28Sme5eMH7RQjGkt8wHxt2a65oLaeFEV/gogo-protobuf/proto"
|
||||||
mh "gx/ipfs/QmZyZDi491cCNTLfAhwcaDii2Kg4pwKRkhqQzURGDvY6ua/go-multihash"
|
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"
|
cid "gx/ipfs/QmcZfnkapfECQGcLZaf9B79NRg7cRa9EnZh4LSbkCzwNvY/go-cid"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -131,59 +130,38 @@ func (r *routingResolver) resolveOnce(ctx context.Context, name string) (path.Pa
|
|||||||
return "", err
|
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.
|
// use the routing system to get the name.
|
||||||
// /ipns/<name>
|
_, ipnsKey := IpnsKeysForID(pid)
|
||||||
h := []byte("/ipns/" + string(hash))
|
val, err := r.routing.GetValue(ctx, ipnsKey)
|
||||||
|
if err != nil {
|
||||||
var entry *pb.IpnsEntry
|
log.Debugf("RoutingResolver: dht get for name %s failed: %s", name, err)
|
||||||
var pubkey ci.PubKey
|
return "", err
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// check sig with pk
|
entry := new(pb.IpnsEntry)
|
||||||
if ok, err := pubkey.Verify(ipnsEntryDataForSig(entry), entry.GetSignature()); err != nil || !ok {
|
err = proto.Unmarshal(val, entry)
|
||||||
return "", fmt.Errorf("ipns entry for %s has invalid signature", h)
|
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:
|
// check for old style record:
|
||||||
valh, err := mh.Cast(entry.GetValue())
|
valh, err := mh.Cast(entry.GetValue())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -197,7 +175,7 @@ func (r *routingResolver) resolveOnce(ctx context.Context, name string) (path.Pa
|
|||||||
return p, nil
|
return p, nil
|
||||||
} else {
|
} else {
|
||||||
// Its an old style multihash record
|
// 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))
|
p := path.FromCid(cid.NewCidV0(valh))
|
||||||
r.cacheSet(name, p, entry)
|
r.cacheSet(name, p, entry)
|
||||||
return p, nil
|
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