diff --git a/CHANGELOG.md b/CHANGELOG.md index 5329c317b..11264dfb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,11 @@ The following emojis are used to highlight certain changes: ### Changed +* 🛠 The `ipns` package has been refactored. You should no longer use the direct Protobuf + version of the IPNS Record. Instead, we have a shiny new `ipns.Record` type that wraps + all the required functionality to work the best as possible with IPNS v2 Records. Please + check the [documentation](https://pkg.go.dev/github.com/ipfs/boxo/ipns) for more information. + ### Removed ### Fixed diff --git a/coreiface/name.go b/coreiface/name.go index 0c06183e6..8c3e8e89a 100644 --- a/coreiface/name.go +++ b/coreiface/name.go @@ -5,20 +5,13 @@ import ( "errors" path "github.com/ipfs/boxo/coreiface/path" + "github.com/ipfs/boxo/ipns" "github.com/ipfs/boxo/coreiface/options" ) var ErrResolveFailed = errors.New("could not resolve name") -// IpnsEntry specifies the interface to IpnsEntries -type IpnsEntry interface { - // Name returns IpnsEntry name - Name() string - // Value returns IpnsEntry value - Value() path.Path -} - type IpnsResult struct { path.Path Err error @@ -34,7 +27,7 @@ type IpnsResult struct { // You can use .Key API to list and generate more names and their respective keys. type NameAPI interface { // Publish announces new IPNS name - Publish(ctx context.Context, path path.Path, opts ...options.NamePublishOption) (IpnsEntry, error) + Publish(ctx context.Context, path path.Path, opts ...options.NamePublishOption) (ipns.Name, error) // Resolve attempts to resolve the newest version of the specified name Resolve(ctx context.Context, name string, opts ...options.NameResolveOption) (path.Path, error) diff --git a/coreiface/options/name.go b/coreiface/options/name.go index ae8be9ae9..8e9b5183d 100644 --- a/coreiface/options/name.go +++ b/coreiface/options/name.go @@ -11,12 +11,11 @@ const ( ) type NamePublishSettings struct { - ValidTime time.Duration - Key string - - TTL *time.Duration - - AllowOffline bool + ValidTime time.Duration + Key string + TTL *time.Duration + CompatibleWithV1 bool + AllowOffline bool } type NameResolveSettings struct { @@ -104,6 +103,15 @@ func (nameOpts) TTL(ttl time.Duration) NamePublishOption { } } +// CompatibleWithV1 is an option for [Name.Publish] which specifies if the +// created record should be backwards compatible with V1 IPNS Records. +func (nameOpts) CompatibleWithV1(compatible bool) NamePublishOption { + return func(settings *NamePublishSettings) error { + settings.CompatibleWithV1 = compatible + return nil + } +} + // Cache is an option for Name.Resolve which specifies if cache should be used. // Default value is true func (nameOpts) Cache(cache bool) NameResolveOption { diff --git a/coreiface/options/namesys/opts.go b/coreiface/options/namesys/opts.go index 0cd1ba778..ed568200b 100644 --- a/coreiface/options/namesys/opts.go +++ b/coreiface/options/namesys/opts.go @@ -84,8 +84,9 @@ func ProcessOpts(opts []ResolveOpt) ResolveOpts { // PublishOptions specifies options for publishing an IPNS record. type PublishOptions struct { - EOL time.Time - TTL time.Duration + EOL time.Time + TTL time.Duration + CompatibleWithV1 bool } // DefaultPublishOptions returns the default options for publishing an IPNS record. @@ -113,6 +114,13 @@ func PublishWithTTL(ttl time.Duration) PublishOption { } } +// PublishCompatibleWithV1 sets compatibility with IPNS Records V1. +func PublishCompatibleWithV1(compatible bool) PublishOption { + return func(o *PublishOptions) { + o.CompatibleWithV1 = compatible + } +} + // ProcessPublishOptions converts an array of PublishOpt into a PublishOpts object. func ProcessPublishOptions(opts []PublishOption) PublishOptions { rsopts := DefaultPublishOptions() diff --git a/coreiface/tests/name.go b/coreiface/tests/name.go index 8fa733567..ab55d0425 100644 --- a/coreiface/tests/name.go +++ b/coreiface/tests/name.go @@ -8,12 +8,12 @@ import ( "testing" "time" - path "github.com/ipfs/boxo/coreiface/path" - - "github.com/ipfs/boxo/files" - coreiface "github.com/ipfs/boxo/coreiface" opt "github.com/ipfs/boxo/coreiface/options" + path "github.com/ipfs/boxo/coreiface/path" + "github.com/ipfs/boxo/files" + "github.com/ipfs/boxo/ipns" + "github.com/stretchr/testify/require" ) func (tp *TestSuite) TestName(t *testing.T) { @@ -44,138 +44,68 @@ func (tp *TestSuite) TestPublishResolve(t *testing.T) { defer cancel() init := func() (coreiface.CoreAPI, path.Path) { apis, err := tp.MakeAPISwarm(t, ctx, 5) - if err != nil { - t.Fatal(err) - return nil, nil - } + require.NoError(t, err) api := apis[0] p, err := addTestObject(ctx, api) - if err != nil { - t.Fatal(err) - return nil, nil - } + require.NoError(t, err) return api, p } run := func(t *testing.T, ropts []opt.NameResolveOption) { t.Run("basic", func(t *testing.T) { api, p := init() - e, err := api.Name().Publish(ctx, p) - if err != nil { - t.Fatal(err) - } + name, err := api.Name().Publish(ctx, p) + require.NoError(t, err) self, err := api.Key().Self(ctx) - if err != nil { - t.Fatal(err) - } - - if e.Name() != coreiface.FormatKeyID(self.ID()) { - t.Errorf("expected e.Name to equal '%s', got '%s'", coreiface.FormatKeyID(self.ID()), e.Name()) - } - - if e.Value().String() != p.String() { - t.Errorf("expected paths to match, '%s'!='%s'", e.Value().String(), p.String()) - } - - resPath, err := api.Name().Resolve(ctx, e.Name(), ropts...) - if err != nil { - t.Fatal(err) - } - - if resPath.String() != p.String() { - t.Errorf("expected paths to match, '%s'!='%s'", resPath.String(), p.String()) - } + require.NoError(t, err) + require.Equal(t, name.String(), ipns.NameFromPeer(self.ID()).String()) + + resPath, err := api.Name().Resolve(ctx, name.String(), ropts...) + require.NoError(t, err) + require.Equal(t, p.String(), resPath.String()) }) t.Run("publishPath", func(t *testing.T) { api, p := init() - e, err := api.Name().Publish(ctx, appendPath(p, "/test")) - if err != nil { - t.Fatal(err) - } + name, err := api.Name().Publish(ctx, appendPath(p, "/test")) + require.NoError(t, err) self, err := api.Key().Self(ctx) - if err != nil { - t.Fatal(err) - } - - if e.Name() != coreiface.FormatKeyID(self.ID()) { - t.Errorf("expected e.Name to equal '%s', got '%s'", coreiface.FormatKeyID(self.ID()), e.Name()) - } - - if e.Value().String() != p.String()+"/test" { - t.Errorf("expected paths to match, '%s'!='%s'", e.Value().String(), p.String()) - } - - resPath, err := api.Name().Resolve(ctx, e.Name(), ropts...) - if err != nil { - t.Fatal(err) - } - - if resPath.String() != p.String()+"/test" { - t.Errorf("expected paths to match, '%s'!='%s'", resPath.String(), p.String()+"/test") - } + require.NoError(t, err) + require.Equal(t, name.String(), ipns.NameFromPeer(self.ID()).String()) + + resPath, err := api.Name().Resolve(ctx, name.String(), ropts...) + require.NoError(t, err) + require.Equal(t, p.String()+"/test", resPath.String()) }) t.Run("revolvePath", func(t *testing.T) { api, p := init() - e, err := api.Name().Publish(ctx, p) - if err != nil { - t.Fatal(err) - } + name, err := api.Name().Publish(ctx, p) + require.NoError(t, err) self, err := api.Key().Self(ctx) - if err != nil { - t.Fatal(err) - } - - if e.Name() != coreiface.FormatKeyID(self.ID()) { - t.Errorf("expected e.Name to equal '%s', got '%s'", coreiface.FormatKeyID(self.ID()), e.Name()) - } - - if e.Value().String() != p.String() { - t.Errorf("expected paths to match, '%s'!='%s'", e.Value().String(), p.String()) - } - - resPath, err := api.Name().Resolve(ctx, e.Name()+"/test", ropts...) - if err != nil { - t.Fatal(err) - } - - if resPath.String() != p.String()+"/test" { - t.Errorf("expected paths to match, '%s'!='%s'", resPath.String(), p.String()+"/test") - } + require.NoError(t, err) + require.Equal(t, name.String(), ipns.NameFromPeer(self.ID()).String()) + + resPath, err := api.Name().Resolve(ctx, name.String()+"/test", ropts...) + require.NoError(t, err) + require.Equal(t, p.String()+"/test", resPath.String()) }) t.Run("publishRevolvePath", func(t *testing.T) { api, p := init() - e, err := api.Name().Publish(ctx, appendPath(p, "/a")) - if err != nil { - t.Fatal(err) - } + name, err := api.Name().Publish(ctx, appendPath(p, "/a")) + require.NoError(t, err) self, err := api.Key().Self(ctx) - if err != nil { - t.Fatal(err) - } - - if e.Name() != coreiface.FormatKeyID(self.ID()) { - t.Errorf("expected e.Name to equal '%s', got '%s'", coreiface.FormatKeyID(self.ID()), e.Name()) - } - - if e.Value().String() != p.String()+"/a" { - t.Errorf("expected paths to match, '%s'!='%s'", e.Value().String(), p.String()) - } - - resPath, err := api.Name().Resolve(ctx, e.Name()+"/b", ropts...) - if err != nil { - t.Fatal(err) - } - - if resPath.String() != p.String()+"/a/b" { - t.Errorf("expected paths to match, '%s'!='%s'", resPath.String(), p.String()+"/a/b") - } + require.NoError(t, err) + require.Equal(t, name.String(), ipns.NameFromPeer(self.ID()).String()) + + resPath, err := api.Name().Resolve(ctx, name.String()+"/b", ropts...) + require.NoError(t, err) + require.Equal(t, p.String()+"/a/b", resPath.String()) }) } @@ -192,42 +122,22 @@ func (tp *TestSuite) TestBasicPublishResolveKey(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() apis, err := tp.MakeAPISwarm(t, ctx, 5) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) api := apis[0] k, err := api.Key().Generate(ctx, "foo") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) p, err := addTestObject(ctx, api) - if err != nil { - t.Fatal(err) - } - - e, err := api.Name().Publish(ctx, p, opt.Name.Key(k.Name())) - if err != nil { - t.Fatal(err) - } - - if e.Name() != coreiface.FormatKey(k) { - t.Errorf("expected e.Name to equal %s, got '%s'", e.Name(), coreiface.FormatKey(k)) - } - - if e.Value().String() != p.String() { - t.Errorf("expected paths to match, '%s'!='%s'", e.Value().String(), p.String()) - } + require.NoError(t, err) - resPath, err := api.Name().Resolve(ctx, e.Name()) - if err != nil { - t.Fatal(err) - } + name, err := api.Name().Publish(ctx, p, opt.Name.Key(k.Name())) + require.NoError(t, err) + require.Equal(t, name.String(), ipns.NameFromPeer(k.ID()).String()) - if resPath.String() != p.String() { - t.Errorf("expected paths to match, '%s'!='%s'", resPath.String(), p.String()) - } + resPath, err := api.Name().Resolve(ctx, name.String()) + require.NoError(t, err) + require.Equal(t, p.String(), resPath.String()) } func (tp *TestSuite) TestBasicPublishResolveTimeout(t *testing.T) { @@ -236,39 +146,22 @@ func (tp *TestSuite) TestBasicPublishResolveTimeout(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() apis, err := tp.MakeAPISwarm(t, ctx, 5) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) api := apis[0] p, err := addTestObject(ctx, api) - if err != nil { - t.Fatal(err) - } - - e, err := api.Name().Publish(ctx, p, opt.Name.ValidTime(time.Millisecond*100)) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) self, err := api.Key().Self(ctx) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) - if e.Name() != coreiface.FormatKeyID(self.ID()) { - t.Errorf("expected e.Name to equal '%s', got '%s'", coreiface.FormatKeyID(self.ID()), e.Name()) - } - - if e.Value().String() != p.String() { - t.Errorf("expected paths to match, '%s'!='%s'", e.Value().String(), p.String()) - } + name, err := api.Name().Publish(ctx, p, opt.Name.ValidTime(time.Millisecond*100)) + require.NoError(t, err) + require.Equal(t, name.String(), ipns.NameFromPeer(self.ID()).String()) time.Sleep(time.Second) - _, err = api.Name().Resolve(ctx, e.Name()) - if err == nil { - t.Fatal("Expected an error") - } + _, err = api.Name().Resolve(ctx, name.String()) + require.NoError(t, err) } //TODO: When swarm api is created, add multinode tests diff --git a/coreiface/tests/routing.go b/coreiface/tests/routing.go index 6fca1a003..4e9632475 100644 --- a/coreiface/tests/routing.go +++ b/coreiface/tests/routing.go @@ -5,10 +5,11 @@ import ( "testing" "time" - "github.com/gogo/protobuf/proto" iface "github.com/ipfs/boxo/coreiface" "github.com/ipfs/boxo/coreiface/options" - ipns_pb "github.com/ipfs/boxo/ipns/pb" + "github.com/ipfs/boxo/coreiface/path" + "github.com/ipfs/boxo/ipns" + "github.com/stretchr/testify/require" ) func (tp *TestSuite) TestRouting(t *testing.T) { @@ -24,19 +25,15 @@ func (tp *TestSuite) TestRouting(t *testing.T) { t.Run("TestRoutingPutOffline", tp.TestRoutingPutOffline) } -func (tp *TestSuite) testRoutingPublishKey(t *testing.T, ctx context.Context, api iface.CoreAPI, opts ...options.NamePublishOption) iface.IpnsEntry { +func (tp *TestSuite) testRoutingPublishKey(t *testing.T, ctx context.Context, api iface.CoreAPI, opts ...options.NamePublishOption) (path.Path, ipns.Name) { p, err := addTestObject(ctx, api) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) - entry, err := api.Name().Publish(ctx, p, opts...) - if err != nil { - t.Fatal(err) - } + name, err := api.Name().Publish(ctx, p, opts...) + require.NoError(t, err) time.Sleep(3 * time.Second) - return entry + return p, name } func (tp *TestSuite) TestRoutingGet(t *testing.T) { @@ -44,53 +41,39 @@ func (tp *TestSuite) TestRoutingGet(t *testing.T) { defer cancel() apis, err := tp.MakeAPISwarm(t, ctx, 2) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) // Node 1: publishes an IPNS name - ipnsEntry := tp.testRoutingPublishKey(t, ctx, apis[0]) + p, name := tp.testRoutingPublishKey(t, ctx, apis[0]) // Node 2: retrieves the best value for the IPNS name. - data, err := apis[1].Routing().Get(ctx, "/ipns/"+ipnsEntry.Name()) - if err != nil { - t.Fatal(err) - } - - // Checks if values match. - var entry ipns_pb.IpnsEntry - err = proto.Unmarshal(data, &entry) - if err != nil { - t.Fatal(err) - } - - if string(entry.GetValue()) != ipnsEntry.Value().String() { - t.Fatalf("routing key has wrong value, expected %s, got %s", ipnsEntry.Value().String(), string(entry.GetValue())) - } + data, err := apis[1].Routing().Get(ctx, name.String()) + require.NoError(t, err) + + rec, err := ipns.UnmarshalRecord(data) + require.NoError(t, err) + + val, err := rec.Value() + require.NoError(t, err) + require.Equal(t, p.String(), val.String()) } func (tp *TestSuite) TestRoutingPut(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() apis, err := tp.MakeAPISwarm(t, ctx, 2) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) // Create and publish IPNS entry. - ipnsEntry := tp.testRoutingPublishKey(t, ctx, apis[0]) + _, name := tp.testRoutingPublishKey(t, ctx, apis[0]) // Get valid routing value. - data, err := apis[0].Routing().Get(ctx, "/ipns/"+ipnsEntry.Name()) - if err != nil { - t.Fatal(err) - } + data, err := apis[0].Routing().Get(ctx, name.String()) + require.NoError(t, err) // Put routing value. - err = apis[1].Routing().Put(ctx, "/ipns/"+ipnsEntry.Name(), data) - if err != nil { - t.Fatal(err) - } + err = apis[1].Routing().Put(ctx, name.String(), data) + require.NoError(t, err) } func (tp *TestSuite) TestRoutingPutOffline(t *testing.T) { @@ -99,29 +82,19 @@ func (tp *TestSuite) TestRoutingPutOffline(t *testing.T) { // init a swarm & publish an IPNS entry to get a valid payload apis, err := tp.MakeAPISwarm(t, ctx, 2) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) - ipnsEntry := tp.testRoutingPublishKey(t, ctx, apis[0], options.Name.AllowOffline(true)) - data, err := apis[0].Routing().Get(ctx, "/ipns/"+ipnsEntry.Name()) - if err != nil { - t.Fatal(err) - } + _, name := tp.testRoutingPublishKey(t, ctx, apis[0], options.Name.AllowOffline(true)) + data, err := apis[0].Routing().Get(ctx, name.String()) + require.NoError(t, err) // init our offline node and try to put the payload api, err := tp.makeAPIWithIdentityAndOffline(t, ctx) - if err != nil { - t.Fatal(err) - } - - err = api.Routing().Put(ctx, "/ipns/"+ipnsEntry.Name(), data) - if err == nil { - t.Fatal("this operation should fail because we are offline") - } - - err = api.Routing().Put(ctx, "/ipns/"+ipnsEntry.Name(), data, options.Put.AllowOffline(true)) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) + + err = api.Routing().Put(ctx, name.String(), data) + require.Error(t, err, "this operation should fail because we are offline") + + err = api.Routing().Put(ctx, name.String(), data, options.Put.AllowOffline(true)) + require.NoError(t, err) } diff --git a/examples/gateway/proxy/routing.go b/examples/gateway/proxy/routing.go index 29d112894..f29fbb4d1 100644 --- a/examples/gateway/proxy/routing.go +++ b/examples/gateway/proxy/routing.go @@ -7,10 +7,7 @@ import ( "net/http" "strings" - "github.com/gogo/protobuf/proto" "github.com/ipfs/boxo/ipns" - ipns_pb "github.com/ipfs/boxo/ipns/pb" - "github.com/libp2p/go-libp2p/core/peer" "github.com/libp2p/go-libp2p/core/routing" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" ) @@ -42,13 +39,12 @@ func (ps *proxyRouting) GetValue(ctx context.Context, k string, opts ...routing. return nil, routing.ErrNotSupported } - k = strings.TrimPrefix(k, "/ipns/") - id, err := peer.IDFromBytes([]byte(k)) + name, err := ipns.NameFromRoutingKey([]byte(k)) if err != nil { return nil, err } - return ps.fetch(ctx, id) + return ps.fetch(ctx, name) } func (ps *proxyRouting) SearchValue(ctx context.Context, k string, opts ...routing.Option) (<-chan []byte, error) { @@ -56,8 +52,7 @@ func (ps *proxyRouting) SearchValue(ctx context.Context, k string, opts ...routi return nil, routing.ErrNotSupported } - k = strings.TrimPrefix(k, "/ipns/") - id, err := peer.IDFromBytes([]byte(k)) + name, err := ipns.NameFromRoutingKey([]byte(k)) if err != nil { return nil, err } @@ -65,7 +60,7 @@ func (ps *proxyRouting) SearchValue(ctx context.Context, k string, opts ...routi ch := make(chan []byte) go func() { - v, err := ps.fetch(ctx, id) + v, err := ps.fetch(ctx, name) if err != nil { close(ch) } else { @@ -77,8 +72,8 @@ func (ps *proxyRouting) SearchValue(ctx context.Context, k string, opts ...routi return ch, nil } -func (ps *proxyRouting) fetch(ctx context.Context, id peer.ID) ([]byte, error) { - urlStr := fmt.Sprintf("%s/ipns/%s", ps.gatewayURL, peer.ToCid(id).String()) +func (ps *proxyRouting) fetch(ctx context.Context, name ipns.Name) ([]byte, error) { + urlStr := fmt.Sprintf("%s/ipns/%s", ps.gatewayURL, name.String()) req, err := http.NewRequestWithContext(ctx, http.MethodGet, urlStr, nil) if err != nil { return nil, err @@ -104,13 +99,12 @@ func (ps *proxyRouting) fetch(ctx context.Context, id peer.ID) ([]byte, error) { return nil, err } - var entry ipns_pb.IpnsEntry - err = proto.Unmarshal(rb, &entry) + rec, err := ipns.UnmarshalRecord(rb) if err != nil { return nil, err } - err = ipns.ValidateWithPeerID(id, &entry) + err = ipns.ValidateWithName(rec, name) if err != nil { return nil, err } diff --git a/examples/go.mod b/examples/go.mod index a7aa94964..90f408492 100644 --- a/examples/go.mod +++ b/examples/go.mod @@ -3,16 +3,15 @@ module github.com/ipfs/boxo/examples go 1.19 require ( - github.com/gogo/protobuf v1.3.2 - github.com/ipfs/boxo v0.7.1-0.20230323075409-f4a8dd6614df + github.com/ipfs/boxo v0.8.0 github.com/ipfs/go-block-format v0.1.2 - github.com/ipfs/go-cid v0.4.0 + github.com/ipfs/go-cid v0.4.1 github.com/ipfs/go-datastore v0.6.0 github.com/ipld/go-ipld-prime v0.20.0 github.com/libp2p/go-libp2p v0.26.3 github.com/libp2p/go-libp2p-routing-helpers v0.7.0 github.com/multiformats/go-multiaddr v0.8.0 - github.com/multiformats/go-multicodec v0.8.1 + github.com/multiformats/go-multicodec v0.9.0 github.com/prometheus/client_golang v1.14.0 github.com/stretchr/testify v1.8.2 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.40.0 @@ -45,6 +44,7 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/mock v1.6.0 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/google/gopacket v1.1.19 // indirect @@ -64,7 +64,6 @@ require ( github.com/ipfs/go-ipld-cbor v0.0.6 // indirect github.com/ipfs/go-ipld-format v0.5.0 // indirect github.com/ipfs/go-ipld-legacy v0.2.1 // indirect - github.com/ipfs/go-ipns v0.3.0 // indirect github.com/ipfs/go-log v1.0.5 // indirect github.com/ipfs/go-log/v2 v2.5.1 // indirect github.com/ipfs/go-metrics-interface v0.0.1 // indirect @@ -82,7 +81,7 @@ require ( github.com/libp2p/go-doh-resolver v0.4.0 // indirect github.com/libp2p/go-flow-metrics v0.1.0 // indirect github.com/libp2p/go-libp2p-asn-util v0.2.0 // indirect - github.com/libp2p/go-libp2p-kad-dht v0.21.1 // indirect + github.com/libp2p/go-libp2p-kad-dht v0.23.0 // indirect github.com/libp2p/go-libp2p-kbucket v0.5.0 // indirect github.com/libp2p/go-libp2p-record v0.2.0 // indirect github.com/libp2p/go-msgio v0.3.0 // indirect @@ -102,8 +101,8 @@ require ( github.com/multiformats/go-base36 v0.2.0 // indirect github.com/multiformats/go-multiaddr-dns v0.3.1 // indirect github.com/multiformats/go-multiaddr-fmt v0.1.0 // indirect - github.com/multiformats/go-multibase v0.1.1 // indirect - github.com/multiformats/go-multihash v0.2.1 // indirect + github.com/multiformats/go-multibase v0.2.0 // indirect + github.com/multiformats/go-multihash v0.2.3 // indirect github.com/multiformats/go-multistream v0.4.1 // indirect github.com/multiformats/go-varint v0.0.7 // indirect github.com/onsi/ginkgo/v2 v2.5.1 // indirect @@ -160,9 +159,10 @@ require ( golang.org/x/text v0.7.0 // indirect golang.org/x/tools v0.3.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect + gonum.org/v1/gonum v0.11.0 // indirect google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect google.golang.org/grpc v1.53.0 // indirect - google.golang.org/protobuf v1.28.1 // indirect + google.golang.org/protobuf v1.30.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect lukechampine.com/blake3 v1.1.7 // indirect nhooyr.io/websocket v1.8.7 // indirect diff --git a/examples/go.sum b/examples/go.sum index aa888c694..5930e46ca 100644 --- a/examples/go.sum +++ b/examples/go.sum @@ -284,8 +284,8 @@ github.com/ipfs/go-blockservice v0.5.0 h1:B2mwhhhVQl2ntW2EIpaWPwSCxSuqr5fFA93Ms4 github.com/ipfs/go-cid v0.0.1/go.mod h1:GHWU/WuQdMPmIosc4Yn1bcCT7dSeX4lBafM7iqUPQvM= github.com/ipfs/go-cid v0.0.3/go.mod h1:GHWU/WuQdMPmIosc4Yn1bcCT7dSeX4lBafM7iqUPQvM= github.com/ipfs/go-cid v0.0.6/go.mod h1:6Ux9z5e+HpkQdckYoX1PG/6xqKspzlEIR5SDmgqgC/I= -github.com/ipfs/go-cid v0.4.0 h1:a4pdZq0sx6ZSxbCizebnKiMCx/xI/aBBFlB73IgH4rA= -github.com/ipfs/go-cid v0.4.0/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk= +github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s= +github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk= github.com/ipfs/go-datastore v0.6.0 h1:JKyz+Gvz1QEZw0LsX1IBn+JFCJQH4SJVFtM4uWU0Myk= github.com/ipfs/go-datastore v0.6.0/go.mod h1:rt5M3nNbSO/8q1t4LNkLyUwRs8HupMeN/8O4Vn9YAT8= github.com/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ6cOk= @@ -312,8 +312,6 @@ github.com/ipfs/go-ipld-format v0.5.0 h1:WyEle9K96MSrvr47zZHKKcDxJ/vlpET6PSiQsAF github.com/ipfs/go-ipld-format v0.5.0/go.mod h1:ImdZqJQaEouMjCvqCe0ORUS+uoBmf7Hf+EO/jh+nk3M= github.com/ipfs/go-ipld-legacy v0.2.1 h1:mDFtrBpmU7b//LzLSypVrXsD8QxkEWxu5qVxN99/+tk= github.com/ipfs/go-ipld-legacy v0.2.1/go.mod h1:782MOUghNzMO2DER0FlBR94mllfdCJCkTtDtPM51otM= -github.com/ipfs/go-ipns v0.3.0 h1:ai791nTgVo+zTuq2bLvEGmWP1M0A6kGTXUsgv/Yq67A= -github.com/ipfs/go-ipns v0.3.0/go.mod h1:3cLT2rbvgPZGkHJoPO1YMJeh6LtkxopCkKFcio/wE24= github.com/ipfs/go-log v1.0.5 h1:2dOuUCB1Z7uoczMWgAyDck5JLb72zHzrMnGnCNNbvY8= github.com/ipfs/go-log v1.0.5/go.mod h1:j0b8ZoR+7+R99LD9jZ6+AJsrzkPbSXbZfGakb5JPtIo= github.com/ipfs/go-log/v2 v2.1.3/go.mod h1:/8d0SH3Su5Ooc31QlL1WysJhvyOTDCjcCZ9Axpmri6g= @@ -393,8 +391,8 @@ github.com/libp2p/go-libp2p v0.26.3 h1:6g/psubqwdaBqNNoidbRKSTBEYgaOuKBhHl8Q5tO+ github.com/libp2p/go-libp2p v0.26.3/go.mod h1:x75BN32YbwuY0Awm2Uix4d4KOz+/4piInkp4Wr3yOo8= github.com/libp2p/go-libp2p-asn-util v0.2.0 h1:rg3+Os8jbnO5DxkC7K/Utdi+DkY3q/d1/1q+8WeNAsw= github.com/libp2p/go-libp2p-asn-util v0.2.0/go.mod h1:WoaWxbHKBymSN41hWSq/lGKJEca7TNm58+gGJi2WsLI= -github.com/libp2p/go-libp2p-kad-dht v0.21.1 h1:xpfp8/t9+X2ip1l8Umap1/UGNnJ3RHJgKGAEsnRAlTo= -github.com/libp2p/go-libp2p-kad-dht v0.21.1/go.mod h1:Oy8wvbdjpB70eS5AaFaI68tOtrdo3KylTvXDjikxqFo= +github.com/libp2p/go-libp2p-kad-dht v0.23.0 h1:sxE6LxLopp79eLeV695n7+c77V/Vn4AMF28AdM/XFqM= +github.com/libp2p/go-libp2p-kad-dht v0.23.0/go.mod h1:oO5N308VT2msnQI6qi5M61wzPmJYg7Tr9e16m5n7uDU= github.com/libp2p/go-libp2p-kbucket v0.5.0 h1:g/7tVm8ACHDxH29BGrpsQlnNeu+6OF1A9bno/4/U1oA= github.com/libp2p/go-libp2p-kbucket v0.5.0/go.mod h1:zGzGCpQd78b5BNTDGHNDLaTt9aDK/A02xeZp9QeFC4U= github.com/libp2p/go-libp2p-record v0.2.0 h1:oiNUOCWno2BFuxt3my4i1frNrt7PerzB3queqa1NkQ0= @@ -469,16 +467,16 @@ github.com/multiformats/go-multiaddr-fmt v0.1.0 h1:WLEFClPycPkp4fnIzoFoV9FVd49/e github.com/multiformats/go-multiaddr-fmt v0.1.0/go.mod h1:hGtDIW4PU4BqJ50gW2quDuPVjyWNZxToGUh/HwTZYJo= github.com/multiformats/go-multibase v0.0.1/go.mod h1:bja2MqRZ3ggyXtZSEDKpl0uO/gviWFaSteVbWT51qgs= github.com/multiformats/go-multibase v0.0.3/go.mod h1:5+1R4eQrT3PkYZ24C3W2Ue2tPwIdYQD509ZjSb5y9Oc= -github.com/multiformats/go-multibase v0.1.1 h1:3ASCDsuLX8+j4kx58qnJ4YFq/JWTJpCyDW27ztsVTOI= -github.com/multiformats/go-multibase v0.1.1/go.mod h1:ZEjHE+IsUrgp5mhlEAYjMtZwK1k4haNkcaPg9aoe1a8= -github.com/multiformats/go-multicodec v0.8.1 h1:ycepHwavHafh3grIbR1jIXnKCsFm0fqsfEOsJ8NtKE8= -github.com/multiformats/go-multicodec v0.8.1/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI16i14xuaojr/H7Ai54k= +github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g= +github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= +github.com/multiformats/go-multicodec v0.9.0 h1:pb/dlPnzee/Sxv/j4PmkDRxCOi3hXTz3IbPKOXWJkmg= +github.com/multiformats/go-multicodec v0.9.0/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI16i14xuaojr/H7Ai54k= github.com/multiformats/go-multihash v0.0.1/go.mod h1:w/5tugSrLEbWqlcgJabL3oHFKTwfvkofsjW2Qa1ct4U= github.com/multiformats/go-multihash v0.0.8/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa63epEDmG8nTduyAew= github.com/multiformats/go-multihash v0.0.10/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa63epEDmG8nTduyAew= github.com/multiformats/go-multihash v0.0.13/go.mod h1:VdAWLKTwram9oKAatUcLxBNUjdtcVwxObEQBtRfuyjc= -github.com/multiformats/go-multihash v0.2.1 h1:aem8ZT0VA2nCHHk7bPJ1BjUbHNciqZC/d16Vve9l108= -github.com/multiformats/go-multihash v0.2.1/go.mod h1:WxoMcYG85AZVQUyRyo9s4wULvW5qrI9vb2Lt6evduFc= +github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= +github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= github.com/multiformats/go-multistream v0.4.1 h1:rFy0Iiyn3YT0asivDUIR05leAdwZq3de4741sbiSdfo= github.com/multiformats/go-multistream v0.4.1/go.mod h1:Mz5eykRVAjJWckE2U78c6xqdtyNUEhKSM0Lwar2p77Q= github.com/multiformats/go-varint v0.0.1/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE= @@ -963,6 +961,8 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +gonum.org/v1/gonum v0.11.0 h1:f1IJhK4Km5tBJmaiJXtk/PkL4cdVX6J+tGiM187uT5E= +gonum.org/v1/gonum v0.11.0/go.mod h1:fSG4YDCxxUZQJ7rKsQrj0gMOg00Il0Z96/qMA4bVQhA= google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= @@ -1062,8 +1062,8 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= -google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/gateway/handler.go b/gateway/handler.go index 89c64a983..bfb0c7e37 100644 --- a/gateway/handler.go +++ b/gateway/handler.go @@ -18,6 +18,7 @@ import ( ipath "github.com/ipfs/boxo/coreiface/path" "github.com/ipfs/boxo/gateway/assets" + "github.com/ipfs/boxo/ipns" cid "github.com/ipfs/go-cid" logging "github.com/ipfs/go-log/v2" "github.com/libp2p/go-libp2p/core/peer" @@ -342,8 +343,7 @@ func (i *handler) isTrustlessRequest(contentPath ipath.Path, responseFormat stri } // Only valid, cryptographically verifiable IPNS record names (no DNSLink on trustless gateways) - // TODO: replace with ipns.Name as part of https://github.com/ipfs/specs/issues/376 - if _, err := peer.Decode(pathComponents[1]); err != nil { + if _, err := ipns.NameFromString(pathComponents[1]); err != nil { return false } diff --git a/gateway/handler_ipns_record.go b/gateway/handler_ipns_record.go index 66023f0d3..40804e005 100644 --- a/gateway/handler_ipns_record.go +++ b/gateway/handler_ipns_record.go @@ -49,7 +49,7 @@ func (i *handler) serveIpnsRecord(ctx context.Context, w http.ResponseWriter, r return false } - record, err := ipns.UnmarshalIpnsEntry(rawRecord) + record, err := ipns.UnmarshalRecord(rawRecord) if err != nil { i.webError(w, r, err, http.StatusInternalServerError) return false @@ -69,9 +69,8 @@ func (i *handler) serveIpnsRecord(ctx context.Context, w http.ResponseWriter, r return false } - if record.Ttl != nil { - seconds := int(time.Duration(*record.Ttl).Seconds()) - w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d", seconds)) + if ttl, err := record.TTL(); err == nil { + w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d", int(ttl.Seconds()))) } else { w.Header().Set("Last-Modified", time.Now().UTC().Format(http.TimeFormat)) } diff --git a/go.mod b/go.mod index 49fce9b75..55b77e0b1 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/ipfs/bbloom v0.0.4 github.com/ipfs/go-bitfield v1.1.0 github.com/ipfs/go-block-format v0.1.2 - github.com/ipfs/go-cid v0.4.0 + github.com/ipfs/go-cid v0.4.1 github.com/ipfs/go-cidutil v0.1.0 github.com/ipfs/go-datastore v0.6.0 github.com/ipfs/go-detect-race v0.0.1 @@ -38,7 +38,7 @@ require ( github.com/libp2p/go-buffer-pool v0.1.0 github.com/libp2p/go-doh-resolver v0.4.0 github.com/libp2p/go-libp2p v0.26.3 - github.com/libp2p/go-libp2p-kad-dht v0.21.1 + github.com/libp2p/go-libp2p-kad-dht v0.23.0 github.com/libp2p/go-libp2p-record v0.2.0 github.com/libp2p/go-libp2p-routing-helpers v0.7.0 github.com/libp2p/go-libp2p-testing v0.12.0 @@ -48,9 +48,9 @@ require ( github.com/multiformats/go-base32 v0.1.0 github.com/multiformats/go-multiaddr v0.8.0 github.com/multiformats/go-multiaddr-dns v0.3.1 - github.com/multiformats/go-multibase v0.1.1 - github.com/multiformats/go-multicodec v0.8.1 - github.com/multiformats/go-multihash v0.2.1 + github.com/multiformats/go-multibase v0.2.0 + github.com/multiformats/go-multicodec v0.9.0 + github.com/multiformats/go-multihash v0.2.3 github.com/multiformats/go-multistream v0.4.1 github.com/multiformats/go-varint v0.0.7 github.com/petar/GoLLRB v0.0.0-20210522233825-ae3b015fd3e9 @@ -79,6 +79,7 @@ require ( golang.org/x/oauth2 v0.4.0 golang.org/x/sync v0.1.0 golang.org/x/sys v0.6.0 + google.golang.org/protobuf v1.30.0 ) require ( @@ -112,7 +113,6 @@ require ( github.com/ipfs/go-ipfs-ds-help v1.1.0 // indirect github.com/ipfs/go-ipfs-pq v0.0.3 // indirect github.com/ipfs/go-ipfs-util v0.0.2 // indirect - github.com/ipfs/go-ipns v0.3.0 // indirect github.com/ipfs/go-log v1.0.5 // indirect github.com/ipfs/go-unixfs v0.4.5 // indirect github.com/ipld/go-car/v2 v2.9.1-0.20230325062757-fff0e4397a3d // indirect @@ -167,10 +167,10 @@ require ( golang.org/x/text v0.7.0 // indirect golang.org/x/tools v0.3.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect + gonum.org/v1/gonum v0.11.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect google.golang.org/grpc v1.53.0 // indirect - google.golang.org/protobuf v1.28.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect lukechampine.com/blake3 v1.1.7 // indirect nhooyr.io/websocket v1.8.7 // indirect diff --git a/go.sum b/go.sum index c05d4d76f..c3361eb78 100644 --- a/go.sum +++ b/go.sum @@ -289,8 +289,8 @@ github.com/ipfs/go-cid v0.0.1/go.mod h1:GHWU/WuQdMPmIosc4Yn1bcCT7dSeX4lBafM7iqUP github.com/ipfs/go-cid v0.0.3/go.mod h1:GHWU/WuQdMPmIosc4Yn1bcCT7dSeX4lBafM7iqUPQvM= github.com/ipfs/go-cid v0.0.5/go.mod h1:plgt+Y5MnOey4vO4UlUazGqdbEXuFYitED67FexhXog= github.com/ipfs/go-cid v0.0.6/go.mod h1:6Ux9z5e+HpkQdckYoX1PG/6xqKspzlEIR5SDmgqgC/I= -github.com/ipfs/go-cid v0.4.0 h1:a4pdZq0sx6ZSxbCizebnKiMCx/xI/aBBFlB73IgH4rA= -github.com/ipfs/go-cid v0.4.0/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk= +github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s= +github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk= github.com/ipfs/go-cidutil v0.1.0 h1:RW5hO7Vcf16dplUU60Hs0AKDkQAVPVplr7lk97CFL+Q= github.com/ipfs/go-cidutil v0.1.0/go.mod h1:e7OEVBMIv9JaOxt9zaGEmAoSlXW9jdFZ5lP/0PwcfpA= github.com/ipfs/go-datastore v0.5.0/go.mod h1:9zhEApYMTl17C8YDp7JmU7sQZi2/wqiYh73hakZ90Bk= @@ -325,8 +325,6 @@ github.com/ipfs/go-ipld-format v0.5.0 h1:WyEle9K96MSrvr47zZHKKcDxJ/vlpET6PSiQsAF github.com/ipfs/go-ipld-format v0.5.0/go.mod h1:ImdZqJQaEouMjCvqCe0ORUS+uoBmf7Hf+EO/jh+nk3M= github.com/ipfs/go-ipld-legacy v0.2.1 h1:mDFtrBpmU7b//LzLSypVrXsD8QxkEWxu5qVxN99/+tk= github.com/ipfs/go-ipld-legacy v0.2.1/go.mod h1:782MOUghNzMO2DER0FlBR94mllfdCJCkTtDtPM51otM= -github.com/ipfs/go-ipns v0.3.0 h1:ai791nTgVo+zTuq2bLvEGmWP1M0A6kGTXUsgv/Yq67A= -github.com/ipfs/go-ipns v0.3.0/go.mod h1:3cLT2rbvgPZGkHJoPO1YMJeh6LtkxopCkKFcio/wE24= github.com/ipfs/go-log v0.0.1/go.mod h1:kL1d2/hzSpI0thNYjiKfjanbVNU+IIGA/WnNESY9leM= github.com/ipfs/go-log v1.0.5 h1:2dOuUCB1Z7uoczMWgAyDck5JLb72zHzrMnGnCNNbvY8= github.com/ipfs/go-log v1.0.5/go.mod h1:j0b8ZoR+7+R99LD9jZ6+AJsrzkPbSXbZfGakb5JPtIo= @@ -413,8 +411,8 @@ github.com/libp2p/go-libp2p v0.26.3 h1:6g/psubqwdaBqNNoidbRKSTBEYgaOuKBhHl8Q5tO+ github.com/libp2p/go-libp2p v0.26.3/go.mod h1:x75BN32YbwuY0Awm2Uix4d4KOz+/4piInkp4Wr3yOo8= github.com/libp2p/go-libp2p-asn-util v0.2.0 h1:rg3+Os8jbnO5DxkC7K/Utdi+DkY3q/d1/1q+8WeNAsw= github.com/libp2p/go-libp2p-asn-util v0.2.0/go.mod h1:WoaWxbHKBymSN41hWSq/lGKJEca7TNm58+gGJi2WsLI= -github.com/libp2p/go-libp2p-kad-dht v0.21.1 h1:xpfp8/t9+X2ip1l8Umap1/UGNnJ3RHJgKGAEsnRAlTo= -github.com/libp2p/go-libp2p-kad-dht v0.21.1/go.mod h1:Oy8wvbdjpB70eS5AaFaI68tOtrdo3KylTvXDjikxqFo= +github.com/libp2p/go-libp2p-kad-dht v0.23.0 h1:sxE6LxLopp79eLeV695n7+c77V/Vn4AMF28AdM/XFqM= +github.com/libp2p/go-libp2p-kad-dht v0.23.0/go.mod h1:oO5N308VT2msnQI6qi5M61wzPmJYg7Tr9e16m5n7uDU= github.com/libp2p/go-libp2p-kbucket v0.5.0 h1:g/7tVm8ACHDxH29BGrpsQlnNeu+6OF1A9bno/4/U1oA= github.com/libp2p/go-libp2p-kbucket v0.5.0/go.mod h1:zGzGCpQd78b5BNTDGHNDLaTt9aDK/A02xeZp9QeFC4U= github.com/libp2p/go-libp2p-record v0.2.0 h1:oiNUOCWno2BFuxt3my4i1frNrt7PerzB3queqa1NkQ0= @@ -492,16 +490,16 @@ github.com/multiformats/go-multiaddr-fmt v0.1.0 h1:WLEFClPycPkp4fnIzoFoV9FVd49/e github.com/multiformats/go-multiaddr-fmt v0.1.0/go.mod h1:hGtDIW4PU4BqJ50gW2quDuPVjyWNZxToGUh/HwTZYJo= github.com/multiformats/go-multibase v0.0.1/go.mod h1:bja2MqRZ3ggyXtZSEDKpl0uO/gviWFaSteVbWT51qgs= github.com/multiformats/go-multibase v0.0.3/go.mod h1:5+1R4eQrT3PkYZ24C3W2Ue2tPwIdYQD509ZjSb5y9Oc= -github.com/multiformats/go-multibase v0.1.1 h1:3ASCDsuLX8+j4kx58qnJ4YFq/JWTJpCyDW27ztsVTOI= -github.com/multiformats/go-multibase v0.1.1/go.mod h1:ZEjHE+IsUrgp5mhlEAYjMtZwK1k4haNkcaPg9aoe1a8= -github.com/multiformats/go-multicodec v0.8.1 h1:ycepHwavHafh3grIbR1jIXnKCsFm0fqsfEOsJ8NtKE8= -github.com/multiformats/go-multicodec v0.8.1/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI16i14xuaojr/H7Ai54k= +github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g= +github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= +github.com/multiformats/go-multicodec v0.9.0 h1:pb/dlPnzee/Sxv/j4PmkDRxCOi3hXTz3IbPKOXWJkmg= +github.com/multiformats/go-multicodec v0.9.0/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI16i14xuaojr/H7Ai54k= github.com/multiformats/go-multihash v0.0.1/go.mod h1:w/5tugSrLEbWqlcgJabL3oHFKTwfvkofsjW2Qa1ct4U= github.com/multiformats/go-multihash v0.0.8/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa63epEDmG8nTduyAew= github.com/multiformats/go-multihash v0.0.10/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa63epEDmG8nTduyAew= github.com/multiformats/go-multihash v0.0.13/go.mod h1:VdAWLKTwram9oKAatUcLxBNUjdtcVwxObEQBtRfuyjc= -github.com/multiformats/go-multihash v0.2.1 h1:aem8ZT0VA2nCHHk7bPJ1BjUbHNciqZC/d16Vve9l108= -github.com/multiformats/go-multihash v0.2.1/go.mod h1:WxoMcYG85AZVQUyRyo9s4wULvW5qrI9vb2Lt6evduFc= +github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= +github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= github.com/multiformats/go-multistream v0.4.1 h1:rFy0Iiyn3YT0asivDUIR05leAdwZq3de4741sbiSdfo= github.com/multiformats/go-multistream v0.4.1/go.mod h1:Mz5eykRVAjJWckE2U78c6xqdtyNUEhKSM0Lwar2p77Q= github.com/multiformats/go-varint v0.0.1/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE= @@ -982,6 +980,8 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +gonum.org/v1/gonum v0.11.0 h1:f1IJhK4Km5tBJmaiJXtk/PkL4cdVX6J+tGiM187uT5E= +gonum.org/v1/gonum v0.11.0/go.mod h1:fSG4YDCxxUZQJ7rKsQrj0gMOg00Il0Z96/qMA4bVQhA= google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= @@ -1083,8 +1083,8 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= -google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/ipns/README.md b/ipns/README.md index 0afa0be00..502f6dded 100644 --- a/ipns/README.md +++ b/ipns/README.md @@ -1,31 +1,48 @@ -## Usage +# IPNS -To create a new IPNS record: +> A reference implementation of the IPNS Record and Verification specification. + +## Documentation + +- Go Documentation: https://pkg.go.dev/github.com/ipfs/boxo/ipns +- IPNS Record Specification: https://specs.ipfs.tech/ipns/ipns-record/ + +## Example + +Here's an example on how to create an IPNS Record: ```go import ( + "crypto/rand" "time" - ipns "github.com/ipfs/boxo/ipns" - crypto "github.com/libp2p/go-libp2p/core/crypto" + "github.com/ipfs/boxo/ipns" + "github.com/ipfs/boxo/path" + ic "github.com/libp2p/go-libp2p/core/crypto" ) -// Generate a private key to sign the IPNS record with. Most of the time, -// however, you'll want to retrieve an already-existing key from IPFS using the -// go-ipfs/core/coreapi CoreAPI.KeyAPI() interface. -privateKey, publicKey, err := crypto.GenerateKeyPair(crypto.RSA, 2048) -if err != nil { - panic(err) -} +func main() { + // Create a private key to sign your IPNS record. Most of the time, you + // will want to retrieve an already-existing key from Kubo, for example. + sk, _, err := ic.GenerateEd25519Key(rand.Reader) + if err != nil { + panic(err) + } -// Create an IPNS record that expires in one hour and points to the IPFS address -// /ipfs/Qme1knMqwt1hKZbc1BmQFmnm9f36nyQGwXxPGVpVJ9rMK5 -ipnsRecord, err := ipns.Create(privateKey, []byte("/ipfs/Qme1knMqwt1hKZbc1BmQFmnm9f36nyQGwXxPGVpVJ9rMK5"), 0, time.Now().Add(1*time.Hour)) -if err != nil { - panic(err) -} -``` + // Define the path this record will point to. + path := path.FromString("/ipfs/bafkqac3jobxhgidsn5rww4yk") + + // Until when the record is valid. + eol := time.Now().Add(time.Hour) + + // For how long should caches cache the record. + ttl := time.Second * 20 -Once you have the record, you’ll need to use IPFS to *publish* it. + record, err := ipns.NewRecord(sk, path, 1, eol, ttl) + if err != nil { + panic(err) + } -There are several other major operations you can do with `go-ipns`. Check out the [API docs](https://pkg.go.dev/github.com/ipfs/boxo/ipns) or look at the tests in this repo for examples. + // Now you have an IPNS Record. +} +``` diff --git a/ipns/errors.go b/ipns/errors.go index d78aafffa..7c476484c 100644 --- a/ipns/errors.go +++ b/ipns/errors.go @@ -4,41 +4,45 @@ import ( "errors" ) -// ErrExpiredRecord should be returned when an ipns record is -// invalid due to being too old -var ErrExpiredRecord = errors.New("expired record") +// MaxRecordSize is the IPNS Record [size limit]. +// +// [size limit]: https://specs.ipfs.tech/ipns/ipns-record/#record-size-limit +const MaxRecordSize int = 10 << (10 * 1) -// ErrUnrecognizedValidity is returned when an IpnsRecord has an -// unknown validity type. -var ErrUnrecognizedValidity = errors.New("unrecognized validity type") +// ErrExpiredRecord is returned when an IPNS [Record] is invalid due to being expired. +var ErrExpiredRecord = errors.New("the IPNS record is expired") -// ErrInvalidPath should be returned when an ipns record path -// is not in a valid format -var ErrInvalidPath = errors.New("record path invalid") +// ErrUnrecognizedValidity is returned when an IPNS [Record] has an unknown validity type. +var ErrUnrecognizedValidity = errors.New("the IPNS record contains an unrecognized validity type") -// ErrSignature should be returned when an ipns record fails -// signature verification -var ErrSignature = errors.New("record signature verification failed") +// ErrInvalidValidity is returned when an IPNS [Record] has a known validity type, +// but the validity value is invalid. +var ErrInvalidValidity = errors.New("the IPNS record contains an invalid validity") -// ErrKeyFormat should be returned when an ipns record key is -// incorrectly formatted (not a peer ID) -var ErrKeyFormat = errors.New("record key could not be parsed into peer ID") +// ErrRecordSize is returned when an IPNS [Record] exceeds the maximum size. +var ErrRecordSize = errors.New("the IPNS record exceeds allowed size limit") -// ErrPublicKeyNotFound should be returned when the public key -// corresponding to the ipns record path cannot be retrieved -// from the peer store -var ErrPublicKeyNotFound = errors.New("public key not found in peer store") +// ErrDataMissing is returned when an IPNS [Record] is missing the data field. +var ErrDataMissing = errors.New("the IPNS record is missing the data field") -// ErrPublicKeyMismatch should be returned when the public key embedded in the -// record doesn't match the expected public key. -var ErrPublicKeyMismatch = errors.New("public key in record did not match expected pubkey") +// ErrInvalidRecord is returned when an IPNS [Record] is malformed. +var ErrInvalidRecord = errors.New("the IPNS record is malformed") -// ErrBadRecord should be returned when an ipns record cannot be unmarshalled -var ErrBadRecord = errors.New("record could not be unmarshalled") +// ErrPublicKeyMismatch is return when the public key embedded in an IPNS [Record] +// does not match the expected public key. +var ErrPublicKeyMismatch = errors.New("the IPNS record public key does not match the expected public key") -// 10 KiB limit defined in https://github.com/ipfs/specs/pull/319 -const MaxRecordSize int = 10 << (10 * 1) +// ErrPublicKeyNotFound is returned when the public key is not found. +var ErrPublicKeyNotFound = errors.New("public key not found") + +// ErrInvalidPublicKey is returned when an IPNS [Record] has an invalid public key, +var ErrInvalidPublicKey = errors.New("the IPNS record public key invalid") + +// ErrSignature is returned when an IPNS [Record] fails signature verification. +var ErrSignature = errors.New("the IPNS record signature verification failed") + +// ErrInvalidName is returned when an IPNS [Name] is invalid. +var ErrInvalidName = errors.New("the IPNS name is invalid") -// ErrRecordSize should be returned when an ipns record is -// invalid due to being too big -var ErrRecordSize = errors.New("record exceeds allowed size limit") +// ErrInvalidPath is returned when an IPNS Record has an invalid path. +var ErrInvalidPath = errors.New("the IPNS record path invalid") diff --git a/ipns/examples/embed.go b/ipns/examples/embed.go deleted file mode 100644 index 1f33d514d..000000000 --- a/ipns/examples/embed.go +++ /dev/null @@ -1,27 +0,0 @@ -package examples - -import ( - "time" - - pb "github.com/ipfs/boxo/ipns/pb" - - "github.com/ipfs/boxo/ipns" - "github.com/libp2p/go-libp2p/core/crypto" -) - -// CreateEntryWithEmbed shows how you can create an IPNS entry -// and embed it with a public key. For ed25519 keys this is not needed -// so attempting to embed with an ed25519 key, will not actually embed the key -func CreateEntryWithEmbed(ipfsPath string, publicKey crypto.PubKey, privateKey crypto.PrivKey) (*pb.IpnsEntry, error) { - ipfsPathByte := []byte(ipfsPath) - eol := time.Now().Add(time.Hour * 48) - entry, err := ipns.Create(privateKey, ipfsPathByte, 1, eol, 0) - if err != nil { - return nil, err - } - err = ipns.EmbedPublicKey(publicKey, entry) - if err != nil { - return nil, err - } - return entry, nil -} diff --git a/ipns/examples/examples_test.go b/ipns/examples/examples_test.go deleted file mode 100644 index 0fe182d9f..000000000 --- a/ipns/examples/examples_test.go +++ /dev/null @@ -1,60 +0,0 @@ -package examples_test - -import ( - "testing" - - "github.com/ipfs/boxo/ipns/examples" - "github.com/libp2p/go-libp2p/core/crypto" -) - -var testPath = "/ipfs/Qme1knMqwt1hKZbc1BmQFmnm9f36nyQGwXxPGVpVJ9rMK5" - -func TestKeyGeneration(t *testing.T) { - _, err := generateRSAKey() - if err != nil { - t.Error(err) - } - - _, err = generateEDKey() - if err != nil { - t.Error(err) - } -} - -func TestEmbeddedEntryCreation(t *testing.T) { - rk, err := generateRSAKey() - if err != nil { - t.Fatal(err) - } - - ek, err := generateEDKey() - if err != nil { - t.Fatal(err) - } - _, err = examples.CreateEntryWithEmbed(testPath, rk.GetPublic(), rk) - if err != nil { - t.Error(err) - } - - _, err = examples.CreateEntryWithEmbed(testPath, ek.GetPublic(), ek) - if err != nil { - t.Error(err) - } - -} -func generateRSAKey() (crypto.PrivKey, error) { - k, err := examples.GenerateRSAKeyPair(2048) - if err != nil { - return nil, err - } - return k, nil -} - -func generateEDKey() (crypto.PrivKey, error) { - // ED25519 uses 256bit keys, and ignore the bit param - k, err := examples.GenerateEDKeyPair() - if err != nil { - return nil, err - } - return k, nil -} diff --git a/ipns/examples/key.go b/ipns/examples/key.go deleted file mode 100644 index 94f219b8d..000000000 --- a/ipns/examples/key.go +++ /dev/null @@ -1,24 +0,0 @@ -package examples - -import ( - "github.com/libp2p/go-libp2p/core/crypto" -) - -// GenerateRSAKeyPair is used to generate an RSA key pair -func GenerateRSAKeyPair(bits int) (crypto.PrivKey, error) { - priv, _, err := crypto.GenerateKeyPair(crypto.RSA, bits) - if err != nil { - return nil, err - } - return priv, nil -} - -// GenerateEDKeyPair is used to generate an ED25519 keypair -func GenerateEDKeyPair() (crypto.PrivKey, error) { - // ED25519 ignores the bit param and uses 256bit keys - priv, _, err := crypto.GenerateKeyPair(crypto.Ed25519, 256) - if err != nil { - return nil, err - } - return priv, nil -} diff --git a/ipns/ipns.go b/ipns/ipns.go deleted file mode 100644 index db92b39ff..000000000 --- a/ipns/ipns.go +++ /dev/null @@ -1,419 +0,0 @@ -package ipns - -import ( - "bytes" - "fmt" - "sort" - "time" - - "github.com/multiformats/go-multicodec" - "github.com/pkg/errors" - - "github.com/ipld/go-ipld-prime" - _ "github.com/ipld/go-ipld-prime/codec/dagcbor" // used to import the DagCbor encoder/decoder - ipldcodec "github.com/ipld/go-ipld-prime/multicodec" - basicnode "github.com/ipld/go-ipld-prime/node/basic" - - "github.com/gogo/protobuf/proto" - - pb "github.com/ipfs/boxo/ipns/pb" - - u "github.com/ipfs/boxo/util" - ic "github.com/libp2p/go-libp2p/core/crypto" - "github.com/libp2p/go-libp2p/core/peer" -) - -const ( - validity = "Validity" - validityType = "ValidityType" - value = "Value" - sequence = "Sequence" - ttl = "TTL" -) - -// Create creates a new IPNS entry and signs it with the given private key. -// -// This function does not embed the public key. If you want to do that, use -// `EmbedPublicKey`. -func Create(sk ic.PrivKey, val []byte, seq uint64, eol time.Time, ttl time.Duration) (*pb.IpnsEntry, error) { - entry := new(pb.IpnsEntry) - - entry.Value = val - typ := pb.IpnsEntry_EOL - entry.ValidityType = &typ - entry.Sequence = &seq - entry.Validity = []byte(u.FormatRFC3339(eol)) - - ttlNs := uint64(ttl.Nanoseconds()) - entry.Ttl = proto.Uint64(ttlNs) - - cborData, err := createCborDataForIpnsEntry(entry) - if err != nil { - return nil, err - } - entry.Data = cborData - - // For now we still create V1 signatures. These are deprecated, and not - // used during verification anymore (Validate func requires SignatureV2), - // but setting it here allows legacy nodes (e.g., go-ipfs < v0.9.0) to - // still resolve IPNS published by modern nodes. - sig1, err := sk.Sign(ipnsEntryDataForSigV1(entry)) - if err != nil { - return nil, errors.Wrap(err, "could not compute signature data") - } - entry.SignatureV1 = sig1 - - sig2Data, err := ipnsEntryDataForSigV2(entry) - if err != nil { - return nil, err - } - sig2, err := sk.Sign(sig2Data) - if err != nil { - return nil, err - } - entry.SignatureV2 = sig2 - - return entry, nil -} - -func createCborDataForIpnsEntry(e *pb.IpnsEntry) ([]byte, error) { - m := make(map[string]ipld.Node) - var keys []string - m[value] = basicnode.NewBytes(e.GetValue()) - keys = append(keys, value) - - m[validity] = basicnode.NewBytes(e.GetValidity()) - keys = append(keys, validity) - - m[validityType] = basicnode.NewInt(int64(e.GetValidityType())) - keys = append(keys, validityType) - - m[sequence] = basicnode.NewInt(int64(e.GetSequence())) - keys = append(keys, sequence) - - m[ttl] = basicnode.NewInt(int64(e.GetTtl())) - keys = append(keys, ttl) - - sort.Sort(cborMapKeyString_RFC7049(keys)) - - newNd := basicnode.Prototype__Map{}.NewBuilder() - ma, err := newNd.BeginMap(int64(len(keys))) - if err != nil { - return nil, err - } - - for _, k := range keys { - if err := ma.AssembleKey().AssignString(k); err != nil { - return nil, err - } - if err := ma.AssembleValue().AssignNode(m[k]); err != nil { - return nil, err - } - } - - if err := ma.Finish(); err != nil { - return nil, err - } - - nd := newNd.Build() - - enc, err := ipldcodec.LookupEncoder(uint64(multicodec.DagCbor)) - if err != nil { - return nil, err - } - - buf := new(bytes.Buffer) - if err := enc(nd, buf); err != nil { - return nil, err - } - - return buf.Bytes(), nil -} - -// ValidateWithPeerID validates the given IPNS entry against the given peer ID. -func ValidateWithPeerID(pid peer.ID, entry *pb.IpnsEntry) error { - pk, err := ExtractPublicKey(pid, entry) - if err != nil { - return err - } - - return Validate(pk, entry) -} - -// Validates validates the given IPNS entry against the given public key. -func Validate(pk ic.PubKey, entry *pb.IpnsEntry) error { - // Make sure max size is respected - if entry.Size() > MaxRecordSize { - return ErrRecordSize - } - - // Check the ipns record signature with the public key - if entry.GetSignatureV2() == nil { - // always error if no valid signature could be found - return ErrSignature - } - - sig2Data, err := ipnsEntryDataForSigV2(entry) - if err != nil { - return fmt.Errorf("could not compute signature data: %w", err) - } - if ok, err := pk.Verify(sig2Data, entry.GetSignatureV2()); err != nil || !ok { - return ErrSignature - } - - // TODO: If we switch from pb.IpnsEntry to a more generic IpnsRecord type then perhaps we should only check - // this if there is no v1 signature. In the meanwhile this helps avoid some potential rough edges around people - // checking the entry fields instead of doing CBOR decoding everywhere. - // See https://github.com/ipfs/boxo/ipns/pull/42 for next steps here - if err := validateCborDataMatchesPbData(entry); err != nil { - return err - } - - eol, err := GetEOL(entry) - if err != nil { - return err - } - if time.Now().After(eol) { - return ErrExpiredRecord - } - return nil -} - -// TODO: Most of this function could probably be replaced with codegen -func validateCborDataMatchesPbData(entry *pb.IpnsEntry) error { - if len(entry.GetData()) == 0 { - return fmt.Errorf("record data is missing") - } - - dec, err := ipldcodec.LookupDecoder(uint64(multicodec.DagCbor)) - if err != nil { - return err - } - - ndbuilder := basicnode.Prototype__Map{}.NewBuilder() - if err := dec(ndbuilder, bytes.NewReader(entry.GetData())); err != nil { - return err - } - - fullNd := ndbuilder.Build() - nd, err := fullNd.LookupByString(value) - if err != nil { - return err - } - ndBytes, err := nd.AsBytes() - if err != nil { - return err - } - if !bytes.Equal(entry.GetValue(), ndBytes) { - return fmt.Errorf("field \"%v\" did not match between protobuf and CBOR", value) - } - - nd, err = fullNd.LookupByString(validity) - if err != nil { - return err - } - ndBytes, err = nd.AsBytes() - if err != nil { - return err - } - if !bytes.Equal(entry.GetValidity(), ndBytes) { - return fmt.Errorf("field \"%v\" did not match between protobuf and CBOR", validity) - } - - nd, err = fullNd.LookupByString(validityType) - if err != nil { - return err - } - ndInt, err := nd.AsInt() - if err != nil { - return err - } - if int64(entry.GetValidityType()) != ndInt { - return fmt.Errorf("field \"%v\" did not match between protobuf and CBOR", validityType) - } - - nd, err = fullNd.LookupByString(sequence) - if err != nil { - return err - } - ndInt, err = nd.AsInt() - if err != nil { - return err - } - - if entry.GetSequence() != uint64(ndInt) { - return fmt.Errorf("field \"%v\" did not match between protobuf and CBOR", sequence) - } - - nd, err = fullNd.LookupByString("TTL") - if err != nil { - return err - } - ndInt, err = nd.AsInt() - if err != nil { - return err - } - if entry.GetTtl() != uint64(ndInt) { - return fmt.Errorf("field \"%v\" did not match between protobuf and CBOR", ttl) - } - - return nil -} - -// GetEOL returns the EOL of this IPNS entry -// -// This function returns ErrUnrecognizedValidity if the validity type of the -// record isn't EOL. Otherwise, it returns an error if it can't parse the EOL. -func GetEOL(entry *pb.IpnsEntry) (time.Time, error) { - if entry.GetValidityType() != pb.IpnsEntry_EOL { - return time.Time{}, ErrUnrecognizedValidity - } - return u.ParseRFC3339(string(entry.GetValidity())) -} - -// EmbedPublicKey embeds the given public key in the given ipns entry. While not -// strictly required, some nodes (e.g., DHT servers) may reject IPNS entries -// that don't embed their public keys as they may not be able to validate them -// efficiently. -func EmbedPublicKey(pk ic.PubKey, entry *pb.IpnsEntry) error { - // Try extracting the public key from the ID. If we can, *don't* embed - // it. - id, err := peer.IDFromPublicKey(pk) - if err != nil { - return err - } - if _, err := id.ExtractPublicKey(); err != peer.ErrNoPublicKey { - // Either a *real* error or nil. - return err - } - - // We failed to extract the public key from the peer ID, embed it in the - // record. - pkBytes, err := ic.MarshalPublicKey(pk) - if err != nil { - return err - } - entry.PubKey = pkBytes - return nil -} - -// UnmarshalIpnsEntry unmarshalls an IPNS entry from a slice of bytes. -func UnmarshalIpnsEntry(data []byte) (*pb.IpnsEntry, error) { - var entry pb.IpnsEntry - err := proto.Unmarshal(data, &entry) - if err != nil { - return nil, err - } - - return &entry, nil -} - -// ExtractPublicKey extracts a public key matching `pid` from the IPNS record, -// if possible. -// -// This function returns (nil, nil) when no public key can be extracted and -// nothing is malformed. -func ExtractPublicKey(pid peer.ID, entry *pb.IpnsEntry) (ic.PubKey, error) { - if entry.PubKey != nil { - pk, err := ic.UnmarshalPublicKey(entry.PubKey) - if err != nil { - 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 - } - - return pid.ExtractPublicKey() -} - -// Compare compares two IPNS entries. It returns: -// -// * -1 if a is older than b -// * 0 if a and b cannot be ordered (this doesn't mean that they are equal) -// * +1 if a is newer than b -// -// It returns an error when either a or b are malformed. -// -// NOTE: It *does not* validate the records, the caller is responsible for calling -// `Validate` first. -// -// NOTE: If a and b cannot be ordered by this function, you can determine their -// order by comparing their serialized byte representations (using -// `bytes.Compare`). You must do this if you are implementing a libp2p record -// validator (or you can just use the one provided for you by this package). -func Compare(a, b *pb.IpnsEntry) (int, error) { - aHasV2Sig := a.GetSignatureV2() != nil - bHasV2Sig := b.GetSignatureV2() != nil - - // Having a newer signature version is better than an older signature version - if aHasV2Sig && !bHasV2Sig { - return 1, nil - } else if !aHasV2Sig && bHasV2Sig { - return -1, nil - } - - as := a.GetSequence() - bs := b.GetSequence() - - if as > bs { - return 1, nil - } else if as < bs { - return -1, nil - } - - at, err := u.ParseRFC3339(string(a.GetValidity())) - if err != nil { - return 0, err - } - - bt, err := u.ParseRFC3339(string(b.GetValidity())) - if err != nil { - return 0, err - } - - if at.After(bt) { - return 1, nil - } else if bt.After(at) { - return -1, nil - } - - return 0, nil -} - -func ipnsEntryDataForSigV1(e *pb.IpnsEntry) []byte { - return bytes.Join([][]byte{ - e.Value, - e.Validity, - []byte(fmt.Sprint(e.GetValidityType())), - }, - []byte{}) -} - -func ipnsEntryDataForSigV2(e *pb.IpnsEntry) ([]byte, error) { - dataForSig := []byte("ipns-signature:") - dataForSig = append(dataForSig, e.Data...) - - return dataForSig, nil -} - -type cborMapKeyString_RFC7049 []string - -func (x cborMapKeyString_RFC7049) Len() int { return len(x) } -func (x cborMapKeyString_RFC7049) Swap(i, j int) { x[i], x[j] = x[j], x[i] } -func (x cborMapKeyString_RFC7049) Less(i, j int) bool { - li, lj := len(x[i]), len(x[j]) - if li == lj { - return x[i] < x[j] - } - return li < lj -} - -var _ sort.Interface = (cborMapKeyString_RFC7049)(nil) diff --git a/ipns/ipns_test.go b/ipns/ipns_test.go deleted file mode 100644 index e6d521ce4..000000000 --- a/ipns/ipns_test.go +++ /dev/null @@ -1,63 +0,0 @@ -package ipns - -import ( - "fmt" - "testing" - "time" - - u "github.com/ipfs/boxo/util" - ci "github.com/libp2p/go-libp2p/core/crypto" - "github.com/libp2p/go-libp2p/core/peer" -) - -func TestEmbedPublicKey(t *testing.T) { - - sr := u.NewTimeSeededRand() - priv, pub, err := ci.GenerateKeyPairWithReader(ci.RSA, 2048, sr) - if err != nil { - t.Fatal(err) - } - - pid, err := peer.IDFromPublicKey(pub) - if err != nil { - t.Fatal(err) - } - - e, err := Create(priv, []byte("/a/b"), 0, time.Now().Add(1*time.Hour), 0) - if err != nil { - t.Fatal(err) - } - if err := EmbedPublicKey(pub, e); err != nil { - t.Fatal(err) - } - embeddedPk, err := ci.UnmarshalPublicKey(e.PubKey) - if err != nil { - t.Fatal(err) - } - embeddedPid, err := peer.IDFromPublicKey(embeddedPk) - if err != nil { - t.Fatal(err) - } - if embeddedPid != pid { - t.Fatalf("pid mismatch: %s != %s", pid, embeddedPid) - } -} - -func ExampleCreate() { - // Generate a private key to sign the IPNS record with. Most of the time, - // however, you'll want to retrieve an already-existing key from IPFS using - // go-ipfs/core/coreapi CoreAPI.KeyAPI() interface. - privateKey, _, err := ci.GenerateKeyPair(ci.RSA, 2048) - if err != nil { - panic(err) - } - - // Create an IPNS record that expires in one hour and points to the IPFS address - // /ipfs/Qme1knMqwt1hKZbc1BmQFmnm9f36nyQGwXxPGVpVJ9rMK5 - ipnsRecord, err := Create(privateKey, []byte("/ipfs/Qme1knMqwt1hKZbc1BmQFmnm9f36nyQGwXxPGVpVJ9rMK5"), 0, time.Now().Add(1*time.Hour), 0) - if err != nil { - panic(err) - } - - fmt.Println(ipnsRecord) -} diff --git a/ipns/name.go b/ipns/name.go new file mode 100644 index 000000000..cc7cee4b1 --- /dev/null +++ b/ipns/name.go @@ -0,0 +1,130 @@ +package ipns + +import ( + "bytes" + "encoding/json" + "fmt" + "strings" + + "github.com/ipfs/go-cid" + "github.com/libp2p/go-libp2p/core/peer" + mb "github.com/multiformats/go-multibase" + mc "github.com/multiformats/go-multicodec" + mh "github.com/multiformats/go-multihash" +) + +// Name represents a [Multihash] of a serialized public key according to the +// [IPNS Name] specifications. +// +// [Multihash]: https://multiformats.io/multihash/ +// [IPNS Name]: https://specs.ipfs.tech/ipns/ipns-record/#ipns-name +type Name struct { + src []byte +} + +// NameFromString creates a [Name] from the given IPNS Name in its [string representation]. +// +// [string representation]: https://specs.ipfs.tech/ipns/ipns-record/#string-representation +func NameFromString(str string) (Name, error) { + str = strings.TrimPrefix(str, "/ipns/") + pid, err := peer.Decode(str) + if err != nil { + return Name{}, err + } + return NameFromPeer(pid), nil +} + +// NameFromRoutingKey creates a [Name] from the given IPNS Name in its routing key +// representation. See [Name.RoutingKey] for more information. +func NameFromRoutingKey(data []byte) (Name, error) { + if !bytes.HasPrefix(data, []byte("/ipns/")) { + return Name{}, ErrInvalidName + } + + data = bytes.TrimPrefix(data, []byte("/ipns/")) + pid, err := peer.IDFromBytes(data) + if err != nil { + return Name{}, err + } + return NameFromPeer(pid), nil +} + +// NameFromPeer creates a [Name] from the given [peer.ID]. +func NameFromPeer(pid peer.ID) Name { + return Name{src: []byte(pid)} +} + +// NameFromCid creates a [Name] from the given [cid.Cid]. +func NameFromCid(c cid.Cid) (Name, error) { + code := mc.Code(c.Type()) + if code != mc.Libp2pKey { + return Name{}, fmt.Errorf("CID codec %q is not allowed for IPNS Names, use %q instead", code, mc.Libp2pKey) + } + return Name{src: c.Hash()}, nil +} + +// RoutingKey returns the binary IPNS Routing Key for the given [Name]. Note that +// the intended use of this function is for [routing purposes] only. The output of +// this function is binary, not human readable. For a human-readable string, see [Name.Key]. +// +// [routing purposes]: https://specs.ipfs.tech/ipns/ipns-record/#routing-record +func (n Name) RoutingKey() []byte { + var buffer bytes.Buffer + buffer.WriteString("/ipns/") + buffer.Write(n.src) // Note: we append raw multihash bytes (no multibase) + return buffer.Bytes() +} + +// Cid returns [Name] encoded as a [cid.Cid] of the public key. If the IPNS Name +// is invalid (e.g., empty), this will return the empty Cid. +func (n Name) Cid() cid.Cid { + m, err := mh.Cast([]byte(n.src)) + if err != nil { + return cid.Undef + } + return cid.NewCidV1(cid.Libp2pKey, m) +} + +// Peer returns [Name] as a [peer.ID]. +func (n Name) Peer() peer.ID { + return peer.ID(n.src) +} + +// String returns the human-readable IPNS Name, encoded as a CIDv1 with libp2p-key +// multicodec (0x72) with case-insensitive Base36. +func (n Name) String() string { + name, err := n.Cid().StringOfBase(mb.Base36) + if err != nil { + panic(fmt.Errorf("cid.StringOfBase was called with wrong parameters: %w", err)) + } + return name +} + +// UnmarshalJSON implements [json.Unmarshaler] interface. IPNS Name will +// unmarshal from a string via [NameFromString]. +func (n *Name) UnmarshalJSON(b []byte) error { + var str string + err := json.Unmarshal(b, &str) + if err != nil { + return err + } + + v, err := NameFromString(str) + if err != nil { + return err + } + + *n = v + return nil +} + +// MarshalJSON implements [json.Marshaler] interface. IPNS Name will +// marshal as a string using [Name.String]. +func (n Name) MarshalJSON() ([]byte, error) { + return json.Marshal(n.String()) +} + +// Equal returns whether the records are equal. +func (n Name) Equal(other Name) bool { + return bytes.Equal(n.src, other.src) +} diff --git a/ipns/name_test.go b/ipns/name_test.go new file mode 100644 index 000000000..b515a85b7 --- /dev/null +++ b/ipns/name_test.go @@ -0,0 +1,73 @@ +package ipns + +import ( + "encoding/json" + "testing" + + "github.com/ipfs/go-cid" + "github.com/stretchr/testify/require" +) + +func TestName(t *testing.T) { + t.Parallel() + + testFromCid := func(t *testing.T, name, input, expected string) { + t.Run("NameFromCid method: "+name, func(t *testing.T) { + t.Parallel() + + c, err := cid.Parse(input) + require.NoError(t, err) + + name, err := NameFromCid(c) + require.NoError(t, err) + require.Equal(t, expected, name.String()) + }) + } + + testString := func(t *testing.T, name, input, expected string) { + t.Run("String method: "+name, func(t *testing.T) { + t.Parallel() + + name, err := NameFromString(input) + require.NoError(t, err) + require.Equal(t, expected, name.String()) + }) + } + + testMarshalJSON := func(t *testing.T, name, input, expected string) { + t.Run("Marshal JSON: "+name, func(t *testing.T) { + t.Parallel() + + name, err := NameFromString(input) + require.NoError(t, err) + raw, err := json.Marshal(name) + require.NoError(t, err) + require.Equal(t, expected, string(raw)) + }) + } + + testUnmarshalJSON := func(t *testing.T, name string, input []byte, expected string) { + t.Run("Unmarshal JSON: "+name, func(t *testing.T) { + t.Parallel() + + var name Name + err := json.Unmarshal(input, &name) + require.NoError(t, err) + require.Equal(t, expected, name.String()) + }) + } + + for _, v := range [][]string{ + {"RSA", "QmRp2LvtSQtCkUWCpi92ph5MdQyRtfb9jHbkNgZzGExGuG", "k2k4r8kpauqq30hoj9oktej5btbgz1jeos16d3te36xd78trvak0jcor"}, + {"Ed25519", "12D3KooWSzRuSFHgLsKr6jJboAPdP7xMga2YBgBspYuErxswcgvt", "k51qzi5uqu5dmjjgoe7s21dncepi970722cn30qlhm9qridas1c9ktkjb6ejux"}, + {"ECDSA", "QmSBUTocZ9LxE53Br9PDDcPWnR1FJQRv94U96Wkt8eypAw", "k2k4r8ku8cnc1sl2h5xn7i07dma9abfnkqkxi4a6nd1xq0knoxe7b0y4"}, + {"Secp256k1", "16Uiu2HAmUymv6JpFwNZppdKUMxGJuHsTeicXgHGKbBasu4Ruj3K1", "kzwfwjn5ji4puw3jc1qw4b073j74xvq21iziuqw4rem21pr7f0l4dj8i9yb978s"}, + } { + testFromCid(t, v[0], v[2], v[2]) + testString(t, v[0], v[1], v[2]) + testString(t, v[0], "/ipns/"+v[1], v[2]) + testMarshalJSON(t, v[0], v[1], `"`+v[2]+`"`) + testMarshalJSON(t, v[0], "/ipns/"+v[1], `"`+v[2]+`"`) + testUnmarshalJSON(t, v[0], []byte(`"`+v[2]+`"`), v[2]) + } +} diff --git a/ipns/pb/Makefile b/ipns/pb/Makefile deleted file mode 100644 index eb14b5768..000000000 --- a/ipns/pb/Makefile +++ /dev/null @@ -1,11 +0,0 @@ -PB = $(wildcard *.proto) -GO = $(PB:.proto=.pb.go) - -all: $(GO) - -%.pb.go: %.proto - protoc --proto_path=$(GOPATH)/src:. --gogofast_out=. $< - -clean: - rm -f *.pb.go - rm -f *.go diff --git a/ipns/pb/ipns.pb.go b/ipns/pb/ipns.pb.go deleted file mode 100644 index 1e2488852..000000000 --- a/ipns/pb/ipns.pb.go +++ /dev/null @@ -1,992 +0,0 @@ -// Code generated by protoc-gen-gogo. DO NOT EDIT. -// source: ipns.proto - -package ipns_pb - -import ( - fmt "fmt" - proto "github.com/gogo/protobuf/proto" - io "io" - math "math" - math_bits "math/bits" -) - -// Reference imports to suppress errors if they are not otherwise used. -var _ = proto.Marshal -var _ = fmt.Errorf -var _ = math.Inf - -// This is a compile-time assertion to ensure that this generated file -// is compatible with the proto package it is being compiled against. -// A compilation error at this line likely means your copy of the -// proto package needs to be updated. -const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package - -type IpnsEntry_ValidityType int32 - -const ( - // setting an EOL says "this record is valid until..." - IpnsEntry_EOL IpnsEntry_ValidityType = 0 -) - -var IpnsEntry_ValidityType_name = map[int32]string{ - 0: "EOL", -} - -var IpnsEntry_ValidityType_value = map[string]int32{ - "EOL": 0, -} - -func (x IpnsEntry_ValidityType) Enum() *IpnsEntry_ValidityType { - p := new(IpnsEntry_ValidityType) - *p = x - return p -} - -func (x IpnsEntry_ValidityType) String() string { - return proto.EnumName(IpnsEntry_ValidityType_name, int32(x)) -} - -func (x *IpnsEntry_ValidityType) UnmarshalJSON(data []byte) error { - value, err := proto.UnmarshalJSONEnum(IpnsEntry_ValidityType_value, data, "IpnsEntry_ValidityType") - if err != nil { - return err - } - *x = IpnsEntry_ValidityType(value) - return nil -} - -func (IpnsEntry_ValidityType) EnumDescriptor() ([]byte, []int) { - return fileDescriptor_4d5b16fb32bfe8ea, []int{0, 0} -} - -type IpnsEntry struct { - Value []byte `protobuf:"bytes,1,opt,name=value" json:"value,omitempty"` - SignatureV1 []byte `protobuf:"bytes,2,opt,name=signatureV1" json:"signatureV1,omitempty"` - ValidityType *IpnsEntry_ValidityType `protobuf:"varint,3,opt,name=validityType,enum=ipns.v1.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"` - SignatureV2 []byte `protobuf:"bytes,8,opt,name=signatureV2" json:"signatureV2,omitempty"` - Data []byte `protobuf:"bytes,9,opt,name=data" json:"data,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` -} - -func (m *IpnsEntry) Reset() { *m = IpnsEntry{} } -func (m *IpnsEntry) String() string { return proto.CompactTextString(m) } -func (*IpnsEntry) ProtoMessage() {} -func (*IpnsEntry) Descriptor() ([]byte, []int) { - return fileDescriptor_4d5b16fb32bfe8ea, []int{0} -} -func (m *IpnsEntry) XXX_Unmarshal(b []byte) error { - return m.Unmarshal(b) -} -func (m *IpnsEntry) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - if deterministic { - return xxx_messageInfo_IpnsEntry.Marshal(b, m, deterministic) - } else { - b = b[:cap(b)] - n, err := m.MarshalToSizedBuffer(b) - if err != nil { - return nil, err - } - return b[:n], nil - } -} -func (m *IpnsEntry) XXX_Merge(src proto.Message) { - xxx_messageInfo_IpnsEntry.Merge(m, src) -} -func (m *IpnsEntry) XXX_Size() int { - return m.Size() -} -func (m *IpnsEntry) XXX_DiscardUnknown() { - xxx_messageInfo_IpnsEntry.DiscardUnknown(m) -} - -var xxx_messageInfo_IpnsEntry proto.InternalMessageInfo - -func (m *IpnsEntry) GetValue() []byte { - if m != nil { - return m.Value - } - return nil -} - -func (m *IpnsEntry) GetSignatureV1() []byte { - if m != nil { - return m.SignatureV1 - } - return nil -} - -func (m *IpnsEntry) GetValidityType() IpnsEntry_ValidityType { - if m != nil && m.ValidityType != nil { - return *m.ValidityType - } - return IpnsEntry_EOL -} - -func (m *IpnsEntry) GetValidity() []byte { - if m != nil { - return m.Validity - } - return nil -} - -func (m *IpnsEntry) GetSequence() uint64 { - if m != nil && m.Sequence != nil { - return *m.Sequence - } - return 0 -} - -func (m *IpnsEntry) GetTtl() uint64 { - if m != nil && m.Ttl != nil { - return *m.Ttl - } - return 0 -} - -func (m *IpnsEntry) GetPubKey() []byte { - if m != nil { - return m.PubKey - } - return nil -} - -func (m *IpnsEntry) GetSignatureV2() []byte { - if m != nil { - return m.SignatureV2 - } - return nil -} - -func (m *IpnsEntry) GetData() []byte { - if m != nil { - return m.Data - } - return nil -} - -type IpnsSignatureV2Checker struct { - PubKey []byte `protobuf:"bytes,7,opt,name=pubKey" json:"pubKey,omitempty"` - SignatureV2 []byte `protobuf:"bytes,8,opt,name=signatureV2" json:"signatureV2,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` -} - -func (m *IpnsSignatureV2Checker) Reset() { *m = IpnsSignatureV2Checker{} } -func (m *IpnsSignatureV2Checker) String() string { return proto.CompactTextString(m) } -func (*IpnsSignatureV2Checker) ProtoMessage() {} -func (*IpnsSignatureV2Checker) Descriptor() ([]byte, []int) { - return fileDescriptor_4d5b16fb32bfe8ea, []int{1} -} -func (m *IpnsSignatureV2Checker) XXX_Unmarshal(b []byte) error { - return m.Unmarshal(b) -} -func (m *IpnsSignatureV2Checker) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - if deterministic { - return xxx_messageInfo_IpnsSignatureV2Checker.Marshal(b, m, deterministic) - } else { - b = b[:cap(b)] - n, err := m.MarshalToSizedBuffer(b) - if err != nil { - return nil, err - } - return b[:n], nil - } -} -func (m *IpnsSignatureV2Checker) XXX_Merge(src proto.Message) { - xxx_messageInfo_IpnsSignatureV2Checker.Merge(m, src) -} -func (m *IpnsSignatureV2Checker) XXX_Size() int { - return m.Size() -} -func (m *IpnsSignatureV2Checker) XXX_DiscardUnknown() { - xxx_messageInfo_IpnsSignatureV2Checker.DiscardUnknown(m) -} - -var xxx_messageInfo_IpnsSignatureV2Checker proto.InternalMessageInfo - -func (m *IpnsSignatureV2Checker) GetPubKey() []byte { - if m != nil { - return m.PubKey - } - return nil -} - -func (m *IpnsSignatureV2Checker) GetSignatureV2() []byte { - if m != nil { - return m.SignatureV2 - } - return nil -} - -func init() { - proto.RegisterEnum("ipns.v1.pb.IpnsEntry_ValidityType", IpnsEntry_ValidityType_name, IpnsEntry_ValidityType_value) - proto.RegisterType((*IpnsEntry)(nil), "ipns.v1.pb.IpnsEntry") - proto.RegisterType((*IpnsSignatureV2Checker)(nil), "ipns.v1.pb.IpnsSignatureV2Checker") -} - -func init() { proto.RegisterFile("ipns.proto", fileDescriptor_4d5b16fb32bfe8ea) } - -var fileDescriptor_4d5b16fb32bfe8ea = []byte{ - // 272 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0xca, 0x2c, 0xc8, 0x2b, - 0xd6, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x82, 0xb0, 0xcb, 0x0c, 0xf5, 0x0a, 0x92, 0x94, 0xf6, - 0x30, 0x71, 0x71, 0x7a, 0x16, 0xe4, 0x15, 0xbb, 0xe6, 0x95, 0x14, 0x55, 0x0a, 0x89, 0x70, 0xb1, - 0x96, 0x25, 0xe6, 0x94, 0xa6, 0x4a, 0x30, 0x2a, 0x30, 0x6a, 0xf0, 0x04, 0x41, 0x38, 0x42, 0x0a, - 0x5c, 0xdc, 0xc5, 0x99, 0xe9, 0x79, 0x89, 0x25, 0xa5, 0x45, 0xa9, 0x61, 0x86, 0x12, 0x4c, 0x60, - 0x39, 0x64, 0x21, 0x21, 0x37, 0x2e, 0x9e, 0xb2, 0xc4, 0x9c, 0xcc, 0x94, 0xcc, 0x92, 0xca, 0x90, - 0xca, 0x82, 0x54, 0x09, 0x66, 0x05, 0x46, 0x0d, 0x3e, 0x23, 0x25, 0x3d, 0x84, 0x45, 0x7a, 0x70, - 0x4b, 0xf4, 0xc2, 0x90, 0x54, 0x06, 0xa1, 0xe8, 0x13, 0x92, 0xe2, 0xe2, 0x80, 0xf1, 0x25, 0x58, - 0xc0, 0xd6, 0xc0, 0xf9, 0x20, 0xb9, 0xe2, 0xd4, 0xc2, 0xd2, 0xd4, 0xbc, 0xe4, 0x54, 0x09, 0x56, - 0x05, 0x46, 0x0d, 0x96, 0x20, 0x38, 0x5f, 0x48, 0x80, 0x8b, 0xb9, 0xa4, 0x24, 0x47, 0x82, 0x0d, - 0x2c, 0x0c, 0x62, 0x0a, 0x89, 0x71, 0xb1, 0x15, 0x94, 0x26, 0x79, 0xa7, 0x56, 0x4a, 0xb0, 0x83, - 0xcd, 0x81, 0xf2, 0x50, 0xfd, 0x62, 0x24, 0xc1, 0x81, 0xee, 0x17, 0x23, 0x21, 0x21, 0x2e, 0x96, - 0x94, 0xc4, 0x92, 0x44, 0x09, 0x4e, 0xb0, 0x14, 0x98, 0xad, 0x24, 0xce, 0xc5, 0x83, 0xec, 0x6a, - 0x21, 0x76, 0x2e, 0x66, 0x57, 0x7f, 0x1f, 0x01, 0x06, 0xa5, 0x20, 0x2e, 0x31, 0x90, 0xc7, 0x82, - 0x11, 0xfa, 0x9d, 0x33, 0x52, 0x93, 0xb3, 0x53, 0x8b, 0xc8, 0x77, 0x80, 0x93, 0xe8, 0x89, 0x47, - 0x72, 0x8c, 0x17, 0x1e, 0xc9, 0x31, 0x3e, 0x78, 0x24, 0xc7, 0x18, 0xc5, 0x0e, 0x0a, 0xc3, 0xf8, - 0x82, 0x24, 0x40, 0x00, 0x00, 0x00, 0xff, 0xff, 0xbd, 0x45, 0xdd, 0x1a, 0xc2, 0x01, 0x00, 0x00, -} - -func (m *IpnsEntry) Marshal() (dAtA []byte, err error) { - size := m.Size() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBuffer(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *IpnsEntry) MarshalTo(dAtA []byte) (int, error) { - size := m.Size() - return m.MarshalToSizedBuffer(dAtA[:size]) -} - -func (m *IpnsEntry) MarshalToSizedBuffer(dAtA []byte) (int, error) { - i := len(dAtA) - _ = i - var l int - _ = l - if m.XXX_unrecognized != nil { - i -= len(m.XXX_unrecognized) - copy(dAtA[i:], m.XXX_unrecognized) - } - if m.Data != nil { - i -= len(m.Data) - copy(dAtA[i:], m.Data) - i = encodeVarintIpns(dAtA, i, uint64(len(m.Data))) - i-- - dAtA[i] = 0x4a - } - if m.SignatureV2 != nil { - i -= len(m.SignatureV2) - copy(dAtA[i:], m.SignatureV2) - i = encodeVarintIpns(dAtA, i, uint64(len(m.SignatureV2))) - i-- - dAtA[i] = 0x42 - } - if m.PubKey != nil { - i -= len(m.PubKey) - copy(dAtA[i:], m.PubKey) - i = encodeVarintIpns(dAtA, i, uint64(len(m.PubKey))) - i-- - dAtA[i] = 0x3a - } - if m.Ttl != nil { - i = encodeVarintIpns(dAtA, i, uint64(*m.Ttl)) - i-- - dAtA[i] = 0x30 - } - if m.Sequence != nil { - i = encodeVarintIpns(dAtA, i, uint64(*m.Sequence)) - i-- - dAtA[i] = 0x28 - } - if m.Validity != nil { - i -= len(m.Validity) - copy(dAtA[i:], m.Validity) - i = encodeVarintIpns(dAtA, i, uint64(len(m.Validity))) - i-- - dAtA[i] = 0x22 - } - if m.ValidityType != nil { - i = encodeVarintIpns(dAtA, i, uint64(*m.ValidityType)) - i-- - dAtA[i] = 0x18 - } - if m.SignatureV1 != nil { - i -= len(m.SignatureV1) - copy(dAtA[i:], m.SignatureV1) - i = encodeVarintIpns(dAtA, i, uint64(len(m.SignatureV1))) - i-- - dAtA[i] = 0x12 - } - if m.Value != nil { - i -= len(m.Value) - copy(dAtA[i:], m.Value) - i = encodeVarintIpns(dAtA, i, uint64(len(m.Value))) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - -func (m *IpnsSignatureV2Checker) Marshal() (dAtA []byte, err error) { - size := m.Size() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBuffer(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *IpnsSignatureV2Checker) MarshalTo(dAtA []byte) (int, error) { - size := m.Size() - return m.MarshalToSizedBuffer(dAtA[:size]) -} - -func (m *IpnsSignatureV2Checker) MarshalToSizedBuffer(dAtA []byte) (int, error) { - i := len(dAtA) - _ = i - var l int - _ = l - if m.XXX_unrecognized != nil { - i -= len(m.XXX_unrecognized) - copy(dAtA[i:], m.XXX_unrecognized) - } - if m.SignatureV2 != nil { - i -= len(m.SignatureV2) - copy(dAtA[i:], m.SignatureV2) - i = encodeVarintIpns(dAtA, i, uint64(len(m.SignatureV2))) - i-- - dAtA[i] = 0x42 - } - if m.PubKey != nil { - i -= len(m.PubKey) - copy(dAtA[i:], m.PubKey) - i = encodeVarintIpns(dAtA, i, uint64(len(m.PubKey))) - i-- - dAtA[i] = 0x3a - } - return len(dAtA) - i, nil -} - -func encodeVarintIpns(dAtA []byte, offset int, v uint64) int { - offset -= sovIpns(v) - base := offset - for v >= 1<<7 { - dAtA[offset] = uint8(v&0x7f | 0x80) - v >>= 7 - offset++ - } - dAtA[offset] = uint8(v) - return base -} -func (m *IpnsEntry) Size() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - if m.Value != nil { - l = len(m.Value) - n += 1 + l + sovIpns(uint64(l)) - } - if m.SignatureV1 != nil { - l = len(m.SignatureV1) - n += 1 + l + sovIpns(uint64(l)) - } - if m.ValidityType != nil { - n += 1 + sovIpns(uint64(*m.ValidityType)) - } - if m.Validity != nil { - l = len(m.Validity) - n += 1 + l + sovIpns(uint64(l)) - } - if m.Sequence != nil { - n += 1 + sovIpns(uint64(*m.Sequence)) - } - if m.Ttl != nil { - n += 1 + sovIpns(uint64(*m.Ttl)) - } - if m.PubKey != nil { - l = len(m.PubKey) - n += 1 + l + sovIpns(uint64(l)) - } - if m.SignatureV2 != nil { - l = len(m.SignatureV2) - n += 1 + l + sovIpns(uint64(l)) - } - if m.Data != nil { - l = len(m.Data) - n += 1 + l + sovIpns(uint64(l)) - } - if m.XXX_unrecognized != nil { - n += len(m.XXX_unrecognized) - } - return n -} - -func (m *IpnsSignatureV2Checker) Size() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - if m.PubKey != nil { - l = len(m.PubKey) - n += 1 + l + sovIpns(uint64(l)) - } - if m.SignatureV2 != nil { - l = len(m.SignatureV2) - n += 1 + l + sovIpns(uint64(l)) - } - if m.XXX_unrecognized != nil { - n += len(m.XXX_unrecognized) - } - return n -} - -func sovIpns(x uint64) (n int) { - return (math_bits.Len64(x|1) + 6) / 7 -} -func sozIpns(x uint64) (n int) { - return sovIpns(uint64((x << 1) ^ uint64((int64(x) >> 63)))) -} -func (m *IpnsEntry) Unmarshal(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowIpns - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: IpnsEntry: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: IpnsEntry: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Value", wireType) - } - var byteLen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowIpns - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - byteLen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if byteLen < 0 { - return ErrInvalidLengthIpns - } - postIndex := iNdEx + byteLen - if postIndex < 0 { - return ErrInvalidLengthIpns - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Value = append(m.Value[:0], dAtA[iNdEx:postIndex]...) - if m.Value == nil { - m.Value = []byte{} - } - iNdEx = postIndex - case 2: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field SignatureV1", wireType) - } - var byteLen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowIpns - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - byteLen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if byteLen < 0 { - return ErrInvalidLengthIpns - } - postIndex := iNdEx + byteLen - if postIndex < 0 { - return ErrInvalidLengthIpns - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.SignatureV1 = append(m.SignatureV1[:0], dAtA[iNdEx:postIndex]...) - if m.SignatureV1 == nil { - m.SignatureV1 = []byte{} - } - iNdEx = postIndex - case 3: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field ValidityType", wireType) - } - var v IpnsEntry_ValidityType - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowIpns - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - v |= IpnsEntry_ValidityType(b&0x7F) << shift - if b < 0x80 { - break - } - } - m.ValidityType = &v - case 4: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Validity", wireType) - } - var byteLen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowIpns - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - byteLen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if byteLen < 0 { - return ErrInvalidLengthIpns - } - postIndex := iNdEx + byteLen - if postIndex < 0 { - return ErrInvalidLengthIpns - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Validity = append(m.Validity[:0], dAtA[iNdEx:postIndex]...) - if m.Validity == nil { - m.Validity = []byte{} - } - iNdEx = postIndex - case 5: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Sequence", wireType) - } - var v uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowIpns - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - v |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - m.Sequence = &v - case 6: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Ttl", wireType) - } - var v uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowIpns - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - v |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - m.Ttl = &v - case 7: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field PubKey", wireType) - } - var byteLen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowIpns - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - byteLen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if byteLen < 0 { - return ErrInvalidLengthIpns - } - postIndex := iNdEx + byteLen - if postIndex < 0 { - return ErrInvalidLengthIpns - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.PubKey = append(m.PubKey[:0], dAtA[iNdEx:postIndex]...) - if m.PubKey == nil { - m.PubKey = []byte{} - } - iNdEx = postIndex - case 8: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field SignatureV2", wireType) - } - var byteLen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowIpns - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - byteLen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if byteLen < 0 { - return ErrInvalidLengthIpns - } - postIndex := iNdEx + byteLen - if postIndex < 0 { - return ErrInvalidLengthIpns - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.SignatureV2 = append(m.SignatureV2[:0], dAtA[iNdEx:postIndex]...) - if m.SignatureV2 == nil { - m.SignatureV2 = []byte{} - } - iNdEx = postIndex - case 9: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Data", wireType) - } - var byteLen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowIpns - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - byteLen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if byteLen < 0 { - return ErrInvalidLengthIpns - } - postIndex := iNdEx + byteLen - if postIndex < 0 { - return ErrInvalidLengthIpns - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Data = append(m.Data[:0], dAtA[iNdEx:postIndex]...) - if m.Data == nil { - m.Data = []byte{} - } - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skipIpns(dAtA[iNdEx:]) - if err != nil { - return err - } - if skippy < 0 { - return ErrInvalidLengthIpns - } - if (iNdEx + skippy) < 0 { - return ErrInvalidLengthIpns - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *IpnsSignatureV2Checker) Unmarshal(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowIpns - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: IpnsSignatureV2Checker: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: IpnsSignatureV2Checker: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 7: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field PubKey", wireType) - } - var byteLen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowIpns - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - byteLen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if byteLen < 0 { - return ErrInvalidLengthIpns - } - postIndex := iNdEx + byteLen - if postIndex < 0 { - return ErrInvalidLengthIpns - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.PubKey = append(m.PubKey[:0], dAtA[iNdEx:postIndex]...) - if m.PubKey == nil { - m.PubKey = []byte{} - } - iNdEx = postIndex - case 8: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field SignatureV2", wireType) - } - var byteLen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowIpns - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - byteLen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if byteLen < 0 { - return ErrInvalidLengthIpns - } - postIndex := iNdEx + byteLen - if postIndex < 0 { - return ErrInvalidLengthIpns - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.SignatureV2 = append(m.SignatureV2[:0], dAtA[iNdEx:postIndex]...) - if m.SignatureV2 == nil { - m.SignatureV2 = []byte{} - } - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skipIpns(dAtA[iNdEx:]) - if err != nil { - return err - } - if skippy < 0 { - return ErrInvalidLengthIpns - } - if (iNdEx + skippy) < 0 { - return ErrInvalidLengthIpns - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func skipIpns(dAtA []byte) (n int, err error) { - l := len(dAtA) - iNdEx := 0 - depth := 0 - for iNdEx < l { - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return 0, ErrIntOverflowIpns - } - if iNdEx >= l { - return 0, io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= (uint64(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - wireType := int(wire & 0x7) - switch wireType { - case 0: - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return 0, ErrIntOverflowIpns - } - if iNdEx >= l { - return 0, io.ErrUnexpectedEOF - } - iNdEx++ - if dAtA[iNdEx-1] < 0x80 { - break - } - } - case 1: - iNdEx += 8 - case 2: - var length int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return 0, ErrIntOverflowIpns - } - if iNdEx >= l { - return 0, io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - length |= (int(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - if length < 0 { - return 0, ErrInvalidLengthIpns - } - iNdEx += length - case 3: - depth++ - case 4: - if depth == 0 { - return 0, ErrUnexpectedEndOfGroupIpns - } - depth-- - case 5: - iNdEx += 4 - default: - return 0, fmt.Errorf("proto: illegal wireType %d", wireType) - } - if iNdEx < 0 { - return 0, ErrInvalidLengthIpns - } - if depth == 0 { - return iNdEx, nil - } - } - return 0, io.ErrUnexpectedEOF -} - -var ( - ErrInvalidLengthIpns = fmt.Errorf("proto: negative length found during unmarshaling") - ErrIntOverflowIpns = fmt.Errorf("proto: integer overflow") - ErrUnexpectedEndOfGroupIpns = fmt.Errorf("proto: unexpected end of group") -) diff --git a/ipns/pb/ipns.proto b/ipns/pb/ipns.proto deleted file mode 100644 index bd89a34ec..000000000 --- a/ipns/pb/ipns.proto +++ /dev/null @@ -1,36 +0,0 @@ -syntax = "proto2"; - -package ipns.v1.pb; - -option go_package = "ipns_pb"; - -message IpnsEntry { - enum ValidityType { - // setting an EOL says "this record is valid until..." - EOL = 0; - } - optional bytes value = 1; - optional bytes signatureV1 = 2; - - optional ValidityType validityType = 3; - optional bytes validity = 4; - - 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; - - optional bytes signatureV2 = 8; - - optional bytes data = 9; -} - -message IpnsSignatureV2Checker { - optional bytes pubKey = 7; - optional bytes signatureV2 = 8; -} diff --git a/ipns/pb/record.pb.go b/ipns/pb/record.pb.go new file mode 100644 index 000000000..fe6950929 --- /dev/null +++ b/ipns/pb/record.pb.go @@ -0,0 +1,283 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.30.0 +// protoc v3.21.12 +// source: record.proto + +package pb + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type IpnsRecord_ValidityType int32 + +const ( + IpnsRecord_EOL IpnsRecord_ValidityType = 0 +) + +// Enum value maps for IpnsRecord_ValidityType. +var ( + IpnsRecord_ValidityType_name = map[int32]string{ + 0: "EOL", + } + IpnsRecord_ValidityType_value = map[string]int32{ + "EOL": 0, + } +) + +func (x IpnsRecord_ValidityType) Enum() *IpnsRecord_ValidityType { + p := new(IpnsRecord_ValidityType) + *p = x + return p +} + +func (x IpnsRecord_ValidityType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (IpnsRecord_ValidityType) Descriptor() protoreflect.EnumDescriptor { + return file_record_proto_enumTypes[0].Descriptor() +} + +func (IpnsRecord_ValidityType) Type() protoreflect.EnumType { + return &file_record_proto_enumTypes[0] +} + +func (x IpnsRecord_ValidityType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use IpnsRecord_ValidityType.Descriptor instead. +func (IpnsRecord_ValidityType) EnumDescriptor() ([]byte, []int) { + return file_record_proto_rawDescGZIP(), []int{0, 0} +} + +type IpnsRecord struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Value []byte `protobuf:"bytes,1,opt,name=value,proto3,oneof" json:"value,omitempty"` + SignatureV1 []byte `protobuf:"bytes,2,opt,name=signatureV1,proto3,oneof" json:"signatureV1,omitempty"` + ValidityType *IpnsRecord_ValidityType `protobuf:"varint,3,opt,name=validityType,proto3,enum=github.com.boxo.ipns.pb.IpnsRecord_ValidityType,oneof" json:"validityType,omitempty"` + Validity []byte `protobuf:"bytes,4,opt,name=validity,proto3,oneof" json:"validity,omitempty"` + Sequence *uint64 `protobuf:"varint,5,opt,name=sequence,proto3,oneof" json:"sequence,omitempty"` + Ttl *uint64 `protobuf:"varint,6,opt,name=ttl,proto3,oneof" json:"ttl,omitempty"` + PubKey []byte `protobuf:"bytes,7,opt,name=pubKey,proto3,oneof" json:"pubKey,omitempty"` + SignatureV2 []byte `protobuf:"bytes,8,opt,name=signatureV2,proto3,oneof" json:"signatureV2,omitempty"` + Data []byte `protobuf:"bytes,9,opt,name=data,proto3,oneof" json:"data,omitempty"` +} + +func (x *IpnsRecord) Reset() { + *x = IpnsRecord{} + if protoimpl.UnsafeEnabled { + mi := &file_record_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *IpnsRecord) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*IpnsRecord) ProtoMessage() {} + +func (x *IpnsRecord) ProtoReflect() protoreflect.Message { + mi := &file_record_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use IpnsRecord.ProtoReflect.Descriptor instead. +func (*IpnsRecord) Descriptor() ([]byte, []int) { + return file_record_proto_rawDescGZIP(), []int{0} +} + +func (x *IpnsRecord) GetValue() []byte { + if x != nil { + return x.Value + } + return nil +} + +func (x *IpnsRecord) GetSignatureV1() []byte { + if x != nil { + return x.SignatureV1 + } + return nil +} + +func (x *IpnsRecord) GetValidityType() IpnsRecord_ValidityType { + if x != nil && x.ValidityType != nil { + return *x.ValidityType + } + return IpnsRecord_EOL +} + +func (x *IpnsRecord) GetValidity() []byte { + if x != nil { + return x.Validity + } + return nil +} + +func (x *IpnsRecord) GetSequence() uint64 { + if x != nil && x.Sequence != nil { + return *x.Sequence + } + return 0 +} + +func (x *IpnsRecord) GetTtl() uint64 { + if x != nil && x.Ttl != nil { + return *x.Ttl + } + return 0 +} + +func (x *IpnsRecord) GetPubKey() []byte { + if x != nil { + return x.PubKey + } + return nil +} + +func (x *IpnsRecord) GetSignatureV2() []byte { + if x != nil { + return x.SignatureV2 + } + return nil +} + +func (x *IpnsRecord) GetData() []byte { + if x != nil { + return x.Data + } + return nil +} + +var File_record_proto protoreflect.FileDescriptor + +var file_record_proto_rawDesc = []byte{ + 0x0a, 0x0c, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x17, + 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x62, 0x6f, 0x78, 0x6f, 0x2e, + 0x69, 0x70, 0x6e, 0x73, 0x2e, 0x70, 0x62, 0x22, 0xe9, 0x03, 0x0a, 0x0a, 0x49, 0x70, 0x6e, 0x73, + 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x19, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x00, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x88, 0x01, + 0x01, 0x12, 0x25, 0x0a, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x56, 0x31, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x01, 0x52, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, + 0x75, 0x72, 0x65, 0x56, 0x31, 0x88, 0x01, 0x01, 0x12, 0x59, 0x0a, 0x0c, 0x76, 0x61, 0x6c, 0x69, + 0x64, 0x69, 0x74, 0x79, 0x54, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x30, + 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x62, 0x6f, 0x78, 0x6f, + 0x2e, 0x69, 0x70, 0x6e, 0x73, 0x2e, 0x70, 0x62, 0x2e, 0x49, 0x70, 0x6e, 0x73, 0x52, 0x65, 0x63, + 0x6f, 0x72, 0x64, 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x69, 0x74, 0x79, 0x54, 0x79, 0x70, 0x65, + 0x48, 0x02, 0x52, 0x0c, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x69, 0x74, 0x79, 0x54, 0x79, 0x70, 0x65, + 0x88, 0x01, 0x01, 0x12, 0x1f, 0x0a, 0x08, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x69, 0x74, 0x79, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x03, 0x52, 0x08, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x69, 0x74, + 0x79, 0x88, 0x01, 0x01, 0x12, 0x1f, 0x0a, 0x08, 0x73, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x65, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x48, 0x04, 0x52, 0x08, 0x73, 0x65, 0x71, 0x75, 0x65, 0x6e, + 0x63, 0x65, 0x88, 0x01, 0x01, 0x12, 0x15, 0x0a, 0x03, 0x74, 0x74, 0x6c, 0x18, 0x06, 0x20, 0x01, + 0x28, 0x04, 0x48, 0x05, 0x52, 0x03, 0x74, 0x74, 0x6c, 0x88, 0x01, 0x01, 0x12, 0x1b, 0x0a, 0x06, + 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x06, 0x52, 0x06, + 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x88, 0x01, 0x01, 0x12, 0x25, 0x0a, 0x0b, 0x73, 0x69, 0x67, + 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x56, 0x32, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x07, + 0x52, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x56, 0x32, 0x88, 0x01, 0x01, + 0x12, 0x17, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x08, + 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x88, 0x01, 0x01, 0x22, 0x17, 0x0a, 0x0c, 0x56, 0x61, 0x6c, + 0x69, 0x64, 0x69, 0x74, 0x79, 0x54, 0x79, 0x70, 0x65, 0x12, 0x07, 0x0a, 0x03, 0x45, 0x4f, 0x4c, + 0x10, 0x00, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x0e, 0x0a, 0x0c, + 0x5f, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x56, 0x31, 0x42, 0x0f, 0x0a, 0x0d, + 0x5f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x69, 0x74, 0x79, 0x54, 0x79, 0x70, 0x65, 0x42, 0x0b, 0x0a, + 0x09, 0x5f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x69, 0x74, 0x79, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x73, + 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x65, 0x42, 0x06, 0x0a, 0x04, 0x5f, 0x74, 0x74, 0x6c, 0x42, + 0x09, 0x0a, 0x07, 0x5f, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x73, + 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x56, 0x32, 0x42, 0x07, 0x0a, 0x05, 0x5f, 0x64, + 0x61, 0x74, 0x61, 0x42, 0x06, 0x5a, 0x04, 0x2e, 0x3b, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x33, +} + +var ( + file_record_proto_rawDescOnce sync.Once + file_record_proto_rawDescData = file_record_proto_rawDesc +) + +func file_record_proto_rawDescGZIP() []byte { + file_record_proto_rawDescOnce.Do(func() { + file_record_proto_rawDescData = protoimpl.X.CompressGZIP(file_record_proto_rawDescData) + }) + return file_record_proto_rawDescData +} + +var file_record_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_record_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_record_proto_goTypes = []interface{}{ + (IpnsRecord_ValidityType)(0), // 0: github.com.boxo.ipns.pb.IpnsRecord.ValidityType + (*IpnsRecord)(nil), // 1: github.com.boxo.ipns.pb.IpnsRecord +} +var file_record_proto_depIdxs = []int32{ + 0, // 0: github.com.boxo.ipns.pb.IpnsRecord.validityType:type_name -> github.com.boxo.ipns.pb.IpnsRecord.ValidityType + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_record_proto_init() } +func file_record_proto_init() { + if File_record_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_record_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*IpnsRecord); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + file_record_proto_msgTypes[0].OneofWrappers = []interface{}{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_record_proto_rawDesc, + NumEnums: 1, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_record_proto_goTypes, + DependencyIndexes: file_record_proto_depIdxs, + EnumInfos: file_record_proto_enumTypes, + MessageInfos: file_record_proto_msgTypes, + }.Build() + File_record_proto = out.File + file_record_proto_rawDesc = nil + file_record_proto_goTypes = nil + file_record_proto_depIdxs = nil +} diff --git a/ipns/pb/record.proto b/ipns/pb/record.proto new file mode 100644 index 000000000..95a3b2e04 --- /dev/null +++ b/ipns/pb/record.proto @@ -0,0 +1,24 @@ +syntax = "proto3"; + +package github.com.boxo.ipns.pb; + +option go_package = ".;pb"; + +// https://specs.ipfs.tech/ipns/ipns-record/#record-serialization-format +message IpnsRecord { + enum ValidityType { + EOL = 0; + } + + // 1-6 are legacy fields used only in V1+V2 records + optional bytes value = 1; + optional bytes signatureV1 = 2; + optional ValidityType validityType = 3; + optional bytes validity = 4; + optional uint64 sequence = 5; + optional uint64 ttl = 6; + // 7-9 are V2 records + optional bytes pubKey = 7; + optional bytes signatureV2 = 8; + optional bytes data = 9; +} diff --git a/ipns/record.go b/ipns/record.go index b479dab90..0a4c26961 100644 --- a/ipns/record.go +++ b/ipns/record.go @@ -1,126 +1,465 @@ +//go:generate protoc -I=pb --go_out=pb pb/record.proto package ipns import ( "bytes" "errors" + "fmt" + "sort" + "time" - pb "github.com/ipfs/boxo/ipns/pb" - - "github.com/gogo/protobuf/proto" + ipns_pb "github.com/ipfs/boxo/ipns/pb" + "github.com/ipfs/boxo/path" + "github.com/ipfs/boxo/util" logging "github.com/ipfs/go-log/v2" - record "github.com/libp2p/go-libp2p-record" + "github.com/ipld/go-ipld-prime" + "github.com/ipld/go-ipld-prime/codec/dagcbor" + "github.com/ipld/go-ipld-prime/datamodel" + basicnode "github.com/ipld/go-ipld-prime/node/basic" ic "github.com/libp2p/go-libp2p/core/crypto" "github.com/libp2p/go-libp2p/core/peer" - pstore "github.com/libp2p/go-libp2p/core/peerstore" + "go.uber.org/multierr" + "google.golang.org/protobuf/proto" ) var log = logging.Logger("ipns") -var _ record.Validator = Validator{} +type ValidityType int64 -// RecordKey returns the libp2p record key for a given peer ID. -func RecordKey(pid peer.ID) string { - return "/ipns/" + string(pid) -} +// ValidityEOL means "this record is valid until {Validity}". This is currently +// the only supported Validity type. +const ValidityEOL ValidityType = 0 -// Validator is an IPNS record validator that satisfies the libp2p record -// validator interface. -type Validator struct { - // KeyBook, if non-nil, will be used to lookup keys for validating IPNS - // records. - KeyBook pstore.KeyBook +// Record represents an [IPNS Record]. +// +// [IPNS Record]: https://specs.ipfs.tech/ipns/ipns-record/ +type Record struct { + pb *ipns_pb.IpnsRecord + node datamodel.Node } -// Validate validates an IPNS record. -func (v Validator) Validate(key string, value []byte) error { - ns, pidString, err := record.SplitKey(key) - if err != nil || ns != "ipns" { - return ErrInvalidPath +// UnmarshalRecord parses the [Protobuf-serialized] IPNS Record into a usable +// [Record] struct. Please note that this function does not perform a full +// validation of the record. For that use [Validate]. +// +// [Protobuf-serialized]: https://specs.ipfs.tech/ipns/ipns-record/#record-serialization-format +func UnmarshalRecord(data []byte) (*Record, error) { + if len(data) > MaxRecordSize { + return nil, ErrRecordSize } - // Parse the value into an IpnsEntry - entry := new(pb.IpnsEntry) - err = proto.Unmarshal(value, entry) + var pb ipns_pb.IpnsRecord + err := proto.Unmarshal(data, &pb) if err != nil { - return ErrBadRecord + return nil, multierr.Combine(ErrInvalidRecord, err) + } + + record := &Record{ + pb: &pb, } - // Get the public key defined by the ipns path - pid, err := peer.IDFromBytes([]byte(pidString)) + // Ensure the record has DAG-CBOR data because we need it. + if len(pb.GetData()) == 0 { + return nil, multierr.Combine(ErrInvalidRecord, ErrDataMissing) + } + + // Decode CBOR data. + builder := basicnode.Prototype__Map{}.NewBuilder() + if err := dagcbor.Decode(builder, bytes.NewReader(pb.GetData())); err != nil { + return nil, multierr.Combine(ErrInvalidRecord, err) + } + record.node = builder.Build() + + return record, nil +} + +// MarshalRecord encodes the given IPNS Record into its [Protobuf serialization format]. +// +// [Protobuf serialization format]: https://specs.ipfs.tech/ipns/ipns-record/#record-serialization-format +func MarshalRecord(rec *Record) ([]byte, error) { + return proto.Marshal(rec.pb) +} + +// Value returns the [path.Path] that is embedded in this IPNS Record. If the +// path is invalid, an [ErrInvalidPath] is returned. +func (rec *Record) Value() (path.Path, error) { + value, err := rec.getBytesValue(cborValueKey) if err != nil { - log.Debugf("failed to parse ipns record key %s into peer ID", pidString) - return ErrKeyFormat + return "", err + } + + p := path.FromString(string(value)) + if err := p.IsValid(); err != nil { + return "", multierr.Combine(ErrInvalidPath, err) } - pubk, err := v.getPublicKey(pid, entry) + return p, nil +} + +func (rec *Record) ValidityType() (ValidityType, error) { + value, err := rec.getIntValue(cborValidityTypeKey) if err != nil { - return err + return -1, err } - return Validate(pubk, entry) + return ValidityType(value), nil } -func (v Validator) getPublicKey(pid peer.ID, entry *pb.IpnsEntry) (ic.PubKey, error) { - switch pk, err := ExtractPublicKey(pid, entry); err { - case peer.ErrNoPublicKey: - case nil: - return pk, nil +// Validity returns the validity of the IPNS Record. This function returns +// [ErrUnrecognizedValidity] if the validity type of the record isn't EOL. +// Otherwise, it returns an error if it can't parse the EOL. +func (rec *Record) Validity() (time.Time, error) { + validityType, err := rec.ValidityType() + if err != nil { + return time.Time{}, err + } + + switch validityType { + case ValidityEOL: + value, err := rec.getBytesValue(cborValidityKey) + if err != nil { + return time.Time{}, err + } + + v, err := util.ParseRFC3339(string(value)) + if err != nil { + return time.Time{}, multierr.Combine(ErrInvalidValidity, err) + } + return v, nil default: - return nil, err + return time.Time{}, ErrUnrecognizedValidity } +} - if v.KeyBook == nil { - log.Debugf("public key with hash %s not found in IPNS record and no peer store provided", pid) - return nil, ErrPublicKeyNotFound +func (rec *Record) Sequence() (uint64, error) { + value, err := rec.getIntValue(cborSequenceKey) + if err != nil { + return 0, err } - 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 uint64(value), nil +} + +func (rec *Record) TTL() (time.Duration, error) { + value, err := rec.getIntValue(cborTTLKey) + if err != nil { + return 0, err } - return pubk, nil + + return time.Duration(value), nil } -// Select selects the best record by checking which has the highest sequence -// number and latest EOL. -// -// This function returns an error if any of the records fail to parse. Validate -// your records first! -func (v Validator) Select(k string, vals [][]byte) (int, error) { - var recs []*pb.IpnsEntry - for _, v := range vals { - e := new(pb.IpnsEntry) - if err := proto.Unmarshal(v, e); err != nil { - return -1, err - } - recs = append(recs, e) +func (rec *Record) PubKey() (ic.PubKey, error) { + if pk := rec.pb.GetPubKey(); len(pk) != 0 { + return ic.UnmarshalPublicKey(pk) } - return selectRecord(recs, vals) + return nil, ErrPublicKeyNotFound } -func selectRecord(recs []*pb.IpnsEntry, vals [][]byte) (int, error) { - switch len(recs) { - case 0: - return -1, errors.New("no usable records in given set") - case 1: - return 0, nil +func (rec *Record) getBytesValue(key string) ([]byte, error) { + node, err := rec.node.LookupByString(key) + if err != nil { + return nil, multierr.Combine(ErrInvalidRecord, err) + } + + value, err := node.AsBytes() + if err != nil { + return nil, multierr.Combine(ErrInvalidRecord, err) + } + + return value, nil +} + +func (rec *Record) getIntValue(key string) (int64, error) { + node, err := rec.node.LookupByString(key) + if err != nil { + return -1, multierr.Combine(ErrInvalidRecord, err) + } + + value, err := node.AsInt() + if err != nil { + return -1, multierr.Combine(ErrInvalidRecord, err) + } + + return value, nil +} + +const ( + cborValidityKey = "Validity" + cborValidityTypeKey = "ValidityType" + cborValueKey = "Value" + cborSequenceKey = "Sequence" + cborTTLKey = "TTL" +) + +type options struct { + v1Compatibility bool + embedPublicKey *bool +} + +type Option func(*options) + +func WithV1Compatibility(compatible bool) Option { + return func(o *options) { + o.v1Compatibility = compatible + } +} + +func WithPublicKey(embedded bool) Option { + return func(o *options) { + o.embedPublicKey = &embedded + } +} + +func processOptions(opts ...Option) *options { + options := &options{} + for _, opt := range opts { + opt(options) + } + return options +} + +// NewRecord creates a new IPNS [Record] and signs it with the given private key. +// By default, we embed the public key for key types whose peer IDs do not encode +// the public key, such as RSA and ECDSA key types. This can be changed with the +// option [WithPublicKey]. +func NewRecord(sk ic.PrivKey, value path.Path, seq uint64, eol time.Time, ttl time.Duration, opts ...Option) (*Record, error) { + options := processOptions(opts...) + + node, err := createNode(value, seq, eol, ttl) + if err != nil { + return nil, err + } + + cborData, err := nodeToCBOR(node) + if err != nil { + return nil, err + } + + sig2Data, err := recordDataForSignatureV2(cborData) + if err != nil { + return nil, err + } + + sig2, err := sk.Sign(sig2Data) + if err != nil { + return nil, err + } + + pb := ipns_pb.IpnsRecord{ + Data: cborData, + SignatureV2: sig2, } - var i int - for j := 1; j < len(recs); j++ { - cmp, err := Compare(recs[i], recs[j]) + if options.v1Compatibility { + pb.Value = []byte(value) + typ := ipns_pb.IpnsRecord_EOL + pb.ValidityType = &typ + pb.Sequence = &seq + pb.Validity = []byte(util.FormatRFC3339(eol)) + ttlNs := uint64(ttl.Nanoseconds()) + pb.Ttl = proto.Uint64(ttlNs) + + // For now we still create V1 signatures. These are deprecated, and not + // used during verification anymore (Validate func requires SignatureV2), + // but setting it here allows legacy nodes (e.g., go-ipfs < v0.9.0) to + // still resolve IPNS published by modern nodes. + sig1, err := sk.Sign(recordDataForSignatureV1(&pb)) if err != nil { - return -1, err + return nil, fmt.Errorf("%w: could not compute signature data", err) } - if cmp == 0 { - cmp = bytes.Compare(vals[i], vals[j]) + pb.SignatureV1 = sig1 + } + + embedPublicKey := false + if options.embedPublicKey == nil { + embedPublicKey, err = needToEmbedPublicKey(sk.GetPublic()) + if err != nil { + return nil, err } - if cmp < 0 { - i = j + } else { + embedPublicKey = *options.embedPublicKey + } + + if embedPublicKey { + pkBytes, err := ic.MarshalPublicKey(sk.GetPublic()) + if err != nil { + return nil, err } + pb.PubKey = pkBytes } - return i, nil + return &Record{ + pb: &pb, + node: node, + }, nil +} + +func createNode(value path.Path, seq uint64, eol time.Time, ttl time.Duration) (datamodel.Node, error) { + m := make(map[string]ipld.Node) + var keys []string + + m[cborValueKey] = basicnode.NewBytes([]byte(value)) + keys = append(keys, cborValueKey) + + m[cborValidityKey] = basicnode.NewBytes([]byte(util.FormatRFC3339(eol))) + keys = append(keys, cborValidityKey) + + m[cborValidityTypeKey] = basicnode.NewInt(int64(ValidityEOL)) + keys = append(keys, cborValidityTypeKey) + + m[cborSequenceKey] = basicnode.NewInt(int64(seq)) + keys = append(keys, cborSequenceKey) + + m[cborTTLKey] = basicnode.NewInt(int64(ttl)) + keys = append(keys, cborTTLKey) + + sort.Slice(keys, func(i, j int) bool { + li, lj := len(keys[i]), len(keys[j]) + if li == lj { + return keys[i] < keys[j] + } + return li < lj + }) + + newNd := basicnode.Prototype__Map{}.NewBuilder() + ma, err := newNd.BeginMap(int64(len(keys))) + if err != nil { + return nil, err + } + + for _, k := range keys { + if err := ma.AssembleKey().AssignString(k); err != nil { + return nil, err + } + if err := ma.AssembleValue().AssignNode(m[k]); err != nil { + return nil, err + } + } + + if err := ma.Finish(); err != nil { + return nil, err + } + + return newNd.Build(), nil +} + +func nodeToCBOR(node datamodel.Node) ([]byte, error) { + buf := new(bytes.Buffer) + if err := dagcbor.Encode(node, buf); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +func recordDataForSignatureV1(e *ipns_pb.IpnsRecord) []byte { + return bytes.Join([][]byte{ + e.Value, + e.Validity, + []byte(fmt.Sprint(e.GetValidityType())), + }, + []byte{}) +} + +func recordDataForSignatureV2(data []byte) ([]byte, error) { + dataForSig := []byte("ipns-signature:") + dataForSig = append(dataForSig, data...) + return dataForSig, nil +} + +func needToEmbedPublicKey(pk ic.PubKey) (bool, error) { + // First try extracting the peer ID from the public key. + pid, err := peer.IDFromPublicKey(pk) + if err != nil { + return false, fmt.Errorf("cannot convert public key to peer ID: %w", err) + } + + _, err = pid.ExtractPublicKey() + if err == nil { + // Can be extracted, therefore no need to embed the public key. + return false, nil + } + + if errors.Is(err, peer.ErrNoPublicKey) { + return true, nil + } + + return false, fmt.Errorf("cannot extract ID from public key: %w", err) +} + +// compare compares two IPNS Records. It returns: +// +// - -1 if a is older than b +// - 0 if a and b cannot be ordered (this doesn't mean that they are equal) +// - +1 if a is newer than b +// +// This function does not validate the records. The caller is responsible for +// ensuring that the Records are valid by using [Validate]. +func compare(a, b *Record) (int, error) { + aHasV2Sig := a.pb.GetSignatureV2() != nil + bHasV2Sig := b.pb.GetSignatureV2() != nil + + // Having a newer signature version is better than an older signature version + if aHasV2Sig && !bHasV2Sig { + return 1, nil + } else if !aHasV2Sig && bHasV2Sig { + return -1, nil + } + + as, err := a.Sequence() + if err != nil { + return 0, err + } + + bs, err := b.Sequence() + if err != nil { + return 0, err + } + + if as > bs { + return 1, nil + } else if as < bs { + return -1, nil + } + + at, err := a.Validity() + if err != nil { + return 0, err + } + + bt, err := b.Validity() + if err != nil { + return 0, err + } + + if at.After(bt) { + return 1, nil + } else if bt.After(at) { + return -1, nil + } + + return 0, nil +} + +// ExtractPublicKey extracts a [crypto.PubKey] matching the given [Name] from +// the IPNS Record, if possible. +func ExtractPublicKey(rec *Record, name Name) (ic.PubKey, error) { + if pk, err := rec.PubKey(); err == nil { + expPid, err := peer.IDFromPublicKey(pk) + if err != nil { + return nil, multierr.Combine(ErrInvalidPublicKey, err) + } + + if !name.Equal(NameFromPeer(expPid)) { + return nil, ErrPublicKeyMismatch + } + + return pk, nil + } else if !errors.Is(err, ErrPublicKeyNotFound) { + return nil, multierr.Combine(ErrInvalidPublicKey, err) + } else { + return name.Peer().ExtractPublicKey() + } } diff --git a/ipns/record_test.go b/ipns/record_test.go new file mode 100644 index 000000000..4c4ab7018 --- /dev/null +++ b/ipns/record_test.go @@ -0,0 +1,283 @@ +package ipns + +import ( + "bytes" + "testing" + "time" + + ipns_pb "github.com/ipfs/boxo/ipns/pb" + "github.com/ipfs/boxo/path" + "github.com/ipfs/boxo/util" + "github.com/ipld/go-ipld-prime/codec/dagcbor" + basicnode "github.com/ipld/go-ipld-prime/node/basic" + ic "github.com/libp2p/go-libp2p/core/crypto" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +const ( + testPath = path.Path("/ipfs/bafkqac3jobxhgidsn5rww4yk") +) + +func mustKeyPair(t *testing.T, typ int) (ic.PrivKey, ic.PubKey, Name) { + sr := util.NewTimeSeededRand() + sk, pk, err := ic.GenerateKeyPairWithReader(typ, 2048, sr) + require.NoError(t, err) + + pid, err := peer.IDFromPublicKey(pk) + require.NoError(t, err) + + return sk, pk, NameFromPeer(pid) +} + +func mustNewRecord(t *testing.T, sk ic.PrivKey, value path.Path, seq uint64, eol time.Time, ttl time.Duration, opts ...Option) *Record { + rec, err := NewRecord(sk, value, seq, eol, ttl, opts...) + require.NoError(t, err) + require.NoError(t, Validate(rec, sk.GetPublic())) + return rec +} + +func mustMarshal(t *testing.T, entry *Record) []byte { + data, err := MarshalRecord(entry) + require.NoError(t, err) + return data +} + +func fieldsMatch(t *testing.T, rec *Record, value path.Path, seq uint64, eol time.Time, ttl time.Duration) { + recPath, err := rec.Value() + require.NoError(t, err) + require.Equal(t, value.String(), recPath.String()) + + recSeq, err := rec.Sequence() + require.NoError(t, err) + require.Equal(t, seq, recSeq) + + recValidityType, err := rec.ValidityType() + require.NoError(t, err) + require.Equal(t, recValidityType, ValidityEOL) + + recEOL, err := rec.Validity() + require.NoError(t, err) + require.True(t, recEOL.Equal(eol)) + + recTTL, err := rec.TTL() + require.NoError(t, err) + require.Equal(t, ttl, recTTL) +} + +func fieldsMatchV1(t *testing.T, rec *Record, value path.Path, seq uint64, eol time.Time, ttl time.Duration) { + require.Equal(t, value.String(), string(rec.pb.GetValue())) + require.Equal(t, seq, rec.pb.GetSequence()) + require.Equal(t, rec.pb.GetValidityType(), ipns_pb.IpnsRecord_EOL) + require.Equal(t, time.Duration(rec.pb.GetTtl()), ttl) + + recEOL, err := util.ParseRFC3339(string(rec.pb.GetValidity())) + require.NoError(t, err) + require.NoError(t, err) + require.True(t, recEOL.Equal(eol)) +} + +func TestNewRecord(t *testing.T) { + t.Parallel() + + sk, _, _ := mustKeyPair(t, ic.Ed25519) + + seq := uint64(0) + eol := time.Now().Add(time.Hour) + ttl := time.Minute * 10 + + t.Run("V2 only record by default", func(t *testing.T) { + t.Parallel() + + rec := mustNewRecord(t, sk, testPath, seq, eol, ttl) + require.Empty(t, rec.pb.SignatureV1) + + _, err := rec.PubKey() + require.ErrorIs(t, err, ErrPublicKeyNotFound) + + fieldsMatch(t, rec, testPath, seq, eol, ttl) + require.Empty(t, rec.pb.GetValue()) + require.Empty(t, rec.pb.GetSequence()) + require.Empty(t, rec.pb.GetValidity()) + require.Empty(t, rec.pb.GetValidityType()) + require.Empty(t, rec.pb.GetTtl()) + }) + + t.Run("V1+V2 with option", func(t *testing.T) { + t.Parallel() + + rec := mustNewRecord(t, sk, testPath, seq, eol, ttl, WithV1Compatibility(true)) + require.NotEmpty(t, rec.pb.SignatureV1) + + _, err := rec.PubKey() + require.ErrorIs(t, err, ErrPublicKeyNotFound) + + fieldsMatch(t, rec, testPath, seq, eol, ttl) + fieldsMatchV1(t, rec, testPath, seq, eol, ttl) + }) + + t.Run("Public key embedded by default for RSA and ECDSA keys", func(t *testing.T) { + t.Parallel() + + for _, keyType := range []int{ic.RSA, ic.ECDSA} { + sk, _, _ := mustKeyPair(t, keyType) + rec := mustNewRecord(t, sk, testPath, seq, eol, ttl) + fieldsMatch(t, rec, testPath, seq, eol, ttl) + + pk, err := rec.PubKey() + require.NoError(t, err) + require.True(t, pk.Equals(sk.GetPublic())) + } + }) + + t.Run("Public key not embedded by default for Ed25519 and Secp256k1 keys", func(t *testing.T) { + t.Parallel() + + for _, keyType := range []int{ic.Ed25519, ic.Secp256k1} { + sk, _, _ := mustKeyPair(t, keyType) + rec := mustNewRecord(t, sk, testPath, seq, eol, ttl) + fieldsMatch(t, rec, testPath, seq, eol, ttl) + + _, err := rec.PubKey() + require.ErrorIs(t, err, ErrPublicKeyNotFound) + } + }) +} + +func TestExtractPublicKey(t *testing.T) { + t.Parallel() + + t.Run("Returns expected public key when embedded in Peer ID", func(t *testing.T) { + sk, pk, name := mustKeyPair(t, ic.Ed25519) + rec := mustNewRecord(t, sk, testPath, 0, time.Now().Add(time.Hour), time.Minute*10, WithPublicKey(false)) + + pk2, err := ExtractPublicKey(rec, name) + require.Nil(t, err) + require.Equal(t, pk, pk2) + }) + + t.Run("Returns expected public key when embedded in Record (by default)", func(t *testing.T) { + sk, pk, name := mustKeyPair(t, ic.RSA) + rec := mustNewRecord(t, sk, testPath, 0, time.Now().Add(time.Hour), time.Minute*10) + + pk2, err := ExtractPublicKey(rec, name) + require.Nil(t, err) + require.True(t, pk.Equals(pk2)) + }) + + t.Run("Errors when not embedded in Record or Peer ID", func(t *testing.T) { + sk, _, name := mustKeyPair(t, ic.RSA) + rec := mustNewRecord(t, sk, testPath, 0, time.Now().Add(time.Hour), time.Minute*10, WithPublicKey(false)) + + pk, err := ExtractPublicKey(rec, name) + require.Error(t, err) + require.Nil(t, pk) + }) + + t.Run("Errors on invalid public key bytes", func(t *testing.T) { + sk, _, name := mustKeyPair(t, ic.Ed25519) + rec := mustNewRecord(t, sk, testPath, 0, time.Now().Add(time.Hour), time.Minute*10) + + // Force bad pub key information. + rec.pb.PubKey = []byte("invalid stuff") + + pk, err := ExtractPublicKey(rec, name) + require.ErrorIs(t, err, ErrInvalidPublicKey) + require.Nil(t, pk) + }) +} + +func TestCBORDataSerialization(t *testing.T) { + t.Parallel() + + sk, _, _ := mustKeyPair(t, ic.Ed25519) + + eol := time.Now().Add(time.Hour) + path := path.FromString(string(append([]byte("/path/1"), 0x00))) + seq := uint64(1) + ttl := time.Hour + + rec := mustNewRecord(t, sk, path, seq, eol, ttl) + + builder := basicnode.Prototype__Map{}.NewBuilder() + err := dagcbor.Decode(builder, bytes.NewReader(rec.pb.GetData())) + require.NoError(t, err) + + node := builder.Build() + iter := node.MapIterator() + var fields []string + for !iter.Done() { + k, v, err := iter.Next() + require.NoError(t, err) + kStr, err := k.AsString() + require.NoError(t, err) + + switch kStr { + case cborValueKey: + b, err := v.AsBytes() + require.NoError(t, err) + require.Equal(t, b, []byte(path)) + case cborSequenceKey: + s, err := v.AsInt() + require.NoError(t, err) + require.Equal(t, seq, uint64(s)) + case cborValidityKey: + val, err := v.AsBytes() + require.NoError(t, err) + require.Equal(t, []byte(util.FormatRFC3339(eol)), val) + case cborValidityTypeKey: + vt, err := v.AsInt() + require.NoError(t, err) + require.Equal(t, uint64(0), uint64(vt)) + case cborTTLKey: + ttlVal, err := v.AsInt() + require.NoError(t, err) + require.Equal(t, ttl, time.Duration(ttlVal)) + } + + fields = append(fields, kStr) + } + + // Ensure correct key order, i.e., by length then value. + expectedOrder := []string{"TTL", "Value", "Sequence", "Validity", "ValidityType"} + require.Len(t, fields, len(expectedOrder)) + for i, f := range fields { + expected := expectedOrder[i] + assert.Equal(t, expected, f) + } +} + +func TestUnmarshal(t *testing.T) { + t.Parallel() + + t.Run("Errors on invalid bytes", func(t *testing.T) { + _, err := UnmarshalRecord([]byte("blah blah blah")) + require.ErrorIs(t, err, ErrInvalidRecord) + }) + + t.Run("Errors if record is too long", func(t *testing.T) { + data := make([]byte, MaxRecordSize+1) + _, err := UnmarshalRecord(data) + require.ErrorIs(t, err, ErrRecordSize) + }) + + t.Run("Errors with V1-only records", func(t *testing.T) { + pb := ipns_pb.IpnsRecord{} + data, err := proto.Marshal(&pb) + require.NoError(t, err) + _, err = UnmarshalRecord(data) + require.ErrorIs(t, err, ErrDataMissing) + }) + + t.Run("Errors on bad data", func(t *testing.T) { + pb := ipns_pb.IpnsRecord{ + Data: []byte("definitely not cbor"), + } + data, err := proto.Marshal(&pb) + require.NoError(t, err) + _, err = UnmarshalRecord(data) + require.ErrorIs(t, err, ErrInvalidRecord) + }) +} diff --git a/ipns/select_test.go b/ipns/select_test.go deleted file mode 100644 index 5b435f62d..000000000 --- a/ipns/select_test.go +++ /dev/null @@ -1,126 +0,0 @@ -package ipns - -import ( - "fmt" - "math/rand" - "testing" - "time" - - pb "github.com/ipfs/boxo/ipns/pb" - - "github.com/gogo/protobuf/proto" - u "github.com/ipfs/boxo/util" - ci "github.com/libp2p/go-libp2p/core/crypto" -) - -func shuffle(a []*pb.IpnsEntry) { - for n := 0; n < 5; n++ { - for i := range a { - j := rand.Intn(len(a)) - a[i], a[j] = a[j], a[i] - } - } -} - -func AssertSelected(r *pb.IpnsEntry, from ...*pb.IpnsEntry) error { - shuffle(from) - var vals [][]byte - for _, r := range from { - data, err := proto.Marshal(r) - if err != nil { - return err - } - vals = append(vals, data) - } - - i, err := selectRecord(from, vals) - if err != nil { - return err - } - - if from[i] != r { - return fmt.Errorf("selected incorrect record %d", i) - } - - return nil -} - -func TestOrdering(t *testing.T) { - // select timestamp so selection is deterministic - ts := time.Unix(1000000, 0) - - // generate a key for signing the records - r := u.NewSeededRand(15) // generate deterministic keypair - priv, _, err := ci.GenerateKeyPairWithReader(ci.RSA, 2048, r) - if err != nil { - t.Fatal(err) - } - - e1, err := Create(priv, []byte("foo"), 1, ts.Add(time.Hour), 0) - if err != nil { - t.Fatal(err) - } - - e2, err := Create(priv, []byte("bar"), 2, ts.Add(time.Hour), 0) - if err != nil { - t.Fatal(err) - } - - e3, err := Create(priv, []byte("baz"), 3, ts.Add(time.Hour), 0) - if err != nil { - t.Fatal(err) - } - - e4, err := Create(priv, []byte("cat"), 3, ts.Add(time.Hour*2), 0) - if err != nil { - t.Fatal(err) - } - - e5, err := Create(priv, []byte("dog"), 4, ts.Add(time.Hour*3), 0) - if err != nil { - t.Fatal(err) - } - - e6, err := Create(priv, []byte("fish"), 4, ts.Add(time.Hour*3), 0) - if err != nil { - t.Fatal(err) - } - - // e1 is the only record, i hope it gets this right - err = AssertSelected(e1, e1) - if err != nil { - t.Fatal(err) - } - - // e2 has the highest sequence number - err = AssertSelected(e2, e1, e2) - if err != nil { - t.Fatal(err) - } - - // e3 has the highest sequence number - err = AssertSelected(e3, e1, e2, e3) - if err != nil { - t.Fatal(err) - } - - // e4 has a higher timeout - err = AssertSelected(e4, e1, e2, e3, e4) - if err != nil { - t.Fatal(err) - } - - // e5 has the highest sequence number - err = AssertSelected(e5, e1, e2, e3, e4, e5) - if err != nil { - t.Fatal(err) - } - - // e6 should be selected as its signauture will win in the comparison - err = AssertSelected(e6, e1, e2, e3, e4, e5, e6) - if err != nil { - t.Fatal(err) - } - - _ = []interface{}{e1, e2, e3, e4, e5, e6} -} diff --git a/ipns/validate_test.go b/ipns/validate_test.go deleted file mode 100644 index 1b24ec492..000000000 --- a/ipns/validate_test.go +++ /dev/null @@ -1,449 +0,0 @@ -package ipns - -import ( - "bytes" - "errors" - "fmt" - "math/rand" - "strings" - "testing" - "time" - - "github.com/gogo/protobuf/proto" - pb "github.com/ipfs/boxo/ipns/pb" - u "github.com/ipfs/boxo/util" - ipldcodec "github.com/ipld/go-ipld-prime/multicodec" - basicnode "github.com/ipld/go-ipld-prime/node/basic" - "github.com/libp2p/go-libp2p/core/crypto" - "github.com/libp2p/go-libp2p/core/peer" - pstore "github.com/libp2p/go-libp2p/core/peerstore" - "github.com/libp2p/go-libp2p/p2p/host/peerstore/pstoremem" - "github.com/multiformats/go-multicodec" - "github.com/stretchr/testify/assert" -) - -func testValidatorCase(t *testing.T, priv crypto.PrivKey, kbook pstore.KeyBook, key string, val []byte, eol time.Time, exp error) { - t.Helper() - - 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 crypto.PrivKey, kbook pstore.KeyBook, key string, val []byte, eol time.Time, matchf func(*testing.T, error)) { - t.Helper() - validator := Validator{kbook} - - data := val - if data == nil { - p := []byte("/ipfs/QmfM2r8seH2GiRaC4esTjeraXEachRt8ZsSeGaWTPLyMoG") - entry, err := Create(priv, p, 1, eol, 0) - if err != nil { - t.Fatal(err) - } - - data, err = proto.Marshal(entry) - if err != nil { - t.Fatal(err) - } - } - - matchf(t, validator.Validate(key, data)) -} - -func TestValidator(t *testing.T) { - ts := time.Now() - - priv, id, _ := genKeys(t) - priv2, id2, _ := genKeys(t) - kbook, err := pstoremem.NewPeerstore() - if err != nil { - t.Fatal(err) - } - if err := kbook.AddPubKey(id, priv.GetPublic()); err != nil { - t.Fatal(err) - } - emptyKbook, err := pstoremem.NewPeerstore() - if err != nil { - t.Fatal(err) - } - - testValidatorCase(t, priv, kbook, "/ipns/"+string(id), nil, ts.Add(time.Hour), nil) - testValidatorCase(t, priv, kbook, "/ipns/"+string(id), nil, ts.Add(time.Hour*-1), ErrExpiredRecord) - testValidatorCase(t, priv, kbook, "/ipns/"+string(id), []byte("bad data"), ts.Add(time.Hour), ErrBadRecord) - testValidatorCase(t, priv, kbook, "/ipns/"+"bad key", nil, ts.Add(time.Hour), ErrKeyFormat) - testValidatorCase(t, priv, emptyKbook, "/ipns/"+string(id), nil, ts.Add(time.Hour), ErrPublicKeyNotFound) - testValidatorCase(t, priv2, kbook, "/ipns/"+string(id2), nil, ts.Add(time.Hour), ErrPublicKeyNotFound) - testValidatorCase(t, priv2, kbook, "/ipns/"+string(id), nil, ts.Add(time.Hour), ErrSignature) - testValidatorCase(t, priv, kbook, "//"+string(id), nil, ts.Add(time.Hour), ErrInvalidPath) - 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, err := pstoremem.NewPeerstore() - if err != nil { - t.Fatal(err) - } - - pth := []byte("/ipfs/QmfM2r8seH2GiRaC4esTjeraXEachRt8ZsSeGaWTPLyMoG") - - priv, _, ipnsk := genKeys(t) - - entry, err := Create(priv, pth, 1, goodeol, 0) - if err != nil { - t.Fatal(err) - } - - testValidatorCase(t, priv, kbook, ipnsk, mustMarshal(t, entry), goodeol, ErrPublicKeyNotFound) - - pubkb, err := crypto.MarshalPublicKey(priv.GetPublic()) - 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 := crypto.MarshalPublicKey(opriv.GetPublic()) - 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, err := pstoremem.NewPeerstore() - if err != nil { - t.Fatal(err) - } - - pth := []byte("/ipfs/QmfM2r8seH2GiRaC4esTjeraXEachRt8ZsSeGaWTPLyMoG") - - sk, pk, err := crypto.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 := Create(sk, pth, 1, goodeol, 0) - 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 TestOnlySignatureV2Validate(t *testing.T) { - goodeol := time.Now().Add(time.Hour) - - sk, pk, err := crypto.GenerateEd25519Key(rand.New(rand.NewSource(42))) - if err != nil { - t.Fatal(err) - } - - path1 := []byte("/path/1") - entry, err := Create(sk, path1, 1, goodeol, 0) - if err != nil { - t.Fatal(err) - } - - if err := Validate(pk, entry); err != nil { - t.Fatal(err) - } - - entry.SignatureV2 = nil - if err := Validate(pk, entry); !errors.Is(err, ErrSignature) { - t.Fatal(err) - } -} - -func TestSignatureV1Ignored(t *testing.T) { - goodeol := time.Now().Add(time.Hour) - - sk, pk, err := crypto.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) - - path1 := []byte("/path/1") - entry1, err := Create(sk, path1, 1, goodeol, 0) - if err != nil { - t.Fatal(err) - } - - path2 := []byte("/path/2") - entry2, err := Create(sk, path2, 2, goodeol, 0) - if err != nil { - t.Fatal(err) - } - - if err := Validate(pk, entry1); err != nil { - t.Fatal(err) - } - - if err := Validate(pk, entry2); err != nil { - t.Fatal(err) - } - - v := Validator{} - best, err := v.Select(ipnsk, [][]byte{mustMarshal(t, entry1), mustMarshal(t, entry2)}) - if err != nil { - t.Fatal(err) - } - if best != 1 { - t.Fatal("entry2 should be better than entry1") - } - - // Having only the v1 signature should be invalid - entry2.SignatureV2 = nil - if err := Validate(pk, entry2); !errors.Is(err, ErrSignature) { - t.Fatal(err) - } - - // Record with v2 signature should always be preferred - best, err = v.Select(ipnsk, [][]byte{mustMarshal(t, entry1), mustMarshal(t, entry2)}) - if err != nil { - t.Fatal(err) - } - if best != 0 { - t.Fatal("entry1 should be better than entry2") - } - - // Having a missing v1 signature is acceptable as long as there is a valid v2 signature - entry1.SignatureV1 = nil - if err := Validate(pk, entry1); err != nil { - t.Fatal(err) - } - - // Having an invalid v1 signature is acceptable as long as there is a valid v2 signature - entry1.SignatureV1 = []byte("garbage") - if err := Validate(pk, entry1); err != nil { - t.Fatal(err) - } -} - -func TestMaxSizeValidate(t *testing.T) { - goodeol := time.Now().Add(time.Hour) - - sk, pk, err := crypto.GenerateEd25519Key(rand.New(rand.NewSource(42))) - if err != nil { - t.Fatal(err) - } - - // Create record over the max size (value+other fields) - value := make([]byte, MaxRecordSize) - entry, err := Create(sk, value, 1, goodeol, 0) - if err != nil { - t.Fatal(err) - } - // Must fail with ErrRecordSize - if err := Validate(pk, entry); !errors.Is(err, ErrRecordSize) { - t.Fatal(err) - } -} - -func TestCborDataCanonicalization(t *testing.T) { - goodeol := time.Now().Add(time.Hour) - - sk, pk, err := crypto.GenerateEd25519Key(rand.New(rand.NewSource(42))) - if err != nil { - t.Fatal(err) - } - - path := append([]byte("/path/1"), 0x00) - seqnum := uint64(1) - entry, err := Create(sk, path, seqnum, goodeol, time.Hour) - if err != nil { - t.Fatal(err) - } - - if err := Validate(pk, entry); err != nil { - t.Fatal(err) - } - - dec, err := ipldcodec.LookupDecoder(uint64(multicodec.DagCbor)) - if err != nil { - t.Fatal(err) - } - - ndbuilder := basicnode.Prototype__Map{}.NewBuilder() - if err := dec(ndbuilder, bytes.NewReader(entry.GetData())); err != nil { - t.Fatal(err) - } - - nd := ndbuilder.Build() - iter := nd.MapIterator() - var fields []string - for !iter.Done() { - k, v, err := iter.Next() - if err != nil { - t.Fatal(err) - } - kstr, err := k.AsString() - if err != nil { - t.Fatal(err) - } - - switch kstr { - case value: - b, err := v.AsBytes() - if err != nil { - t.Fatal(err) - } - if !bytes.Equal(path, b) { - t.Fatal("value did not match") - } - case sequence: - s, err := v.AsInt() - if err != nil { - t.Fatal(err) - } - if uint64(s) != seqnum { - t.Fatal("sequence numbers did not match") - } - case validity: - val, err := v.AsBytes() - if err != nil { - t.Fatal(err) - } - if !bytes.Equal(val, []byte(u.FormatRFC3339(goodeol))) { - t.Fatal("validity did not match") - } - case validityType: - vt, err := v.AsInt() - if err != nil { - t.Fatal(err) - } - if uint64(vt) != 0 { - t.Fatal("validity types did not match") - } - case ttl: - ttlVal, err := v.AsInt() - if err != nil { - t.Fatal(err) - } - // TODO: test non-zero TTL - if uint64(ttlVal) != uint64(time.Hour.Nanoseconds()) { - t.Fatal("TTLs did not match") - } - } - - fields = append(fields, kstr) - } - - // Check for map sort order (i.e. by length then by value) - expectedOrder := []string{"TTL", "Value", "Sequence", "Validity", "ValidityType"} - if len(fields) != len(expectedOrder) { - t.Fatal("wrong number of fields") - } - - for i, f := range fields { - expected := expectedOrder[i] - if f != expected { - t.Fatalf("expected %s, got %s", expected, f) - } - } -} - -func genKeys(t *testing.T) (crypto.PrivKey, peer.ID, string) { - sr := u.NewTimeSeededRand() - priv, _, err := crypto.GenerateKeyPairWithReader(crypto.RSA, 2048, sr) - if err != nil { - t.Fatal(err) - } - - // Create entry with expiry in one hour - pid, err := peer.IDFromPrivateKey(priv) - if err != nil { - t.Fatal(err) - } - ipnsKey := RecordKey(pid) - - return priv, pid, ipnsKey -} - -func TestValidateWithPeerID(t *testing.T) { - path := []byte("/ipfs/bafkreifjjcie6lypi6ny7amxnfftagclbuxndqonfipmb64f2km2devei4") - eol := time.Now().Add(time.Hour) - - rnd := rand.New(rand.NewSource(42)) - - sk, pk, err := crypto.GenerateEd25519Key(rnd) - assert.NoError(t, err) - - pid, err := peer.IDFromPublicKey(pk) - assert.NoError(t, err) - - entry, err := Create(sk, path, 1, eol, 0) - assert.NoError(t, err) - - t.Run("valid peer ID", func(t *testing.T) { - t.Parallel() - err = ValidateWithPeerID(pid, entry) - assert.NoError(t, err) - }) - - t.Run("invalid peer ID", func(t *testing.T) { - t.Parallel() - - _, pk2, err := crypto.GenerateEd25519Key(rnd) - assert.NoError(t, err) - - pid2, err := peer.IDFromPublicKey(pk2) - assert.NoError(t, err) - - err = ValidateWithPeerID(pid2, entry) - assert.ErrorIs(t, err, ErrSignature) - }) -} diff --git a/ipns/validation.go b/ipns/validation.go new file mode 100644 index 000000000..b296f77f7 --- /dev/null +++ b/ipns/validation.go @@ -0,0 +1,251 @@ +package ipns + +import ( + "bytes" + "errors" + "fmt" + "time" + + ipns_pb "github.com/ipfs/boxo/ipns/pb" + "github.com/ipld/go-ipld-prime/codec/dagcbor" + basicnode "github.com/ipld/go-ipld-prime/node/basic" + record "github.com/libp2p/go-libp2p-record" + ic "github.com/libp2p/go-libp2p/core/crypto" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/libp2p/go-libp2p/core/peerstore" + "google.golang.org/protobuf/proto" +) + +// ValidateWithName validates the given IPNS [Record] against the given [Name]. +func ValidateWithName(rec *Record, name Name) error { + pk, err := ExtractPublicKey(rec, name) + if err != nil { + return err + } + + return Validate(rec, pk) +} + +// Validates validates the given IPNS Record against the given [crypto.PubKey], +// following the [Record Verification] specification. +// +// [Record Verification]: https://specs.ipfs.tech/ipns/ipns-record/#record-verification +func Validate(rec *Record, pk ic.PubKey) error { + // (1) Ensure size is not over maximum record size. + if proto.Size(rec.pb) > MaxRecordSize { + return ErrRecordSize + } + + // (2) Ensure SignatureV2 and Data are present and not empty. + if len(rec.pb.GetSignatureV2()) == 0 { + return ErrSignature + } + if len(rec.pb.GetData()) == 0 { + return ErrInvalidRecord + } + + // (3) Extract Public Key - not necessary. Done via [ValidateWithName]. + + // (4) Get deserialized Data as DAG-CBOR document. + sig2Data, err := recordDataForSignatureV2(rec.pb.GetData()) + if err != nil { + return fmt.Errorf("could not compute signature data: %w", err) + } + + // (6) Verify signature against concatenation. + if ok, err := pk.Verify(sig2Data, rec.pb.GetSignatureV2()); err != nil || !ok { + return ErrSignature + } + + // (5) Ensure that CBOR data matches Protobuf, only if non-CBOR Value or SignatureV1 are present. + if len(rec.pb.GetSignatureV1()) != 0 || len(rec.pb.GetValue()) != 0 { + if err := validateCborDataMatchesPbData(rec.pb); err != nil { + return err + } + } + + // Check EOL. + eol, err := rec.Validity() + if err != nil { + return err + } + + if time.Now().After(eol) { + return ErrExpiredRecord + } + + return nil +} + +// TODO: Most of this function could probably be replaced with codegen +func validateCborDataMatchesPbData(entry *ipns_pb.IpnsRecord) error { + if len(entry.GetData()) == 0 { + return fmt.Errorf("record data is missing") + } + + ndbuilder := basicnode.Prototype__Map{}.NewBuilder() + if err := dagcbor.Decode(ndbuilder, bytes.NewReader(entry.GetData())); err != nil { + return err + } + + fullNd := ndbuilder.Build() + nd, err := fullNd.LookupByString(cborValueKey) + if err != nil { + return err + } + ndBytes, err := nd.AsBytes() + if err != nil { + return err + } + if !bytes.Equal(entry.GetValue(), ndBytes) { + return fmt.Errorf("field \"%v\" did not match between protobuf and CBOR", cborValueKey) + } + + nd, err = fullNd.LookupByString(cborValidityKey) + if err != nil { + return err + } + ndBytes, err = nd.AsBytes() + if err != nil { + return err + } + if !bytes.Equal(entry.GetValidity(), ndBytes) { + return fmt.Errorf("field \"%v\" did not match between protobuf and CBOR", cborValidityKey) + } + + nd, err = fullNd.LookupByString(cborValidityTypeKey) + if err != nil { + return err + } + ndInt, err := nd.AsInt() + if err != nil { + return err + } + if int64(entry.GetValidityType()) != ndInt { + return fmt.Errorf("field \"%v\" did not match between protobuf and CBOR", cborValidityTypeKey) + } + + nd, err = fullNd.LookupByString(cborSequenceKey) + if err != nil { + return err + } + ndInt, err = nd.AsInt() + if err != nil { + return err + } + + if entry.GetSequence() != uint64(ndInt) { + return fmt.Errorf("field \"%v\" did not match between protobuf and CBOR", cborSequenceKey) + } + + nd, err = fullNd.LookupByString("TTL") + if err != nil { + return err + } + ndInt, err = nd.AsInt() + if err != nil { + return err + } + if entry.GetTtl() != uint64(ndInt) { + return fmt.Errorf("field \"%v\" did not match between protobuf and CBOR", cborTTLKey) + } + + return nil +} + +var _ record.Validator = Validator{} + +// Validator is an IPNS Record validator that satisfies the [record.Validator] +// interface from Libp2p. +type Validator struct { + // KeyBook, if non-nil, is used to lookup keys for validating IPNS Records. + KeyBook peerstore.KeyBook +} + +// Validate validates an IPNS record. +func (v Validator) Validate(key string, value []byte) error { + name, err := NameFromRoutingKey([]byte(key)) + if err != nil { + log.Debugf("failed to parse ipns routing key %q into name", key) + return ErrInvalidName + } + + r, err := UnmarshalRecord(value) + if err != nil { + return err + } + + pk, err := v.getPublicKey(r, name) + if err != nil { + return err + } + + return Validate(r, pk) +} + +func (v Validator) getPublicKey(r *Record, name Name) (ic.PubKey, error) { + switch pk, err := ExtractPublicKey(r, name); err { + case peer.ErrNoPublicKey: + case nil: + return pk, nil + default: + return nil, err + } + + if v.KeyBook == nil { + log.Debugf("public key with hash %q not found in IPNS record and no peer store provided", name.Peer()) + return nil, ErrPublicKeyNotFound + } + + pk := v.KeyBook.PubKey(name.Peer()) + if pk == nil { + log.Debugf("public key with hash %q not found in peer store", name.Peer()) + return nil, ErrPublicKeyNotFound + } + + return pk, nil +} + +// Select selects the best record by checking which has the highest sequence +// number and latest validity. This function returns an error if any of the +// records fail to parse. +// +// This function does not validate the records. The caller is responsible for +// ensuring that the Records are valid by using [Validate]. +func (v Validator) Select(k string, vals [][]byte) (int, error) { + var recs []*Record + for _, v := range vals { + r, err := UnmarshalRecord(v) + if err != nil { + return -1, err + } + recs = append(recs, r) + } + + return selectRecord(recs, vals) +} + +func selectRecord(recs []*Record, vals [][]byte) (int, error) { + switch len(recs) { + case 0: + return -1, errors.New("no usable records in given set") + case 1: + return 0, nil + } + + var i int + for j := 1; j < len(recs); j++ { + cmp, err := compare(recs[i], recs[j]) + if err != nil { + return -1, err + } + if cmp == 0 { + cmp = bytes.Compare(vals[i], vals[j]) + } + if cmp < 0 { + i = j + } + } + + return i, nil +} diff --git a/ipns/validation_test.go b/ipns/validation_test.go new file mode 100644 index 000000000..e7e1cd078 --- /dev/null +++ b/ipns/validation_test.go @@ -0,0 +1,245 @@ +package ipns + +import ( + "math/rand" + "testing" + "time" + + "github.com/ipfs/boxo/path" + ic "github.com/libp2p/go-libp2p/core/crypto" + "github.com/libp2p/go-libp2p/core/peerstore" + "github.com/libp2p/go-libp2p/p2p/host/peerstore/pstoremem" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func shuffle[T any](a []T) { + for n := 0; n < 5; n++ { + for i := range a { + j := rand.Intn(len(a)) + a[i], a[j] = a[j], a[i] + } + } +} + +func TestOrdering(t *testing.T) { + t.Parallel() + + sk, _, _ := mustKeyPair(t, ic.Ed25519) + ts := time.Unix(1000000, 0) + + assert := func(name string, r *Record, from ...*Record) { + t.Run(name, func(t *testing.T) { + t.Parallel() + shuffle(from) + var vals [][]byte + for _, r := range from { + data, err := MarshalRecord(r) + require.NoError(t, err) + vals = append(vals, data) + } + + i, err := selectRecord(from, vals) + require.NoError(t, err) + require.Equal(t, r, from[i], "selected incorrect record") + }) + } + + r1, err := NewRecord(sk, testPath, 1, ts.Add(time.Hour), 0) + require.NoError(t, err) + + r2, err := NewRecord(sk, testPath, 2, ts.Add(time.Hour), 0) + require.NoError(t, err) + + r3, err := NewRecord(sk, testPath, 3, ts.Add(time.Hour), 0) + require.NoError(t, err) + + r4, err := NewRecord(sk, testPath, 3, ts.Add(time.Hour*2), 0) + require.NoError(t, err) + + r5, err := NewRecord(sk, testPath, 4, ts.Add(time.Hour*3), 0) + require.NoError(t, err) + + assert("R1 is the only record", r1, r1) + assert("R2 has the highest sequence number", r2, r1, r2) + assert("R3 has the highest sequence number", r3, r1, r2, r3) + assert("R4 has the highest EOL", r4, r1, r2, r3, r4) + assert("R5 has the highest sequence number", r5, r1, r2, r3, r4, r5) +} + +func TestValidator(t *testing.T) { + t.Parallel() + + check := func(t *testing.T, sk ic.PrivKey, keybook peerstore.KeyBook, key, val []byte, eol time.Time, exp error, opts ...Option) { + validator := Validator{keybook} + data := val + if data == nil { + // do not call mustNewRecord because that validates the record! + rec, err := NewRecord(sk, testPath, 1, eol, 0, opts...) + require.NoError(t, err) + data = mustMarshal(t, rec) + } + require.ErrorIs(t, validator.Validate(string(key), data), exp) + } + + t.Run("validator returns correct errors", func(t *testing.T) { + t.Parallel() + + ts := time.Now() + sk, _, name := mustKeyPair(t, ic.RSA) + sk2, _, name2 := mustKeyPair(t, ic.RSA) + kb, err := pstoremem.NewPeerstore() + require.NoError(t, err) + err = kb.AddPubKey(name.Peer(), sk.GetPublic()) + require.NoError(t, err) + emptyKB, err := pstoremem.NewPeerstore() + require.NoError(t, err) + + check(t, sk, kb, name.RoutingKey(), nil, ts.Add(time.Hour), nil) + check(t, sk, kb, name.RoutingKey(), nil, ts.Add(time.Hour*-1), ErrExpiredRecord) + check(t, sk, kb, name.RoutingKey(), []byte("bad data"), ts.Add(time.Hour), ErrInvalidRecord) + check(t, sk, kb, []byte("/ipns/"+"bad key"), nil, ts.Add(time.Hour), ErrInvalidName) + check(t, sk, emptyKB, name.RoutingKey(), nil, ts.Add(time.Hour), ErrPublicKeyNotFound, WithPublicKey(false)) + check(t, sk2, kb, name2.RoutingKey(), nil, ts.Add(time.Hour), ErrPublicKeyNotFound, WithPublicKey(false)) + check(t, sk2, kb, name.RoutingKey(), nil, ts.Add(time.Hour), ErrPublicKeyMismatch) + check(t, sk2, kb, name.RoutingKey(), nil, ts.Add(time.Hour), ErrSignature, WithPublicKey(false)) + check(t, sk, kb, []byte("//"+name.String()), nil, ts.Add(time.Hour), ErrInvalidName) + check(t, sk, kb, []byte("/wrong/"+name.String()), nil, ts.Add(time.Hour), ErrInvalidName) + }) + + t.Run("validator uses public key", func(t *testing.T) { + t.Parallel() + + eol := time.Now().Add(time.Hour) + kb, err := pstoremem.NewPeerstore() + require.NoError(t, err) + + sk, _, name := mustKeyPair(t, ic.Ed25519) + rec := mustNewRecord(t, sk, testPath, 1, eol, 0) + require.Empty(t, rec.pb.PubKey) + dataNoKey := mustMarshal(t, rec) + + check(t, sk, kb, name.RoutingKey(), dataNoKey, eol, nil) + }) + + t.Run("TestEmbeddedPubKeyValidate", func(t *testing.T) { + t.Parallel() + + eol := time.Now().Add(time.Hour) + kb, err := pstoremem.NewPeerstore() + require.NoError(t, err) + + sk, _, name := mustKeyPair(t, ic.RSA) + rec := mustNewRecord(t, sk, testPath, 1, eol, 0, WithPublicKey(false)) + + // Fails with RSA key without embedded public key. + check(t, sk, kb, name.RoutingKey(), mustMarshal(t, rec), eol, ErrPublicKeyNotFound) + + // Embeds public key, must work now. + rec = mustNewRecord(t, sk, testPath, 1, eol, 0) + check(t, sk, kb, name.RoutingKey(), mustMarshal(t, rec), eol, nil) + + // Force bad public key. Validation fails. + rec.pb.PubKey = []byte("probably not a public key") + check(t, sk, kb, name.RoutingKey(), mustMarshal(t, rec), eol, ErrInvalidPublicKey) + + // Does not work with wrong key. + sk2, _, _ := mustKeyPair(t, ic.RSA) + wrongKey, err := ic.MarshalPublicKey(sk2.GetPublic()) + require.NoError(t, err) + rec.pb.PubKey = wrongKey + check(t, sk, kb, name.RoutingKey(), mustMarshal(t, rec), eol, ErrPublicKeyMismatch) + }) +} + +func TestValidate(t *testing.T) { + t.Parallel() + + t.Run("signature v1 is ignored", func(t *testing.T) { + t.Parallel() + + eol := time.Now().Add(time.Hour) + sk, pk, name := mustKeyPair(t, ic.Ed25519) + ipnsRoutingKey := string(name.RoutingKey()) + + v := Validator{} + + rec1 := mustNewRecord(t, sk, path.FromString("/path/1"), 1, eol, 0, WithV1Compatibility(true)) + rec2 := mustNewRecord(t, sk, path.FromString("/path/2"), 2, eol, 0, WithV1Compatibility(true)) + + best, err := v.Select(ipnsRoutingKey, [][]byte{mustMarshal(t, rec1), mustMarshal(t, rec2)}) + require.NoError(t, err) + require.Equal(t, 1, best) + + // Having only the v1 signature is invalid. + rec2.pb.SignatureV2 = nil + require.Error(t, Validate(rec2, pk), ErrSignature) + + // Record with v2 signature is always be preferred. + best, err = v.Select(ipnsRoutingKey, [][]byte{mustMarshal(t, rec1), mustMarshal(t, rec2)}) + require.NoError(t, err) + require.Equal(t, 0, best) + + // Missing v1 signature is acceptable as long as there is a valid v2 signature. + rec1.pb.SignatureV1 = nil + require.NoError(t, Validate(rec1, pk)) + + // Invalid v1 signature is acceptable as long as there is a valid v2 signature. + rec1.pb.SignatureV1 = []byte("garbage") + require.NoError(t, Validate(rec1, pk)) + }) + + t.Run("only signature v2 is validated", func(t *testing.T) { + t.Parallel() + + eol := time.Now().Add(time.Hour) + sk, pk, _ := mustKeyPair(t, ic.Ed25519) + + entry, err := NewRecord(sk, testPath, 1, eol, 0) + require.NoError(t, err) + require.NoError(t, Validate(entry, pk)) + + entry.pb.SignatureV2 = nil + require.ErrorIs(t, Validate(entry, pk), ErrSignature) + }) + + t.Run("max size validation", func(t *testing.T) { + t.Parallel() + + eol := time.Now().Add(time.Hour) + sk, pk, _ := mustKeyPair(t, ic.RSA) + + // Create a record that is too large (value + other fields). + value := make([]byte, MaxRecordSize) + rec, err := NewRecord(sk, path.FromString(string(value)), 1, eol, 0) + require.NoError(t, err) + + err = Validate(rec, pk) + require.ErrorIs(t, err, ErrRecordSize) + }) +} + +func TestValidateWithName(t *testing.T) { + t.Parallel() + + sk, _, pid := mustKeyPair(t, ic.Ed25519) + eol := time.Now().Add(time.Hour) + + r, err := NewRecord(sk, testPath, 1, eol, 0) + require.NoError(t, err) + + t.Run("valid peer ID", func(t *testing.T) { + t.Parallel() + + err = ValidateWithName(r, pid) + assert.NoError(t, err) + }) + + t.Run("invalid peer ID", func(t *testing.T) { + t.Parallel() + + _, _, pid2 := mustKeyPair(t, ic.Ed25519) + err = ValidateWithName(r, pid2) + assert.ErrorIs(t, err, ErrSignature) + }) +} diff --git a/namesys/ipns_resolver_validation_test.go b/namesys/ipns_resolver_validation_test.go deleted file mode 100644 index 9799e5ba3..000000000 --- a/namesys/ipns_resolver_validation_test.go +++ /dev/null @@ -1,209 +0,0 @@ -package namesys - -import ( - "context" - "testing" - "time" - - opts "github.com/ipfs/boxo/coreiface/options/namesys" - "github.com/ipfs/boxo/ipns" - ipns_pb "github.com/ipfs/boxo/ipns/pb" - "github.com/ipfs/boxo/path" - mockrouting "github.com/ipfs/boxo/routing/mock" - "github.com/ipfs/boxo/routing/offline" - ds "github.com/ipfs/go-datastore" - dssync "github.com/ipfs/go-datastore/sync" - record "github.com/libp2p/go-libp2p-record" - testutil "github.com/libp2p/go-libp2p-testing/net" - ci "github.com/libp2p/go-libp2p/core/crypto" - "github.com/libp2p/go-libp2p/core/peer" - pstore "github.com/libp2p/go-libp2p/core/peerstore" - "github.com/libp2p/go-libp2p/core/routing" - "github.com/libp2p/go-libp2p/core/test" - "github.com/libp2p/go-libp2p/p2p/host/peerstore/pstoremem" -) - -func TestResolverValidation(t *testing.T) { - t.Run("RSA", - func(t *testing.T) { - testResolverValidation(t, ci.RSA) - }) - t.Run("Ed25519", - func(t *testing.T) { - testResolverValidation(t, ci.Ed25519) - }) - t.Run("ECDSA", - func(t *testing.T) { - testResolverValidation(t, ci.ECDSA) - }) - t.Run("Secp256k1", - func(t *testing.T) { - testResolverValidation(t, ci.Secp256k1) - }) -} - -func testResolverValidation(t *testing.T, keyType int) { - ctx := context.Background() - rid := testutil.RandIdentityOrFatal(t) - dstore := dssync.MutexWrap(ds.NewMapDatastore()) - peerstore, err := pstoremem.NewPeerstore() - if err != nil { - t.Fatal(err) - } - - vstore := newMockValueStore(rid, dstore, peerstore) - resolver := NewIpnsResolver(vstore) - - nvVstore := offline.NewOfflineRouter(dstore, mockrouting.MockValidator{}) - - // Create entry with expiry in one hour - priv, id, _, ipnsDHTPath := genKeys(t, keyType) - ts := time.Now() - p := []byte("/ipfs/QmfM2r8seH2GiRaC4esTjeraXEachRt8ZsSeGaWTPLyMoG") - entry, err := createIPNSRecordWithEmbeddedPublicKey(priv, p, 1, ts.Add(time.Hour), 0) - if err != nil { - t.Fatal(err) - } - - // Publish entry - err = PublishEntry(ctx, vstore, ipnsDHTPath, entry) - if err != nil { - t.Fatal(err) - } - - // Resolve entry - resp, err := resolve(ctx, resolver, id.Pretty(), opts.DefaultResolveOpts()) - if err != nil { - t.Fatal(err) - } - if resp != path.Path(p) { - t.Fatalf("Mismatch between published path %s and resolved path %s", p, resp) - } - // Create expired entry - expiredEntry, err := createIPNSRecordWithEmbeddedPublicKey(priv, p, 1, ts.Add(-1*time.Hour), 0) - if err != nil { - t.Fatal(err) - } - - // Publish entry - err = PublishEntry(ctx, nvVstore, ipnsDHTPath, expiredEntry) - if err != nil { - t.Fatal(err) - } - - // Record should fail validation because entry is expired - _, err = resolve(ctx, resolver, id.Pretty(), opts.DefaultResolveOpts()) - if err == nil { - t.Fatal("ValidateIpnsRecord should have returned error") - } - - // Create IPNS record path with a different private key - priv2, id2, _, ipnsDHTPath2 := genKeys(t, keyType) - - // Publish entry - err = PublishEntry(ctx, nvVstore, 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 = resolve(ctx, resolver, id2.Pretty(), opts.DefaultResolveOpts()) - if err == nil { - t.Fatal("ValidateIpnsRecord should have failed signature verification") - } - - // Try embedding the incorrect private key inside the entry - if err := ipns.EmbedPublicKey(priv2.GetPublic(), entry); err != nil { - t.Fatal(err) - } - - // Publish entry - err = PublishEntry(ctx, nvVstore, 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 = resolve(ctx, resolver, id2.Pretty(), opts.DefaultResolveOpts()) - if err == nil { - t.Fatal("ValidateIpnsRecord should have failed signature verification") - } -} - -func genKeys(t *testing.T, keyType int) (ci.PrivKey, peer.ID, string, string) { - bits := 0 - if keyType == ci.RSA { - bits = 2048 - } - - sk, pk, err := test.RandTestKeyPair(keyType, bits) - if err != nil { - t.Fatal(err) - } - id, err := peer.IDFromPublicKey(pk) - if err != nil { - t.Fatal(err) - } - return sk, id, PkKeyForID(id), ipns.RecordKey(id) -} - -func createIPNSRecordWithEmbeddedPublicKey(sk ci.PrivKey, val []byte, seq uint64, eol time.Time, ttl time.Duration) (*ipns_pb.IpnsEntry, error) { - entry, err := ipns.Create(sk, val, seq, eol, ttl) - if err != nil { - return nil, err - } - if err := ipns.EmbedPublicKey(sk.GetPublic(), entry); err != nil { - return nil, err - } - - return entry, nil -} - -type mockValueStore struct { - r routing.ValueStore - kbook pstore.KeyBook -} - -func newMockValueStore(id testutil.Identity, dstore ds.Datastore, kbook pstore.KeyBook) *mockValueStore { - return &mockValueStore{ - r: offline.NewOfflineRouter(dstore, record.NamespacedValidator{ - "ipns": ipns.Validator{KeyBook: kbook}, - "pk": record.PublicKeyValidator{}, - }), - kbook: kbook, - } -} - -func (m *mockValueStore) GetValue(ctx context.Context, k string, opts ...routing.Option) ([]byte, error) { - return m.r.GetValue(ctx, k, opts...) -} - -func (m *mockValueStore) SearchValue(ctx context.Context, k string, opts ...routing.Option) (<-chan []byte, error) { - return m.r.SearchValue(ctx, k, opts...) -} - -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) PutValue(ctx context.Context, k string, d []byte, opts ...routing.Option) error { - return m.r.PutValue(ctx, k, d, opts...) -} diff --git a/namesys/publisher.go b/namesys/publisher.go index 24a0b8e4d..c913b0bbc 100644 --- a/namesys/publisher.go +++ b/namesys/publisher.go @@ -6,10 +6,8 @@ import ( "sync" "time" - "github.com/gogo/protobuf/proto" opts "github.com/ipfs/boxo/coreiface/options/namesys" "github.com/ipfs/boxo/ipns" - pb "github.com/ipfs/boxo/ipns/pb" "github.com/ipfs/boxo/path" ds "github.com/ipfs/go-datastore" dsquery "github.com/ipfs/go-datastore/query" @@ -68,7 +66,7 @@ func IpnsDsKey(id peer.ID) ds.Key { // // This method will not search the routing system for records published by other // nodes. -func (p *IpnsPublisher) ListPublished(ctx context.Context) (map[peer.ID]*pb.IpnsEntry, error) { +func (p *IpnsPublisher) ListPublished(ctx context.Context) (map[peer.ID]*ipns.Record, error) { query, err := p.ds.Query(ctx, dsquery.Query{ Prefix: ipnsPrefix, }) @@ -77,7 +75,7 @@ func (p *IpnsPublisher) ListPublished(ctx context.Context) (map[peer.ID]*pb.Ipns } defer query.Close() - records := make(map[peer.ID]*pb.IpnsEntry) + records := make(map[peer.ID]*ipns.Record) for { select { case result, ok := <-query.Next(): @@ -87,8 +85,8 @@ func (p *IpnsPublisher) ListPublished(ctx context.Context) (map[peer.ID]*pb.Ipns if result.Error != nil { return nil, result.Error } - e := new(pb.IpnsEntry) - if err := proto.Unmarshal(result.Value, e); err != nil { + rec, err := ipns.UnmarshalRecord(result.Value) + if err != nil { // Might as well return what we can. log.Error("found an invalid IPNS entry:", err) continue @@ -103,7 +101,7 @@ func (p *IpnsPublisher) ListPublished(ctx context.Context) (map[peer.ID]*pb.Ipns log.Errorf("ipns ds key invalid: %s", result.Key) continue } - records[peer.ID(pid)] = e + records[peer.ID(pid)] = rec case <-ctx.Done(): return nil, ctx.Err() } @@ -115,7 +113,7 @@ func (p *IpnsPublisher) ListPublished(ctx context.Context) (map[peer.ID]*pb.Ipns // // If `checkRouting` is true and we have no existing record, this method will // check the routing system for any existing records. -func (p *IpnsPublisher) GetPublished(ctx context.Context, id peer.ID, checkRouting bool) (*pb.IpnsEntry, error) { +func (p *IpnsPublisher) GetPublished(ctx context.Context, id peer.ID, checkRouting bool) (*ipns.Record, error) { ctx, cancel := context.WithTimeout(ctx, time.Second*30) defer cancel() @@ -126,7 +124,7 @@ func (p *IpnsPublisher) GetPublished(ctx context.Context, id peer.ID, checkRouti if !checkRouting { return nil, nil } - ipnskey := ipns.RecordKey(id) + ipnskey := string(ipns.NameFromPeer(id).RoutingKey()) value, err = p.routing.GetValue(ctx, ipnskey) if err != nil { // Not found or other network issue. Can't really do @@ -140,14 +138,11 @@ func (p *IpnsPublisher) GetPublished(ctx context.Context, id peer.ID, checkRouti default: return nil, err } - e := new(pb.IpnsEntry) - if err := proto.Unmarshal(value, e); err != nil { - return nil, err - } - return e, nil + + return ipns.UnmarshalRecord(value) } -func (p *IpnsPublisher) updateRecord(ctx context.Context, k crypto.PrivKey, value path.Path, options ...opts.PublishOption) (*pb.IpnsEntry, error) { +func (p *IpnsPublisher) updateRecord(ctx context.Context, k crypto.PrivKey, value path.Path, options ...opts.PublishOption) (*ipns.Record, error) { id, err := peer.IDFromPrivateKey(k) if err != nil { return nil, err @@ -162,22 +157,33 @@ func (p *IpnsPublisher) updateRecord(ctx context.Context, k crypto.PrivKey, valu return nil, err } - seqno := rec.GetSequence() // returns 0 if rec is nil - if rec != nil && value != path.Path(rec.GetValue()) { - // Don't bother incrementing the sequence number unless the - // value changes. - seqno++ + seqno := uint64(0) + if rec != nil { + seqno, err = rec.Sequence() + if err != nil { + return nil, err + } + + p, err := rec.Value() + if err != nil { + return nil, err + } + if value != path.Path(p.String()) { + // Don't bother incrementing the sequence number unless the + // value changes. + seqno++ + } } opts := opts.ProcessPublishOptions(options) // Create record - entry, err := ipns.Create(k, []byte(value), seqno, opts.EOL, opts.TTL) + r, err := ipns.NewRecord(k, value, seqno, opts.EOL, opts.TTL, ipns.WithV1Compatibility(opts.CompatibleWithV1)) if err != nil { return nil, err } - data, err := proto.Marshal(entry) + data, err := ipns.MarshalRecord(r) if err != nil { return nil, err } @@ -190,13 +196,13 @@ func (p *IpnsPublisher) updateRecord(ctx context.Context, k crypto.PrivKey, valu if err := p.ds.Sync(ctx, key); err != nil { return nil, err } - return entry, nil + return r, nil } // PutRecordToRouting publishes the given entry using the provided ValueStore, // keyed on the ID associated with the provided public key. The public key is // also made available to the routing system so that entries can be verified. -func PutRecordToRouting(ctx context.Context, r routing.ValueStore, k crypto.PubKey, entry *pb.IpnsEntry) error { +func PutRecordToRouting(ctx context.Context, r routing.ValueStore, k crypto.PubKey, rec *ipns.Record) error { ctx, span := StartSpan(ctx, "PutRecordToRouting") defer span.End() @@ -205,17 +211,13 @@ func PutRecordToRouting(ctx context.Context, r routing.ValueStore, k crypto.PubK errs := make(chan error, 2) // At most two errors (IPNS, and public key) - if err := ipns.EmbedPublicKey(k, entry); err != nil { - return err - } - id, err := peer.IDFromPublicKey(k) if err != nil { return err } go func() { - errs <- PublishEntry(ctx, r, ipns.RecordKey(id), entry) + errs <- PublishEntry(ctx, r, string(ipns.NameFromPeer(id).RoutingKey()), rec) }() // Publish the public key if a public key cannot be extracted from the ID @@ -225,7 +227,7 @@ func PutRecordToRouting(ctx context.Context, r routing.ValueStore, k crypto.PubK // NOTE: This check actually checks if the public key has been embedded // in the IPNS entry. This check is sufficient because we embed the // public key in the IPNS entry if it can't be extracted from the ID. - if entry.PubKey != nil { + if _, err := rec.PubKey(); err == nil { go func() { errs <- PublishPublicKey(ctx, r, PkKeyForID(id), k) }() @@ -265,11 +267,11 @@ func PublishPublicKey(ctx context.Context, r routing.ValueStore, k string, pubk // PublishEntry stores the given IpnsEntry in the ValueStore with the given // ipnskey. -func PublishEntry(ctx context.Context, r routing.ValueStore, ipnskey string, rec *pb.IpnsEntry) error { +func PublishEntry(ctx context.Context, r routing.ValueStore, ipnskey string, rec *ipns.Record) error { ctx, span := StartSpan(ctx, "PublishEntry", trace.WithAttributes(attribute.String("IPNSKey", ipnskey))) defer span.End() - data, err := proto.Marshal(rec) + data, err := ipns.MarshalRecord(rec) if err != nil { return err } diff --git a/namesys/publisher_test.go b/namesys/publisher_test.go index b40593c76..ad975f59a 100644 --- a/namesys/publisher_test.go +++ b/namesys/publisher_test.go @@ -56,7 +56,7 @@ func testNamekeyPublisher(t *testing.T, keyType int, expectedErr error, expected } // Value - value := []byte("ipfs/TESTING") + value := path.Path("ipfs/TESTING") // Seqnum seqnum := uint64(0) @@ -76,12 +76,12 @@ func testNamekeyPublisher(t *testing.T, keyType int, expectedErr error, expected serv := mockrouting.NewServer() r := serv.ClientWithDatastore(context.Background(), &identity{p}, dstore) - entry, err := ipns.Create(privKey, value, seqnum, eol, 0) + rec, err := ipns.NewRecord(privKey, value, seqnum, eol, 0) if err != nil { t.Fatal(err) } - err = PutRecordToRouting(ctx, r, pubKey, entry) + err = PutRecordToRouting(ctx, r, pubKey, rec) if err != nil { t.Fatal(err) } diff --git a/namesys/republisher/repub.go b/namesys/republisher/repub.go index 276899859..87200ff5c 100644 --- a/namesys/republisher/repub.go +++ b/namesys/republisher/repub.go @@ -12,10 +12,8 @@ import ( "github.com/ipfs/boxo/path" "go.opentelemetry.io/otel/attribute" - "github.com/gogo/protobuf/proto" opts "github.com/ipfs/boxo/coreiface/options/namesys" "github.com/ipfs/boxo/ipns" - pb "github.com/ipfs/boxo/ipns/pb" ds "github.com/ipfs/go-datastore" logging "github.com/ipfs/go-log/v2" "github.com/jbenet/goprocess" @@ -140,7 +138,7 @@ func (rp *Republisher) republishEntry(ctx context.Context, priv ic.PrivKey) erro log.Debugf("republishing ipns entry for %s", id) // Look for it locally only - e, err := rp.getLastIPNSEntry(ctx, id) + rec, err := rp.getLastIPNSRecord(ctx, id) if err != nil { if err == errNoEntry { span.SetAttributes(attribute.Bool("NoEntry", true)) @@ -150,8 +148,13 @@ func (rp *Republisher) republishEntry(ctx context.Context, priv ic.PrivKey) erro return err } - p := path.Path(e.GetValue()) - prevEol, err := ipns.GetEOL(e) + p, err := rec.Value() + if err != nil { + span.RecordError(err) + return err + } + + prevEol, err := rec.Validity() if err != nil { span.RecordError(err) return err @@ -162,12 +165,12 @@ func (rp *Republisher) republishEntry(ctx context.Context, priv ic.PrivKey) erro if prevEol.After(eol) { eol = prevEol } - err = rp.ns.Publish(ctx, priv, p, opts.PublishWithEOL(eol)) + err = rp.ns.Publish(ctx, priv, path.Path(p.String()), opts.PublishWithEOL(eol)) span.RecordError(err) return err } -func (rp *Republisher) getLastIPNSEntry(ctx context.Context, id peer.ID) (*pb.IpnsEntry, error) { +func (rp *Republisher) getLastIPNSRecord(ctx context.Context, id peer.ID) (*ipns.Record, error) { // Look for it locally only val, err := rp.ds.Get(ctx, namesys.IpnsDsKey(id)) switch err { @@ -178,9 +181,5 @@ func (rp *Republisher) getLastIPNSEntry(ctx context.Context, id peer.ID) (*pb.Ip return nil, err } - e := new(pb.IpnsEntry) - if err := proto.Unmarshal(val, e); err != nil { - return nil, err - } - return e, nil + return ipns.UnmarshalRecord(val) } diff --git a/namesys/republisher/repub_test.go b/namesys/republisher/repub_test.go index bfd8730c0..d6c7b0d85 100644 --- a/namesys/republisher/repub_test.go +++ b/namesys/republisher/repub_test.go @@ -6,8 +6,6 @@ import ( "testing" "time" - "github.com/gogo/protobuf/proto" - "github.com/jbenet/goprocess" "github.com/libp2p/go-libp2p" dht "github.com/libp2p/go-libp2p-kad-dht" @@ -18,7 +16,6 @@ import ( opts "github.com/ipfs/boxo/coreiface/options/namesys" "github.com/ipfs/boxo/ipns" - ipns_pb "github.com/ipfs/boxo/ipns/pb" "github.com/ipfs/boxo/path" ds "github.com/ipfs/go-datastore" dssync "github.com/ipfs/go-datastore/sync" @@ -208,12 +205,12 @@ func TestLongEOLRepublish(t *testing.T) { t.Fatal(err) } - entry, err := getLastIPNSEntry(ctx, publisher.store, publisher.h.ID()) + rec, err := getLastIPNSRecord(ctx, publisher.store, publisher.h.ID()) if err != nil { t.Fatal(err) } - finalEol, err := ipns.GetEOL(entry) + finalEol, err := rec.Validity() if err != nil { t.Fatal(err) } @@ -223,18 +220,14 @@ func TestLongEOLRepublish(t *testing.T) { } } -func getLastIPNSEntry(ctx context.Context, dstore ds.Datastore, id peer.ID) (*ipns_pb.IpnsEntry, error) { +func getLastIPNSRecord(ctx context.Context, dstore ds.Datastore, id peer.ID) (*ipns.Record, error) { // Look for it locally only val, err := dstore.Get(ctx, namesys.IpnsDsKey(id)) if err != nil { return nil, err } - e := new(ipns_pb.IpnsEntry) - if err := proto.Unmarshal(val, e); err != nil { - return nil, err - } - return e, nil + return ipns.UnmarshalRecord(val) } func verifyResolution(nsystems []namesys.NameSystem, key string, exp path.Path) error { diff --git a/namesys/resolve_test.go b/namesys/resolve_test.go index d2da31215..3aecdccaf 100644 --- a/namesys/resolve_test.go +++ b/namesys/resolve_test.go @@ -54,7 +54,7 @@ func TestPrexistingExpiredRecord(t *testing.T) { h := path.FromString("/ipfs/QmZULkCELmmk5XNfCgTnCyFgAVxBRBXyDHGGMVoLFLiXEN") eol := time.Now().Add(time.Hour * -1) - entry, err := ipns.Create(identity.PrivateKey(), []byte(h), 0, eol, 0) + entry, err := ipns.NewRecord(identity.PrivateKey(), h, 0, eol, 0) if err != nil { t.Fatal(err) } @@ -87,7 +87,7 @@ func TestPrexistingRecord(t *testing.T) { // Make a good record and put it in the datastore h := path.FromString("/ipfs/QmZULkCELmmk5XNfCgTnCyFgAVxBRBXyDHGGMVoLFLiXEN") eol := time.Now().Add(time.Hour) - entry, err := ipns.Create(identity.PrivateKey(), []byte(h), 0, eol, 0) + entry, err := ipns.NewRecord(identity.PrivateKey(), h, 0, eol, 0) if err != nil { t.Fatal(err) } diff --git a/namesys/routing.go b/namesys/routing.go index 4ec7fe377..6b706bd92 100644 --- a/namesys/routing.go +++ b/namesys/routing.go @@ -5,17 +5,13 @@ import ( "strings" "time" - "github.com/gogo/protobuf/proto" opts "github.com/ipfs/boxo/coreiface/options/namesys" "github.com/ipfs/boxo/ipns" - pb "github.com/ipfs/boxo/ipns/pb" "github.com/ipfs/boxo/path" - "github.com/ipfs/go-cid" logging "github.com/ipfs/go-log/v2" dht "github.com/libp2p/go-libp2p-kad-dht" "github.com/libp2p/go-libp2p/core/peer" "github.com/libp2p/go-libp2p/core/routing" - mh "github.com/multiformats/go-multihash" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" ) @@ -81,7 +77,7 @@ func (r *IpnsResolver) resolveOnceAsync(ctx context.Context, name string, option // Use the routing system to get the name. // Note that the DHT will call the ipns validator when retrieving // the value, which in turn verifies the ipns record signature - ipnsKey := ipns.RecordKey(pid) + ipnsKey := string(ipns.NameFromPeer(pid).RoutingKey()) vals, err := r.routing.SearchValue(ctx, ipnsKey, dht.Quorum(int(options.DhtRecordCount))) if err != nil { @@ -105,34 +101,25 @@ func (r *IpnsResolver) resolveOnceAsync(ctx context.Context, name string, option return } - entry := new(pb.IpnsEntry) - err = proto.Unmarshal(val, entry) + rec, err := ipns.UnmarshalRecord(val) if err != nil { log.Debugf("RoutingResolver: could not unmarshal value for name %s: %s", name, err) emitOnceResult(ctx, out, onceResult{err: err}) return } - var p path.Path - // check for old style record: - if valh, err := mh.Cast(entry.GetValue()); err == nil { - // Its an old style multihash record - log.Debugf("encountered CIDv0 ipns entry: %s", valh) - p = path.FromCid(cid.NewCidV0(valh)) - } else { - // Not a multihash, probably a new style record - p, err = path.ParsePath(string(entry.GetValue())) - if err != nil { - emitOnceResult(ctx, out, onceResult{err: err}) - return - } + p, err := rec.Value() + if err != nil { + emitOnceResult(ctx, out, onceResult{err: err}) + return } ttl := DefaultResolverCacheTTL - if entry.Ttl != nil { - ttl = time.Duration(*entry.Ttl) + if recordTTL, err := rec.TTL(); err == nil { + ttl = recordTTL } - switch eol, err := ipns.GetEOL(entry); err { + + switch eol, err := rec.Validity(); err { case ipns.ErrUnrecognizedValidity: // No EOL. case nil: @@ -149,7 +136,7 @@ func (r *IpnsResolver) resolveOnceAsync(ctx context.Context, name string, option return } - emitOnceResult(ctx, out, onceResult{value: p, ttl: ttl}) + emitOnceResult(ctx, out, onceResult{value: path.Path(p.String()), ttl: ttl}) case <-ctx.Done(): return }