diff --git a/namesys/ipns_validate_test.go b/namesys/ipns_validate_test.go index f9cdf024a63..d7e46f3285a 100644 --- a/namesys/ipns_validate_test.go +++ b/namesys/ipns_validate_test.go @@ -3,10 +3,13 @@ package namesys import ( "context" "fmt" + "math/rand" + "strings" "testing" "time" opts "github.com/ipfs/go-ipfs/namesys/opts" + pb "github.com/ipfs/go-ipfs/namesys/pb" path "github.com/ipfs/go-ipfs/path" u "gx/ipfs/QmNiJuT8Ja3hMVpBHXv3Q6dwmperaQ6JjLtpMQgMCD7xvx/go-ipfs-util" @@ -26,32 +29,42 @@ import ( func testValidatorCase(t *testing.T, priv ci.PrivKey, kbook pstore.KeyBook, key string, val []byte, eol time.Time, exp error) { t.Helper() - validator := IpnsValidator{kbook} - - p := path.Path("/ipfs/QmfM2r8seH2GiRaC4esTjeraXEachRt8ZsSeGaWTPLyMoG") - entry, err := CreateRoutingEntryData(priv, p, 1, eol) - if err != nil { - t.Fatal(err) + match := func(t *testing.T, err error) { + t.Helper() + if err != exp { + params := fmt.Sprintf("key: %s\neol: %s\n", key, eol) + if exp == nil { + t.Fatalf("Unexpected error %s for params %s", err, params) + } else if err == nil { + t.Fatalf("Expected error %s but there was no error for params %s", exp, params) + } else { + t.Fatalf("Expected error %s but got %s for params %s", exp, err, params) + } + } } + testValidatorCaseMatchFunc(t, priv, kbook, key, val, eol, match) +} + +func testValidatorCaseMatchFunc(t *testing.T, priv ci.PrivKey, kbook pstore.KeyBook, key string, val []byte, eol time.Time, matchf func(*testing.T, error)) { + t.Helper() + validator := IpnsValidator{kbook} + data := val if data == nil { - data, err = proto.Marshal(entry) + p := path.Path("/ipfs/QmfM2r8seH2GiRaC4esTjeraXEachRt8ZsSeGaWTPLyMoG") + entry, err := CreateRoutingEntryData(priv, p, 1, eol) if err != nil { t.Fatal(err) } - } - err = validator.Validate(key, data) - if err != exp { - params := fmt.Sprintf("key: %s\neol: %s\n", key, eol) - if exp == nil { - t.Fatalf("Unexpected error %s for params %s", err, params) - } else if err == nil { - t.Fatalf("Expected error %s but there was no error for params %s", exp, params) - } else { - t.Fatalf("Expected error %s but got %s for params %s", exp, err, params) + + data, err = proto.Marshal(entry) + if err != nil { + t.Fatal(err) } } + + matchf(t, validator.Validate(key, data)) } func TestValidator(t *testing.T) { @@ -74,6 +87,86 @@ func TestValidator(t *testing.T) { testValidatorCase(t, priv, kbook, "/wrong/"+string(id), nil, ts.Add(time.Hour), ErrInvalidPath) } +func mustMarshal(t *testing.T, entry *pb.IpnsEntry) []byte { + t.Helper() + data, err := proto.Marshal(entry) + if err != nil { + t.Fatal(err) + } + return data +} + +func TestEmbeddedPubKeyValidate(t *testing.T) { + goodeol := time.Now().Add(time.Hour) + kbook := pstore.NewPeerstore() + + pth := path.Path("/ipfs/QmfM2r8seH2GiRaC4esTjeraXEachRt8ZsSeGaWTPLyMoG") + + priv, _, _, ipnsk := genKeys(t) + + entry, err := CreateRoutingEntryData(priv, pth, 1, goodeol) + if err != nil { + t.Fatal(err) + } + + testValidatorCase(t, priv, kbook, ipnsk, mustMarshal(t, entry), goodeol, ErrPublicKeyNotFound) + + pubkb, err := priv.GetPublic().Bytes() + if err != nil { + t.Fatal(err) + } + + entry.PubKey = pubkb + testValidatorCase(t, priv, kbook, ipnsk, mustMarshal(t, entry), goodeol, nil) + + entry.PubKey = []byte("probably not a public key") + testValidatorCaseMatchFunc(t, priv, kbook, ipnsk, mustMarshal(t, entry), goodeol, func(t *testing.T, err error) { + if !strings.Contains(err.Error(), "unmarshaling pubkey in record:") { + t.Fatal("expected pubkey unmarshaling to fail") + } + }) + + opriv, _, _, _ := genKeys(t) + wrongkeydata, err := opriv.GetPublic().Bytes() + if err != nil { + t.Fatal(err) + } + + entry.PubKey = wrongkeydata + testValidatorCase(t, priv, kbook, ipnsk, mustMarshal(t, entry), goodeol, ErrPublicKeyMismatch) +} + +func TestPeerIDPubKeyValidate(t *testing.T) { + goodeol := time.Now().Add(time.Hour) + kbook := pstore.NewPeerstore() + + pth := path.Path("/ipfs/QmfM2r8seH2GiRaC4esTjeraXEachRt8ZsSeGaWTPLyMoG") + + sk, pk, err := ci.GenerateEd25519Key(rand.New(rand.NewSource(42))) + if err != nil { + t.Fatal(err) + } + + pid, err := peer.IDFromPublicKey(pk) + if err != nil { + t.Fatal(err) + } + + ipnsk := "/ipns/" + string(pid) + + entry, err := CreateRoutingEntryData(sk, pth, 1, goodeol) + if err != nil { + t.Fatal(err) + } + + dataNoKey, err := proto.Marshal(entry) + if err != nil { + t.Fatal(err) + } + + testValidatorCase(t, sk, kbook, ipnsk, dataNoKey, goodeol, nil) +} + func TestResolverValidation(t *testing.T) { ctx := context.Background() rid := testutil.RandIdentityOrFatal(t) diff --git a/namesys/pb/namesys.pb.go b/namesys/pb/namesys.pb.go index 31e6355d7dd..66626ca7db1 100644 --- a/namesys/pb/namesys.pb.go +++ b/namesys/pb/namesys.pb.go @@ -1,12 +1,12 @@ // Code generated by protoc-gen-gogo. -// source: namesys.proto +// source: namesys/pb/namesys.proto // DO NOT EDIT! /* Package namesys_pb is a generated protocol buffer package. It is generated from these files: - namesys.proto + namesys/pb/namesys.proto It has these top-level messages: IpnsEntry @@ -14,10 +14,12 @@ It has these top-level messages: package namesys_pb import proto "gx/ipfs/QmZ4Qi3GaRbjcx28Sme5eMH7RQjGkt8wHxt2a65oLaeFEV/gogo-protobuf/proto" +import fmt "fmt" import math "math" // Reference imports to suppress errors if they are not otherwise used. var _ = proto.Marshal +var _ = fmt.Errorf var _ = math.Inf type IpnsEntry_ValidityType int32 @@ -52,13 +54,18 @@ func (x *IpnsEntry_ValidityType) UnmarshalJSON(data []byte) error { } type IpnsEntry struct { - Value []byte `protobuf:"bytes,1,req,name=value" json:"value,omitempty"` - Signature []byte `protobuf:"bytes,2,req,name=signature" json:"signature,omitempty"` - ValidityType *IpnsEntry_ValidityType `protobuf:"varint,3,opt,name=validityType,enum=namesys.pb.IpnsEntry_ValidityType" json:"validityType,omitempty"` - Validity []byte `protobuf:"bytes,4,opt,name=validity" json:"validity,omitempty"` - Sequence *uint64 `protobuf:"varint,5,opt,name=sequence" json:"sequence,omitempty"` - Ttl *uint64 `protobuf:"varint,6,opt,name=ttl" json:"ttl,omitempty"` - XXX_unrecognized []byte `json:"-"` + Value []byte `protobuf:"bytes,1,req,name=value" json:"value,omitempty"` + Signature []byte `protobuf:"bytes,2,req,name=signature" json:"signature,omitempty"` + ValidityType *IpnsEntry_ValidityType `protobuf:"varint,3,opt,name=validityType,enum=namesys.pb.IpnsEntry_ValidityType" json:"validityType,omitempty"` + Validity []byte `protobuf:"bytes,4,opt,name=validity" json:"validity,omitempty"` + Sequence *uint64 `protobuf:"varint,5,opt,name=sequence" json:"sequence,omitempty"` + Ttl *uint64 `protobuf:"varint,6,opt,name=ttl" json:"ttl,omitempty"` + // in order for nodes to properly validate a record upon receipt, they need the public + // key associated with it. For old RSA keys, its easiest if we just send this as part of + // the record itself. For newer ed25519 keys, the public key can be embedded in the + // peerID, making this field unnecessary. + PubKey []byte `protobuf:"bytes,7,opt,name=pubKey" json:"pubKey,omitempty"` + XXX_unrecognized []byte `json:"-"` } func (m *IpnsEntry) Reset() { *m = IpnsEntry{} } @@ -107,6 +114,14 @@ func (m *IpnsEntry) GetTtl() uint64 { return 0 } +func (m *IpnsEntry) GetPubKey() []byte { + if m != nil { + return m.PubKey + } + return nil +} + func init() { + proto.RegisterType((*IpnsEntry)(nil), "namesys.pb.IpnsEntry") proto.RegisterEnum("namesys.pb.IpnsEntry_ValidityType", IpnsEntry_ValidityType_name, IpnsEntry_ValidityType_value) } diff --git a/namesys/pb/namesys.proto b/namesys/pb/namesys.proto index d6eaf3243fe..b72d4984387 100644 --- a/namesys/pb/namesys.proto +++ b/namesys/pb/namesys.proto @@ -14,4 +14,10 @@ message IpnsEntry { optional uint64 sequence = 5; optional uint64 ttl = 6; + + // in order for nodes to properly validate a record upon receipt, they need the public + // key associated with it. For old RSA keys, its easiest if we just send this as part of + // the record itself. For newer ed25519 keys, the public key can be embedded in the + // peerID, making this field unnecessary. + optional bytes pubKey = 7; } diff --git a/namesys/publisher.go b/namesys/publisher.go index f5f7d3695ff..80bc10d4771 100644 --- a/namesys/publisher.go +++ b/namesys/publisher.go @@ -240,6 +240,17 @@ func PutRecordToRouting(ctx context.Context, r routing.ValueStore, k ci.PubKey, return err } + // if we can't derive the public key from the peerID, embed the entire pubkey in + // the record to make the verifiers job easier + if extractedPublicKey == nil { + pubkeyBytes, err := k.Bytes() + if err != nil { + return err + } + + entry.PubKey = pubkeyBytes + } + namekey, ipnskey := IpnsKeysForID(id) go func() { @@ -247,6 +258,8 @@ func PutRecordToRouting(ctx context.Context, r routing.ValueStore, k ci.PubKey, }() // Publish the public key if a public key cannot be extracted from the ID + // TODO: once v0.4.16 is widespread enough, we can stop doing this + // and at that point we can even deprecate the /pk/ namespace in the dht if extractedPublicKey == nil { go func() { errs <- PublishPublicKey(ctx, r, namekey, k) diff --git a/namesys/validator.go b/namesys/validator.go index 941d6a66787..94eb912b6be 100644 --- a/namesys/validator.go +++ b/namesys/validator.go @@ -3,11 +3,13 @@ package namesys import ( "bytes" "errors" + "fmt" "time" pb "github.com/ipfs/go-ipfs/namesys/pb" peer "gx/ipfs/QmcJukH2sAFjY3HdBKq35WDzWoL3UUu2gt9wdfqZTUyM74/go-libp2p-peer" pstore "gx/ipfs/QmdeiKhUy1TVGBaKxt7y1QmBDLBdisSrLJ1x58Eoj4PXUh/go-libp2p-peerstore" + ic "gx/ipfs/Qme1knMqwt1hKZbc1BmQFmnm9f36nyQGwXxPGVpVJ9rMK5/go-libp2p-crypto" u "gx/ipfs/QmNiJuT8Ja3hMVpBHXv3Q6dwmperaQ6JjLtpMQgMCD7xvx/go-ipfs-util" record "gx/ipfs/QmTUyK82BVPA6LmSzEJpfEunk9uBaQzWtMsNP917tVj4sT/go-libp2p-record" @@ -42,6 +44,8 @@ var ErrKeyFormat = errors.New("record key could not be parsed into peer ID") // from the peer store var ErrPublicKeyNotFound = errors.New("public key not found in peer store") +var ErrPublicKeyMismatch = errors.New("public key in record did not match expected pubkey") + type IpnsValidator struct { KeyBook pstore.KeyBook } @@ -65,10 +69,10 @@ func (v IpnsValidator) Validate(key string, value []byte) error { log.Debugf("failed to parse ipns record key %s into peer ID", pidString) return ErrKeyFormat } - pubk := v.KeyBook.PubKey(pid) - if pubk == nil { - log.Debugf("public key with hash %s not found in peer store", pid) - return ErrPublicKeyNotFound + + pubk, err := v.getPublicKey(pid, entry) + if err != nil { + return err } // Check the ipns record signature with the public key @@ -94,6 +98,34 @@ func (v IpnsValidator) Validate(key string, value []byte) error { return nil } +func (v IpnsValidator) getPublicKey(pid peer.ID, entry *pb.IpnsEntry) (ic.PubKey, error) { + if entry.PubKey != nil { + pk, err := ic.UnmarshalPublicKey(entry.PubKey) + if err != nil { + log.Debugf("public key in ipns record failed to parse: ", err) + return nil, fmt.Errorf("unmarshaling pubkey in record: %s", err) + } + + expPid, err := peer.IDFromPublicKey(pk) + if err != nil { + return nil, fmt.Errorf("could not regenerate peerID from pubkey: %s", err) + } + + if pid != expPid { + return nil, ErrPublicKeyMismatch + } + + return pk, nil + } + + pubk := v.KeyBook.PubKey(pid) + if pubk == nil { + log.Debugf("public key with hash %s not found in peer store", pid) + return nil, ErrPublicKeyNotFound + } + return pubk, nil +} + // IpnsSelectorFunc selects the best record by checking which has the highest // sequence number and latest EOL func (v IpnsValidator) Select(k string, vals [][]byte) (int, error) {