diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fa92975 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.out \ No newline at end of file diff --git a/client/connection.go b/client/connection.go index 7232cbc..6eb4f54 100644 --- a/client/connection.go +++ b/client/connection.go @@ -4,10 +4,10 @@ import ( "crypto/sha256" "fmt" "hash" + "iter" "github.com/storacha-network/go-ucanto/core/invocation" "github.com/storacha-network/go-ucanto/core/ipld/block" - "github.com/storacha-network/go-ucanto/core/iterable" "github.com/storacha-network/go-ucanto/core/message" "github.com/storacha-network/go-ucanto/transport" "github.com/storacha-network/go-ucanto/transport/car" @@ -96,7 +96,7 @@ func (c *conn) Hasher() hash.Hash { type ExecutionResponse interface { // Blocks returns an iterator of all the IPLD blocks that are included in // the response. - Blocks() iterable.Iterator[block.Block] + Blocks() iter.Seq2[block.Block, error] // Get returns a link to a receipt, given an invocation link. Get(inv ucan.Link) (ucan.Link, bool) } diff --git a/core/car/car.go b/core/car/car.go index aec721e..95911f4 100644 --- a/core/car/car.go +++ b/core/car/car.go @@ -4,22 +4,23 @@ import ( "bufio" "fmt" "io" + "iter" "github.com/ipfs/go-cid" cbor "github.com/ipfs/go-ipld-cbor" ipldcar "github.com/ipld/go-car" "github.com/ipld/go-car/util" cidlink "github.com/ipld/go-ipld-prime/linking/cid" + "github.com/multiformats/go-varint" "github.com/storacha-network/go-ucanto/core/ipld" "github.com/storacha-network/go-ucanto/core/ipld/block" - "github.com/storacha-network/go-ucanto/core/iterable" ) // ContentType is the value the HTTP Content-Type header should have for CARs. // See https://www.iana.org/assignments/media-types/application/vnd.ipld.car const ContentType = "application/vnd.ipld.car" -func Encode(roots []ipld.Link, blocks iterable.Iterator[ipld.Block]) io.Reader { +func Encode(roots []ipld.Link, blocks iter.Seq2[ipld.Block, error]) io.Reader { reader, writer := io.Pipe() go func() { cids := []cid.Cid{} @@ -41,23 +42,43 @@ func Encode(roots []ipld.Link, blocks iterable.Iterator[ipld.Block]) io.Reader { return } util.LdWrite(writer, hb) - for { - block, err := blocks.Next() + for block, err := range blocks { + if err != nil { + writer.CloseWithError(fmt.Errorf("writing CAR blocks: %s", err)) + return + } + err = util.LdWrite(writer, []byte(block.Link().Binary()), block.Bytes()) if err != nil { - if err == io.EOF { - break - } writer.CloseWithError(fmt.Errorf("writing CAR blocks: %s", err)) return } - util.LdWrite(writer, []byte(block.Link().Binary()), block.Bytes()) } writer.Close() }() return reader } -func Decode(reader io.Reader) ([]ipld.Link, iterable.Iterator[ipld.Block], error) { +type CarBlock interface { + ipld.Block + Offset() uint64 + Length() uint64 +} + +type carBlock struct { + ipld.Block + offset uint64 + length uint64 +} + +func (cb carBlock) Offset() uint64 { + return cb.offset +} + +func (cb carBlock) Length() uint64 { + return cb.length +} + +func Decode(reader io.Reader) ([]ipld.Link, iter.Seq2[ipld.Block, error], error) { br := bufio.NewReader(reader) h, err := ipldcar.ReadHeader(br) @@ -69,29 +90,52 @@ func Decode(reader io.Reader) ([]ipld.Link, iterable.Iterator[ipld.Block], error return nil, nil, fmt.Errorf("invalid car version: %d", h.Version) } + offset, err := ipldcar.HeaderSize(h) + if err != nil { + return nil, nil, err + } + var roots []ipld.Link for _, r := range h.Roots { roots = append(roots, cidlink.Link{Cid: r}) } - return roots, iterable.NewIterator(func() (ipld.Block, error) { - cid, bytes, err := util.ReadNode(br) - if err != nil { + r := &blkReader{br, offset} + return roots, func(yield func(ipld.Block, error) bool) { + for { + blk, err := r.next() if err == io.EOF { - br = nil + return + } + if !yield(blk, err) { + return } - return nil, err } + }, nil +} - hashed, err := cid.Prefix().Sum(bytes) - if err != nil { - return nil, err - } +type blkReader struct { + br *bufio.Reader + offset uint64 +} - if !hashed.Equals(cid) { - return nil, fmt.Errorf("mismatch in content integrity, name: %s, data: %s", cid, hashed) - } +func (r *blkReader) next() (CarBlock, error) { + cid, bytes, err := util.ReadNode(r.br) + if err != nil { + return nil, err + } + + hashed, err := cid.Prefix().Sum(bytes) + if err != nil { + return nil, err + } + + if !hashed.Equals(cid) { + return nil, fmt.Errorf("mismatch in content integrity, name: %s, data: %s", cid, hashed) + } + + ss := uint64(cid.ByteLen()) + uint64(len(bytes)) + r.offset += uint64(varint.UvarintSize(ss)) + ss - return block.NewBlock(cidlink.Link{Cid: cid}, bytes), nil - }), nil + return carBlock{block.NewBlock(cidlink.Link{Cid: cid}, bytes), r.offset - uint64(len(bytes)), uint64(len(bytes))}, nil } diff --git a/core/car/car_test.go b/core/car/car_test.go index d885198..080de0b 100644 --- a/core/car/car_test.go +++ b/core/car/car_test.go @@ -47,16 +47,16 @@ func TestDecodeCAR(t *testing.T) { t.Fatalf("unexpected root: %s, expected: %s", roots[0], fixtures[0].root) } - var blks []ipld.Block - for { - b, err := blocks.Next() + var blks []CarBlock + for b, err := range blocks { if err != nil { - if err == io.EOF { - break - } t.Fatalf("reading blocks: %s", err) } - blks = append(blks, b) + cb, ok := b.(CarBlock) + if !ok { + t.Fatalf("should have returned a car block") + } + blks = append(blks, cb) } if len(blks) != len(fixtures[0].blocks) { @@ -66,6 +66,22 @@ func TestDecodeCAR(t *testing.T) { if b.String() != blks[i].Link().String() { t.Fatalf("unexpected block: %s, expected: %s", b, blks[i].Link()) } + // verify offset and length can be used to directly read the block in the CAR file + file.Seek(int64(blks[i].Offset()), io.SeekStart) + data := make([]byte, blks[i].Length()) + _, err := file.Read(data) + if err != nil { + t.Fatalf("error reading block from raw file") + } + hashed, err := blks[i].Link().(cidlink.Link).Cid.Prefix().Sum(data) + if err != nil { + t.Fatalf("error hashing block from raw file") + } + + if hashed.String() != blks[i].Link().String() { + t.Fatalf("raw read from offset block: %s, expected: %s", hashed, blks[i].Link()) + } + } } diff --git a/core/dag/blockstore/blockstore.go b/core/dag/blockstore/blockstore.go index eedab90..b058356 100644 --- a/core/dag/blockstore/blockstore.go +++ b/core/dag/blockstore/blockstore.go @@ -2,16 +2,15 @@ package blockstore import ( "fmt" - "io" + "iter" "sync" "github.com/storacha-network/go-ucanto/core/ipld" - "github.com/storacha-network/go-ucanto/core/iterable" ) type BlockReader interface { Get(link ipld.Link) (ipld.Block, bool, error) - Iterator() iterable.Iterator[ipld.Block] + Iterator() iter.Seq2[ipld.Block, error] } type BlockWriter interface { @@ -33,20 +32,19 @@ func (br *blockreader) Get(link ipld.Link) (ipld.Block, bool, error) { return b, ok, nil } -func (br *blockreader) Iterator() iterable.Iterator[ipld.Block] { - i := 0 - return iterable.NewIterator(func() (ipld.Block, error) { - if len(br.keys) <= i { - return nil, io.EOF - } - k := br.keys[i] - v, ok := br.blks[k] - if !ok { - return nil, fmt.Errorf("missing block for key: %s", k) +func (br *blockreader) Iterator() iter.Seq2[ipld.Block, error] { + return func(yield func(ipld.Block, error) bool) { + for _, k := range br.keys { + v, ok := br.blks[k] + var err error + if !ok { + err = fmt.Errorf("missing block for key: %s", k) + } + if !yield(v, err) { + return + } } - i++ - return v, nil - }) + } } type blockstore struct { @@ -75,23 +73,21 @@ func (bs *blockstore) Get(link ipld.Link) (ipld.Block, bool, error) { return bs.blockreader.Get(link) } -func (bs *blockstore) Iterator() iterable.Iterator[ipld.Block] { +func (bs *blockstore) Iterator() iter.Seq2[ipld.Block, error] { bs.Lock() defer bs.Unlock() - keys := bs.keys[:] - i := 0 - return iterable.NewIterator(func() (ipld.Block, error) { - if len(keys) <= i { - return nil, io.EOF - } - k := keys[i] - v, ok := bs.blks[k] - if !ok { - return nil, fmt.Errorf("missing block for key: %s", k) + return func(yield func(ipld.Block, error) bool) { + for _, k := range bs.keys { + v, ok := bs.blks[k] + var err error + if !ok { + err = fmt.Errorf("missing block for key: %s", k) + } + if !yield(v, err) { + return + } } - i++ - return v, nil - }) + } } // Option is an option configuring a block reader/writer. @@ -99,7 +95,7 @@ type Option func(cfg *bsConfig) error type bsConfig struct { blks []ipld.Block - blksiter iterable.Iterator[ipld.Block] + blksiter iter.Seq2[ipld.Block, error] } // WithBlocks configures the blocks the blockstore should contain. @@ -111,7 +107,7 @@ func WithBlocks(blks []ipld.Block) Option { } // WithBlocksIterator configures the blocks the blockstore should contain. -func WithBlocksIterator(blks iterable.Iterator[ipld.Block]) Option { +func WithBlocksIterator(blks iter.Seq2[ipld.Block, error]) Option { return func(cfg *bsConfig) error { cfg.blksiter = blks return nil @@ -138,15 +134,11 @@ func NewBlockStore(options ...Option) (BlockStore, error) { } } if cfg.blksiter != nil { - for { - b, err := cfg.blksiter.Next() + for b, err := range cfg.blksiter { if err != nil { - if err == io.EOF { - break - } return nil, err } - err = bs.Put(b) + err := bs.Put(b) if err != nil { return nil, err } @@ -175,12 +167,8 @@ func NewBlockReader(options ...Option) (BlockReader, error) { keys = append(keys, b.Link().String()) } if cfg.blksiter != nil { - for { - b, err := cfg.blksiter.Next() + for b, err := range cfg.blksiter { if err != nil { - if err == io.EOF { - break - } return nil, err } _, ok := blks[b.Link().String()] @@ -196,16 +184,8 @@ func NewBlockReader(options ...Option) (BlockReader, error) { } func WriteInto(view ipld.View, bs BlockWriter) error { - blks := view.Blocks() - for { - b, err := blks.Next() - if err != nil { - if err == io.EOF { - break - } - return fmt.Errorf("reading proof blocks: %s", err) - } - err = bs.Put(b) + for b := range view.Blocks() { + err := bs.Put(b) if err != nil { return fmt.Errorf("putting proof block: %s", err) } diff --git a/core/delegation/delegate.go b/core/delegation/delegate.go index 83a4375..a4a7866 100644 --- a/core/delegation/delegate.go +++ b/core/delegation/delegate.go @@ -4,7 +4,6 @@ import ( "fmt" "github.com/storacha-network/go-ucanto/core/dag/blockstore" - "github.com/storacha-network/go-ucanto/core/ipld" "github.com/storacha-network/go-ucanto/core/ipld/block" "github.com/storacha-network/go-ucanto/core/ipld/codec/cbor" "github.com/storacha-network/go-ucanto/core/ipld/hash/sha256" @@ -16,25 +15,38 @@ import ( type Option func(cfg *delegationConfig) error type delegationConfig struct { - exp uint64 - nbf uint64 - nnc string - fct []ucan.FactBuilder - prf Proofs + exp *int + noexp bool + nbf int + nnc string + fct []ucan.FactBuilder + prf Proofs } // WithExpiration configures the expiration time in UTC seconds since Unix -// epoch. Set this to -1 for no expiration. -func WithExpiration(exp uint64) Option { +// epoch. +func WithExpiration(exp int) Option { return func(cfg *delegationConfig) error { - cfg.exp = exp + cfg.exp = &exp + cfg.noexp = false + return nil + } +} + +// WithNoExpiration configures the UCAN to never expire. +// +// WARNING: this will cause the delegation to be valid FOREVER, unless revoked. +func WithNoExpiration() Option { + return func(cfg *delegationConfig) error { + cfg.exp = nil + cfg.noexp = true return nil } } // WithNotBefore configures the time in UTC seconds since Unix epoch when the // UCAN will become valid. -func WithNotBefore(nbf uint64) Option { +func WithNotBefore(nbf int) Option { return func(cfg *delegationConfig) error { cfg.nbf = nbf return nil @@ -68,10 +80,19 @@ func WithProofs(prf Proofs) Option { } } +// WithProof configures the proofs for the UCAN in the case where there is only +// a single proof. +func WithProof(prf Proof) Option { + return func(cfg *delegationConfig) error { + cfg.prf = Proofs{prf} + return nil + } +} + // Delegate creates a new signed token with a given `options.issuer`. If // expiration is not set it defaults to 30 seconds from now. Returns UCAN in // primary IPLD representation. -func Delegate(issuer ucan.Signer, audience ucan.Principal, capabilities []ucan.Capability[ipld.Builder], options ...Option) (Delegation, error) { +func Delegate[C ucan.CaveatBuilder](issuer ucan.Signer, audience ucan.Principal, capabilities []ucan.Capability[C], options ...Option) (Delegation, error) { cfg := delegationConfig{} for _, opt := range options { if err := opt(&cfg); err != nil { @@ -89,16 +110,20 @@ func Delegate(issuer ucan.Signer, audience ucan.Principal, capabilities []ucan.C return nil, err } - data, err := ucan.Issue( - issuer, - audience, - capabilities, - ucan.WithExpiration(cfg.exp), + opts := []ucan.Option{ ucan.WithFacts(cfg.fct), ucan.WithNonce(cfg.nnc), ucan.WithNotBefore(cfg.nbf), ucan.WithProofs(links), - ) + } + if cfg.noexp { + opts = append(opts, ucan.WithNoExpiration()) + } + if cfg.exp != nil { + opts = append(opts, ucan.WithExpiration(*cfg.exp)) + } + + data, err := ucan.Issue(issuer, audience, capabilities, opts...) if err != nil { return nil, fmt.Errorf("issuing UCAN: %s", err) } diff --git a/core/delegation/delegation.go b/core/delegation/delegation.go index d6349c3..7a58e48 100644 --- a/core/delegation/delegation.go +++ b/core/delegation/delegation.go @@ -4,6 +4,7 @@ import ( "bytes" "fmt" "io" + "iter" "sync" "github.com/storacha-network/go-ucanto/core/car" @@ -13,7 +14,6 @@ import ( "github.com/storacha-network/go-ucanto/core/ipld/block" "github.com/storacha-network/go-ucanto/core/ipld/codec/cbor" "github.com/storacha-network/go-ucanto/core/ipld/hash/sha256" - "github.com/storacha-network/go-ucanto/core/iterable" "github.com/storacha-network/go-ucanto/ucan" "github.com/storacha-network/go-ucanto/ucan/crypto/signature" udm "github.com/storacha-network/go-ucanto/ucan/datamodel/ucan" @@ -23,51 +23,32 @@ import ( // into a UCAN token and used as proof for an invocation or further delegations. type Delegation interface { ipld.View + ucan.UCAN + // Data returns the UCAN view of the delegation. + Data() ucan.View // Link returns the IPLD link of the root block of the delegation. Link() ucan.Link // Archive writes the delegation to a Content Addressed aRchive (CAR). Archive() io.Reader - // Issuer is the signer of the UCAN. - Issuer() ucan.Principal - // Audience is the principal delegated to. - Audience() ucan.Principal - // Version is the spec version the UCAN conforms to. - Version() ucan.Version - // Capabilities are claimed abilities that can be performed on a resource. - Capabilities() []ucan.Capability[any] - // Expiration is the time in seconds since the Unix epoch that the UCAN - // becomes invalid. - Expiration() ucan.UTCUnixTimestamp - // NotBefore is the time in seconds since the Unix epoch that the UCAN - // becomes valid. - NotBefore() ucan.UTCUnixTimestamp - // Nonce is a randomly generated string to provide a unique UCAN. - Nonce() ucan.Nonce - // Facts are arbitrary facts and proofs of knowledge. - Facts() []ucan.Fact - // Proofs of delegation. - Proofs() []ucan.Link - // Signature of the UCAN issuer. - Signature() signature.SignatureView } type delegation struct { rt ipld.Block blks blockstore.BlockReader - ucan ucan.UCANView + ucan ucan.View once sync.Once } var _ Delegation = (*delegation)(nil) -func (d *delegation) data() ucan.UCANView { +func (d *delegation) Data() ucan.View { d.once.Do(func() { data := udm.UCANModel{} err := block.Decode(d.rt, &data, udm.Type(), cbor.Codec, sha256.Hasher) if err != nil { fmt.Printf("Error: decoding UCAN: %s\n", err) } - d.ucan, err = ucan.NewUCANView(&data) + d.ucan, err = ucan.NewUCAN(&data) if err != nil { fmt.Printf("Error: constructing UCAN view: %s\n", err) } @@ -83,7 +64,7 @@ func (d *delegation) Link() ucan.Link { return d.rt.Link() } -func (d *delegation) Blocks() iterable.Iterator[ipld.Block] { +func (d *delegation) Blocks() iter.Seq2[ipld.Block, error] { return d.blks.Iterator() } @@ -92,43 +73,43 @@ func (d *delegation) Archive() io.Reader { } func (d *delegation) Issuer() ucan.Principal { - return d.data().Issuer() + return d.Data().Issuer() } func (d *delegation) Audience() ucan.Principal { - return d.data().Audience() + return d.Data().Audience() } func (d *delegation) Version() ucan.Version { - return d.data().Version() + return d.Data().Version() } func (d *delegation) Capabilities() []ucan.Capability[any] { - return d.data().Capabilities() + return d.Data().Capabilities() } -func (d *delegation) Expiration() ucan.UTCUnixTimestamp { - return d.data().Expiration() +func (d *delegation) Expiration() *ucan.UTCUnixTimestamp { + return d.Data().Expiration() } func (d *delegation) NotBefore() ucan.UTCUnixTimestamp { - return d.data().NotBefore() + return d.Data().NotBefore() } func (d *delegation) Nonce() ucan.Nonce { - return d.data().Nonce() + return d.Data().Nonce() } func (d *delegation) Facts() []ucan.Fact { - return d.data().Facts() + return d.Data().Facts() } func (d *delegation) Proofs() []ucan.Link { - return d.data().Proofs() + return d.Data().Proofs() } func (d *delegation) Signature() signature.SignatureView { - return d.data().Signature() + return d.Data().Signature() } func NewDelegation(root ipld.Block, bs blockstore.BlockReader) Delegation { diff --git a/core/invocation/invocation.go b/core/invocation/invocation.go index 7109ae4..5340709 100644 --- a/core/invocation/invocation.go +++ b/core/invocation/invocation.go @@ -32,7 +32,6 @@ type IssuedInvocation interface { Invocation } -func Invoke[C ipld.Builder](issuer ucan.Signer, audience ucan.Principal, capability ucan.Capability[C], options ...delegation.Option) (IssuedInvocation, error) { - bcap := ucan.NewCapability(capability.Can(), capability.With(), ipld.Builder(capability.Nb())) - return delegation.Delegate(issuer, audience, []ucan.Capability[ipld.Builder]{bcap}, options...) +func Invoke[C ucan.CaveatBuilder](issuer ucan.Signer, audience ucan.Principal, capability ucan.Capability[C], options ...delegation.Option) (IssuedInvocation, error) { + return delegation.Delegate(issuer, audience, []ucan.Capability[C]{capability}, options...) } diff --git a/core/ipld/view.go b/core/ipld/view.go index 814a7c2..135fb59 100644 --- a/core/ipld/view.go +++ b/core/ipld/view.go @@ -1,7 +1,7 @@ package ipld import ( - "github.com/storacha-network/go-ucanto/core/iterable" + "iter" ) // View represents a materialized IPLD DAG View, which provides a generic @@ -19,7 +19,7 @@ type View interface { // // Iterator MUST include the root block otherwise it will lead encoders into // omitting it when encoding the view into a CAR archive. - Blocks() iterable.Iterator[Block] + Blocks() iter.Seq2[Block, error] } // ViewBuilder represents a materializable IPLD DAG View. It is a useful diff --git a/core/iterable/iterable.go b/core/iterable/iterable.go index 669f2e9..f8183ac 100644 --- a/core/iterable/iterable.go +++ b/core/iterable/iterable.go @@ -1,77 +1,55 @@ package iterable import ( - "io" + "iter" ) -// Iterator returns items in a collection with every call to Next(). -// The error will be set to io.EOF when the iterator is complete. -type Iterator[T any] interface { - Next() (T, error) -} - -type iterator[T any] struct { - next func() (T, error) -} - -func (it *iterator[T]) Next() (T, error) { - return it.next() -} - -func NewIterator[T any](next func() (T, error)) Iterator[T] { - return &iterator[T]{next} -} +// TODO: remove when https://github.com/golang/go/issues/61898 lands -func From[Slice ~[]T, T any](slice Slice) Iterator[T] { - i := 0 - return NewIterator(func() (T, error) { - if i < len(slice) { - item := slice[i] - i++ - return item, nil +// Map returns an iterator over f applied to seq. +func Map[In, Out any](f func(In) Out, seq iter.Seq[In]) iter.Seq[Out] { + return func(yield func(Out) bool) { + for in := range seq { + if !yield(f(in)) { + return + } } - var undef T - return undef, io.EOF - }) + } } -func Collect[T any](it Iterator[T]) ([]T, error) { - var items []T - for { - item, err := it.Next() - if err != nil { - if err == io.EOF { - break +// Map2 returns an iterator over f applied to seq. +func Map2[KIn, VIn, KOut, VOut any](f func(KIn, VIn) (KOut, VOut), seq iter.Seq2[KIn, VIn]) iter.Seq2[KOut, VOut] { + return func(yield func(KOut, VOut) bool) { + for k, v := range seq { + if !yield(f(k, v)) { + return } - return nil, err } - items = append(items, item) } - return items, nil } -func Concat[T any](iterators ...Iterator[T]) Iterator[T] { - if len(iterators) == 0 { - return From([]T{}) +// Concat returns an iterator over the concatenation of the sequences. +func Concat[V any](seqs ...iter.Seq[V]) iter.Seq[V] { + return func(yield func(V) bool) { + for _, seq := range seqs { + for e := range seq { + if !yield(e) { + return + } + } + } } +} - i := 0 - iterator := iterators[i] - return NewIterator(func() (T, error) { - for { - item, err := iterator.Next() - if err != nil { - if err == io.EOF { - i++ - if i < len(iterators) { - iterator = iterators[i] - continue - } +// Concat2 returns an iterator over the concatenation of the sequences. +func Concat2[K, V any](seqs ...iter.Seq2[K, V]) iter.Seq2[K, V] { + return func(yield func(K, V) bool) { + for _, seq := range seqs { + for k, v := range seq { + if !yield(k, v) { + return } - var undef T - return undef, err } - return item, nil } - }) + } } diff --git a/core/iterable/mapiterable.go b/core/iterable/mapiterable.go deleted file mode 100644 index b4b5bcd..0000000 --- a/core/iterable/mapiterable.go +++ /dev/null @@ -1,91 +0,0 @@ -package iterable - -import "io" - -// Iterator2 returns two values with every call to Next(). -// The error will be set to io.EOF when the iterator is complete. -type Iterator2[K any, V any] interface { - Next() (K, V, error) -} - -type iterator2[K any, V any] struct { - next func() (K, V, error) -} - -func (mit *iterator2[K, V]) Next() (K, V, error) { - return mit.next() -} - -func NewIterator2[K any, V any](next func() (K, V, error)) Iterator2[K, V] { - return &iterator2[K, V]{next} -} - -type mapEntry[K comparable, V any] struct { - k K - v V -} - -func FromMap[Map ~map[K]V, K comparable, V any](m Map) Iterator2[K, V] { - entries := make([]mapEntry[K, V], 0, len(m)) - for k, v := range m { - entries = append(entries, mapEntry[K, V]{k, v}) - } - i := 0 - return NewIterator2(func() (K, V, error) { - if i < len(entries) { - k := entries[i].k - v := entries[i].v - i++ - return k, v, nil - } - var k K - var v V - return k, v, io.EOF - }) -} - -func CollectMap[K comparable, V any](mit Iterator2[K, V]) (map[K]V, error) { - items := make(map[K]V) - for { - k, v, err := mit.Next() - if err != nil { - if err == io.EOF { - break - } - return nil, err - } - items[k] = v - } - return items, nil -} - -func Concat2[K any, V any](iterators ...Iterator2[K, V]) Iterator2[K, V] { - if len(iterators) == 0 { - return NewIterator2(func() (K, V, error) { - var k K - var v V - return k, v, io.EOF - }) - } - - i := 0 - iterator := iterators[i] - return NewIterator2(func() (K, V, error) { - for { - k, v, err := iterator.Next() - if err != nil { - if err == io.EOF { - i++ - if i < len(iterators) { - iterator = iterators[i] - continue - } - } - var k K - var v V - return k, v, err - } - return k, v, nil - } - }) -} diff --git a/core/iterable/mapiterable_test.go b/core/iterable/mapiterable_test.go deleted file mode 100644 index 2ff157b..0000000 --- a/core/iterable/mapiterable_test.go +++ /dev/null @@ -1,114 +0,0 @@ -package iterable_test - -import ( - "errors" - "fmt" - "io" - "testing" - - "github.com/storacha-network/go-ucanto/core/iterable" - "github.com/stretchr/testify/require" -) - -func TestCollectMap(t *testing.T) { - someErr := errors.New("some error") - testCases := []struct { - name string - iterator2 func() iterable.Iterator2[string, int] - expectedMap map[string]int - expectedErr error - }{ - { - name: "converts successful iterator to expected map", - iterator2: func() iterable.Iterator2[string, int] { - count := 0 - return iterable.NewIterator2(func() (string, int, error) { - defer func() { - count++ - }() - switch count { - case 0: - return "apples", 7, nil - case 1: - return "oranges", 4, nil - case 2: - return "", 0, io.EOF - default: - return "", 0, fmt.Errorf("too many calls to iterator: %d", count+1) - } - }) - }, - expectedMap: map[string]int{"apples": 7, "oranges": 4}, - }, - { - name: "fails when iterator fails", - iterator2: func() iterable.Iterator2[string, int] { - count := 0 - return iterable.NewIterator2(func() (string, int, error) { - defer func() { - count++ - }() - switch count { - case 0: - return "apples", 7, nil - default: - return "", 0, fmt.Errorf("mistake iterating: %w", someErr) - } - }) - }, - expectedMap: nil, - expectedErr: someErr, - }, - } - for _, testCase := range testCases { - t.Run(testCase.name, func(t *testing.T) { - resultMap, err := iterable.CollectMap(testCase.iterator2()) - require.Equal(t, testCase.expectedMap, resultMap) - require.ErrorIs(t, err, testCase.expectedErr) - }) - } -} - -func TestFromMap(t *testing.T) { - verifyFromMap(t, "string -> int", map[string]int{"apples": 7, "oranges": 4}) - verifyFromMap(t, "int -> bool", map[int]bool{7: true, 4: false, 3: true}) - verifyFromMap(t, "any -> any", map[any]any{ - 7: true, - 4: "apples", - "oranges": struct{ head string }{head: "bucket"}, - }) -} - -func TestRoundtrip(t *testing.T) { - roundTrip(t, "string -> int", map[string]int{"apples": 7, "oranges": 4}) - roundTrip(t, "int -> bool", map[int]bool{7: true, 4: false, 3: true}) - roundTrip(t, "any -> any", map[any]any{ - 7: true, - 4: "apples", - "oranges": struct{ head string }{head: "bucket"}, - }) -} - -func verifyFromMap[K comparable, V any](t *testing.T, testCase string, inputMap map[K]V) { - t.Run(testCase, func(t *testing.T) { - iterator := iterable.FromMap(inputMap) - outputMap := make(map[K]V, len(inputMap)) - for { - k, v, err := iterator.Next() - if err != nil { - require.ErrorIs(t, err, io.EOF) - require.Equal(t, inputMap, outputMap) - return - } - outputMap[k] = v - } - }) -} - -func roundTrip[K comparable, V any](t *testing.T, testCase string, inputMap map[K]V) { - t.Run(testCase, func(t *testing.T) { - outputMap, err := iterable.CollectMap(iterable.FromMap(inputMap)) - require.NoError(t, err) - require.Equal(t, inputMap, outputMap) - }) -} diff --git a/core/message/message.go b/core/message/message.go index 5f7d85c..74d0d2a 100644 --- a/core/message/message.go +++ b/core/message/message.go @@ -2,6 +2,7 @@ package message import ( "fmt" + "iter" "github.com/storacha-network/go-ucanto/core/dag/blockstore" "github.com/storacha-network/go-ucanto/core/invocation" @@ -9,7 +10,6 @@ import ( "github.com/storacha-network/go-ucanto/core/ipld/block" "github.com/storacha-network/go-ucanto/core/ipld/codec/cbor" "github.com/storacha-network/go-ucanto/core/ipld/hash/sha256" - "github.com/storacha-network/go-ucanto/core/iterable" mdm "github.com/storacha-network/go-ucanto/core/message/datamodel" "github.com/storacha-network/go-ucanto/core/receipt" ) @@ -38,7 +38,7 @@ func (m *message) Root() ipld.Block { return m.root } -func (m *message) Blocks() iterable.Iterator[ipld.Block] { +func (m *message) Blocks() iter.Seq2[ipld.Block, error] { return m.blks.Iterator() } diff --git a/core/receipt/receipt.go b/core/receipt/receipt.go index 83cdc88..a4a95bf 100644 --- a/core/receipt/receipt.go +++ b/core/receipt/receipt.go @@ -4,6 +4,7 @@ import ( // for go:embed _ "embed" "fmt" + "iter" "github.com/ipld/go-ipld-prime/datamodel" "github.com/ipld/go-ipld-prime/schema" @@ -77,8 +78,8 @@ type receipt[O, X any] struct { var _ Receipt[any, any] = (*receipt[any, any])(nil) -func (r *receipt[O, X]) Blocks() iterable.Iterator[block.Block] { - var iterators []iterable.Iterator[block.Block] +func (r *receipt[O, X]) Blocks() iter.Seq2[block.Block, error] { + var iterators []iter.Seq2[block.Block, error] iterators = append(iterators, r.Ran().Blocks()) for _, prf := range r.Proofs() { @@ -87,9 +88,9 @@ func (r *receipt[O, X]) Blocks() iterable.Iterator[block.Block] { } } - iterators = append(iterators, iterable.From([]block.Block{r.Root()})) + iterators = append(iterators, func(yield func(block.Block, error) bool) { yield(r.Root(), nil) }) - return iterable.Concat(iterators...) + return iterable.Concat2(iterators...) } func (r *receipt[O, X]) Fx() Effects { @@ -165,14 +166,14 @@ func NewReceipt[O, X any](root ipld.Link, blocks blockstore.BlockReader, typ sch } type ReceiptReader[O, X any] interface { - Read(rcpt ipld.Link, blks iterable.Iterator[block.Block]) (Receipt[O, X], error) + Read(rcpt ipld.Link, blks iter.Seq2[block.Block, error]) (Receipt[O, X], error) } type receiptReader[O, X any] struct { typ schema.Type } -func (rr *receiptReader[O, X]) Read(rcpt ipld.Link, blks iterable.Iterator[block.Block]) (Receipt[O, X], error) { +func (rr *receiptReader[O, X]) Read(rcpt ipld.Link, blks iter.Seq2[block.Block, error]) (Receipt[O, X], error) { br, err := blockstore.NewBlockReader(blockstore.WithBlocksIterator(blks)) if err != nil { return nil, fmt.Errorf("creating block reader: %s", err) diff --git a/core/result/datamodel/failure.go b/core/result/failure/datamodel/failure.go similarity index 74% rename from core/result/datamodel/failure.go rename to core/result/failure/datamodel/failure.go index 694ae32..7c18e56 100644 --- a/core/result/datamodel/failure.go +++ b/core/result/failure/datamodel/failure.go @@ -13,20 +13,18 @@ import ( //go:embed failure.ipldsch var failureSchema []byte -// Failure is a generic failure -type Failure struct { +// FailureModel is a generic failure +type FailureModel struct { Name *string Message string Stack *string } -func (f *Failure) Build() (ipld.Node, error) { +func (f *FailureModel) Build() (ipld.Node, error) { return ucanipld.WrapWithRecovery(f, typ) } -var ( - typ schema.Type -) +var typ schema.Type func init() { ts, err := ipld.LoadSchemaBytes(failureSchema) @@ -35,3 +33,7 @@ func init() { } typ = ts.TypeByName("Failure") } + +func Schema() []byte { + return failureSchema +} diff --git a/core/result/datamodel/failure.ipldsch b/core/result/failure/datamodel/failure.ipldsch similarity index 100% rename from core/result/datamodel/failure.ipldsch rename to core/result/failure/datamodel/failure.ipldsch diff --git a/core/result/failure/faillure.go b/core/result/failure/faillure.go new file mode 100644 index 0000000..b999973 --- /dev/null +++ b/core/result/failure/faillure.go @@ -0,0 +1,113 @@ +package failure + +import ( + "fmt" + "runtime" + + "github.com/pkg/errors" + "github.com/storacha-network/go-ucanto/core/ipld" + "github.com/storacha-network/go-ucanto/core/result/failure/datamodel" +) + +// Named is an error that you can read a name from +type Named interface { + Name() string +} + +// WithStackTrace is an error that you can read a stack trace from +type WithStackTrace interface { + Stack() string +} + +// IPLDConvertableError is an error with a custom method to convert to an IPLD Node +type IPLDConvertableError interface { + error + ipld.Builder +} + +type Failure interface { + error + Named +} + +type IPLDBuilderFailure interface { + IPLDConvertableError + Failure +} + +type NamedWithStackTrace interface { + Named + WithStackTrace +} + +type namedWithStackTrace struct { + name string + stack errors.StackTrace +} + +func (n namedWithStackTrace) Name() string { + return n.name +} + +func (n namedWithStackTrace) Stack() string { + return fmt.Sprintf("%+v", n.stack) +} + +func NamedWithCurrentStackTrace(name string) NamedWithStackTrace { + const depth = 32 + + var pcs [depth]uintptr + n := runtime.Callers(3, pcs[:]) + + f := make(errors.StackTrace, n) + for i := 0; i < n; i++ { + f[i] = errors.Frame(pcs[i]) + } + + return namedWithStackTrace{name, f} +} + +type failure struct { + model datamodel.FailureModel + build func() (ipld.Node, error) +} + +func (f failure) Name() string { + return *f.model.Name +} + +func (f failure) Message() string { + return f.model.Message +} + +func (f failure) Error() string { + return f.model.Message +} + +func (f failure) Stack() string { + return *f.model.Stack +} + +func (f failure) Build() (ipld.Node, error) { + if f.build != nil { + return f.build() + } + return f.model.Build() +} + +func FromError(err error) IPLDBuilderFailure { + model := datamodel.FailureModel{Message: err.Error()} + if named, ok := err.(Named); ok { + name := named.Name() + model.Name = &name + } + if withStackTrace, ok := err.(WithStackTrace); ok { + stack := withStackTrace.Stack() + model.Stack = &stack + } + fail := failure{model: model} + if builder, ok := err.(ipld.Builder); ok { + fail.build = builder.Build + } + return fail +} diff --git a/core/result/result.go b/core/result/result.go index c990ee4..829becd 100644 --- a/core/result/result.go +++ b/core/result/result.go @@ -1,16 +1,12 @@ package result import ( - "fmt" - "runtime" - - "github.com/pkg/errors" "github.com/storacha-network/go-ucanto/core/ipld" - "github.com/storacha-network/go-ucanto/core/result/datamodel" + "github.com/storacha-network/go-ucanto/core/result/failure" + "github.com/storacha-network/go-ucanto/core/result/failure/datamodel" ) // Result is a golang compatible generic result type -// https://github.com/ucan-wg/receipt/#6-result type Result[O any, X any] interface { isResult(ok O, err X) } @@ -22,8 +18,7 @@ type errResult[O any, X any] struct { value X } -func (o *okResult[O, X]) isResult(ok O, err X) {} - +func (o *okResult[O, X]) isResult(ok O, err X) {} func (e *errResult[O, X]) isResult(ok O, err X) {} // MatchResultR3 handles a result with functions returning 3 values @@ -176,72 +171,22 @@ func Wrap[O any, X comparable](inner func() (O, X)) Result[O, X] { return Ok[O, X](o) } -// Named is an error that you can read a name from -type Named interface { - Name() string -} - -// WithStackTrace is an error that you can read a stack trace from -type WithStackTrace interface { - Stack() string -} - -// IPLDConvertableError is an error with a custom method to convert to an IPLD Node -type IPLDConvertableError interface { - error - ipld.Builder -} - -type Failure interface { - error - Named -} - -type NamedWithStackTrace interface { - Named - WithStackTrace -} - -type namedWithStackTrace struct { - name string - stack errors.StackTrace -} - -func (n namedWithStackTrace) Name() string { - return n.name -} - -func (n namedWithStackTrace) Stack() string { - return fmt.Sprintf("%+v", n.stack) -} - -func NamedWithCurrentStackTrace(name string) NamedWithStackTrace { - const depth = 32 - - var pcs [depth]uintptr - n := runtime.Callers(3, pcs[:]) - - f := make(errors.StackTrace, n) - for i := 0; i < n; i++ { - f[i] = errors.Frame(pcs[i]) - } - - return namedWithStackTrace{name, f} -} - func NewFailure(err error) Result[ipld.Builder, ipld.Builder] { - if ipldConvertableError, ok := err.(IPLDConvertableError); ok { + if ipldConvertableError, ok := err.(failure.IPLDConvertableError); ok { return Error[ipld.Builder, ipld.Builder](ipldConvertableError) } - failure := datamodel.Failure{Message: err.Error()} - if named, ok := err.(Named); ok { + model := datamodel.FailureModel{Message: err.Error()} + if named, ok := err.(failure.Named); ok { name := named.Name() - failure.Name = &name + model.Name = &name } - if withStackTrace, ok := err.(WithStackTrace); ok { + if withStackTrace, ok := err.(failure.WithStackTrace); ok { stack := withStackTrace.Stack() - failure.Stack = &stack + model.Stack = &stack } - return Error[ipld.Builder, ipld.Builder](&failure) + return Error[ipld.Builder, ipld.Builder](&model) } + +// https://en.wikipedia.org/wiki/Unit_type +type Unit interface{} diff --git a/core/schema/did.go b/core/schema/did.go index 4f971c6..1a6c03e 100644 --- a/core/schema/did.go +++ b/core/schema/did.go @@ -1,33 +1,58 @@ package schema import ( - "github.com/storacha-network/go-ucanto/core/result" + "fmt" + "strings" + + "github.com/storacha-network/go-ucanto/core/result/failure" "github.com/storacha-network/go-ucanto/did" ) -var didreader = reader[string, did.DID]{ - readFunc: func(input string) result.Result[did.DID, result.Failure] { - d, err := did.Parse(input) - if err != nil { - return result.Error[did.DID](NewSchemaError(err.Error())) - } - return result.Ok[did.DID, result.Failure](d) - }, +type didConfig struct { + method string } -func DID() Reader[string, did.DID] { - return &didreader +type DIDOption func(*didConfig) + +func WithMethod(method string) DIDOption { + return func(c *didConfig) { + c.method = method + } } -// DIDString read a string that is in DID format. -func DIDString() Reader[string, string] { - return &didstrreader +func DID(opts ...DIDOption) Reader[string, did.DID] { + c := &didConfig{} + for _, opt := range opts { + opt(c) + } + return reader[string, did.DID]{ + readFunc: func(input string) (did.DID, failure.Failure) { + pfx := "did:" + if c.method != "" { + pfx = fmt.Sprintf("%s%s:", pfx, c.method) + } + if !strings.HasPrefix(input, pfx) { + return did.Undef, NewSchemaError(fmt.Sprintf(`Expected a "%s" but got "%s" instead`, pfx, input)) + } + d, err := did.Parse(input) + if err != nil { + return did.Undef, NewSchemaError(err.Error()) + } + return d, nil + }, + } } -var didstrreader = reader[string, string]{ - readFunc: func(input string) result.Result[string, result.Failure] { - return result.MapOk(DID().Read(input), func(id did.DID) string { - return id.String() - }) - }, +// DIDString read a string that is in DID format. +func DIDString(opts ...DIDOption) Reader[string, string] { + rdr := DID(opts...) + return reader[string, string]{ + readFunc: func(input string) (string, failure.Failure) { + d, err := rdr.Read(input) + if err != nil { + return "", err + } + return d.String(), nil + }, + } } diff --git a/core/schema/did_test.go b/core/schema/did_test.go index d16d199..54cdae6 100644 --- a/core/schema/did_test.go +++ b/core/schema/did_test.go @@ -3,32 +3,23 @@ package schema import ( "testing" - "github.com/storacha-network/go-ucanto/core/result" "github.com/storacha-network/go-ucanto/did" "github.com/stretchr/testify/require" ) func TestReadDID(t *testing.T) { - res := DID().Read("notadid") - result.MatchResultR0(res, func(ok did.DID) { - t.Fatalf("unexpectedly parsed a non-DID as a DID: %s", ok.String()) - }, func(err result.Failure) { - require.Equal(t, err.Name(), "SchemaError") - }) + res, err := DID().Read("notadid") + require.Error(t, err) + require.Equal(t, res, did.Undef) + require.Equal(t, err.Name(), "SchemaError") - res = DID().Read("did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z") - result.MatchResultR0(res, func(ok did.DID) { - require.Equal(t, ok.DID().String(), "did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z") - }, func(err result.Failure) { - t.Fatalf("unexpected error reading DID: %s", err) - }) + res, err = DID().Read("did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z") + require.NoError(t, err) + require.Equal(t, res.String(), "did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z") } func TestReadDIDString(t *testing.T) { - res := DIDString().Read("did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z") - result.MatchResultR0(res, func(ok string) { - require.Equal(t, ok, "did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z") - }, func(err result.Failure) { - t.Fatalf("unexpected error reading DID: %s", err) - }) + res, err := DIDString().Read("did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z") + require.NoError(t, err) + require.Equal(t, res, "did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z") } diff --git a/core/schema/link.go b/core/schema/link.go new file mode 100644 index 0000000..8d84437 --- /dev/null +++ b/core/schema/link.go @@ -0,0 +1,129 @@ +package schema + +import ( + "bytes" + "fmt" + + cidlink "github.com/ipld/go-ipld-prime/linking/cid" + "github.com/multiformats/go-base32" + mh "github.com/multiformats/go-multihash" + "github.com/storacha-network/go-ucanto/core/ipld" + "github.com/storacha-network/go-ucanto/core/result/failure" +) + +type linkReader struct { + lc *linkCfg +} + +func (lr linkReader) Read(input any) (ipld.Link, failure.Failure) { + link, asLink := input.(ipld.Link) + if !asLink { + node, asNode := input.(ipld.Node) + if !asNode { + // If input is not an IPLD node, can it be converted to one? + if builder, ok := input.(ipld.Builder); ok { + n, err := builder.Build() + if err != nil { + return nil, NewSchemaError(err.Error()) + } + node = n + } else { + return nil, NewSchemaError("unexpected input: not an IPLD node or link") + } + } + var err error + link, err = node.AsLink() + if err != nil { + return nil, NewSchemaError(err.Error()) + } + } + + cidLink, ok := link.(cidlink.Link) + if !ok { + return nil, NewSchemaError("Unsupported Link Type") + } + cid := cidLink.Cid + if lr.lc.codec != nil && cid.Prefix().Codec != *lr.lc.codec { + return nil, NewSchemaError(fmt.Sprintf("Expected link to be CID with %X codec", *lr.lc.codec)) + } + + if lr.lc.version != nil && cid.Prefix().Version != *lr.lc.version { + return nil, NewSchemaError(fmt.Sprintf( + "Expected link to be CID version %d instead of %d", *lr.lc.version, cid.Prefix().Version)) + } + + if lr.lc.multihash != nil { + multihash := lr.lc.multihash + if multihash.code != nil && cid.Prefix().MhType != *multihash.code { + return nil, NewSchemaError(fmt.Sprintf("Expected link to be CID with %X hashing algorithm", *&multihash.code)) + } + if multihash.digest != nil { + decoded, err := mh.Decode(cid.Hash()) + if err != nil { + return nil, NewSchemaError(err.Error()) + } + + if bytes.Compare(decoded.Digest, *multihash.digest) != 0 { + return nil, NewSchemaError(fmt.Sprintf("Expected link with %s hash digest instead of %s", base32.StdEncoding.EncodeToString(*multihash.digest), base32.StdEncoding.EncodeToString(decoded.Digest))) + } + } + } + return link, nil +} + +type multihashConfig struct { + code *uint64 + digest *[]byte +} + +type MultihashOption func(*multihashConfig) + +func WithAlg(code uint64) MultihashOption { + return func(mc *multihashConfig) { + mc.code = &code + } +} + +func WithDigest(digest []byte) MultihashOption { + return func(mc *multihashConfig) { + mc.digest = &digest + } +} + +type linkCfg struct { + version *uint64 + codec *uint64 + multihash *multihashConfig +} + +type LinkOption func(*linkCfg) + +func WithVersion(version uint64) LinkOption { + return func(lc *linkCfg) { + lc.version = &version + } +} + +func WithCodec(codec uint64) LinkOption { + return func(lc *linkCfg) { + lc.codec = &codec + } +} + +func WithMultihashConfig(opts ...MultihashOption) LinkOption { + return func(lc *linkCfg) { + mc := &multihashConfig{} + for _, opt := range opts { + opt(mc) + } + lc.multihash = mc + } +} + +func Link(opts ...LinkOption) Reader[any, ipld.Link] { + lc := &linkCfg{} + for _, opt := range opts { + opt(lc) + } + return linkReader{lc} +} diff --git a/core/schema/link_test.go b/core/schema/link_test.go new file mode 100644 index 0000000..e9ad3b5 --- /dev/null +++ b/core/schema/link_test.go @@ -0,0 +1,110 @@ +package schema_test + +import ( + "fmt" + "iter" + "maps" + "regexp" + "testing" + + "github.com/ipfs/go-cid" + cidlink "github.com/ipld/go-ipld-prime/linking/cid" + basicnode "github.com/ipld/go-ipld-prime/node/basic" + "github.com/multiformats/go-base32" + mh "github.com/multiformats/go-multihash" + "github.com/storacha-network/go-ucanto/core/iterable" + "github.com/storacha-network/go-ucanto/core/schema" + "github.com/storacha-network/go-ucanto/testing/helpers" + "github.com/stretchr/testify/require" +) + +func NewSet[T comparable](list iter.Seq[T]) iter.Seq[T] { + set := make(map[T]struct{}) + for elem := range list { + set[elem] = struct{}{} + } + return maps.Keys(set) +} +func TestReadLink(t *testing.T) { + fixtures := map[string]cid.Cid{ + "pb": cid.MustParse("QmTgnQBKj7eTV7ohraBCmh1DLwerUd2X9Rxzgf3gyMJbC8"), + "cbor": cid.MustParse("bafyreieuo63r3y2nuycaq4b3q2xvco3nprlxiwzcfp4cuupgaywat3z6mq"), + "rawIdentity": cid.MustParse("bafkqaaa"), + "ipns": cid.MustParse("k2k4r8kuj2bs2l996lhjx8rc727xlvthtak8o6eia3qm5adxvs5k84gf"), + "sha512": cid.MustParse("kgbuwaen1jrbjip6iwe9mqg54spvuucyz7f5jho2tkc2o0c7xzqwpxtogbyrwck57s9is6zqlwt9rsxbuvszym10nbaxt9jn7sf4eksqd"), + } + + links := maps.Values(fixtures) + versions := NewSet(iterable.Map(func(c cid.Cid) uint64 { return c.Version() }, maps.Values(fixtures))) + codecs := NewSet(iterable.Map(func(c cid.Cid) uint64 { return c.Prefix().Codec }, maps.Values(fixtures))) + algs := NewSet(iterable.Map(func(c cid.Cid) uint64 { return c.Prefix().MhType }, maps.Values(fixtures))) + digests := NewSet(iterable.Map(func(c cid.Cid) string { + h := c.Hash() + dh := helpers.Must(mh.Decode(h)) + return string(dh.Digest) + }, maps.Values(fixtures))) + + for link := range links { + t.Run(fmt.Sprintf("%s ➡ schema.Link()", link), func(t *testing.T) { + output, err := schema.Link().Read(basicnode.NewLink(cidlink.Link{Cid: link})) + require.NoError(t, err) + require.Equal(t, output, cidlink.Link{Cid: link}, link.String()) + }) + + for version := range versions { + t.Run(fmt.Sprintf("%s ➡ schema.Link(WithVersion(%d))", link, version), func(t *testing.T) { + reader := schema.Link(schema.WithVersion(version)) + output, err := reader.Read(basicnode.NewLink(cidlink.Link{Cid: link})) + if link.Version() == version { + require.NoError(t, err) + require.Equal(t, output, cidlink.Link{Cid: link}) + } else { + require.Error(t, err) + require.Contains(t, err.Error(), "Expected link to be CID version") + } + }) + } + + for codec := range codecs { + t.Run(fmt.Sprintf("%s ➡ schema.Link(WithCodec(%d))", link, codec), func(t *testing.T) { + reader := schema.Link(schema.WithCodec(codec)) + output, err := reader.Read(basicnode.NewLink(cidlink.Link{Cid: link})) + if link.Prefix().Codec == codec { + require.NoError(t, err) + require.Equal(t, output, cidlink.Link{Cid: link}) + } else { + require.Error(t, err) + require.Regexp(t, helpers.Must(regexp.Compile("Expected link to be CID with .* code")), err.Error()) + } + }) + } + + for alg := range algs { + t.Run(fmt.Sprintf("%s ➡ schema.Link(WithMultihashConfig(WithAlg(%d)))", link, alg), func(t *testing.T) { + reader := schema.Link(schema.WithMultihashConfig(schema.WithAlg(alg))) + output, err := reader.Read(basicnode.NewLink(cidlink.Link{Cid: link})) + if link.Prefix().MhType == alg { + require.NoError(t, err) + require.Equal(t, output, cidlink.Link{Cid: link}) + } else { + require.Error(t, err) + require.Regexp(t, helpers.Must(regexp.Compile("Expected link to be CID with .* hashing algorithm")), err.Error()) + } + }) + } + + for digest := range digests { + t.Run(fmt.Sprintf("%s ➡ schema.Link(WithMultihashConfig(WithDigest(%s)))", link, base32.StdEncoding.EncodeToString([]byte(digest))), func(t *testing.T) { + reader := schema.Link(schema.WithMultihashConfig(schema.WithDigest([]byte(digest)))) + output, err := reader.Read(basicnode.NewLink(cidlink.Link{Cid: link})) + if string(helpers.Must(mh.Decode(link.Hash())).Digest) == digest { + require.NoError(t, err) + require.Equal(t, output, cidlink.Link{Cid: link}) + } else { + require.Error(t, err) + require.Regexp(t, helpers.Must(regexp.Compile("Expected link with .* hash digest")), err.Error()) + } + }) + } + } +} diff --git a/core/schema/literal.go b/core/schema/literal.go new file mode 100644 index 0000000..a63fdf0 --- /dev/null +++ b/core/schema/literal.go @@ -0,0 +1,18 @@ +package schema + +import ( + "fmt" + + "github.com/storacha-network/go-ucanto/core/result/failure" +) + +func Literal(expected string) Reader[string, string] { + return reader[string, string]{ + readFunc: func(input string) (string, failure.Failure) { + if input != expected { + return "", NewSchemaError(fmt.Sprintf("expected literal %s instead got %s", expected, input)) + } + return input, nil + }, + } +} diff --git a/core/schema/mapped.go b/core/schema/mapped.go new file mode 100644 index 0000000..abdff7a --- /dev/null +++ b/core/schema/mapped.go @@ -0,0 +1,21 @@ +package schema + +import "github.com/storacha-network/go-ucanto/core/result/failure" + +type mapped[I, O, O2 any] struct { + reader Reader[I, O] + converter func(O) (O2, failure.Failure) +} + +func (m mapped[I, O, O2]) Read(i I) (O2, failure.Failure) { + o, err := m.reader.Read(i) + if err != nil { + var o2 O2 + return o2, err + } + return m.converter(o) +} + +func Mapped[I, O, O2 any](reader Reader[I, O], converter func(O) (O2, failure.Failure)) Reader[I, O2] { + return mapped[I, O, O2]{reader, converter} +} diff --git a/core/schema/mapped_test.go b/core/schema/mapped_test.go new file mode 100644 index 0000000..153dff1 --- /dev/null +++ b/core/schema/mapped_test.go @@ -0,0 +1,81 @@ +package schema_test + +import ( + "fmt" + "net/url" + "testing" + + "github.com/ipld/go-ipld-prime" + "github.com/ipld/go-ipld-prime/node/basicnode" + "github.com/storacha-network/go-ucanto/core/result/failure" + "github.com/storacha-network/go-ucanto/core/schema" + "github.com/storacha-network/go-ucanto/testing/helpers" + "github.com/stretchr/testify/require" +) + +func TestReadStruct(t *testing.T) { + type TestStruct struct { + Url string + } + + ts := helpers.Must(ipld.LoadSchemaBytes([]byte(` + type TestStruct struct { + url String + } + `))) + + type URLStruct struct { + Url url.URL + } + + converter := func(ts TestStruct) (URLStruct, failure.Failure) { + url, err := url.Parse(ts.Url) + if err != nil { + return URLStruct{}, failure.FromError(err) + } + return URLStruct{Url: *url}, nil + } + + t.Run("Success", func(t *testing.T) { + np := basicnode.Prototype.Any + nb := np.NewBuilder() + ma := helpers.Must(nb.BeginMap(2)) + ma.AssembleKey().AssignString("url") + ma.AssembleValue().AssignString("http://www.yahoo.com") + ma.Finish() + nd := nb.Build() + + res, err := schema.Mapped(schema.Struct[TestStruct](ts.TypeByName("TestStruct"), nil), converter).Read(nd) + require.NoError(t, err) + fmt.Printf("%+v\n", res) + require.Equal(t, res.Url.Host, "www.yahoo.com") + }) + + t.Run("Failure, underlying reader", func(t *testing.T) { + np := basicnode.Prototype.Any + nb := np.NewBuilder() + ma := helpers.Must(nb.BeginMap(2)) + ma.AssembleKey().AssignString("foo") + ma.AssembleValue().AssignString("bar") + ma.Finish() + nd := nb.Build() + + _, err := schema.Mapped(schema.Struct[TestStruct](ts.TypeByName("TestStruct"), nil), converter).Read(nd) + require.Error(t, err) + fmt.Printf("%+v\n", err) + require.Equal(t, err.Name(), "SchemaError") + }) + t.Run("Failure, conversion", func(t *testing.T) { + np := basicnode.Prototype.Any + nb := np.NewBuilder() + ma := helpers.Must(nb.BeginMap(2)) + ma.AssembleKey().AssignString("url") + ma.AssembleValue().AssignString(":apple") + ma.Finish() + nd := nb.Build() + + _, err := schema.Mapped(schema.Struct[TestStruct](ts.TypeByName("TestStruct"), nil), converter).Read(nd) + require.Error(t, err) + fmt.Printf("%+v\n", err) + }) +} diff --git a/core/schema/or.go b/core/schema/or.go new file mode 100644 index 0000000..8d837dd --- /dev/null +++ b/core/schema/or.go @@ -0,0 +1,60 @@ +package schema + +import ( + "errors" + "fmt" + "strings" + + "github.com/storacha-network/go-ucanto/core/result/failure" +) + +type unionError struct { + failures []failure.Failure +} + +func (ue unionError) Unwrap() []error { + errors := make([]error, 0, len(ue.failures)) + for _, failure := range ue.failures { + errors = append(errors, failure) + } + return errors +} + +func indent(message string) string { + indent := " " + return indent + strings.Join(strings.Split(message, "\n"), "\n"+indent) +} + +func li(message string) string { + return indent("- " + message) +} + +func (ue unionError) Error() string { + return fmt.Sprintf("Value does not match any type of the union:\n%s", li(errors.Join(ue.Unwrap()...).Error())) +} + +func (ue unionError) Name() string { + return "Union Error" +} + +type orReader[I, O any] struct { + readers []Reader[I, O] +} + +func (or orReader[I, O]) Read(input I) (O, failure.Failure) { + failures := make([]failure.Failure, 0, len(or.readers)) + for _, reader := range or.readers { + o, err := reader.Read(input) + if err != nil { + failures = append(failures, err) + } else { + return o, nil + } + } + var o O + return o, failure.FromError(unionError{failures: failures}) +} + +func Or[I, O any](readers ...Reader[I, O]) Reader[I, O] { + return orReader[I, O]{readers} +} diff --git a/core/schema/schema.go b/core/schema/schema.go index 8790f7e..8eb5cbc 100644 --- a/core/schema/schema.go +++ b/core/schema/schema.go @@ -3,18 +3,18 @@ package schema import ( "github.com/ipld/go-ipld-prime/node/basicnode" "github.com/storacha-network/go-ucanto/core/ipld" - "github.com/storacha-network/go-ucanto/core/result" + "github.com/storacha-network/go-ucanto/core/result/failure" ) type Reader[I, O any] interface { - Read(input I) result.Result[O, result.Failure] + Read(input I) (O, failure.Failure) } type reader[I, O any] struct { - readFunc func(input I) result.Result[O, result.Failure] + readFunc func(input I) (O, failure.Failure) } -func (r *reader[I, O]) Read(input I) result.Result[O, result.Failure] { +func (r reader[I, O]) Read(input I) (O, failure.Failure) { return r.readFunc(input) } @@ -22,15 +22,15 @@ type schemaerr struct { message string } -func (se *schemaerr) Name() string { +func (se schemaerr) Name() string { return "SchemaError" } -func (se *schemaerr) Error() string { +func (se schemaerr) Error() string { return se.message } -func (se *schemaerr) Build() (ipld.Node, error) { +func (se schemaerr) Build() (ipld.Node, error) { np := basicnode.Prototype.Any nb := np.NewBuilder() ma, err := nb.BeginMap(2) @@ -45,9 +45,6 @@ func (se *schemaerr) Build() (ipld.Node, error) { return nb.Build(), nil } -var _ result.Failure = (*schemaerr)(nil) -var _ ipld.Builder = (*schemaerr)(nil) - -func NewSchemaError(message string) result.Failure { - return &schemaerr{message} +func NewSchemaError(message string) failure.Failure { + return schemaerr{message} } diff --git a/core/schema/struct.go b/core/schema/struct.go index 48541b0..69b99e2 100644 --- a/core/schema/struct.go +++ b/core/schema/struct.go @@ -3,27 +3,46 @@ package schema import ( "github.com/ipld/go-ipld-prime/schema" "github.com/storacha-network/go-ucanto/core/ipld" - "github.com/storacha-network/go-ucanto/core/result" + "github.com/storacha-network/go-ucanto/core/result/failure" + "github.com/ucan-wg/go-ucan/capability/policy" ) type strukt[T any] struct { - typ schema.Type + typ schema.Type + policy policy.Policy } -func (s *strukt[T]) Read(input any) result.Result[T, result.Failure] { +func (s strukt[T]) Read(input any) (T, failure.Failure) { + var bind T node, ok := input.(ipld.Node) if !ok { - return result.Error[T](NewSchemaError("unexpected input: not an IPLD node")) + // If input is not an IPLD node, can it be converted to one? + if builder, ok := input.(ipld.Builder); ok { + n, err := builder.Build() + if err != nil { + return bind, NewSchemaError(err.Error()) + } + node = n + } else { + return bind, NewSchemaError("unexpected input: not an IPLD node") + } + } + + if s.policy != nil { + ok := policy.Match(s.policy, node) + if !ok { + return bind, NewSchemaError("input did not match policy") + } } bind, err := ipld.Rebind[T](node, s.typ) if err != nil { - return result.Error[T](NewSchemaError(err.Error())) + return bind, NewSchemaError(err.Error()) } - return result.Ok[T, result.Failure](bind) + return bind, nil } -func Struct[T any](typ schema.Type) Reader[any, T] { - return &strukt[T]{typ: typ} +func Struct[T any](typ schema.Type, policy policy.Policy) Reader[any, T] { + return strukt[T]{typ, policy} } diff --git a/core/schema/struct_test.go b/core/schema/struct_test.go index d6a21ae..614da3f 100644 --- a/core/schema/struct_test.go +++ b/core/schema/struct_test.go @@ -6,7 +6,6 @@ import ( "github.com/ipld/go-ipld-prime" "github.com/ipld/go-ipld-prime/node/basicnode" - "github.com/storacha-network/go-ucanto/core/result" "github.com/storacha-network/go-ucanto/testing/helpers" "github.com/stretchr/testify/require" ) @@ -31,13 +30,10 @@ func TestReadStruct(t *testing.T) { ma.Finish() nd := nb.Build() - res := Struct[TestStruct](ts.TypeByName("TestStruct")).Read(nd) - result.MatchResultR0(res, func(ok TestStruct) { - fmt.Printf("%+v\n", ok) - require.Equal(t, ok.Name, "foo") - }, func(err result.Failure) { - t.Fatalf("unexpected error reading struct: %s", err) - }) + res, err := Struct[TestStruct](ts.TypeByName("TestStruct"), nil).Read(nd) + require.NoError(t, err) + fmt.Printf("%+v\n", res) + require.Equal(t, res.Name, "foo") }) t.Run("Failure", func(t *testing.T) { @@ -49,12 +45,9 @@ func TestReadStruct(t *testing.T) { ma.Finish() nd := nb.Build() - res := Struct[TestStruct](ts.TypeByName("TestStruct")).Read(nd) - result.MatchResultR0(res, func(ok TestStruct) { - t.Fatalf("unexpectedly read incompatible struct: %+v", ok) - }, func(err result.Failure) { - fmt.Printf("%+v\n", err) - require.Equal(t, err.Name(), "SchemaError") - }) + _, err := Struct[TestStruct](ts.TypeByName("TestStruct"), nil).Read(nd) + require.Error(t, err) + fmt.Printf("%+v\n", err) + require.Equal(t, err.Name(), "SchemaError") }) } diff --git a/core/schema/uri.go b/core/schema/uri.go new file mode 100644 index 0000000..ed65e40 --- /dev/null +++ b/core/schema/uri.go @@ -0,0 +1,51 @@ +package schema + +import ( + "fmt" + "net/url" + + "github.com/storacha-network/go-ucanto/core/result/failure" +) + +type uriConfig struct { + protocol *string +} + +type uriReader struct { + uc *uriConfig +} + +type URIOption func(*uriConfig) + +func WithProtocol(protocol string) URIOption { + return func(uc *uriConfig) { + uc.protocol = &protocol + } +} + +func (ur uriReader) Read(input any) (url.URL, failure.Failure) { + asString, stringOk := input.(string) + asUrl, urlOk := input.(url.URL) + if !stringOk && !urlOk { + return url.URL{}, NewSchemaError(fmt.Sprintf("Expected URI but got %T", input)) + } + if !urlOk { + u, err := url.ParseRequestURI(asString) + if err != nil { + return url.URL{}, NewSchemaError("Invalid URI") + } + asUrl = *u + } + if ur.uc.protocol != nil && *ur.uc.protocol != asUrl.Scheme+":" { + return url.URL{}, NewSchemaError(fmt.Sprintf("Expected %s URI instead got %s", *ur.uc.protocol, asUrl.String())) + } + return asUrl, nil +} + +func URI(opts ...URIOption) Reader[any, url.URL] { + uc := &uriConfig{} + for _, opt := range opts { + opt(uc) + } + return uriReader{uc} +} diff --git a/core/schema/uri_test.go b/core/schema/uri_test.go new file mode 100644 index 0000000..35407ee --- /dev/null +++ b/core/schema/uri_test.go @@ -0,0 +1,78 @@ +package schema_test + +import ( + "fmt" + "regexp" + "testing" + + "github.com/storacha-network/go-ucanto/core/schema" + "github.com/stretchr/testify/require" +) + +func TestURIDefaultReader(t *testing.T) { + testCases := []struct { + source string + output string + errMatch *regexp.Regexp + }{ + { + source: "", + errMatch: regexp.MustCompile("Invalid URI"), + }, + { + source: "did:key:zAlice", + output: "did:key:zAlice", + }, + { + source: "mailto:alice@mail.net", + output: "mailto:alice@mail.net", + }, + } + + for _, testCase := range testCases { + t.Run(fmt.Sprintf("schema.URI.read(%s)", testCase.source), func(t *testing.T) { + output, err := schema.URI().Read(testCase.source) + if testCase.errMatch == nil { + require.NoError(t, err) + require.Equal(t, testCase.output, output.String()) + } else { + require.Regexp(t, testCase.errMatch, err.Error()) + } + }) + } +} + +func TestURIReaderWithProtocol(t *testing.T) { + testCases := []struct { + source any + protocol string + output string + errMatch *regexp.Regexp + }{ + {nil, "did:", "", regexp.MustCompile("Expected URI but got ")}, + {"", "did:", "", regexp.MustCompile("Invalid URI")}, + {"did:key:zAlice", "did:", "did:key:zAlice", nil}, + {"did:key:zAlice", "mailto:", "", regexp.MustCompile("Expected mailto: URI instead got did:key:zAlice")}, + {"mailto:alice@mail.net", "mailto:", "mailto:alice@mail.net", nil}, + {"mailto:alice@mail.net", "did:", "", regexp.MustCompile("Expected did: URI instead got mailto:alice@mail.net")}, + } + + for _, testCase := range testCases { + t.Run(fmt.Sprintf("schema.URI(WithProtocol(%s)).read(%s)", testCase.protocol, testCase.source), func(t *testing.T) { + output, err := schema.URI(schema.WithProtocol((testCase.protocol))).Read(testCase.source) + if testCase.errMatch == nil { + require.NoError(t, err) + require.Equal(t, testCase.output, output.String()) + } else { + require.Regexp(t, testCase.errMatch, err.Error()) + } + }) + } + // for (const [input, protocol, expect] of dataset) { + // test(`URI.match(${JSON.stringify({ + // protocol, + // })}).read(${JSON.stringify(input)})}}`, () => { + // matchResult(URI.match({ protocol }).read(input), expect) + // }) + // } +} diff --git a/did/did.go b/did/did.go index 483485c..4614b0a 100644 --- a/did/did.go +++ b/did/did.go @@ -49,6 +49,11 @@ func (d DID) String() string { return "did:" + d.str[MethodOffset:] } +// GoString formats the decentralized identity document (DID) as a string. +func (d DID) GoString() string { + return d.String() +} + func Decode(bytes []byte) (DID, error) { code, _, err := varint.FromUvarint(bytes) if err != nil { diff --git a/go.mod b/go.mod index d705408..989d9d9 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/storacha-network/go-ucanto -go 1.21 +go 1.23 require ( github.com/ipfs/go-cid v0.4.1 @@ -11,15 +11,36 @@ require ( github.com/multiformats/go-multihash v0.2.3 github.com/multiformats/go-varint v0.0.7 github.com/pkg/errors v0.9.1 - github.com/stretchr/testify v1.8.4 + github.com/stretchr/testify v1.9.0 + github.com/ucan-wg/go-ucan v0.0.0-20240916120445-37f52863156c +) + +require ( + github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect + github.com/flynn/noise v1.1.0 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect + github.com/libp2p/go-msgio v0.3.0 // indirect + github.com/pion/datachannel v1.5.9 // indirect + github.com/pion/logging v0.2.2 // indirect + github.com/pion/sctp v1.8.33 // indirect + github.com/pion/webrtc/v3 v3.3.2 // indirect + github.com/prometheus/client_golang v1.20.3 // indirect + github.com/quic-go/quic-go v0.47.0 // indirect + github.com/quic-go/webtransport-go v0.8.0 // indirect + github.com/rogpeppe/go-internal v1.10.0 // indirect + golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect + golang.org/x/net v0.28.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect ) require ( github.com/davecgh/go-spew v1.1.1 // indirect - github.com/go-logr/logr v1.2.3 // indirect + github.com/go-logr/logr v1.2.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/gobwas/glob v0.2.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/google/uuid v1.3.0 // indirect + github.com/google/uuid v1.3.1 // indirect github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/ipfs/bbloom v0.0.4 // indirect github.com/ipfs/go-block-format v0.2.0 // indirect @@ -38,9 +59,9 @@ require ( github.com/ipfs/go-verifcid v0.0.2 // indirect github.com/ipld/go-codec-dagpb v1.6.0 // indirect github.com/jbenet/goprocess v0.1.4 // indirect - github.com/klauspost/cpuid/v2 v2.2.3 // indirect + github.com/klauspost/cpuid/v2 v2.2.8 // indirect github.com/mattn/go-isatty v0.0.17 // indirect - github.com/minio/sha256-simd v1.0.0 // indirect + github.com/minio/sha256-simd v1.0.1 // indirect github.com/mr-tron/base58 v1.2.0 // indirect github.com/multiformats/go-base32 v0.1.0 // indirect github.com/multiformats/go-base36 v0.2.0 // indirect @@ -54,10 +75,10 @@ require ( go.uber.org/atomic v1.10.0 // indirect go.uber.org/multierr v1.9.0 // indirect go.uber.org/zap v1.24.0 // indirect - golang.org/x/crypto v0.6.0 // indirect - golang.org/x/sys v0.5.0 // indirect + golang.org/x/crypto v0.26.0 // indirect + golang.org/x/sys v0.23.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect - google.golang.org/protobuf v1.28.1 // indirect + google.golang.org/protobuf v1.34.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - lukechampine.com/blake3 v1.1.7 // indirect + lukechampine.com/blake3 v1.3.0 // indirect ) diff --git a/go.sum b/go.sum index c412866..75ac23c 100644 --- a/go.sum +++ b/go.sum @@ -2,36 +2,50 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cskr/pubsub v1.0.2 h1:vlOzMhl6PFn60gRlTQQsIfVwaPB/B/8MziK8FhEPt/0= github.com/cskr/pubsub v1.0.2/go.mod h1:/8MzYXk/NJAz782G8RPkFzXTZVu63VotefPnR9TIRis= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c h1:pFUpOrbxDR6AkioZ1ySsx5yxlDQZ8stG2b88gTPxgJU= +github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c/go.mod h1:6UhI8N9EjYm1c2odKpFpAYeR8dsBeM7PtzQhRgxRr9U= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc= +github.com/flynn/noise v1.1.0 h1:KjPQoQCEFdZDiP03phOvGi11+SVVhBG2wOWAorLsstg= +github.com/flynn/noise v1.1.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag= +github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= +github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= -github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= +github.com/google/pprof v0.0.0-20230821062121-407c9e7a662f h1:pDhu5sgp8yJlEF/g6osliIIpF9K4F5jvkULXa4daRDQ= +github.com/google/pprof v0.0.0-20230821062121-407c9e7a662f/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= +github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gxed/hashland/keccakpg v0.0.1/go.mod h1:kRzw3HkwxFU1mpmPP8v1WyQzwdGfmKFJ6tItnhQ67kU= @@ -112,6 +126,8 @@ github.com/ipld/go-ipld-prime v0.21.0/go.mod h1:3RLqy//ERg/y5oShXXdx5YIp50cFGOan github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= github.com/jbenet/go-cienv v0.1.0/go.mod h1:TqNnHUmJgXau0nCzC7kXWeotg3J9W34CUv5Djy1+FlA= +github.com/jbenet/go-temp-err-catcher v0.1.0 h1:zpb3ZH6wIE8Shj2sKS+khgRvf7T7RABoLk/+KKHggpk= +github.com/jbenet/go-temp-err-catcher v0.1.0/go.mod h1:0kJRvmDZXNMIiJirNPEYfhpPwbGVtZVWC34vc5WLsDk= github.com/jbenet/goprocess v0.1.4 h1:DRGOFReOMqqDNXwW70QkacFW0YN9QnwLV0Vqk+3oU0o= github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= @@ -119,14 +135,15 @@ github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfV github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.3 h1:sxCkb+qR91z4vsqw4vGGZlDgPz3G7gjaLyK3V8y70BU= -github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= +github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/koron/go-ssdp v0.0.3 h1:JivLMY45N76b4p/vsWGOKewBQu6uf39y8l+AQ7sDKx8= github.com/koron/go-ssdp v0.0.3/go.mod h1:b2MxI6yh02pKrsyNoQUsk4+YNikaGhe4894J+Q5lDvA= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -137,36 +154,32 @@ github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6 github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= github.com/libp2p/go-cidranger v1.1.0 h1:ewPN8EZ0dd1LSnrtuwd4709PXVcITVeuwbag38yPW7c= github.com/libp2p/go-cidranger v1.1.0/go.mod h1:KWZTfSr+r9qEo9OkI9/SIEeAtw+NNoU0dXIXt15Okic= -github.com/libp2p/go-libp2p v0.22.0 h1:2Tce0kHOp5zASFKJbNzRElvh0iZwdtG5uZheNW8chIw= -github.com/libp2p/go-libp2p v0.22.0/go.mod h1:UDolmweypBSjQb2f7xutPnwZ/fxioLbMBxSjRksxxU4= +github.com/libp2p/go-libp2p v0.36.2 h1:BbqRkDaGC3/5xfaJakLV/BrpjlAuYqSB0lRvtzL3B/U= +github.com/libp2p/go-libp2p v0.36.2/go.mod h1:XO3joasRE4Eup8yCTTP/+kX+g92mOgRaadk46LmPhHY= 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-record v0.2.0 h1:oiNUOCWno2BFuxt3my4i1frNrt7PerzB3queqa1NkQ0= github.com/libp2p/go-libp2p-record v0.2.0/go.mod h1:I+3zMkvvg5m2OcSdoL0KPljyJyvNDFGKX7QdlpYUcwk= github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUIK5WDu6iPUA= github.com/libp2p/go-libp2p-testing v0.12.0/go.mod h1:KcGDRXyN7sQCllucn1cOOS+Dmm7ujhfEyXQL5lvkcPg= -github.com/libp2p/go-msgio v0.2.0 h1:W6shmB+FeynDrUVl2dgFQvzfBZcXiyqY4VmpQLu9FqU= -github.com/libp2p/go-msgio v0.2.0/go.mod h1:dBVM1gW3Jk9XqHkU4eKdGvVHdLa51hoGfll6jMJMSlY= +github.com/libp2p/go-msgio v0.3.0 h1:mf3Z8B1xcFN314sWX+2vOTShIE0Mmn2TXn3YCUQGNj0= +github.com/libp2p/go-msgio v0.3.0/go.mod h1:nyRM819GmVaF9LX3l03RMh10QdOroF++NBbxAb0mmDM= github.com/libp2p/go-nat v0.1.0 h1:MfVsH6DLcpa04Xr+p8hmVRG4juse0s3J8HyNWYHffXg= github.com/libp2p/go-nat v0.1.0/go.mod h1:X7teVkwRHNInVNWQiO/tAiAVRwSr5zoRz4YSTC3uRBM= github.com/libp2p/go-netroute v0.2.0 h1:0FpsbsvuSnAhXFnCY0VLFbJOzaK0VnP0r1QT/o4nWRE= github.com/libp2p/go-netroute v0.2.0/go.mod h1:Vio7LTzZ+6hoT4CMZi5/6CpY3Snzh2vgZhWgxMNwlQI= -github.com/libp2p/go-openssl v0.1.0 h1:LBkKEcUv6vtZIQLVTegAil8jbNpJErQ9AnT+bWV+Ooo= -github.com/libp2p/go-openssl v0.1.0/go.mod h1:OiOxwPpL3n4xlenjx2h7AwSGaFSC/KZvf6gNdOBQMtc= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-pointer v0.0.1 h1:n+XhsuGeVO6MEAp7xyEukFINEa+Quek5psIR/ylA6o0= -github.com/mattn/go-pointer v0.0.1/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc= github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8RvIylQ358TN4wwqatJ8rNavkEINozVn9DtGI3dfQ= github.com/minio/sha256-simd v0.0.0-20190131020904-2d45a736cd16/go.mod h1:2FMWW+8GMoPweT6+pI63m9YE3Lmw4J71hV56Chs1E/U= github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= -github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= -github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= +github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= +github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= github.com/mr-tron/base58 v1.1.0/go.mod h1:xcD2VGqlgYjBdcBLw+TuYLr8afG+Hj8g2eTVqeSzSU8= github.com/mr-tron/base58 v1.1.3/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= @@ -199,9 +212,46 @@ github.com/multiformats/go-multistream v0.3.3/go.mod h1:ODRoqamLUsETKS9BNcII4gcR github.com/multiformats/go-varint v0.0.5/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE= github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo/v2 v2.12.0 h1:UIVDowFPwpg6yMUpPjGkYvf06K3RAiJXUhCxEwQVHRI= +github.com/onsi/ginkgo/v2 v2.12.0/go.mod h1:ZNEzXISYlqpb8S36iN71ifqLi3vVD1rVJGvWRCJOUpQ= github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= +github.com/pion/datachannel v1.5.9 h1:LpIWAOYPyDrXtU+BW7X0Yt/vGtYxtXQ8ql7dFfYUVZA= +github.com/pion/datachannel v1.5.9/go.mod h1:kDUuk4CU4Uxp82NH4LQZbISULkX/HtzKa4P7ldf9izE= +github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk= +github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE= +github.com/pion/ice/v2 v2.3.34 h1:Ic1ppYCj4tUOcPAp76U6F3fVrlSw8A9JtRXLqw6BbUM= +github.com/pion/ice/v2 v2.3.34/go.mod h1:mBF7lnigdqgtB+YHkaY/Y6s6tsyRyo4u4rPGRuOjUBQ= +github.com/pion/interceptor v0.1.29 h1:39fsnlP1U8gw2JzOFWdfCU82vHvhW9o0rZnZF56wF+M= +github.com/pion/interceptor v0.1.29/go.mod h1:ri+LGNjRUc5xUNtDEPzfdkmSqISixVTBF/z/Zms/6T4= +github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= +github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= +github.com/pion/mdns v0.0.12 h1:CiMYlY+O0azojWDmxdNr7ADGrnZ+V6Ilfner+6mSVK8= +github.com/pion/mdns v0.0.12/go.mod h1:VExJjv8to/6Wqm1FXK+Ii/Z9tsVk/F5sD/N70cnYFbk= +github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= +github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= +github.com/pion/rtcp v1.2.14 h1:KCkGV3vJ+4DAJmvP0vaQShsb0xkRfWkO540Gy102KyE= +github.com/pion/rtcp v1.2.14/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4= +github.com/pion/rtp v1.8.7 h1:qslKkG8qxvQ7hqaxkmL7Pl0XcUm+/Er7nMnu6Vq+ZxM= +github.com/pion/rtp v1.8.7/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU= +github.com/pion/sctp v1.8.33 h1:dSE4wX6uTJBcNm8+YlMg7lw1wqyKHggsP5uKbdj+NZw= +github.com/pion/sctp v1.8.33/go.mod h1:beTnqSzewI53KWoG3nqB282oDMGrhNxBdb+JZnkCwRM= +github.com/pion/sdp/v3 v3.0.9 h1:pX++dCHoHUwq43kuwf3PyJfHlwIj4hXA7Vrifiq0IJY= +github.com/pion/sdp/v3 v3.0.9/go.mod h1:B5xmvENq5IXJimIO4zfp6LAe1fD9N+kFv+V/1lOdz8M= +github.com/pion/srtp/v2 v2.0.20 h1:HNNny4s+OUmG280ETrCdgFndp4ufx3/uy85EawYEhTk= +github.com/pion/srtp/v2 v2.0.20/go.mod h1:0KJQjA99A6/a0DOVTu1PhDSw0CXF2jTkqOoMg3ODqdA= +github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4= +github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8= +github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q= +github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E= +github.com/pion/turn/v2 v2.1.6 h1:Xr2niVsiPTB0FPtt+yAWKFUkU1eotQbGgpTIld4x1Gc= +github.com/pion/turn/v2 v2.1.6/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY= +github.com/pion/webrtc/v3 v3.3.2 h1:9Cjt3n90UVG1am1JZ3I7qdIosc7F2rVBeJdhlfqn9R4= +github.com/pion/webrtc/v3 v3.3.2/go.mod h1:hVmrDJvwhEertRWObeb1xzulzHGeVUoPlWvxdGzcfU0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -209,25 +259,39 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/polydawn/refmt v0.89.0 h1:ADJTApkvkeBZsN0tBTx8QjpD9JkmxbKp0cxfr9qszm4= github.com/polydawn/refmt v0.89.0/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw= +github.com/prometheus/client_golang v1.20.3 h1:oPksm4K8B+Vt35tUhw6GbSNSgVlVSBH0qELP/7u83l4= +github.com/prometheus/client_golang v1.20.3/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= +github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= +github.com/quic-go/quic-go v0.47.0 h1:yXs3v7r2bm1wmPTYNLKAAJTHMYkPEsfYJmTazXrCZ7Y= +github.com/quic-go/quic-go v0.47.0/go.mod h1:3bCapYsJvXGZcipOHuu7plYtaV6tnF+z7wIFsU0WK9E= +github.com/quic-go/webtransport-go v0.8.0 h1:HxSrwun11U+LlmwpgM1kEqIqH90IT4N8auv/cD7QFJg= +github.com/quic-go/webtransport-go v0.8.0/go.mod h1:N99tjprW432Ut5ONql/aUhSLT0YVSlwHohQsuac9WaM= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs= github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg= github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM= -github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572 h1:RC6RW7j+1+HkWaX/Yh71Ee5ZHaHYt7ZP4sQgUrm6cDU= -github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572/go.mod h1:w0SWMsp6j9O/dk4/ZpIhL+3CkG8ofA2vuv7k+ltqUMc= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/ucan-wg/go-ucan v0.0.0-20240916120445-37f52863156c h1:A1pMNIlHPnJ6KROqNc6SKg7QlSiQA6umiEoy89Os4cM= +github.com/ucan-wg/go-ucan v0.0.0-20240916120445-37f52863156c/go.mod h1:IiRc1OKWUk7FziOTWmOo7iwbcEMr7ch0lgs3UrF13pU= github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/warpfork/go-testmark v0.12.1 h1:rMgCpJfwy1sJ50x0M0NgyphxYYPMOODIJHhsXyEHU0s= github.com/warpfork/go-testmark v0.12.1/go.mod h1:kHwy7wfvGSPh1rQJYKayD4AbtNaeyZdcGi9tNJTaa5Y= @@ -236,6 +300,8 @@ github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvS github.com/whyrusleeping/cbor-gen v0.0.0-20230818171029-f91ae536ca25 h1:yVYDLoN2gmB3OdBXFW8e1UwgVbmCvNlnAKhvHPaNARI= github.com/whyrusleeping/cbor-gen v0.0.0-20230818171029-f91ae536ca25/go.mod h1:fgkXqYy7bV2cFeIEOkVTZS/WjXARfBqSH6Q2qHL33hQ= github.com/whyrusleeping/go-logging v0.0.0-20170515211332-0457bb6b88fc/go.mod h1:bopw91TMyo8J3tvftk8xmU2kPmlrt4nScJQZU2hE5EM= +github.com/wlynxg/anet v0.0.3 h1:PvR53psxFXstc12jelG6f1Lv4MWqE0tI76/hHGjh9rg= +github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= @@ -250,6 +316,8 @@ go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0 go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= +go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= @@ -264,47 +332,55 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= -golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= +golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20190227160552-c95aed5357e7/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190219092855-153ac476189d/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= +golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= @@ -316,21 +392,21 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE= +golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -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.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= 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= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -341,5 +417,5 @@ gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -lukechampine.com/blake3 v1.1.7 h1:GgRMhmdsuK8+ii6UZFDL8Nb+VyMwadAgcJyfYHxG6n0= -lukechampine.com/blake3 v1.1.7/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA= +lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE= +lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= diff --git a/principal/absentee/absentee.go b/principal/absentee/absentee.go new file mode 100644 index 0000000..7b030cc --- /dev/null +++ b/principal/absentee/absentee.go @@ -0,0 +1,33 @@ +package absentee + +import ( + "github.com/storacha-network/go-ucanto/did" + "github.com/storacha-network/go-ucanto/ucan" + "github.com/storacha-network/go-ucanto/ucan/crypto/signature" +) + +type absentee struct { + id did.DID +} + +func (a absentee) DID() did.DID { + return a.id +} + +func (a absentee) Sign(msg []byte) signature.SignatureView { + return signature.NewSignatureView(signature.NewNonStandard(a.SignatureAlgorithm(), []byte{})) +} + +func (a absentee) SignatureAlgorithm() string { + return "" +} + +func (a absentee) SignatureCode() uint64 { + return signature.NON_STANDARD +} + +// From creates a special type of signer that produces an absent signature, +// which signals that verifier needs to verify authorization interactively. +func From(id did.DID) ucan.Signer { + return absentee{id} +} diff --git a/principal/absentee/absentee_test.go b/principal/absentee/absentee_test.go new file mode 100644 index 0000000..dd40dfa --- /dev/null +++ b/principal/absentee/absentee_test.go @@ -0,0 +1,25 @@ +package absentee + +import ( + "testing" + + "github.com/storacha-network/go-ucanto/did" + "github.com/storacha-network/go-ucanto/ucan/crypto/signature" + "github.com/stretchr/testify/require" +) + +func TestAbsentee(t *testing.T) { + t.Run("it can sign", func(t *testing.T) { + alicedid, err := did.Parse("did:mailto:web.mail:alice") + require.NoError(t, err) + + signer := From(alicedid) + require.Equal(t, alicedid, signer.DID()) + require.Equal(t, "", signer.SignatureAlgorithm()) + require.Equal(t, signature.NON_STANDARD, int(signer.SignatureCode())) + + sig := signer.Sign([]byte("hello world")) + require.Equal(t, signature.NON_STANDARD, int(sig.Code())) + require.Equal(t, []byte{}, sig.Raw()) + }) +} diff --git a/principal/lib.go b/principal/lib.go index ce13c9f..0b0d19e 100644 --- a/principal/lib.go +++ b/principal/lib.go @@ -4,6 +4,9 @@ import ( "github.com/storacha-network/go-ucanto/ucan" ) +// Signer is the principal that can issue UCANs (and sign payloads). While it's +// primary role is to sign payloads it also provides a `Verifier` interface so +// it can be used for verifying signed payloads as well. type Signer interface { ucan.Signer Code() uint64 @@ -11,6 +14,8 @@ type Signer interface { Encode() []byte } +// Verifier is the principal that issued a UCAN. In usually represents remote +// principal and is used to verify that certain payloads were signed by it. type Verifier interface { ucan.Verifier Code() uint64 diff --git a/principal/signer/signer.go b/principal/signer/signer.go new file mode 100644 index 0000000..7b1f2c7 --- /dev/null +++ b/principal/signer/signer.go @@ -0,0 +1,72 @@ +package signer + +import ( + "fmt" + "strings" + + "github.com/storacha-network/go-ucanto/did" + "github.com/storacha-network/go-ucanto/principal" + "github.com/storacha-network/go-ucanto/principal/verifier" + "github.com/storacha-network/go-ucanto/ucan/crypto/signature" +) + +type Unwrapper interface { + // Unwrap returns the unwrapped did:key of this signer. + Unwrap() principal.Signer +} + +type WrappedSigner interface { + principal.Signer + Unwrapper +} + +type wrapsgn struct { + key principal.Signer + verifier principal.Verifier +} + +func (w wrapsgn) Code() uint64 { + return w.key.Code() +} + +func (w wrapsgn) DID() did.DID { + return w.verifier.DID() +} + +func (w wrapsgn) Encode() []byte { + return w.key.Encode() +} + +func (w wrapsgn) Sign(msg []byte) signature.SignatureView { + return w.key.Sign(msg) +} + +func (w wrapsgn) SignatureAlgorithm() string { + return w.key.SignatureAlgorithm() +} + +func (w wrapsgn) SignatureCode() uint64 { + return w.key.SignatureCode() +} + +func (w wrapsgn) Unwrap() principal.Signer { + return w.key +} + +func (w wrapsgn) Verifier() principal.Verifier { + return w.verifier +} + +// Wrap the key of this signer into a signer with a different DID. This is +// primarily used to wrap a did:key signer with a signer that has a DID of +// a different method. +func Wrap(key principal.Signer, id did.DID) (WrappedSigner, error) { + if !strings.HasPrefix(key.DID().String(), "did:key:") { + return nil, fmt.Errorf("verifier is not a did:key") + } + vrf, err := verifier.Wrap(key.Verifier(), id) + if err != nil { + return nil, err + } + return wrapsgn{key, vrf}, nil +} diff --git a/principal/verifier/verifier.go b/principal/verifier/verifier.go new file mode 100644 index 0000000..41beeeb --- /dev/null +++ b/principal/verifier/verifier.go @@ -0,0 +1,55 @@ +package verifier + +import ( + "fmt" + "strings" + + "github.com/storacha-network/go-ucanto/did" + "github.com/storacha-network/go-ucanto/principal" + "github.com/storacha-network/go-ucanto/ucan/crypto/signature" +) + +type Unwrapper interface { + // Unwrap returns the unwrapped did:key of this signer. + Unwrap() principal.Verifier +} + +type WrappedVerifier interface { + principal.Verifier + Unwrapper +} + +type wrapvf struct { + id did.DID + key principal.Verifier +} + +func (w wrapvf) Code() uint64 { + return w.key.Code() +} + +func (w wrapvf) DID() did.DID { + return w.id +} + +func (w wrapvf) Encode() []byte { + return w.key.Encode() +} + +func (w wrapvf) Verify(msg []byte, sig signature.Signature) bool { + return w.key.Verify(msg, sig) +} + +func (w wrapvf) Unwrap() principal.Verifier { + return w.key +} + +// Wrap the key of this verifier into a verifier with a different DID. This is +// primarily used to wrap a did:key verifier with a verifier that has a DID of +// a different method. +func Wrap(key principal.Verifier, id did.DID) (WrappedVerifier, error) { + if !strings.HasPrefix(key.DID().String(), "did:key:") { + return nil, fmt.Errorf("verifier is not a did:key") + } + return wrapvf{id, key}, nil +} diff --git a/server/error.go b/server/error.go index 8925bba..1f01c49 100644 --- a/server/error.go +++ b/server/error.go @@ -4,13 +4,13 @@ import ( "fmt" "github.com/storacha-network/go-ucanto/core/ipld" - "github.com/storacha-network/go-ucanto/core/result" + "github.com/storacha-network/go-ucanto/core/result/failure" sdm "github.com/storacha-network/go-ucanto/server/datamodel" "github.com/storacha-network/go-ucanto/ucan" ) type HandlerNotFoundError[Caveats any] interface { - result.Failure + failure.Failure Capability() ucan.Capability[Caveats] } @@ -18,19 +18,19 @@ type handlerNotFoundError[Caveats any] struct { capability ucan.Capability[Caveats] } -func (h *handlerNotFoundError[C]) Capability() ucan.Capability[C] { +func (h handlerNotFoundError[C]) Capability() ucan.Capability[C] { return h.capability } -func (h *handlerNotFoundError[C]) Error() string { +func (h handlerNotFoundError[C]) Error() string { return fmt.Sprintf("service does not implement {can: \"%s\"} handler", h.capability.Can()) } -func (h *handlerNotFoundError[C]) Name() string { +func (h handlerNotFoundError[C]) Name() string { return "HandlerNotFoundError" } -func (h *handlerNotFoundError[C]) Build() (ipld.Node, error) { +func (h handlerNotFoundError[C]) Build() (ipld.Node, error) { name := h.Name() mdl := sdm.HandlerNotFoundErrorModel{ @@ -45,16 +45,13 @@ func (h *handlerNotFoundError[C]) Build() (ipld.Node, error) { return ipld.WrapWithRecovery(&mdl, sdm.HandlerNotFoundErrorType()) } -var _ HandlerNotFoundError[any] = (*handlerNotFoundError[any])(nil) -var _ ipld.Builder = (*handlerNotFoundError[any])(nil) - -func NewHandlerNotFoundError[Caveats any](capability ucan.Capability[Caveats]) *handlerNotFoundError[Caveats] { - return &handlerNotFoundError[Caveats]{capability} +func NewHandlerNotFoundError[Caveats any](capability ucan.Capability[Caveats]) HandlerNotFoundError[Caveats] { + return handlerNotFoundError[Caveats]{capability} } type HandlerExecutionError[Caveats any] interface { - result.Failure - result.WithStackTrace + failure.Failure + failure.WithStackTrace Cause() error Capability() ucan.Capability[Caveats] } @@ -64,41 +61,41 @@ type handlerExecutionError[Caveats any] struct { capability ucan.Capability[Caveats] } -func (h *handlerExecutionError[C]) Capability() ucan.Capability[C] { +func (h handlerExecutionError[C]) Capability() ucan.Capability[C] { return h.capability } -func (h *handlerExecutionError[C]) Cause() error { +func (h handlerExecutionError[C]) Cause() error { return h.cause } -func (h *handlerExecutionError[C]) Error() string { +func (h handlerExecutionError[C]) Error() string { return fmt.Sprintf("service handler {can: \"%s\"} error: %s", h.capability.Can(), h.cause.Error()) } -func (h *handlerExecutionError[C]) Name() string { +func (h handlerExecutionError[C]) Name() string { return "HandlerExecutionError" } -func (h *handlerExecutionError[C]) Stack() string { +func (h handlerExecutionError[C]) Stack() string { var stack string - if serr, ok := h.cause.(result.WithStackTrace); ok { + if serr, ok := h.cause.(failure.WithStackTrace); ok { stack = serr.Stack() } return stack } -func (h *handlerExecutionError[C]) Build() (ipld.Node, error) { +func (h handlerExecutionError[C]) Build() (ipld.Node, error) { name := h.Name() stack := h.Stack() var cname string - if ncause, ok := h.cause.(result.Named); ok { + if ncause, ok := h.cause.(failure.Named); ok { cname = ncause.Name() } var cstack string - if scause, ok := h.cause.(result.WithStackTrace); ok { + if scause, ok := h.cause.(failure.WithStackTrace); ok { cstack = scause.Stack() } @@ -120,15 +117,12 @@ func (h *handlerExecutionError[C]) Build() (ipld.Node, error) { return ipld.WrapWithRecovery(&mdl, sdm.HandlerExecutionErrorType()) } -var _ HandlerExecutionError[any] = (*handlerExecutionError[any])(nil) -var _ ipld.Builder = (*handlerExecutionError[any])(nil) - -func NewHandlerExecutionError[Caveats any](cause error, capability ucan.Capability[Caveats]) *handlerExecutionError[Caveats] { - return &handlerExecutionError[Caveats]{cause, capability} +func NewHandlerExecutionError[Caveats any](cause error, capability ucan.Capability[Caveats]) HandlerExecutionError[Caveats] { + return handlerExecutionError[Caveats]{cause, capability} } type InvocationCapabilityError interface { - result.Failure + failure.Failure Capabilities() []ucan.Capability[any] } @@ -136,19 +130,19 @@ type invocationCapabilityError struct { capabilities []ucan.Capability[any] } -func (i *invocationCapabilityError) Capabilities() []ucan.Capability[any] { +func (i invocationCapabilityError) Capabilities() []ucan.Capability[any] { return i.capabilities } -func (i *invocationCapabilityError) Error() string { +func (i invocationCapabilityError) Error() string { return "Invocation is required to have a single capability." } -func (i *invocationCapabilityError) Name() string { +func (i invocationCapabilityError) Name() string { return "InvocationCapabilityError" } -func (i *invocationCapabilityError) Build() (ipld.Node, error) { +func (i invocationCapabilityError) Build() (ipld.Node, error) { name := i.Name() var capmdls []sdm.CapabilityModel for _, cap := range i.Capabilities() { @@ -167,9 +161,6 @@ func (i *invocationCapabilityError) Build() (ipld.Node, error) { return ipld.WrapWithRecovery(&mdl, sdm.InvocationCapabilityErrorType()) } -var _ InvocationCapabilityError = (*invocationCapabilityError)(nil) -var _ ipld.Builder = (*invocationCapabilityError)(nil) - -func NewInvocationCapabilityError(capabilities []ucan.Capability[any]) *invocationCapabilityError { - return &invocationCapabilityError{capabilities} +func NewInvocationCapabilityError(capabilities []ucan.Capability[any]) InvocationCapabilityError { + return invocationCapabilityError{capabilities} } diff --git a/server/handler.go b/server/handler.go index 6ebbef0..821bee9 100644 --- a/server/handler.go +++ b/server/handler.go @@ -1,49 +1,43 @@ package server import ( - "fmt" - "github.com/storacha-network/go-ucanto/core/invocation" "github.com/storacha-network/go-ucanto/core/ipld" + "github.com/storacha-network/go-ucanto/core/receipt" "github.com/storacha-network/go-ucanto/core/result" - "github.com/storacha-network/go-ucanto/did" + "github.com/storacha-network/go-ucanto/core/result/failure" "github.com/storacha-network/go-ucanto/server/transaction" "github.com/storacha-network/go-ucanto/ucan" "github.com/storacha-network/go-ucanto/validator" ) -type HandlerFunc[C any, O, X ipld.Builder] func(capability ucan.Capability[C], invocation invocation.Invocation, context InvocationContext) (transaction.Transaction[O, X], error) +type HandlerFunc[C any, O ipld.Builder] func(capability ucan.Capability[C], invocation invocation.Invocation, context InvocationContext) (out O, fx receipt.Effects, err error) // Provide is used to define given capability provider. It decorates the passed // handler and takes care of UCAN validation. It only calls the handler // when validation succeeds. -func Provide[C any, O, X ipld.Builder](capability validator.CapabilityParser[C], handler HandlerFunc[C, O, X]) ServiceMethod[O, X] { - return func(invocation invocation.Invocation, context InvocationContext) (transaction.Transaction[O, X], error) { - canIssue := func(capability ucan.Capability[C], issuer did.DID) bool { - anycap := ucan.NewCapability(capability.Can(), capability.With(), any(capability.Nb())) - return context.CanIssue(anycap, issuer) - } - - validateAuthorization := func(auth validator.Authorization[C]) result.Failure { - anycap := ucan.NewCapability(auth.Capability().Can(), auth.Capability().With(), any(auth.Capability().Nb())) - anyauth := validator.NewAuthorization(anycap) - return context.ValidateAuthorization(anyauth) +func Provide[C any, O ipld.Builder](capability validator.CapabilityParser[C], handler HandlerFunc[C, O]) ServiceMethod[O] { + return func(invocation invocation.Invocation, context InvocationContext) (transaction.Transaction[O, ipld.Builder], error) { + vctx := validator.NewValidationContext( + context.ID().Verifier(), + capability, + context.CanIssue, + context.ValidateAuthorization, + context.ResolveProof, + context.ParsePrincipal, + context.ResolveDIDKey, + ) + + auth, aerr := validator.Access(invocation, vctx) + if aerr != nil { + return transaction.NewTransaction(result.Error[O, ipld.Builder](failure.FromError(aerr))), nil } - vctx := validator.NewValidationContext(capability, canIssue, validateAuthorization) - - authorization, err := validator.Access(invocation, vctx) - if err != nil { - return nil, err + o, fx, herr := handler(auth.Capability(), invocation, context) + if herr != nil { + return nil, herr } - return result.MatchResultR2(authorization, func(ok validator.Authorization[C]) (transaction.Transaction[O, X], error) { - return handler(ok.Capability(), invocation, context) - }, func(err result.Failure) (transaction.Transaction[O, X], error) { - if failure, ok := err.(X); ok { - return transaction.NewTransaction(result.Error[O](failure)), nil - } - return nil, fmt.Errorf("error was not an IPLD builder") - }) + return transaction.NewTransaction(result.Ok[O, ipld.Builder](o), transaction.WithEffects(fx)), nil } } diff --git a/server/options.go b/server/options.go index 59b85a0..86ddb30 100644 --- a/server/options.go +++ b/server/options.go @@ -14,29 +14,24 @@ type Option func(cfg *srvConfig) error type srvConfig struct { codec transport.InboundCodec - service map[string]ServiceMethod[ipld.Builder, ipld.Builder] + service Service validateAuthorization validator.RevocationCheckerFunc[any] canIssue validator.CanIssueFunc[any] + resolveProof validator.ProofResolverFunc + parsePrincipal validator.PrincipalParserFunc + resolveDIDKey validator.PrincipalResolverFunc catch ErrorHandlerFunc } -func WithServiceMethod[O, X ipld.Builder](can string, handleFunc ServiceMethod[O, X]) Option { +func WithServiceMethod[O ipld.Builder](can string, handleFunc ServiceMethod[O]) Option { return func(cfg *srvConfig) error { cfg.service[can] = func(input invocation.Invocation, context InvocationContext) (transaction.Transaction[ipld.Builder, ipld.Builder], error) { tx, err := handleFunc(input, context) if err != nil { return nil, err } - out := result.MapResultR0( - tx.Out(), - func(o O) ipld.Builder { return o }, - func(x X) ipld.Builder { return x }, - ) - var opts []transaction.Option - if tx.Fx() != nil { - opts = append(opts, transaction.WithForks(tx.Fx().Fork()), transaction.WithJoin(tx.Fx().Join())) - } - return transaction.NewTransaction(out, opts...), nil + out := result.MapOk(tx.Out(), func(o O) ipld.Builder { return o }) + return transaction.NewTransaction(out, transaction.WithEffects(tx.Fx())), nil } return nil } @@ -78,3 +73,31 @@ func WithCanIssue(fn validator.CanIssueFunc[any]) Option { return nil } } + +// WithProofResolver configures a function that finds delegations corresponding +// to a given link. If a resolver is not provided the validator may not be able +// to explore corresponding path within a proof chain. +func WithProofResolver(fn validator.ProofResolverFunc) Option { + return func(cfg *srvConfig) error { + cfg.resolveProof = fn + return nil + } +} + +// WithPrincipalParser configures a function that provides verifier instances +// that can validate UCANs issued by a given principal. +func WithPrincipalParser(fn validator.PrincipalParserFunc) Option { + return func(cfg *srvConfig) error { + cfg.parsePrincipal = fn + return nil + } +} + +// WithPrincipalResolver configures a function that resolves the key of a +// principal that is identified by DID different from did:key method. +func WithPrincipalResolver(fn validator.PrincipalResolverFunc) Option { + return func(cfg *srvConfig) error { + cfg.resolveDIDKey = fn + return nil + } +} diff --git a/server/server.go b/server/server.go index f982f33..38b73c4 100644 --- a/server/server.go +++ b/server/server.go @@ -8,6 +8,7 @@ import ( "sync" "github.com/storacha-network/go-ucanto/core/dag/blockstore" + "github.com/storacha-network/go-ucanto/core/delegation" "github.com/storacha-network/go-ucanto/core/invocation" "github.com/storacha-network/go-ucanto/core/invocation/ran" "github.com/storacha-network/go-ucanto/core/ipld" @@ -29,17 +30,19 @@ import ( type InvocationContext interface { validator.RevocationChecker[any] validator.CanIssuer[any] + validator.ProofResolver + validator.PrincipalParser + validator.PrincipalResolver // ID is the DID of the service the invocation was sent to. ID() principal.Signer - Principal() validator.PrincipalParser } // ServiceMethod is an invocation handler. -type ServiceMethod[O, X ipld.Builder] func(input invocation.Invocation, context InvocationContext) (transaction.Transaction[O, X], error) +type ServiceMethod[O ipld.Builder] func(input invocation.Invocation, context InvocationContext) (transaction.Transaction[O, ipld.Builder], error) // Service is a mapping of service names to handlers, used to define a // service implementation. -type Service = map[string]ServiceMethod[ipld.Builder, ipld.Builder] +type Service = map[ucan.Ability]ServiceMethod[ipld.Builder] type ServiceInvocation = invocation.IssuedInvocation @@ -95,48 +98,68 @@ func NewServer(id principal.Signer, options ...Option) (ServerView, error) { validateAuthorization := cfg.validateAuthorization if validateAuthorization == nil { - validateAuthorization = func(auth validator.Authorization[any]) result.Failure { + validateAuthorization = func(auth validator.Authorization[any]) validator.Revoked { return nil } } - ctx := &context{id: id, canIssue: canIssue, principal: &principalParser{}} - svr := &server{id: id, service: cfg.service, context: ctx, codec: codec, catch: catch} + resolveProof := cfg.resolveProof + if resolveProof == nil { + resolveProof = validator.ProofUnavailable + } + + parsePrincipal := cfg.parsePrincipal + if parsePrincipal == nil { + parsePrincipal = ParsePrincipal + } + + resolveDIDKey := cfg.resolveDIDKey + if resolveDIDKey == nil { + resolveDIDKey = validator.FailDIDKeyResolution + } + + ctx := context{id, canIssue, validateAuthorization, resolveProof, parsePrincipal, resolveDIDKey} + svr := &server{id, cfg.service, ctx, codec, catch} return svr, nil } -type principalParser struct{} - -func (p *principalParser) Parse(str string) (principal.Verifier, error) { +func ParsePrincipal(str string) (principal.Verifier, error) { + // TODO: Ed or RSA return verifier.Parse(str) } -var _ validator.PrincipalParser = (*principalParser)(nil) - type context struct { id principal.Signer canIssue validator.CanIssueFunc[any] - principal validator.PrincipalParser validateAuthorization validator.RevocationCheckerFunc[any] + resolveProof validator.ProofResolverFunc + parsePrincipal validator.PrincipalParserFunc + resolveDIDKey validator.PrincipalResolverFunc } -func (ctx *context) ID() principal.Signer { +func (ctx context) ID() principal.Signer { return ctx.id } -func (ctx *context) CanIssue(capability ucan.Capability[any], issuer did.DID) bool { +func (ctx context) CanIssue(capability ucan.Capability[any], issuer did.DID) bool { return ctx.canIssue(capability, issuer) } -func (ctx *context) Principal() validator.PrincipalParser { - return ctx.principal +func (ctx context) ValidateAuthorization(auth validator.Authorization[any]) validator.Revoked { + return ctx.validateAuthorization(auth) } -func (ctx *context) ValidateAuthorization(auth validator.Authorization[any]) result.Failure { - return ctx.validateAuthorization(auth) +func (ctx context) ResolveProof(proof ucan.Link) (delegation.Delegation, validator.UnavailableProof) { + return ctx.resolveProof(proof) } -var _ InvocationContext = (*context)(nil) +func (ctx context) ParsePrincipal(str string) (principal.Verifier, error) { + return ctx.parsePrincipal(str) +} + +func (ctx context) ResolveDIDKey(did did.DID) (did.DID, validator.UnresolvedDID) { + return ctx.resolveDIDKey(did) +} type server struct { id principal.Signer @@ -254,20 +277,20 @@ func Run(server Server, invocation ServiceInvocation) (receipt.AnyReceipt, error return receipt.Issue(server.ID(), result.NewFailure(err), ran.FromInvocation(invocation)) } - outcome, err := handle(invocation, server.Context()) + tx, err := handle(invocation, server.Context()) if err != nil { herr := NewHandlerExecutionError(err, cap) server.Catch(herr) return receipt.Issue(server.ID(), result.NewFailure(herr), ran.FromInvocation(invocation)) } - fx := outcome.Fx() + fx := tx.Fx() var opts []receipt.Option if fx != nil { opts = append(opts, receipt.WithJoin(fx.Join()), receipt.WithForks(fx.Fork())) } - rcpt, err := receipt.Issue(server.ID(), outcome.Out(), ran.FromInvocation(invocation), opts...) + rcpt, err := receipt.Issue(server.ID(), tx.Out(), ran.FromInvocation(invocation), opts...) if err != nil { herr := NewHandlerExecutionError(err, cap) server.Catch(herr) diff --git a/server/server_test.go b/server/server_test.go index 1c74285..9e973b9 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -8,18 +8,19 @@ import ( "github.com/ipfs/go-cid" ipldprime "github.com/ipld/go-ipld-prime" + "github.com/ipld/go-ipld-prime/datamodel" cidlink "github.com/ipld/go-ipld-prime/linking/cid" "github.com/ipld/go-ipld-prime/node/basicnode" ipldschema "github.com/ipld/go-ipld-prime/schema" "github.com/storacha-network/go-ucanto/client" + "github.com/storacha-network/go-ucanto/core/delegation" "github.com/storacha-network/go-ucanto/core/invocation" "github.com/storacha-network/go-ucanto/core/ipld" "github.com/storacha-network/go-ucanto/core/receipt" "github.com/storacha-network/go-ucanto/core/result" + fdm "github.com/storacha-network/go-ucanto/core/result/failure/datamodel" "github.com/storacha-network/go-ucanto/core/schema" - "github.com/storacha-network/go-ucanto/principal/ed25519/signer" - sdm "github.com/storacha-network/go-ucanto/server/datamodel" - "github.com/storacha-network/go-ucanto/server/transaction" + "github.com/storacha-network/go-ucanto/testing/fixtures" "github.com/storacha-network/go-ucanto/testing/helpers" "github.com/storacha-network/go-ucanto/transport/car/request" "github.com/storacha-network/go-ucanto/transport/car/response" @@ -36,9 +37,11 @@ type uploadAddCaveats struct { func (c uploadAddCaveats) Build() (ipld.Node, error) { np := basicnode.Prototype.Any nb := np.NewBuilder() - ma, _ := nb.BeginMap(2) - ma.AssembleKey().AssignString("root") - ma.AssembleValue().AssignLink(c.Root) + ma, _ := nb.BeginMap(1) + if c != (uploadAddCaveats{}) { + ma.AssembleKey().AssignString("root") + ma.AssembleValue().AssignLink(c.Root) + } ma.Finish() return nb.Build(), nil } @@ -69,177 +72,253 @@ func (ok uploadAddSuccess) Build() (ipld.Node, error) { return nb.Build(), nil } -func TestHandlerNotFound(t *testing.T) { - service := helpers.Must(signer.Generate()) - alice := helpers.Must(signer.Generate()) - space := helpers.Must(signer.Generate()) - - server := helpers.Must(NewServer(service)) - conn := helpers.Must(client.NewConnection(service, server)) - - rt := cidlink.Link{Cid: cid.MustParse("bafkreiem4twkqzsq2aj4shbycd4yvoj2cx72vezicletlhi7dijjciqpui")} - capability := ucan.NewCapability( - "upload/add", - space.DID().String(), - ipld.Builder(&uploadAddCaveats{Root: rt}), - ) - - invs := []invocation.Invocation{helpers.Must(invocation.Invoke(alice, service, capability))} - resp := helpers.Must(client.Execute(invs, conn)) - rcptlnk, ok := resp.Get(invs[0].Link()) - require.True(t, ok, "missing receipt for invocation: %s", invs[0].Link()) - - rcptsch := bytes.Join([][]byte{sdm.Schema(), []byte(` - type Result union { - | Any "ok" - | HandlerNotFoundError "error" - } representation keyed - `)}, []byte("\n")) - - reader := helpers.Must(receipt.NewReceiptReader[ipld.Node, sdm.HandlerNotFoundErrorModel](rcptsch)) - rcpt := helpers.Must(reader.Read(rcptlnk, resp.Blocks())) - - result.MatchResultR0(rcpt.Out(), func(ipld.Node) { - t.Fatalf("expected error: %s", invs[0].Link()) - }, func(rerr sdm.HandlerNotFoundErrorModel) { - fmt.Printf("%s %+v\n", *rerr.Name, rerr) - require.Equal(t, *rerr.Name, "HandlerNotFoundError") - }) +var rcptsch = []byte(` + type Result union { + | UploadAddSuccess "ok" + | Any "error" + } representation keyed + + type UploadAddSuccess struct { + root Link + status String + } +`) + +// asFailure binds the IPLD node to a FailureModel if possible. This works +// around IPLD requiring data to match the schema exactly +func asFailure(t testing.TB, n ipld.Node) fdm.FailureModel { + t.Helper() + require.Equal(t, n.Kind(), datamodel.Kind_Map) + f := fdm.FailureModel{} + + nn, err := n.LookupByString("name") + if err == nil { + name, err := nn.AsString() + require.NoError(t, err) + f.Name = &name + } + + mn, err := n.LookupByString("message") + require.NoError(t, err) + msg, err := mn.AsString() + require.NoError(t, err) + f.Message = msg + + sn, err := n.LookupByString("stack") + if err == nil { + stack, err := sn.AsString() + require.NoError(t, err) + f.Stack = &stack + } + + return f } -func TestSimpleHandler(t *testing.T) { - service := helpers.Must(signer.Generate()) - alice := helpers.Must(signer.Generate()) - space := helpers.Must(signer.Generate()) - - uploadadd := validator.NewCapability( - "upload/add", - schema.DIDString(), - schema.Struct[uploadAddCaveats](uploadAddCaveatsType()), - ) - - server := helpers.Must(NewServer( - service, - WithServiceMethod(uploadadd.Can(), Provide(uploadadd, func(cap ucan.Capability[uploadAddCaveats], inv invocation.Invocation, ctx InvocationContext) (transaction.Transaction[uploadAddSuccess, ipld.Builder], error) { - r := result.Ok[uploadAddSuccess, ipld.Builder](uploadAddSuccess{Root: cap.Nb().Root, Status: "done"}) - return transaction.NewTransaction(r), nil - })), - )) - - conn := helpers.Must(client.NewConnection(service, server)) - rt := cidlink.Link{Cid: cid.MustParse("bafkreiem4twkqzsq2aj4shbycd4yvoj2cx72vezicletlhi7dijjciqpui")} - cap := uploadadd.New(space.DID().String(), uploadAddCaveats{Root: rt}) - invs := []invocation.Invocation{helpers.Must(invocation.Invoke(alice, service, cap))} - resp := helpers.Must(client.Execute(invs, conn)) - - // get the receipt link for the invocation from the response - rcptlnk, ok := resp.Get(invs[0].Link()) - require.True(t, ok, "missing receipt for invocation: %s", invs[0].Link()) - - rcptsch := bytes.Join([][]byte{sdm.Schema(), []byte(` - type Result union { - | UploadAddSuccess "ok" - | Any "error" - } representation keyed - - type UploadAddSuccess struct { - root Link - status String - } - `)}, []byte("\n")) +func TestExecute(t *testing.T) { + t.Run("self-signed", func(t *testing.T) { + uploadadd := validator.NewCapability( + "upload/add", + schema.DIDString(), + schema.Struct[uploadAddCaveats](uploadAddCaveatsType(), nil), + nil, + ) + + server := helpers.Must(NewServer( + fixtures.Service, + WithServiceMethod( + uploadadd.Can(), + Provide(uploadadd, func(cap ucan.Capability[uploadAddCaveats], inv invocation.Invocation, ctx InvocationContext) (uploadAddSuccess, receipt.Effects, error) { + return uploadAddSuccess{Root: cap.Nb().Root, Status: "done"}, nil, nil + }), + ), + )) + + conn := helpers.Must(client.NewConnection(fixtures.Service, server)) + rt := cidlink.Link{Cid: cid.MustParse("bafkreiem4twkqzsq2aj4shbycd4yvoj2cx72vezicletlhi7dijjciqpui")} + cap := uploadadd.New(fixtures.Service.DID().String(), uploadAddCaveats{Root: rt}) + inv, err := invocation.Invoke(fixtures.Service, fixtures.Service, cap) + require.NoError(t, err) + + resp, err := client.Execute([]invocation.Invocation{inv}, conn) + require.NoError(t, err) + + // get the receipt link for the invocation from the response + rcptlnk, ok := resp.Get(inv.Link()) + require.True(t, ok, "missing receipt for invocation: %s", inv.Link()) + + reader := helpers.Must(receipt.NewReceiptReader[uploadAddSuccess, ipld.Node](rcptsch)) + rcpt := helpers.Must(reader.Read(rcptlnk, resp.Blocks())) + + result.MatchResultR0(rcpt.Out(), func(ok uploadAddSuccess) { + fmt.Printf("%+v\n", ok) + require.Equal(t, ok.Root, rt) + require.Equal(t, ok.Status, "done") + }, func(x ipld.Node) { + f := asFailure(t, x) + fmt.Println(f.Message) + fmt.Println(*f.Stack) + require.Nil(t, f) + }) + }) - reader := helpers.Must(receipt.NewReceiptReader[uploadAddSuccess, ipld.Node](rcptsch)) - rcpt := helpers.Must(reader.Read(rcptlnk, resp.Blocks())) + t.Run("delegated", func(t *testing.T) { + uploadadd := validator.NewCapability( + "upload/add", + schema.DIDString(), + schema.Struct[uploadAddCaveats](uploadAddCaveatsType(), nil), + nil, + ) + + server := helpers.Must(NewServer( + fixtures.Service, + WithServiceMethod( + uploadadd.Can(), + Provide(uploadadd, func(cap ucan.Capability[uploadAddCaveats], inv invocation.Invocation, ctx InvocationContext) (uploadAddSuccess, receipt.Effects, error) { + return uploadAddSuccess{Root: cap.Nb().Root, Status: "done"}, nil, nil + }), + ), + )) + + conn := helpers.Must(client.NewConnection(fixtures.Service, server)) + rt := cidlink.Link{Cid: cid.MustParse("bafkreiem4twkqzsq2aj4shbycd4yvoj2cx72vezicletlhi7dijjciqpui")} + cap := uploadadd.New(fixtures.Service.DID().String(), uploadAddCaveats{Root: rt}) + dgl, err := delegation.Delegate( + fixtures.Service, + fixtures.Alice, + []ucan.Capability[uploadAddCaveats]{ + ucan.NewCapability(uploadadd.Can(), fixtures.Service.DID().String(), uploadAddCaveats{}), + }, + ) + require.NoError(t, err) + + prfs := []delegation.Proof{delegation.FromDelegation(dgl)} + inv, err := invocation.Invoke(fixtures.Alice, fixtures.Service, cap, delegation.WithProofs(prfs)) + require.NoError(t, err) + + resp, err := client.Execute([]invocation.Invocation{inv}, conn) + require.NoError(t, err) + + // get the receipt link for the invocation from the response + rcptlnk, ok := resp.Get(inv.Link()) + require.True(t, ok, "missing receipt for invocation: %s", inv.Link()) + + reader := helpers.Must(receipt.NewReceiptReader[uploadAddSuccess, ipld.Node](rcptsch)) + rcpt := helpers.Must(reader.Read(rcptlnk, resp.Blocks())) + + result.MatchResultR0(rcpt.Out(), func(ok uploadAddSuccess) { + fmt.Printf("%+v\n", ok) + require.Equal(t, ok.Root, rt) + require.Equal(t, ok.Status, "done") + }, func(x ipld.Node) { + f := asFailure(t, x) + fmt.Println(f.Message) + fmt.Println(*f.Stack) + require.Nil(t, f) + }) + }) - result.MatchResultR0(rcpt.Out(), func(ok uploadAddSuccess) { - fmt.Printf("%+v\n", ok) - require.Equal(t, ok.Root, rt) - require.Equal(t, ok.Status, "done") - }, func(rerr ipld.Node) { - t.Fatalf("unexpected error: %+v", rerr) + t.Run("not found", func(t *testing.T) { + server := helpers.Must(NewServer(fixtures.Service)) + conn := helpers.Must(client.NewConnection(fixtures.Service, server)) + + rt := cidlink.Link{Cid: cid.MustParse("bafkreiem4twkqzsq2aj4shbycd4yvoj2cx72vezicletlhi7dijjciqpui")} + capability := ucan.NewCapability( + "upload/add", + fixtures.Alice.DID().String(), + uploadAddCaveats{Root: rt}, + ) + + invs := []invocation.Invocation{helpers.Must(invocation.Invoke(fixtures.Alice, fixtures.Service, capability))} + resp := helpers.Must(client.Execute(invs, conn)) + rcptlnk, ok := resp.Get(invs[0].Link()) + require.True(t, ok, "missing receipt for invocation: %s", invs[0].Link()) + + reader := helpers.Must(receipt.NewReceiptReader[uploadAddSuccess, ipld.Node](rcptsch)) + rcpt := helpers.Must(reader.Read(rcptlnk, resp.Blocks())) + + result.MatchResultR0(rcpt.Out(), func(uploadAddSuccess) { + t.Fatalf("expected error: %s", invs[0].Link()) + }, func(x ipld.Node) { + f := asFailure(t, x) + fmt.Printf("%s %+v\n", *f.Name, f) + require.Equal(t, *f.Name, "HandlerNotFoundError") + }) }) -} -func TestHandlerExecutionError(t *testing.T) { - service := helpers.Must(signer.Generate()) - alice := helpers.Must(signer.Generate()) - space := helpers.Must(signer.Generate()) - - uploadadd := validator.NewCapability( - "upload/add", - schema.DIDString(), - schema.Struct[uploadAddCaveats](uploadAddCaveatsType()), - ) - - server := helpers.Must(NewServer( - service, - WithServiceMethod(uploadadd.Can(), Provide(uploadadd, func(cap ucan.Capability[uploadAddCaveats], inv invocation.Invocation, ctx InvocationContext) (transaction.Transaction[uploadAddSuccess, ipld.Builder], error) { - return nil, fmt.Errorf("test error") - })), - )) - - conn := helpers.Must(client.NewConnection(service, server)) - rt := cidlink.Link{Cid: cid.MustParse("bafkreiem4twkqzsq2aj4shbycd4yvoj2cx72vezicletlhi7dijjciqpui")} - cap := uploadadd.New(space.DID().String(), uploadAddCaveats{Root: rt}) - invs := []invocation.Invocation{helpers.Must(invocation.Invoke(alice, service, cap))} - resp := helpers.Must(client.Execute(invs, conn)) - rcptlnk, ok := resp.Get(invs[0].Link()) - require.True(t, ok, "missing receipt for invocation: %s", invs[0].Link()) - - rcptsch := bytes.Join([][]byte{sdm.Schema(), []byte(` - type Result union { - | Any "ok" - | HandlerExecutionError "error" - } representation keyed - `)}, []byte("\n")) - - reader := helpers.Must(receipt.NewReceiptReader[ipld.Node, sdm.HandlerExecutionErrorModel](rcptsch)) - rcpt := helpers.Must(reader.Read(rcptlnk, resp.Blocks())) - - result.MatchResultR0(rcpt.Out(), func(ipld.Node) { - t.Fatalf("expected error: %s", invs[0].Link()) - }, func(rerr sdm.HandlerExecutionErrorModel) { - fmt.Printf("%s %+v\n", *rerr.Name, rerr) - require.Equal(t, *rerr.Name, "HandlerExecutionError") + t.Run("execution error", func(t *testing.T) { + uploadadd := validator.NewCapability( + "upload/add", + schema.DIDString(), + schema.Struct[uploadAddCaveats](uploadAddCaveatsType(), nil), + nil, + ) + + server := helpers.Must(NewServer( + fixtures.Service, + WithServiceMethod( + uploadadd.Can(), + Provide(uploadadd, func(cap ucan.Capability[uploadAddCaveats], inv invocation.Invocation, ctx InvocationContext) (uploadAddSuccess, receipt.Effects, error) { + return uploadAddSuccess{}, nil, fmt.Errorf("test error") + }), + ), + )) + + conn := helpers.Must(client.NewConnection(fixtures.Service, server)) + rt := cidlink.Link{Cid: cid.MustParse("bafkreiem4twkqzsq2aj4shbycd4yvoj2cx72vezicletlhi7dijjciqpui")} + cap := uploadadd.New(fixtures.Alice.DID().String(), uploadAddCaveats{Root: rt}) + invs := []invocation.Invocation{helpers.Must(invocation.Invoke(fixtures.Alice, fixtures.Service, cap))} + resp := helpers.Must(client.Execute(invs, conn)) + rcptlnk, ok := resp.Get(invs[0].Link()) + require.True(t, ok, "missing receipt for invocation: %s", invs[0].Link()) + + reader := helpers.Must(receipt.NewReceiptReader[uploadAddSuccess, ipld.Node](rcptsch)) + rcpt := helpers.Must(reader.Read(rcptlnk, resp.Blocks())) + + result.MatchResultR0(rcpt.Out(), func(uploadAddSuccess) { + t.Fatalf("expected error: %s", invs[0].Link()) + }, func(x ipld.Node) { + f := asFailure(t, x) + fmt.Printf("%s %+v\n", *f.Name, f) + require.Equal(t, *f.Name, "HandlerExecutionError") + }) }) } -func TestHandleContentTypeError(t *testing.T) { - service := helpers.Must(signer.Generate()) - server := helpers.Must(NewServer(service)) +func TestHandle(t *testing.T) { + t.Run("content type error", func(t *testing.T) { + server := helpers.Must(NewServer(fixtures.Service)) - hd := http.Header{} - hd.Set("Content-Type", "unsupported/media") - hd.Set("Accept", response.ContentType) + hd := http.Header{} + hd.Set("Content-Type", "unsupported/media") + hd.Set("Accept", response.ContentType) - req := thttp.NewHTTPRequest(bytes.NewReader([]byte{}), hd) - res := helpers.Must(Handle(server, req)) - require.Equal(t, res.Status(), http.StatusUnsupportedMediaType) -} + req := thttp.NewHTTPRequest(bytes.NewReader([]byte{}), hd) + res := helpers.Must(Handle(server, req)) + require.Equal(t, res.Status(), http.StatusUnsupportedMediaType) + }) -func TestHandleAcceptError(t *testing.T) { - service := helpers.Must(signer.Generate()) - server := helpers.Must(NewServer(service)) + t.Run("accept error", func(t *testing.T) { + server := helpers.Must(NewServer(fixtures.Service)) - hd := http.Header{} - hd.Set("Content-Type", request.ContentType) - hd.Set("Accept", "not/acceptable") + hd := http.Header{} + hd.Set("Content-Type", request.ContentType) + hd.Set("Accept", "not/acceptable") - req := thttp.NewHTTPRequest(bytes.NewReader([]byte{}), hd) - res := helpers.Must(Handle(server, req)) - require.Equal(t, res.Status(), http.StatusNotAcceptable) -} + req := thttp.NewHTTPRequest(bytes.NewReader([]byte{}), hd) + res := helpers.Must(Handle(server, req)) + require.Equal(t, res.Status(), http.StatusNotAcceptable) + }) -func TestHandleDecodeError(t *testing.T) { - service := helpers.Must(signer.Generate()) - server := helpers.Must(NewServer(service)) + t.Run("decode error", func(t *testing.T) { + server := helpers.Must(NewServer(fixtures.Service)) - hd := http.Header{} - hd.Set("Content-Type", request.ContentType) - hd.Set("Accept", request.ContentType) + hd := http.Header{} + hd.Set("Content-Type", request.ContentType) + hd.Set("Accept", request.ContentType) - // request with invalid payload - req := thttp.NewHTTPRequest(bytes.NewReader([]byte{}), hd) - res := helpers.Must(Handle(server, req)) - require.Equal(t, res.Status(), http.StatusBadRequest) + // request with invalid payload + req := thttp.NewHTTPRequest(bytes.NewReader([]byte{}), hd) + res := helpers.Must(Handle(server, req)) + require.Equal(t, res.Status(), http.StatusBadRequest) + }) } diff --git a/server/transaction/transaction.go b/server/transaction/transaction.go index 9eda9f8..4bb699d 100644 --- a/server/transaction/transaction.go +++ b/server/transaction/transaction.go @@ -8,7 +8,7 @@ import ( // Transaction defines a result & effect pair, used by provider that wishes to // return results that have effects. -type Transaction[O, X any] interface { +type Transaction[O any, X any] interface { Out() result.Result[O, X] Fx() receipt.Effects } @@ -18,50 +18,42 @@ type transaction[O, X any] struct { fx receipt.Effects } -func (t *transaction[O, X]) Out() result.Result[O, X] { +func (t transaction[O, X]) Out() result.Result[O, X] { return t.out } -func (t *transaction[O, X]) Fx() receipt.Effects { +func (t transaction[O, X]) Fx() receipt.Effects { return t.fx } -var _ Transaction[any, any] = (*transaction[any, any])(nil) - type effects struct { fork []ipld.Link join ipld.Link } -func (fx *effects) Fork() []ipld.Link { +func (fx effects) Fork() []ipld.Link { return fx.fork } -func (fx *effects) Join() ipld.Link { +func (fx effects) Join() ipld.Link { return fx.join } -var _ receipt.Effects = (*effects)(nil) +func NewEffects(fork []ipld.Link, join ipld.Link) receipt.Effects { + return effects{fork, join} +} // Option is an option configuring a transaction. type Option func(cfg *txConfig) type txConfig struct { - fork []ipld.Link - join ipld.Link -} - -// WithForks configures the forks for the receipt. -func WithForks(fork []ipld.Link) Option { - return func(cfg *txConfig) { - cfg.fork = fork - } + fx receipt.Effects } -// WithJoin configures the join for the receipt. -func WithJoin(join ipld.Link) Option { +// WithEffects configures the effects for the receipt. +func WithEffects(fx receipt.Effects) Option { return func(cfg *txConfig) { - cfg.join = join + cfg.fx = fx } } @@ -70,14 +62,5 @@ func NewTransaction[O, X any](result result.Result[O, X], options ...Option) Tra for _, opt := range options { opt(&cfg) } - - fx := effects{} - if len(cfg.fork) > 0 { - fx.fork = cfg.fork - } - if cfg.join != nil { - fx.join = cfg.join - } - - return &transaction[O, X]{out: result, fx: &fx} + return transaction[O, X]{out: result, fx: cfg.fx} } diff --git a/testing/fixtures/fixtures.go b/testing/fixtures/fixtures.go new file mode 100644 index 0000000..a4bd7ea --- /dev/null +++ b/testing/fixtures/fixtures.go @@ -0,0 +1,15 @@ +package fixtures + +import "github.com/storacha-network/go-ucanto/principal/ed25519/signer" + +// did:key:z6Mkk89bC3JrVqKie71YEcc5M1SMVxuCgNx6zLZ8SYJsxALi +var Alice, _ = signer.Parse("MgCZT5vOnYZoVAeyjnzuJIVY9J4LNtJ+f8Js0cTPuKUpFne0BVEDJjEu6quFIU8yp91/TY/+MYK8GvlKoTDnqOCovCVM=") + +// did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob +var Bob, _ = signer.Parse("MgCYbj5AJfVvdrjkjNCxB3iAUwx7RQHVQ7H1sKyHy46Iose0BEevXgL1V73PD9snOCIoONgb+yQ9sycYchQC8kygR4qY=") + +// did:key:z6MktafZTREjJkvV5mfJxcLpNBoVPwDLhTuMg9ng7dY4zMAL +var Mallory, _ = signer.Parse("MgCYtH0AvYxiQwBG6+ZXcwlXywq9tI50G2mCAUJbwrrahkO0B0elFYkl3Ulf3Q3A/EvcVY0utb4etiSE8e6pi4H0FEmU=") + +// did:key:z6MkrZ1r5XBFZjBU34qyD8fueMbMRkKw17BZaq2ivKFjnz2z +var Service, _ = signer.Parse("MgCYKXoHVy7Vk4/QjcEGi+MCqjntUiasxXJ8uJKY0qh11e+0Bs8WsdqGK7xothgrDzzWD0ME7ynPjz2okXDh8537lId8=") diff --git a/transport/http.go b/transport/http.go index 7addfcf..1cb4634 100644 --- a/transport/http.go +++ b/transport/http.go @@ -4,7 +4,7 @@ import ( "io" "net/http" - "github.com/storacha-network/go-ucanto/core/result" + "github.com/storacha-network/go-ucanto/core/result/failure" ) type HTTPRequest interface { @@ -19,7 +19,7 @@ type HTTPResponse interface { } type HTTPError interface { - result.Failure + failure.Failure Status() int Headers() http.Header } diff --git a/ucan/capability.go b/ucan/capability.go index a07e036..2d1e736 100644 --- a/ucan/capability.go +++ b/ucan/capability.go @@ -1,5 +1,15 @@ package ucan +import ( + "encoding/json" +) + +type jsonModel struct { + Can Ability `json:"can"` + With Resource `json:"with"` + Nb interface{} `json:"nb,omitempty"` +} + type capability[T any] struct { can Ability nb T @@ -20,6 +30,14 @@ func (c *capability[T]) With() Resource { return c.with } +func (c *capability[T]) MarshalJSON() ([]byte, error) { + return json.Marshal(jsonModel{ + Can: c.can, + With: c.with, + Nb: c.nb, + }) +} + func NewCapability[Caveats any](can Ability, with Resource, nb Caveats) Capability[Caveats] { return &capability[Caveats]{ can: can, diff --git a/ucan/caveats.go b/ucan/caveats.go new file mode 100644 index 0000000..9e93687 --- /dev/null +++ b/ucan/caveats.go @@ -0,0 +1,21 @@ +package ucan + +import ( + "github.com/ipld/go-ipld-prime/datamodel" + "github.com/ipld/go-ipld-prime/node/basicnode" +) + +// NoCaveats can be used when a capability has no additional domain specific +// details and/or restrictions. +type NoCaveats struct{} + +func (c NoCaveats) Build() (datamodel.Node, error) { + np := basicnode.Prototype.Any + nb := np.NewBuilder() + ma, err := nb.BeginMap(0) + if err != nil { + return nil, err + } + ma.Finish() + return nb.Build(), nil +} diff --git a/ucan/crypto/signature/signature.go b/ucan/crypto/signature/signature.go index b1d0909..78e7ba6 100644 --- a/ucan/crypto/signature/signature.go +++ b/ucan/crypto/signature/signature.go @@ -2,11 +2,71 @@ package signature import ( "bytes" + "fmt" "github.com/multiformats/go-varint" ) +const NON_STANDARD = 0xd000 +const ES256K = 0xd0e7 +const BLS12381G1 = 0xd0ea +const BLS12381G2 = 0xd0eb const EdDSA = 0xd0ed +const ES256 = 0xd01200 +const ES384 = 0xd01201 +const ES512 = 0xd01202 +const RS256 = 0xd01205 +const EIP191 = 0xd191 + +func CodeName(code uint64) (string, error) { + switch code { + case ES256K: + return "ES256K", nil + case BLS12381G1: + return "BLS12381G1", nil + case BLS12381G2: + return "BLS12381G2", nil + case EdDSA: + return "EdDSA", nil + case ES256: + return "ES256", nil + case ES384: + return "ES384", nil + case ES512: + return "ES512", nil + case RS256: + return "RS256", nil + case EIP191: + return "EIP191", nil + default: + return "", fmt.Errorf("unknown signature algorithm code 0x%x", code) + } +} + +func NameCode(name string) (uint64, error) { + switch name { + case "ES256K": + return ES256K, nil + case "BLS12381G1": + return BLS12381G1, nil + case "BLS12381G2": + return BLS12381G2, nil + case "EdDSA": + return EdDSA, nil + case "ES256": + return ES256, nil + case "ES384": + return ES384, nil + case "ES512": + return ES512, nil + case "RS256": + return RS256, nil + case "EIP191": + return EIP191, nil + default: + return NON_STANDARD, nil + } +} type Signature interface { Code() uint64 @@ -26,6 +86,18 @@ func NewSignature(code uint64, raw []byte) Signature { return sig } +func NewNonStandard(name string, raw []byte) Signature { + code := uint64(NON_STANDARD) + cl := varint.UvarintSize(code) + rl := varint.UvarintSize(uint64(len(raw))) + sig := make(signature, cl+rl+len(raw)+len(name)) + varint.PutUvarint(sig, code) + varint.PutUvarint(sig[cl:], uint64(len(raw))) + copy(sig[cl+rl:], raw) + copy(sig[cl+rl+len(raw):], name) + return sig +} + func Encode(s Signature) []byte { return s.Bytes() } diff --git a/ucan/crypto/signature/signature_test.go b/ucan/crypto/signature/signature_test.go new file mode 100644 index 0000000..61bb8d2 --- /dev/null +++ b/ucan/crypto/signature/signature_test.go @@ -0,0 +1,19 @@ +package signature + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestSignature(t *testing.T) { + t.Run("roundtrip", func(t *testing.T) { + raw, err := CodeName(EdDSA) + require.NoError(t, err) + + s := NewSignature(EdDSA, []byte(raw)) + d := Decode(Encode(s)) + require.Equal(t, EdDSA, int(d.Code())) + require.Equal(t, raw, string(d.Raw())) + }) +} diff --git a/ucan/datamodel/payload/payload.go b/ucan/datamodel/payload/payload.go index 2cde3ce..1dc3806 100644 --- a/ucan/datamodel/payload/payload.go +++ b/ucan/datamodel/payload/payload.go @@ -38,8 +38,8 @@ type PayloadModel struct { Aud string Att []udm.CapabilityModel Prf []string - Exp uint64 + Exp *int Fct []udm.FactModel Nnc *string - Nbf *uint64 + Nbf *int } diff --git a/ucan/datamodel/payload/payload.ipldsch b/ucan/datamodel/payload/payload.ipldsch index 220953d..30ea23c 100644 --- a/ucan/datamodel/payload/payload.ipldsch +++ b/ucan/datamodel/payload/payload.ipldsch @@ -6,7 +6,7 @@ type Payload struct { att [Capability] # String representation of a link to a UCAN. prf [String] (implied []) - exp Int + exp nullable Int fct optional [Fact] nnc optional String nbf optional Int diff --git a/ucan/datamodel/ucan/ucan.go b/ucan/datamodel/ucan/ucan.go index 310a05f..7c009fc 100644 --- a/ucan/datamodel/ucan/ucan.go +++ b/ucan/datamodel/ucan/ucan.go @@ -40,10 +40,10 @@ type UCANModel struct { S []byte Att []CapabilityModel Prf []ipld.Link - Exp uint64 + Exp *int Fct []FactModel Nnc *string - Nbf *uint64 + Nbf *int } type CapabilityModel struct { diff --git a/ucan/datamodel/ucan/ucan.ipldsch b/ucan/datamodel/ucan/ucan.ipldsch index 992dea2..b66deeb 100644 --- a/ucan/datamodel/ucan/ucan.ipldsch +++ b/ucan/datamodel/ucan/ucan.ipldsch @@ -11,7 +11,7 @@ type UCAN struct { # All proofs are links, however you could still inline proof # by using CID with identity hashing algorithm prf optional [&UCAN] - exp Int + exp nullable Int fct optional [Fact] nnc optional String diff --git a/ucan/formatter/formatter.go b/ucan/formatter/formatter.go index fc5a499..b90d722 100644 --- a/ucan/formatter/formatter.go +++ b/ucan/formatter/formatter.go @@ -11,8 +11,8 @@ import ( pdm "github.com/storacha-network/go-ucanto/ucan/datamodel/payload" ) -func FormatSignPayload(header *hdm.HeaderModel, payload *pdm.PayloadModel) (string, error) { - hdr, err := FormatHeader(header) +func FormatSignPayload(payload pdm.PayloadModel, version string, algorithm string) (string, error) { + hdr, err := FormatHeader(version, algorithm) if err != nil { return "", fmt.Errorf("formatting header: %s", hdr) } @@ -23,16 +23,21 @@ func FormatSignPayload(header *hdm.HeaderModel, payload *pdm.PayloadModel) (stri return fmt.Sprintf("%s.%s", hdr, pld), nil } -func FormatHeader(header *hdm.HeaderModel) (string, error) { - bytes, err := ipld.Marshal(dagjson.Encode, header, hdm.Type()) +func FormatHeader(version string, algorithm string) (string, error) { + header := hdm.HeaderModel{ + Alg: algorithm, + Ucv: version, + Typ: "JWT", + } + bytes, err := ipld.Marshal(dagjson.Encode, &header, hdm.Type()) if err != nil { return "", fmt.Errorf("dag-json encoding header: %s", err) } return base64.RawURLEncoding.EncodeToString(bytes), nil } -func FormatPayload(payload *pdm.PayloadModel) (string, error) { - bytes, err := ipld.Marshal(dagjson.Encode, payload, pdm.Type()) +func FormatPayload(payload pdm.PayloadModel) (string, error) { + bytes, err := ipld.Marshal(dagjson.Encode, &payload, pdm.Type()) if err != nil { return "", fmt.Errorf("dag-json encoding payload: %s", err) } diff --git a/ucan/lib.go b/ucan/lib.go index 37e5be0..e661bc5 100644 --- a/ucan/lib.go +++ b/ucan/lib.go @@ -5,8 +5,7 @@ import ( "time" "github.com/ipld/go-ipld-prime/datamodel" - "github.com/storacha-network/go-ucanto/core/ipld" - hdm "github.com/storacha-network/go-ucanto/ucan/datamodel/header" + "github.com/storacha-network/go-ucanto/ucan/crypto/signature" pdm "github.com/storacha-network/go-ucanto/ucan/datamodel/payload" udm "github.com/storacha-network/go-ucanto/ucan/datamodel/ucan" "github.com/storacha-network/go-ucanto/ucan/formatter" @@ -18,25 +17,38 @@ const version = "0.9.1" type Option func(cfg *ucanConfig) error type ucanConfig struct { - exp uint64 - nbf uint64 - nnc string - fct []FactBuilder - prf []Link + exp *UTCUnixTimestamp + noexp bool + nbf UTCUnixTimestamp + nnc string + fct []FactBuilder + prf []Link } // WithExpiration configures the expiration time in UTC seconds since Unix // epoch. -func WithExpiration(exp uint64) Option { +func WithExpiration(exp UTCUnixTimestamp) Option { return func(cfg *ucanConfig) error { - cfg.exp = exp + cfg.exp = &exp + cfg.noexp = false + return nil + } +} + +// WithNoExpiration configures the UCAN to never expire. +// +// WARNING: this will cause the delegation to be valid FOREVER, unless revoked. +func WithNoExpiration() Option { + return func(cfg *ucanConfig) error { + cfg.exp = nil + cfg.noexp = true return nil } } // WithNotBefore configures the time in UTC seconds since Unix epoch when the // UCAN will become valid. -func WithNotBefore(nbf uint64) Option { +func WithNotBefore(nbf int) Option { return func(cfg *ucanConfig) error { cfg.nbf = nbf return nil @@ -74,9 +86,14 @@ type MapBuilder interface { type FactBuilder = MapBuilder +// CaveatBuilder builds a datamodel.Node from the underlying data. +type CaveatBuilder interface { + Build() (datamodel.Node, error) +} + // Issue creates a new signed token with a given issuer. If expiration is // not set it defaults to 30 seconds from now. -func Issue(issuer Signer, audience Principal, capabilities []Capability[ipld.Builder], options ...Option) (UCANView, error) { +func Issue[C CaveatBuilder](issuer Signer, audience Principal, capabilities []Capability[C], options ...Option) (View, error) { cfg := ucanConfig{} for _, opt := range options { if err := opt(&cfg); err != nil { @@ -84,8 +101,14 @@ func Issue(issuer Signer, audience Principal, capabilities []Capability[ipld.Bui } } - if cfg.exp == 0 { - cfg.exp = Now() + 30 + var exp *int + if !cfg.noexp { + if cfg.exp == nil { + in30s := int(Now() + 30) + exp = &in30s + } else { + exp = cfg.exp + } } var capsmdl []udm.CapabilityModel @@ -123,17 +146,12 @@ func Issue(issuer Signer, audience Principal, capabilities []Capability[ipld.Bui }) } - header := hdm.HeaderModel{ - Alg: issuer.SignatureAlgorithm(), - Ucv: version, - Typ: "JWT", - } payload := pdm.PayloadModel{ Iss: issuer.DID().String(), Aud: audience.DID().String(), Att: capsmdl, Prf: prfstrs, - Exp: cfg.exp, + Exp: exp, Fct: fctsmdl, } if cfg.nnc != "" { @@ -142,7 +160,7 @@ func Issue(issuer Signer, audience Principal, capabilities []Capability[ipld.Bui if cfg.nbf != 0 { payload.Nbf = &cfg.nbf } - bytes, err := encodeSignaturePayload(&header, &payload) + bytes, err := encodeSignaturePayload(payload, version, issuer.SignatureAlgorithm()) if err != nil { return nil, fmt.Errorf("encoding signature payload: %s", err) } @@ -154,7 +172,7 @@ func Issue(issuer Signer, audience Principal, capabilities []Capability[ipld.Bui Aud: audience.DID().Bytes(), Att: capsmdl, Prf: cfg.prf, - Exp: cfg.exp, + Exp: exp, Fct: fctsmdl, } if cfg.nnc != "" { @@ -163,30 +181,62 @@ func Issue(issuer Signer, audience Principal, capabilities []Capability[ipld.Bui if cfg.nbf != 0 { model.Nbf = &cfg.nbf } - return NewUCANView(&model) + return NewUCAN(&model) } -func encodeSignaturePayload(header *hdm.HeaderModel, payload *pdm.PayloadModel) ([]byte, error) { - str, err := formatter.FormatSignPayload(header, payload) +func encodeSignaturePayload(payload pdm.PayloadModel, version string, algorithm string) ([]byte, error) { + str, err := formatter.FormatSignPayload(payload, version, algorithm) if err != nil { return nil, err } return []byte(str), nil } +func VerifySignature(ucan View, verifier Verifier) (bool, error) { + alg, err := signature.CodeName(ucan.Signature().Code()) + if err != nil { + return false, err + } + + var prfstrs []string + for _, link := range ucan.Proofs() { + prfstrs = append(prfstrs, link.String()) + } + + payload := pdm.PayloadModel{ + Iss: ucan.Issuer().DID().String(), + Aud: ucan.Audience().DID().String(), + Att: ucan.Model().Att, + Prf: prfstrs, + Exp: ucan.Expiration(), + Fct: ucan.Model().Fct, + } + + msg, err := encodeSignaturePayload(payload, ucan.Version(), alg) + if err != nil { + return false, err + } + + return ucan.Issuer().DID() == verifier.DID() && verifier.Verify(msg, ucan.Signature()), nil +} + // IsExpired checks if a UCAN is expired. -func IsExpired(ucan UCANView) bool { - return ucan.Expiration() <= Now() +func IsExpired(ucan UCAN) bool { + exp := ucan.Expiration() + if exp == nil { + return false + } + return *exp <= Now() } // IsTooEarly checks if a UCAN is not active yet. -func IsTooEarly(ucan UCANView) bool { +func IsTooEarly(ucan UCAN) bool { nbf := ucan.NotBefore() return nbf != 0 && Now() <= nbf } // Now returns a UTC Unix timestamp for comparing it against time window of the // UCAN. -func Now() uint64 { - return uint64(time.Now().Unix()) +func Now() UTCUnixTimestamp { + return UTCUnixTimestamp(time.Now().Unix()) } diff --git a/ucan/ucan.go b/ucan/ucan.go index 5ef1de4..6f567a3 100644 --- a/ucan/ucan.go +++ b/ucan/ucan.go @@ -1,6 +1,8 @@ package ucan import ( + "encoding/json" + "github.com/ipld/go-ipld-prime" "github.com/storacha-network/go-ucanto/did" "github.com/storacha-network/go-ucanto/ucan/crypto" @@ -15,11 +17,17 @@ type Resource = string // It MUST have format `${string}/${string}` | "*" type Ability = string +// UnknownCapability is a capability whose Nb type is unknown +type UnknownCapability interface { + json.Marshaler + Can() Ability + With() Resource +} + // Capability represents an ability that a UCAN holder can perform with some // resource. type Capability[Caveats any] interface { - Can() Ability - With() Resource + UnknownCapability Nb() Caveats } @@ -35,8 +43,8 @@ type Link = ipld.Link // It MUST have format `${number}.${number}.${number}` type Version = string -// UTCUnixTimestamp is a timestamp in seconds since the Unix epoch. -type UTCUnixTimestamp = uint64 +// UTCUnixTimestamp is a timestamp in milliseconds since the Unix epoch. +type UTCUnixTimestamp = int // https://github.com/ucan-wg/spec/#324-nonce type Nonce = string diff --git a/ucan/view.go b/ucan/view.go index e8868fd..44b4656 100644 --- a/ucan/view.go +++ b/ucan/view.go @@ -8,11 +8,7 @@ import ( udm "github.com/storacha-network/go-ucanto/ucan/datamodel/ucan" ) -// UCANView represents a decoded "view" of a UCAN that can be used in your -// domain logic, etc. -type UCANView interface { - // Model references the underlying IPLD datamodel instance. - Model() *udm.UCANModel +type UCAN interface { // Issuer is the signer of the UCAN. Issuer() Principal // Audience is the principal delegated to. @@ -23,7 +19,7 @@ type UCANView interface { Capabilities() []Capability[any] // Expiration is the time in seconds since the Unix epoch that the UCAN // becomes invalid. - Expiration() UTCUnixTimestamp + Expiration() *UTCUnixTimestamp // NotBefore is the time in seconds since the Unix epoch that the UCAN // becomes valid. NotBefore() UTCUnixTimestamp @@ -37,11 +33,19 @@ type UCANView interface { Signature() signature.SignatureView } +// View represents a decoded "view" of a UCAN that can be used in your +// domain logic, etc. +type View interface { + UCAN + // Model references the underlying IPLD datamodel instance. + Model() *udm.UCANModel +} + type ucanView struct { model *udm.UCANModel } -var _ UCANView = (*ucanView)(nil) +var _ View = (*ucanView)(nil) func (v *ucanView) Audience() Principal { did, err := did.Decode(v.model.Aud) @@ -59,7 +63,7 @@ func (v *ucanView) Capabilities() []Capability[any] { return caps } -func (v *ucanView) Expiration() uint64 { +func (v *ucanView) Expiration() *UTCUnixTimestamp { return v.model.Exp } @@ -94,7 +98,7 @@ func (v *ucanView) Nonce() string { return *v.model.Nnc } -func (v *ucanView) NotBefore() uint64 { +func (v *ucanView) NotBefore() int { if v.model.Nbf == nil { return 0 } @@ -114,7 +118,7 @@ func (v *ucanView) Version() string { return v.model.V } -// NewUCANView creates a UCAN view from the underlying data model. Please note +// NewUCAN creates a UCAN view from the underlying data model. Please note // that this function does no verification of the model and it is callers // responsibility to ensure that: // @@ -124,6 +128,6 @@ func (v *ucanView) Version() string { // // In other words you should never use this function unless you've parsed or // decoded a valid UCAN and want to wrap it into a view. -func NewUCANView(model *udm.UCANModel) (UCANView, error) { +func NewUCAN(model *udm.UCANModel) (View, error) { return &ucanView{model}, nil } diff --git a/validator/authorization.go b/validator/authorization.go index 3f4f0f4..d622bcc 100644 --- a/validator/authorization.go +++ b/validator/authorization.go @@ -2,106 +2,76 @@ package validator import ( "github.com/storacha-network/go-ucanto/core/delegation" - "github.com/storacha-network/go-ucanto/core/invocation" - "github.com/storacha-network/go-ucanto/core/result" - "github.com/storacha-network/go-ucanto/did" - "github.com/storacha-network/go-ucanto/principal" "github.com/storacha-network/go-ucanto/ucan" ) -// PrincipalParser provides verifier instances that can validate UCANs issued -// by a given principal. -type PrincipalParser interface { - Parse(str string) (principal.Verifier, error) -} - -type CanIssuer[Caveats any] interface { - // CanIssue informs validator whether given capability can be issued by a - // given DID or whether it needs to be delegated to the issuer. - CanIssue(capability ucan.Capability[Caveats], issuer did.DID) bool -} - -// CanIssue informs validator whether given capability can be issued by a -// given DID or whether it needs to be delegated to the issuer. -type CanIssueFunc[Caveats any] func(capability ucan.Capability[Caveats], issuer did.DID) bool - -type RevocationChecker[Caveats any] interface { - // ValidateAuthorization validates that the passed authorization has not been - // revoked. - ValidateAuthorization(auth Authorization[Caveats]) result.Failure -} - -// RevocationCheckerFunc validates the passed authorization and returns -// a result indicating validity. -type RevocationCheckerFunc[Caveats any] func(auth Authorization[Caveats]) result.Failure - -type ValidationContext[Caveats any] interface { - RevocationChecker[Caveats] - CanIssuer[Caveats] - Capability() CapabilityParser[Caveats] -} - type Authorization[Caveats any] interface { + Audience() ucan.Principal Capability() ucan.Capability[Caveats] + Delegation() delegation.Delegation + Issuer() ucan.Principal + Proofs() []Authorization[Caveats] } type authorization[Caveats any] struct { - capability ucan.Capability[Caveats] + match Match[Caveats] + proofs []Authorization[Caveats] } -func (a *authorization[Caveats]) Capability() ucan.Capability[Caveats] { - return a.capability +func (a authorization[Caveats]) Audience() ucan.Principal { + return a.Delegation().Audience() } -func NewAuthorization[Caveats any](capability ucan.Capability[Caveats]) Authorization[Caveats] { - return &authorization[Caveats]{capability: capability} +func (a authorization[Caveats]) Capability() ucan.Capability[Caveats] { + return a.match.Value() } -type source struct { - capability ucan.Capability[any] +func (a authorization[Caveats]) Delegation() delegation.Delegation { + return a.match.Source()[0].Delegation() } -func (s *source) Capability() ucan.Capability[any] { - return s.capability +func (a authorization[Caveats]) Issuer() ucan.Principal { + return a.Delegation().Issuer() } -func (s *source) Delegation() delegation.Delegation { - return nil +func (a authorization[Caveats]) Proofs() []Authorization[Caveats] { + return a.proofs } -type validationContext[Caveats any] struct { - capability CapabilityParser[Caveats] - canIssue CanIssueFunc[Caveats] - validateAuthorization RevocationCheckerFunc[Caveats] +func NewAuthorization[Caveats any](match Match[Caveats], proofs []Authorization[Caveats]) Authorization[Caveats] { + return authorization[Caveats]{match, proofs} } -func (vc *validationContext[Caveats]) CanIssue(capability ucan.Capability[Caveats], issuer did.DID) bool { - return vc.canIssue(capability, issuer) +type unknownauth[C any] struct { + auth Authorization[C] } -func (vc *validationContext[Caveats]) ValidateAuthorization(auth Authorization[Caveats]) result.Failure { - return vc.validateAuthorization(auth) +func (a unknownauth[C]) Audience() ucan.Principal { + return a.auth.Audience() } -func (vc *validationContext[Caveats]) Capability() CapabilityParser[Caveats] { - return vc.capability +func (a unknownauth[C]) Capability() ucan.Capability[any] { + cap := a.auth.Capability() + return ucan.NewCapability[any](cap.Can(), cap.With(), cap.Nb()) } -var _ ValidationContext[any] = (*validationContext[any])(nil) - -func NewValidationContext[Caveats any](capability CapabilityParser[Caveats], canIssue CanIssueFunc[Caveats], validateAuthorization RevocationCheckerFunc[Caveats]) ValidationContext[Caveats] { - vc := validationContext[Caveats]{capability, canIssue, validateAuthorization} - return &vc +func (a unknownauth[C]) Delegation() delegation.Delegation { + return a.auth.Delegation() } -func Access[Caveats any](invocation invocation.Invocation, context ValidationContext[Caveats]) (result.Result[Authorization[Caveats], result.Failure], error) { - cap := invocation.Capabilities()[0] - src := source{capability: cap} +func (a unknownauth[C]) Issuer() ucan.Principal { + return a.Delegation().Issuer() +} - // TODO: parser.Select() - match := context.Capability().Match(&src) +func (a unknownauth[C]) Proofs() []Authorization[any] { + var prf []Authorization[any] + for _, p := range a.auth.Proofs() { + prf = append(prf, ConvertUnknownAuthorization(p)) + } + return prf +} - return result.MapOk(match, func(o ucan.Capability[Caveats]) Authorization[Caveats] { - return &authorization[Caveats]{capability: o} - }), nil +// ConvertUnknownAuthorization converts an Authorization[Caveats] to Authorization[any] +func ConvertUnknownAuthorization[Caveats any](auth Authorization[Caveats]) Authorization[any] { + return unknownauth[Caveats]{auth} } diff --git a/validator/capability.go b/validator/capability.go index 54d2819..a33b034 100644 --- a/validator/capability.go +++ b/validator/capability.go @@ -1,8 +1,12 @@ package validator import ( + "fmt" + "strings" + "github.com/storacha-network/go-ucanto/core/delegation" - "github.com/storacha-network/go-ucanto/core/result" + "github.com/storacha-network/go-ucanto/core/invocation" + "github.com/storacha-network/go-ucanto/core/result/failure" "github.com/storacha-network/go-ucanto/core/schema" "github.com/storacha-network/go-ucanto/ucan" ) @@ -12,66 +16,326 @@ type Source interface { Delegation() delegation.Delegation } +type source struct { + capability ucan.Capability[any] + delegation delegation.Delegation +} + +func (s source) Capability() ucan.Capability[any] { + return s.capability +} + +func (s source) Delegation() delegation.Delegation { + return s.delegation +} + +func NewSource(capability ucan.Capability[any], delegation delegation.Delegation) Source { + return source{capability, delegation} +} + +type Matcher[Caveats any] interface { + Match(source Source) (Match[Caveats], InvalidCapability) +} + +type Selector[Caveats any] interface { + Select(sources []Source) ([]Match[Caveats], []DelegationError, []ucan.Capability[any]) +} + +type Match[Caveats any] interface { + Selector[Caveats] + Source() []Source + Value() ucan.Capability[Caveats] + Proofs() []delegation.Delegation + Prune(context CanIssuer[Caveats]) Match[Caveats] +} + +type match[Caveats any] struct { + sources []Source + value ucan.Capability[Caveats] + descriptor Descriptor[Caveats] +} + +func (m match[Caveats]) Proofs() []delegation.Delegation { + return []delegation.Delegation{m.sources[0].Delegation()} +} + +func (m match[Caveats]) Prune(context CanIssuer[Caveats]) Match[Caveats] { + if context.CanIssue(m.value, m.sources[0].Delegation().Issuer().DID()) { + return nil + } + return m +} + +func (m match[Caveats]) Source() []Source { + return m.sources +} + +func (m match[Caveats]) Value() ucan.Capability[Caveats] { + return m.value +} + +func (m match[Caveats]) Select(sources []Source) (matches []Match[Caveats], errors []DelegationError, unknowns []ucan.Capability[any]) { + for _, source := range sources { + cap, err := ResolveCapability(m.descriptor, m.value, source) + if err != nil { + if uerr, ok := err.(UnknownCapability); ok { + unknowns = append(unknowns, uerr.Capability()) + } else if merr, ok := err.(MalformedCapability); ok { + errors = append(errors, NewDelegationError([]DelegationSubError{merr}, m)) + } else { + panic(fmt.Errorf("unexpected error type in resolved capability result: %w", err)) + } + continue + } + + derr := m.descriptor.Derives(m.value, cap) + if derr != nil { + errors = append(errors, NewDelegationError([]DelegationSubError{NewEscalatedCapabilityError(m.value, cap, derr)}, m)) + continue + } + + matches = append(matches, NewMatch(source, cap, m.descriptor)) + } + return +} + +func (m match[Caveats]) String() string { + s, _ := m.value.MarshalJSON() + return string(s) +} + +func NewMatch[Caveats any](source Source, capability ucan.Capability[Caveats], descriptor Descriptor[Caveats]) Match[Caveats] { + return match[Caveats]{[]Source{source}, capability, descriptor} +} + type CapabilityParser[Caveats any] interface { + Matcher[Caveats] + Selector[Caveats] Can() ucan.Ability // New creates a new capability from the passed options. New(with ucan.Resource, nb Caveats) ucan.Capability[Caveats] - Match(source Source) result.Result[ucan.Capability[Caveats], result.Failure] + // Delegate creates a new signed token for this capability. If expiration is + // not set it defaults to 30 seconds from now. + Delegate(issuer ucan.Signer, audience ucan.Principal, with ucan.Resource, nb Caveats, options ...delegation.Option) (delegation.Delegation, error) + // Invoke creates an invocation of this capability. + Invoke(issuer ucan.Signer, audience ucan.Principal, with ucan.Resource, nb Caveats, options ...delegation.Option) (invocation.IssuedInvocation, error) } -type Descriptor[I, O any] interface { +type Derivable[Caveats any] interface { + // Derives determines if a capability is derivable from another. Return `nil` + // to indicate the delegated capability can be derived from the claimed + // capability. + Derives(claimed, delegated ucan.Capability[Caveats]) failure.Failure +} + +// DerivesFunc determines if a capability is derivable from another. Return +// `nil` to indicate the delegated capability can be derived from the claimed +// capability. +type DerivesFunc[Caveats any] func(claimed, delegated ucan.Capability[Caveats]) failure.Failure + +type Descriptor[Caveats any] interface { + Derivable[Caveats] Can() ucan.Ability With() schema.Reader[string, ucan.Resource] - Nb() schema.Reader[I, O] + Nb() schema.Reader[any, Caveats] } type descriptor[Caveats any] struct { - can ucan.Ability - with schema.Reader[string, ucan.Resource] - nb schema.Reader[any, Caveats] + can ucan.Ability + with schema.Reader[string, ucan.Resource] + nb schema.Reader[any, Caveats] + derives DerivesFunc[Caveats] } -func (d *descriptor[C]) Can() ucan.Ability { +func (d descriptor[C]) Can() ucan.Ability { return d.can } -func (d *descriptor[C]) With() schema.Reader[string, ucan.Resource] { +func (d descriptor[C]) With() schema.Reader[string, ucan.Resource] { return d.with } -func (d *descriptor[C]) Nb() schema.Reader[any, C] { +func (d descriptor[C]) Nb() schema.Reader[any, C] { return d.nb } +func (d descriptor[C]) Derives(parent, child ucan.Capability[C]) failure.Failure { + return d.derives(parent, child) +} + type capability[Caveats any] struct { - descriptor Descriptor[any, Caveats] + descriptor Descriptor[Caveats] } -func (c *capability[C]) Can() ucan.Ability { +func (c capability[Caveats]) Can() ucan.Ability { return c.descriptor.Can() } -func (c *capability[Caveats]) Match(source Source) result.Result[ucan.Capability[Caveats], result.Failure] { - return parseCapability(c.descriptor, source) +func (c capability[Caveats]) Select(capabilities []Source) ([]Match[Caveats], []DelegationError, []ucan.Capability[any]) { + return Select(c, capabilities) +} + +func (c capability[Caveats]) Match(source Source) (Match[Caveats], InvalidCapability) { + cap, err := ParseCapability(c.descriptor, source) + if err != nil { + return nil, err + } + return NewMatch(source, cap, c.descriptor), nil +} + +func (c capability[Caveats]) String() string { + return fmt.Sprintf(`{can:"%s"}`, c.Can()) } -func (c *capability[Caveats]) New(with ucan.Resource, nb Caveats) ucan.Capability[Caveats] { +func (c capability[Caveats]) New(with ucan.Resource, nb Caveats) ucan.Capability[Caveats] { return ucan.NewCapability(c.descriptor.Can(), with, nb) } -func NewCapability[Caveats any](can ucan.Ability, with schema.Reader[string, ucan.Resource], nb schema.Reader[any, Caveats]) CapabilityParser[Caveats] { - d := descriptor[Caveats]{can: can, with: with, nb: nb} - return &capability[Caveats]{descriptor: &d} +func (c capability[Caveats]) Delegate(issuer ucan.Signer, audience ucan.Principal, with ucan.Resource, nb Caveats, options ...delegation.Option) (delegation.Delegation, error) { + if bc, ok := any(nb).(ucan.CaveatBuilder); ok { + caps := []ucan.Capability[ucan.CaveatBuilder]{ucan.NewCapability(c.Can(), with, bc)} + return delegation.Delegate(issuer, audience, caps, options...) + } + return nil, fmt.Errorf("not an IPLD builder: %v", nb) } -func parseCapability[O any](descriptor Descriptor[any, O], source Source) result.Result[ucan.Capability[O], result.Failure] { +func (c capability[Caveats]) Invoke(issuer ucan.Signer, audience ucan.Principal, with ucan.Resource, nb Caveats, options ...delegation.Option) (invocation.IssuedInvocation, error) { + if bc, ok := any(nb).(ucan.CaveatBuilder); ok { + cap := ucan.NewCapability(c.Can(), with, bc) + return invocation.Invoke(issuer, audience, cap, options...) + } + return nil, fmt.Errorf("not an IPLD builder: %v", nb) +} + +func NewCapability[Caveats any]( + can ucan.Ability, + with schema.Reader[string, ucan.Resource], + nb schema.Reader[any, Caveats], + derives DerivesFunc[Caveats], +) CapabilityParser[Caveats] { + if derives == nil { + derives = DefaultDerives + } + d := descriptor[Caveats]{can, with, nb, derives} + return &capability[Caveats]{descriptor: d} +} + +func ParseCapability[O any](descriptor Descriptor[O], source Source) (ucan.Capability[O], InvalidCapability) { cap := source.Capability() - return result.MatchResultR1(descriptor.With().Read(cap.With()), func(with ucan.Resource) result.Result[ucan.Capability[O], result.Failure] { - return result.MapOk(descriptor.Nb().Read(cap.Nb()), func(nb O) ucan.Capability[O] { - pcap := ucan.NewCapability(cap.Can(), with, nb) - return pcap - }) - }, func(x result.Failure) result.Result[ucan.Capability[O], result.Failure] { - return result.Error[ucan.Capability[O]](x) - }) + + if descriptor.Can() != cap.Can() { + return nil, NewUnknownCapabilityError(cap) + } + + uri, err := descriptor.With().Read(cap.With()) + if err != nil { + return nil, NewMalformedCapabilityError(cap, err) + } + + nb, err := descriptor.Nb().Read(cap.Nb()) + if err != nil { + return nil, NewMalformedCapabilityError(cap, err) + } + + return ucan.NewCapability(cap.Can(), uri, nb), nil +} + +func Select[Caveats any](matcher Matcher[Caveats], capabilities []Source) (matches []Match[Caveats], errors []DelegationError, unknowns []ucan.Capability[any]) { + for _, capability := range capabilities { + match, err := matcher.Match(capability) + if err != nil { + if ux, ok := err.(UnknownCapability); ok { + unknowns = append(unknowns, ux.Capability()) + } else if sx, ok := err.(DelegationSubError); ok { + errors = append(errors, NewDelegationError([]DelegationSubError{sx}, capability.Capability())) + } else { + panic(fmt.Errorf("unexpected error type in match result: %w", err)) + } + continue + } + matches = append(matches, match) + } + return +} + +// ResolveCapability resolves delegated capability `source` from the `claimed` +// capability using provided capability `parser`. It is similar to +// [ParseCapability] except `source` here is treated as capability pattern which +// is matched against the `claimed` capability. This means we resolve `can` and +// `with` fields from the `claimed` capability and... +// TODO: inherit all missing `nb` fields from the claimed capability. +func ResolveCapability[Caveats any](descriptor Descriptor[Caveats], claimed ucan.Capability[Caveats], source Source) (ucan.Capability[Caveats], InvalidCapability) { + can := ResolveAbility(source.Capability().Can(), claimed.Can()) + if can == "" { + return nil, NewUnknownCapabilityError(source.Capability()) + } + + resource := ResolveResource(source.Capability().With(), claimed.With()) + if resource == "" { + resource = source.Capability().With() + } + + uri, err := descriptor.With().Read(resource) + if err != nil { + return nil, NewMalformedCapabilityError(source.Capability(), err) + } + + // TODO: inherit missing fields + nb, err := descriptor.Nb().Read(claimed.Nb()) + if err != nil { + return nil, NewMalformedCapabilityError(source.Capability(), err) + } + + return ucan.NewCapability(can, uri, nb), nil +} + +// ResolveAbility resolves ability `pattern` of the delegated capability from +// the ability of the claimed capability. If pattern matches returns claimed +// ability otherwise returns "". +// +// - pattern "*" can "store/add" → "store/add" +// - pattern "store/*" can "store/add" → "store/add" +// - pattern "*" can "store/add" → "store/add" +// - pattern "*" can "store/add" → "" +// - pattern "*" can "store/add" → "" +// - pattern "*" can "store/add" → "" +func ResolveAbility(pattern string, can ucan.Ability) ucan.Ability { + if pattern == can || pattern == "*" { + return can + } + if strings.HasSuffix(pattern, "/*") && strings.HasPrefix(can, pattern[0:len(pattern)-1]) { + return can + } + return "" +} + +// ResolveResource resolves `source` resource of the delegated capability from +// the resource `uri` of the claimed capability. If `source` is `"ucan:*""` or +// matches `uri` then it returns `uri` back otherwise it returns "". +// +// - source "ucan:*" uri "did:key:zAlice" → "did:key:zAlice" +// - source "ucan:*" uri "https://example.com" → "https://example.com" +// - source "did:*" uri "did:key:zAlice" → "" +// - source "did:key:zAlice" uri "did:key:zAlice" → "did:key:zAlice" +func ResolveResource(source string, uri ucan.Resource) ucan.Resource { + if source == uri || source == "ucan:*" { + return uri + } + return "" +} + +func DefaultDerives[Caveats any](claimed, delegated ucan.Capability[Caveats]) failure.Failure { + dres := delegated.With() + cres := claimed.With() + + if strings.HasSuffix(dres, "*") { + if !strings.HasPrefix(cres, dres[0:len(dres)-1]) { + return schema.NewSchemaError(fmt.Sprintf("Resource %s does not match delegated %s", cres, dres)) + } + } else if dres != cres { + return schema.NewSchemaError(fmt.Sprintf("Resource %s is not contained by %s", cres, dres)) + } + + // TODO: is it possible to ensure claimed caveats match delegated caveats? + return nil } diff --git a/validator/datamodel/attestation.go b/validator/datamodel/attestation.go new file mode 100644 index 0000000..7e8258c --- /dev/null +++ b/validator/datamodel/attestation.go @@ -0,0 +1,34 @@ +package datamodel + +import ( + _ "embed" + "fmt" + + "github.com/ipld/go-ipld-prime" + "github.com/ipld/go-ipld-prime/schema" + ucanipld "github.com/storacha-network/go-ucanto/core/ipld" +) + +//go:embed attestation.ipldsch +var attestationsch []byte +var attestationTypeSystem *schema.TypeSystem + +func init() { + ts, err := ipld.LoadSchemaBytes(attestationsch) + if err != nil { + panic(fmt.Errorf("failed to load IPLD schema: %s", err)) + } + attestationTypeSystem = ts +} + +func AttestationType() schema.Type { + return attestationTypeSystem.TypeByName("Attestation") +} + +type AttestationModel struct { + Proof ipld.Link +} + +func (m AttestationModel) Build() (ipld.Node, error) { + return ucanipld.WrapWithRecovery(&m, AttestationType()) +} diff --git a/validator/datamodel/attestation.ipldsch b/validator/datamodel/attestation.ipldsch new file mode 100644 index 0000000..e67d2ac --- /dev/null +++ b/validator/datamodel/attestation.ipldsch @@ -0,0 +1,3 @@ +type Attestation struct { + proof Link +} diff --git a/validator/datamodel/errors.go b/validator/datamodel/errors.go new file mode 100644 index 0000000..995a8ca --- /dev/null +++ b/validator/datamodel/errors.go @@ -0,0 +1,63 @@ +package datamodel + +import ( + // for go:embed + _ "embed" + "fmt" + + "github.com/ipld/go-ipld-prime" + "github.com/ipld/go-ipld-prime/schema" +) + +//go:embed errors.ipldsch +var errorsch []byte + +var ( + errorTypeSystem *schema.TypeSystem +) + +func init() { + ts, err := ipld.LoadSchemaBytes(errorsch) + if err != nil { + panic(fmt.Errorf("failed to load IPLD schema: %s", err)) + } + errorTypeSystem = ts +} + +func InvalidAudienceType() schema.Type { + return errorTypeSystem.TypeByName("InvalidAudience") +} + +type Delegation struct { + Audience string +} + +type InvalidAudienceModel struct { + Name *string + Audience string + Delegation Delegation + Message string + Stack *string +} + +type ExpiredModel struct { + Name *string + Message string + ExpiredAt int64 + Stack *string +} + +func ExpiredType() schema.Type { + return errorTypeSystem.TypeByName("Expired") +} + +type NotValidBeforeModel struct { + Name *string + Message string + ValidAt int64 + Stack *string +} + +func NotValidBeforeType() schema.Type { + return errorTypeSystem.TypeByName("NotValidBefore") +} diff --git a/validator/datamodel/errors.ipldsch b/validator/datamodel/errors.ipldsch new file mode 100644 index 0000000..c0dc4f7 --- /dev/null +++ b/validator/datamodel/errors.ipldsch @@ -0,0 +1,25 @@ +type Delegation struct { + audience String +} + +type InvalidAudience struct { + name optional String + audience String + delegation Delegation + message String + stack optional String +} + +type Expired struct { + name optional String + message String + expiredAt Int + stack optional String +} + +type NotValidBefore struct { + name optional String + message String + validAt Int + stack optional String +} diff --git a/validator/error.go b/validator/error.go new file mode 100644 index 0000000..8070b98 --- /dev/null +++ b/validator/error.go @@ -0,0 +1,654 @@ +package validator + +import ( + "encoding/json" + "errors" + "fmt" + "math" + "strings" + "time" + + "github.com/ipld/go-ipld-prime/datamodel" + "github.com/storacha-network/go-ucanto/core/delegation" + "github.com/storacha-network/go-ucanto/core/ipld" + "github.com/storacha-network/go-ucanto/core/result/failure" + "github.com/storacha-network/go-ucanto/did" + "github.com/storacha-network/go-ucanto/ucan" + vdm "github.com/storacha-network/go-ucanto/validator/datamodel" +) + +// go hack for union type -- unexported method cannot be implemented outside module limiting satisfying types +type DelegationSubError interface { + failure.Failure + isDelegationSubError() +} + +type InvalidProof interface { + failure.Failure + isInvalidProof() +} + +type EscalatedCapabilityError[Caveats any] struct { + failure.NamedWithStackTrace + claimed ucan.Capability[Caveats] + delegated ucan.Capability[Caveats] + cause error +} + +func NewEscalatedCapabilityError[Caveats any](claimed ucan.Capability[Caveats], delegated ucan.Capability[Caveats], cause error) EscalatedCapabilityError[Caveats] { + return EscalatedCapabilityError[Caveats]{failure.NamedWithCurrentStackTrace("EscalatedCapability"), claimed, delegated, cause} +} + +func (ece EscalatedCapabilityError[Caveats]) Unwrap() error { + return ece.cause +} + +func (ece EscalatedCapabilityError[Caveats]) Error() string { + return fmt.Sprintf("Constraint violation: %s", ece.cause.Error()) +} + +func (ece EscalatedCapabilityError[Caveats]) isDelegationSubError() {} + +type DelegationError interface { + failure.Failure + Causes() []DelegationSubError + Context() any + isDelegationError() +} + +type delegationError struct { + failure.NamedWithStackTrace + causes []DelegationSubError + context interface{} +} + +func NewDelegationError(causes []DelegationSubError, context interface{}) DelegationError { + return delegationError{failure.NamedWithCurrentStackTrace("InvalidClaim"), causes, context} +} + +func (de delegationError) Error() string { + return fmt.Sprintf("Cannot derive %s from delegated capabilities:\n%s", de.context, li(errors.Join(de.Unwrap()...).Error())) +} + +func (de delegationError) Causes() []DelegationSubError { + return de.causes +} + +func (de delegationError) Context() any { + return de.context +} + +func (de delegationError) Unwrap() []error { + errs := make([]error, 0, len(de.causes)) + for _, cause := range de.causes { + errs = append(errs, cause) + } + return errs +} + +func (de delegationError) isDelegationError() {} +func (de delegationError) isDelegationSubError() {} + +type SessionEscalationError struct { + failure.NamedWithStackTrace + delegation delegation.Delegation + cause error +} + +func NewSessionEscalationError(delegation delegation.Delegation, cause error) InvalidProof { + return SessionEscalationError{failure.NamedWithCurrentStackTrace("SessionEscalation"), delegation, cause} +} + +func (see SessionEscalationError) Error() string { + issuer := see.delegation.Issuer().DID() + return strings.Join([]string{ + fmt.Sprintf("Delegation %s issued by %s has an invalid session", see.delegation.Link(), issuer), + li(see.cause.Error()), + }, "\n") +} + +func (see SessionEscalationError) isInvalidProof() {} + +// BadSignature is a signature that could not be verified or has been verified +// invalid. i.e. it is an [UnverifiableSignature] or an [InvalidSignature]. +type BadSignature interface { + InvalidProof + Issuer() ucan.Principal + Audience() ucan.Principal + Delegation() delegation.Delegation + isBadSignature() +} + +// UnverifiableSignature is a signature that cannot be verified. i.e. some error +// occurred when attempting to verify the signature. +type UnverifiableSignature interface { + BadSignature + Unwrap() error + isUnverifiableSignature() +} + +type UnverifiableSignatureError struct { + failure.NamedWithStackTrace + delegation delegation.Delegation + cause error +} + +func NewUnverifiableSignatureError(delegation delegation.Delegation, cause error) UnverifiableSignature { + return UnverifiableSignatureError{failure.NamedWithCurrentStackTrace("UnverifiableSignature"), delegation, cause} +} + +func (use UnverifiableSignatureError) Issuer() ucan.Principal { + return use.delegation.Issuer() +} + +func (use UnverifiableSignatureError) Audience() ucan.Principal { + return use.delegation.Audience() +} + +func (use UnverifiableSignatureError) Delegation() delegation.Delegation { + return use.delegation +} + +func (use UnverifiableSignatureError) Error() string { + issuer := use.Issuer().DID() + return fmt.Sprintf("Proof %s issued by %s cannot be verified:\n%s", use.delegation.Link(), issuer, li(use.cause.Error())) +} + +func (use UnverifiableSignatureError) Unwrap() error { + return use.cause +} + +func (use UnverifiableSignatureError) isUnverifiableSignature() {} +func (use UnverifiableSignatureError) isBadSignature() {} +func (use UnverifiableSignatureError) isInvalidProof() {} + +// InvalidSignature is a signature that is verified to be invalid. +type InvalidSignature interface { + BadSignature + isInvalidSignature() +} + +type InvalidSignatureError struct { + failure.NamedWithStackTrace + delegation delegation.Delegation + verifier ucan.Verifier +} + +func NewInvalidSignatureError(delegation delegation.Delegation, verifier ucan.Verifier) InvalidSignature { + return InvalidSignatureError{failure.NamedWithCurrentStackTrace("InvalidSignature"), delegation, verifier} +} + +func (ise InvalidSignatureError) Issuer() ucan.Principal { + return ise.delegation.Issuer() +} + +func (ise InvalidSignatureError) Audience() ucan.Principal { + return ise.delegation.Audience() +} + +func (ise InvalidSignatureError) Delegation() delegation.Delegation { + return ise.delegation +} + +func (ise InvalidSignatureError) Error() string { + issuer := ise.Issuer().DID() + key := ise.verifier.DID() + if strings.HasPrefix(issuer.String(), "did:key") { + return fmt.Sprintf(`Proof %s does not have a valid signature from %s`, ise.delegation.Link(), key) + } + return strings.Join([]string{ + fmt.Sprintf("Proof %s issued by %s does not have a valid signature from %s", ise.delegation.Link(), issuer, key), + " ℹ️ Issuer probably signed with a different key, which got rotated, invalidating delegations that were issued with prior keys", + }, "\n") +} + +func (ise InvalidSignatureError) isInvalidSignature() {} +func (ise InvalidSignatureError) isBadSignature() {} +func (ise InvalidSignatureError) isInvalidProof() {} + +type UnavailableProof interface { + InvalidProof + Link() ucan.Link + isUnavailableProof() +} + +type UnavailableProofError struct { + failure.NamedWithStackTrace + link ucan.Link + cause error +} + +func NewUnavailableProofError(link ucan.Link, cause error) UnavailableProof { + return UnavailableProofError{failure.NamedWithCurrentStackTrace("UnavailableProof"), link, cause} +} + +func (upe UnavailableProofError) Unwrap() error { + return upe.cause +} + +func (upe UnavailableProofError) Link() ucan.Link { + return upe.link +} + +func (upe UnavailableProofError) Error() string { + messages := []string{ + fmt.Sprintf(`Linked proof "%s" is not included and could not be resolved`, upe.link), + } + if upe.cause != nil { + messages = append(messages, li(fmt.Sprintf("Proof resolution failed with: %s", upe.cause.Error()))) + } + return strings.Join(messages, "\n") +} + +func (upe UnavailableProofError) isUnavailableProof() {} +func (upe UnavailableProofError) isInvalidProof() {} + +type UnresolvedDID interface { + InvalidProof + DID() did.DID + isUnresolvedDID() +} + +type DIDKeyResolutionError struct { + failure.NamedWithStackTrace + did did.DID + cause error +} + +func NewDIDKeyResolutionError(did did.DID, cause error) UnresolvedDID { + return DIDKeyResolutionError{failure.NamedWithCurrentStackTrace("DIDKeyResolutionError"), did, cause} +} + +func (dkre DIDKeyResolutionError) Unwrap() error { + return dkre.cause +} + +func (dkre DIDKeyResolutionError) DID() did.DID { + return dkre.did +} + +func (dkre DIDKeyResolutionError) Error() string { + return fmt.Sprintf("Unable to resolve '%s' key", dkre.did) +} + +func (dkre DIDKeyResolutionError) isUnresolvedDID() {} +func (dkre DIDKeyResolutionError) isInvalidProof() {} + +type PrincipalAlignmentError struct { + failure.NamedWithStackTrace + audience ucan.Principal + delegation delegation.Delegation +} + +func NewPrincipalAlignmentError(audience ucan.Principal, delegation delegation.Delegation) failure.Failure { + return PrincipalAlignmentError{failure.NamedWithCurrentStackTrace("InvalidAudience"), audience, delegation} +} + +func (pae PrincipalAlignmentError) Error() string { + return fmt.Sprintf("Delegation audience is '%s' instead of '%s'", pae.delegation.Audience().DID(), pae.audience.DID()) +} + +func (pae PrincipalAlignmentError) isInvalidProof() {} + +// InvalidCapability is an error produced when parsing capabilities. +type InvalidCapability interface { + failure.Failure + isInvalidCapability() +} + +type MalformedCapability interface { + InvalidCapability + DelegationSubError + Capability() ucan.Capability[any] + isMalformedCapability() +} + +type MalformedCapabilityError struct { + failure.NamedWithStackTrace + capability ucan.Capability[any] + cause error +} + +func NewMalformedCapabilityError(capability ucan.Capability[any], cause error) MalformedCapability { + return MalformedCapabilityError{failure.NamedWithCurrentStackTrace("MalformedCapability"), capability, cause} +} + +func (mce MalformedCapabilityError) Error() string { + capabilityJSON, _ := json.Marshal(mce.capability) + return strings.Join([]string{ + fmt.Sprintf("Encountered malformed '%s' capability: %s", mce.capability.Can(), string(capabilityJSON)), + li(mce.cause.Error()), + }, "\n") +} + +func (mce MalformedCapabilityError) Capability() ucan.Capability[any] { + return mce.capability +} + +func (mce MalformedCapabilityError) isMalformedCapability() {} +func (mce MalformedCapabilityError) isInvalidCapability() {} +func (mce MalformedCapabilityError) isDelegationSubError() {} + +type UnknownCapability interface { + InvalidCapability + Capability() ucan.Capability[any] + isUnknownCapability() +} + +type UnknownCapabilityError struct { + failure.NamedWithStackTrace + capability ucan.Capability[any] +} + +func NewUnknownCapabilityError(capability ucan.Capability[any]) UnknownCapability { + return UnknownCapabilityError{failure.NamedWithCurrentStackTrace("UnknownCapability"), capability} +} + +func (uce UnknownCapabilityError) Error() string { + capabilityJSON, _ := json.Marshal(uce.capability) + return fmt.Sprintf("Encountered unknown capability: %s", string(capabilityJSON)) +} + +func (uce UnknownCapabilityError) Capability() ucan.Capability[any] { + return uce.capability +} + +func (uce UnknownCapabilityError) isUnknownCapability() {} +func (uce UnknownCapabilityError) isInvalidCapability() {} +func (uce UnknownCapabilityError) isDelegationSubError() {} + +type ExpiredError struct { + failure.NamedWithStackTrace + delegation delegation.Delegation +} + +func NewExpiredError(delegation delegation.Delegation) InvalidProof { + return ExpiredError{failure.NamedWithCurrentStackTrace("Expired"), delegation} +} + +func (ee ExpiredError) Error() string { + exp := ee.delegation.Expiration() + return fmt.Sprintf("Proof %s has expired on %s", ee.delegation.Link(), + time.Unix(int64(*exp), 0).Format(time.RFC3339)) +} + +func (ee ExpiredError) Build() (datamodel.Node, error) { + name := ee.Name() + stack := ee.Stack() + exp := ee.delegation.Expiration() + expiredModel := vdm.ExpiredModel{ + Name: &name, + Message: ee.Error(), + ExpiredAt: int64(*exp), + Stack: &stack, + } + return ipld.WrapWithRecovery(expiredModel, vdm.ExpiredType()) +} + +func (ee ExpiredError) isInvalidProof() {} + +type Revoked interface { + InvalidProof + Delegation() delegation.Delegation + isRevoked() +} + +type RevokedError struct { + failure.NamedWithStackTrace + delegation delegation.Delegation +} + +func NewRevokedError(delegation delegation.Delegation) Revoked { + return RevokedError{failure.NamedWithCurrentStackTrace("Revoked"), delegation} +} + +func (re RevokedError) Delegation() delegation.Delegation { + return re.delegation +} + +func (re RevokedError) Error() string { + return fmt.Sprintf("Proof %s has been revoked", re.delegation.Link()) +} + +func (re RevokedError) isInvalidProof() {} +func (re RevokedError) isRevoked() {} + +type NotValidBeforeError struct { + failure.NamedWithStackTrace + delegation delegation.Delegation +} + +func NewNotValidBeforeError(delegation delegation.Delegation) InvalidProof { + return NotValidBeforeError{failure.NamedWithCurrentStackTrace("NotValidBefore"), delegation} +} + +func (nvbe NotValidBeforeError) Error() string { + return fmt.Sprintf("Proof %s is not valid before %s", nvbe.delegation.Link(), + time.Unix(int64(nvbe.delegation.NotBefore()), 0).Format(time.RFC3339)) +} + +func (nvbe NotValidBeforeError) Build() (datamodel.Node, error) { + name := nvbe.Name() + stack := nvbe.Stack() + notValidBeforeModel := vdm.NotValidBeforeModel{ + Name: &name, + Message: nvbe.Error(), + ValidAt: int64(nvbe.delegation.NotBefore()), + Stack: &stack, + } + return ipld.WrapWithRecovery(notValidBeforeModel, vdm.NotValidBeforeType()) +} + +func (nvbe NotValidBeforeError) isInvalidProof() {} + +type InvalidClaim interface { + failure.Failure + Issuer() ucan.Principal + Delegation() delegation.Delegation +} + +type InvalidClaimError[Caveats any] struct { + failure.NamedWithStackTrace + match Match[Caveats] + delegationErrors []DelegationError + unknownCapabilities []ucan.Capability[any] + invalidProofs []ProofError + failedProofs []InvalidClaim +} + +func NewInvalidClaimError[Caveats any]( + match Match[Caveats], + delegationErrors []DelegationError, + unknownCapabilities []ucan.Capability[any], + invalidProofs []ProofError, + failedProofs []InvalidClaim, +) InvalidClaim { + return InvalidClaimError[Caveats]{ + failure.NamedWithCurrentStackTrace("InvalidClaim"), + match, + delegationErrors, + unknownCapabilities, + invalidProofs, + failedProofs, + } +} + +func (ice InvalidClaimError[Caveats]) Error() string { + errorStrings := make([]string, 0, len(ice.failedProofs)+len(ice.delegationErrors)+len(ice.invalidProofs)) + + for _, failedProof := range ice.failedProofs { + errorStrings = append(errorStrings, li(failedProof.Error())) + } + + for _, delegationError := range ice.delegationErrors { + errorStrings = append(errorStrings, li(delegationError.Error())) + } + + for _, invalidProof := range ice.invalidProofs { + errorStrings = append(errorStrings, li(invalidProof.Error())) + } + + unknowns := make([]string, 0, len(ice.unknownCapabilities)) + for _, unknownCapability := range ice.unknownCapabilities { + out, _ := unknownCapability.MarshalJSON() + unknowns = append(unknowns, li(string(out))) + } + + var finalList []string + finalList = append(finalList, fmt.Sprintf("Capability %s is not authorized because:", ice.match)) + finalList = append(finalList, li(fmt.Sprintf("Capability can not be (self) issued by '%s'", ice.Issuer().DID()))) + if len(errorStrings) > 0 { + finalList = append(finalList, errorStrings...) + } else { + finalList = append(finalList, li("Delegated capability not found")) + } + if len(unknowns) > 0 { + finalList = append(finalList, li(fmt.Sprintf("Encountered unknown capabilities\n%s", strings.Join(unknowns, "\n")))) + } + + return strings.Join(finalList, "\n") +} + +func (ice InvalidClaimError[Caveats]) Issuer() ucan.Principal { + return ice.Delegation().Issuer() +} + +func (ice InvalidClaimError[Caveats]) Delegation() delegation.Delegation { + return ice.match.Source()[0].Delegation() +} + +func (ice InvalidClaimError[Caveats]) DelegationErrors() []DelegationError { + return ice.delegationErrors +} + +func (ice InvalidClaimError[Caveats]) UnknownCapabilities() []ucan.Capability[any] { + return ice.unknownCapabilities +} + +func (ice InvalidClaimError[Caveats]) InvalidProofs() []ProofError { + return ice.invalidProofs +} + +func (ice InvalidClaimError[Caveats]) FailedProofs() []InvalidClaim { + return ice.failedProofs +} + +type Unauthorized interface { + failure.Failure + DelegationErrors() []DelegationError + UnknownCapabilities() []ucan.Capability[any] + InvalidProofs() []InvalidProof + FailedProofs() []InvalidClaim + isUnauthorized() +} + +type UnauthorizedError[Caveats any] struct { + failure.NamedWithStackTrace + capability CapabilityParser[Caveats] + delegationErrors []DelegationError + // this is a hack... it will allow you to make an array of capabilities of different types + unknownCapabilities []ucan.Capability[any] + invalidProofs []InvalidProof + failedProofs []InvalidClaim +} + +func NewUnauthorizedError[Caveats any]( + capability CapabilityParser[Caveats], + delegationErrors []DelegationError, + unknownCapabilities []ucan.Capability[any], + invalidProofs []InvalidProof, + failedProofs []InvalidClaim, +) Unauthorized { + return UnauthorizedError[Caveats]{ + failure.NamedWithCurrentStackTrace("Unauthorized"), + capability, + delegationErrors, + unknownCapabilities, + invalidProofs, + failedProofs, + } +} + +func (ue UnauthorizedError[Caveats]) Error() string { + errorStrings := make([]string, 0, len(ue.failedProofs)+len(ue.delegationErrors)+len(ue.invalidProofs)) + + for _, failedProof := range ue.failedProofs { + errorStrings = append(errorStrings, li(failedProof.Error())) + } + + for _, delegationError := range ue.delegationErrors { + errorStrings = append(errorStrings, li(delegationError.Error())) + } + + for _, invalidProof := range ue.invalidProofs { + errorStrings = append(errorStrings, li(invalidProof.Error())) + } + + unknowns := make([]string, 0, len(ue.unknownCapabilities)) + for _, unknownCapability := range ue.unknownCapabilities { + out, _ := unknownCapability.MarshalJSON() + unknowns = append(unknowns, li(string(out))) + } + + finalList := make([]string, 0, 2+int(math.Min(1, float64(len(errorStrings))))) + finalList = append(finalList, fmt.Sprintf("Claim %s is not authorized", ue.capability)) + if len(errorStrings) > 0 { + finalList = append(finalList, errorStrings...) + } else { + finalList = append(finalList, li("No matching delegated capability found")) + } + if len(unknowns) > 0 { + finalList = append(finalList, li(fmt.Sprintf("Encountered unknown capabilities\n%s", strings.Join(unknowns, "\n")))) + } + + return strings.Join(finalList, "\n") +} + +func (ue UnauthorizedError[Caveats]) DelegationErrors() []DelegationError { + return ue.delegationErrors +} + +func (ue UnauthorizedError[Caveats]) UnknownCapabilities() []ucan.Capability[any] { + return ue.unknownCapabilities +} + +func (ue UnauthorizedError[Caveats]) InvalidProofs() []InvalidProof { + return ue.invalidProofs +} + +func (ue UnauthorizedError[Caveats]) FailedProofs() []InvalidClaim { + return ue.failedProofs +} + +func (ue UnauthorizedError[Caveats]) isUnauthorized() {} + +func indent(message string) string { + indent := " " + return indent + strings.Join(strings.Split(message, "\n"), "\n"+indent) +} + +func li(message string) string { + return indent("- " + message) +} + +type ProofError struct { + failure.NamedWithStackTrace + proof ucan.Link + cause error +} + +func (pe ProofError) Error() string { + return fmt.Sprintf("Capability can not be derived from prf: %s because:\n%s", pe.proof, li(pe.cause.Error())) +} + +func (pe ProofError) Proof() ucan.Link { + return pe.proof +} + +func (pe ProofError) Unwrap() error { + return pe.cause +} + +func NewProofError(proof ucan.Link, cause error) ProofError { + return ProofError{failure.NamedWithCurrentStackTrace("ProofError"), proof, cause} +} diff --git a/validator/lib.go b/validator/lib.go index 19ce12e..31d48da 100644 --- a/validator/lib.go +++ b/validator/lib.go @@ -1,10 +1,477 @@ package validator import ( + "fmt" + "strings" + "sync" + + "github.com/storacha-network/go-ucanto/core/dag/blockstore" + "github.com/storacha-network/go-ucanto/core/delegation" + "github.com/storacha-network/go-ucanto/core/invocation" + "github.com/storacha-network/go-ucanto/core/result/failure" + "github.com/storacha-network/go-ucanto/core/schema" "github.com/storacha-network/go-ucanto/did" + "github.com/storacha-network/go-ucanto/principal" "github.com/storacha-network/go-ucanto/ucan" + vdm "github.com/storacha-network/go-ucanto/validator/datamodel" + "github.com/ucan-wg/go-ucan/capability/policy" + "github.com/ucan-wg/go-ucan/capability/policy/literal" + "github.com/ucan-wg/go-ucan/capability/policy/selector" ) func IsSelfIssued[Caveats any](capability ucan.Capability[Caveats], issuer did.DID) bool { return capability.With() == issuer.DID().String() } + +func ProofUnavailable(p ucan.Link) (delegation.Delegation, UnavailableProof) { + return nil, NewUnavailableProofError(p, fmt.Errorf("no proof resolver configured")) +} + +func FailDIDKeyResolution(d did.DID) (did.DID, UnresolvedDID) { + return did.Undef, NewDIDKeyResolutionError(d, fmt.Errorf("no DID resolver configured")) +} + +// PrincipalParser provides verifier instances that can validate UCANs issued +// by a given principal. +type PrincipalParser interface { + ParsePrincipal(str string) (principal.Verifier, error) +} + +type PrincipalParserFunc func(str string) (principal.Verifier, error) + +// PrincipalResolver is used to resolve a key of the principal that is +// identified by DID different from did:key method. It can be passed into a +// UCAN validator in order to augmented it with additional DID methods support. +type PrincipalResolver interface { + ResolveDIDKey(did did.DID) (did.DID, UnresolvedDID) +} + +// PrincipalResolverFunc resolves the key of a principal that is identified by +// DID different from did:key method. +type PrincipalResolverFunc func(did did.DID) (did.DID, UnresolvedDID) + +// ProofResolver finds a delegations when external proof links are present in +// UCANs. If a resolver is not provided the validator may not be able to explore +// corresponding path within a proof chain. +type ProofResolver interface { + // Resolve finds a delegation corresponding to an external proof link. + ResolveProof(proof ucan.Link) (delegation.Delegation, UnavailableProof) +} + +// Resolve finds a delegation corresponding to an external proof link. +type ProofResolverFunc func(proof ucan.Link) (delegation.Delegation, UnavailableProof) + +type CanIssuer[Caveats any] interface { + // CanIssue informs validator whether given capability can be issued by a + // given DID or whether it needs to be delegated to the issuer. + CanIssue(capability ucan.Capability[Caveats], issuer did.DID) bool +} + +// CanIssue informs validator whether given capability can be issued by a +// given DID or whether it needs to be delegated to the issuer. +type CanIssueFunc[Caveats any] func(capability ucan.Capability[Caveats], issuer did.DID) bool + +// canissuer converts an CanIssuer[any] to CanIssuer[Caveats] +type canissuer[Caveats any] struct { + canIssue CanIssueFunc[any] +} + +func (ci canissuer[Caveats]) CanIssue(c ucan.Capability[Caveats], d did.DID) bool { + return ci.canIssue(ucan.NewCapability[any](c.Can(), c.With(), c.Nb()), d) +} + +type RevocationChecker[Caveats any] interface { + // ValidateAuthorization validates that the passed authorization has not been + // revoked. It returns `nil` if not revoked. + ValidateAuthorization(auth Authorization[Caveats]) Revoked +} + +// RevocationCheckerFunc validates that the passed authorization has not been +// revoked. It returns `nil` if not revoked. +type RevocationCheckerFunc[Caveats any] func(auth Authorization[Caveats]) Revoked + +// Validator must provide a [principal.Verifier] corresponding to local authority. +// +// A capability provider service will use one corresponding to own DID or it's +// supervisor's DID if it acts under it's authority. +// +// This allows a service identified by non did:key e.g. did:web or did:dns to +// pass resolved key so it does not need to be resolved at runtime. +type Validator interface { + Authority() principal.Verifier +} + +type ClaimContext interface { + Validator + RevocationChecker[any] + CanIssuer[any] + ProofResolver + PrincipalParser + PrincipalResolver +} + +type ValidationContext[Caveats any] interface { + ClaimContext + Capability() CapabilityParser[Caveats] +} + +type validationContext[Caveats any] struct { + authority principal.Verifier + capability CapabilityParser[Caveats] + canIssue CanIssueFunc[any] + validateAuthorization RevocationCheckerFunc[any] + resolveProof ProofResolverFunc + parsePrincipal PrincipalParserFunc + resolveDIDKey PrincipalResolverFunc +} + +func (vc validationContext[Caveats]) Authority() principal.Verifier { + return vc.authority +} + +func (vc validationContext[Caveats]) Capability() CapabilityParser[Caveats] { + return vc.capability +} + +func (vc validationContext[Caveats]) CanIssue(capability ucan.Capability[any], issuer did.DID) bool { + return vc.canIssue(capability, issuer) +} + +func (vc validationContext[Caveats]) ValidateAuthorization(auth Authorization[any]) Revoked { + return vc.validateAuthorization(auth) +} + +func (vc validationContext[Caveats]) ResolveProof(proof ucan.Link) (delegation.Delegation, UnavailableProof) { + return vc.resolveProof(proof) +} + +func (vc validationContext[Caveats]) ParsePrincipal(str string) (principal.Verifier, error) { + return vc.parsePrincipal(str) +} + +func (vc validationContext[Caveats]) ResolveDIDKey(did did.DID) (did.DID, UnresolvedDID) { + return vc.resolveDIDKey(did) +} + +func NewValidationContext[Caveats any]( + authority principal.Verifier, + capability CapabilityParser[Caveats], + canIssue CanIssueFunc[any], + validateAuthorization RevocationCheckerFunc[any], + resolveProof ProofResolverFunc, + parsePrincipal PrincipalParserFunc, + resolveDIDKey PrincipalResolverFunc, +) ValidationContext[Caveats] { + return validationContext[Caveats]{ + authority, + capability, + canIssue, + validateAuthorization, + resolveProof, + parsePrincipal, + resolveDIDKey, + } +} + +// Access finds a valid path in a proof chain of the given +// [invocation.Invocation] by exploring every possible option. On success an +// [Authorization] object is returned that illustrates the valid path. If no +// valid path is found [Unauthorized] error is returned detailing all explored +// paths and where they proved to fail. +func Access[Caveats any](invocation invocation.Invocation, context ValidationContext[Caveats]) (Authorization[Caveats], Unauthorized) { + prf := []delegation.Proof{delegation.FromDelegation(invocation)} + return Claim(context.Capability(), prf, context) +} + +// Claim attempts to find a valid proof chain for the claimed [CapabilityParser] +// given set of `proofs`. On success an [Authorization] object with detailed +// proof chain is returned and on failure [Unauthorized] error is returned with +// details on paths explored and why they have failed. +func Claim[Caveats any](capability CapabilityParser[Caveats], proofs []delegation.Proof, context ClaimContext) (Authorization[Caveats], Unauthorized) { + var sources []Source + var invalidprf []InvalidProof + + delegations, rerrs := ResolveProofs(proofs, context) + for _, err := range rerrs { + invalidprf = append(invalidprf, err) + } + + for _, prf := range delegations { + // Validate each proof if valid add each capability to the list of sources + // or collect the error. + validation, err := Validate(prf, delegations, context) + if err != nil { + invalidprf = append(invalidprf, err) + continue + } + + for _, c := range validation.Capabilities() { + sources = append(sources, NewSource(c, prf)) + } + } + + // look for the matching capability + matches, dlgerrs, unknowns := capability.Select(sources) + + var failedprf []InvalidClaim + for _, matched := range matches { + selector := matched.Prune(canissuer[Caveats]{canIssue: context.CanIssue}) + if selector == nil { + auth := NewAuthorization(matched, nil) + revoked := context.ValidateAuthorization(ConvertUnknownAuthorization(auth)) + if revoked != nil { + invalidprf = append(invalidprf, revoked) + continue + } + return auth, nil + } + + a, err := Authorize(matched, context) + if err != nil { + failedprf = append(failedprf, err) + continue + } + + auth := NewAuthorization(matched, []Authorization[Caveats]{a}) + revoked := context.ValidateAuthorization(ConvertUnknownAuthorization(auth)) + if revoked != nil { + invalidprf = append(invalidprf, revoked) + continue + } + + return auth, nil + } + + return nil, NewUnauthorizedError(capability, dlgerrs, unknowns, invalidprf, failedprf) +} + +// ResolveProofs takes `proofs` from the delegation which may contain +// a [delegation.Delegation] or a link to one and attempts to resolve links by +// side loading them. It returns a set of resolved [delegation.Delegation]s and +// errors for the proofs that could not be resolved. +func ResolveProofs(proofs []delegation.Proof, resolver ProofResolver) (dels []delegation.Delegation, errs []UnavailableProof) { + for _, p := range proofs { + d, ok := p.Delegation() + if ok { + dels = append(dels, d) + } else { + d, err := resolver.ResolveProof(p.Link()) + if err != nil { + errs = append(errs, err) + continue + } + dels = append(dels, d) + } + } + return +} + +// Validate a delegation to check it is within the time bound and that it is +// authorized by the issuer. +func Validate(dlg delegation.Delegation, prfs []delegation.Delegation, ctx ClaimContext) (delegation.Delegation, InvalidProof) { + if ucan.IsExpired(dlg) { + return nil, NewExpiredError(dlg) + } + if ucan.IsTooEarly(dlg) { + return nil, NewNotValidBeforeError(dlg) + } + return VerifyAuthorization(dlg, prfs, ctx) +} + +// VerifyAuthorization verifies that delegation has been authorized by the +// issuer. If issued by the did:key principal checks that the signature is +// valid. If issued by the root authority checks that the signature is valid. If +// issued by the principal identified by other DID method attempts to resolve a +// valid `ucan/attest` attestation from the authority, if attestation is not +// found falls back to resolving did:key for the issuer and verifying its +// signature. +func VerifyAuthorization(dlg delegation.Delegation, prfs []delegation.Delegation, ctx ClaimContext) (delegation.Delegation, InvalidProof) { + issuer := dlg.Issuer().DID() + // If the issuer is a did:key we just verify a signature + if strings.HasPrefix(issuer.String(), "did:key:") { + vfr, err := ctx.ParsePrincipal(issuer.String()) + if err != nil { + return nil, NewUnverifiableSignatureError(dlg, err) + } + return VerifySignature(dlg, vfr) + } + + if dlg.Issuer().DID() == ctx.Authority().DID() { + return VerifySignature(dlg, ctx.Authority()) + } + + // Attempt to resolve embedded authorization session from the authority + _, err := VerifySession(dlg, prfs, ctx) + if err != nil { + if len(err.FailedProofs()) > 0 { + return nil, NewSessionEscalationError(dlg, err) + } + + // Otherwise we try to resolve did:key from the DID instead + // and use that to verify the signature + did, err := ctx.ResolveDIDKey(issuer) + if err != nil { + return nil, err + } + + vfr, perr := ctx.ParsePrincipal(did.String()) + if perr != nil { + return nil, NewUnverifiableSignatureError(dlg, perr) + } + + return VerifySignature(dlg, vfr) + } + + return dlg, nil +} + +// VerifySignature verifies the delegation was signed by the passed verifier. +func VerifySignature(dlg delegation.Delegation, vfr principal.Verifier) (delegation.Delegation, BadSignature) { + ok, err := ucan.VerifySignature(dlg.Data(), vfr) + if err != nil { + return nil, NewUnverifiableSignatureError(dlg, err) + } + if !ok { + return nil, NewInvalidSignatureError(dlg, vfr) + } + return dlg, nil +} + +// VerifySession attempts to find an authorization session - an `ucan/attest` +// capability delegation where `with` matches `ctx.Authority()` and `nb.proof` +// matches given delegation. +// +// https://github.com/storacha-network/specs/blob/main/w3-session.md#authorization-session +func VerifySession(dlg delegation.Delegation, prfs []delegation.Delegation, ctx ClaimContext) (Authorization[vdm.AttestationModel], Unauthorized) { + // Create a schema that will match an authorization for this exact delegation + attestation := NewCapability( + "ucan/attest", + schema.Literal(ctx.Authority().DID().String()), + schema.Struct[vdm.AttestationModel]( + vdm.AttestationType(), + policy.Policy{ + policy.Equal(selector.MustParse(".proof"), literal.Link(dlg.Link())), + }, + ), + func(claimed, delegated ucan.Capability[vdm.AttestationModel]) failure.Failure { + err := DefaultDerives(claimed, delegated) + if err != nil { + return err + } + if claimed.Nb().Proof != delegated.Nb().Proof { + return schema.NewSchemaError(fmt.Sprintf(`proof: %s violates %s`, claimed.Nb().Proof, delegated.Nb().Proof)) + } + return nil + }, + ) + + // We only consider attestations otherwise we will end up doing an + // exponential scan if there are other proofs that require attestations. + var aprfs []delegation.Proof + for _, p := range prfs { + if p.Capabilities()[0].Can() == "ucan/attest" { + aprfs = append(aprfs, delegation.FromDelegation(p)) + } + } + + return Claim(attestation, aprfs, ctx) +} + +// Authorize verifies whether any of the delegated proofs grant capability. +func Authorize[Caveats any](match Match[Caveats], context ClaimContext) (Authorization[Caveats], InvalidClaim) { + // load proofs from all delegations + sources, invalidprf := ResolveMatch(match, context) + + matches, dlgerrs, unknowns := match.Select(sources) + + var failedprf []InvalidClaim + for _, matched := range matches { + selector := matched.Prune(canissuer[Caveats]{canIssue: context.CanIssue}) + if selector == nil { + return NewAuthorization(matched, nil), nil + } + + auth, err := Authorize(selector, context) + if err != nil { + failedprf = append(failedprf, err) + continue + } + + return NewAuthorization(matched, []Authorization[Caveats]{auth}), nil + } + + return nil, NewInvalidClaimError(match, dlgerrs, unknowns, invalidprf, failedprf) +} + +func ResolveMatch[Caveats any](match Match[Caveats], context ClaimContext) (sources []Source, errors []ProofError) { + includes := map[string]struct{}{} + var wg sync.WaitGroup + var lock sync.RWMutex + for _, source := range match.Source() { + id := source.Delegation().Link().String() + if _, ok := includes[id]; !ok { + includes[id] = struct{}{} + wg.Add(1) + go func(s Source) { + srcs, errs := ResolveSources(s, context) + lock.Lock() + defer lock.Unlock() + defer wg.Done() + sources = append(sources, srcs...) + errors = append(errors, errs...) + }(source) + } + } + wg.Wait() + return +} + +func ResolveSources(source Source, context ClaimContext) (sources []Source, errors []ProofError) { + dlg := source.Delegation() + var prfs []delegation.Delegation + + br, err := blockstore.NewBlockReader(blockstore.WithBlocksIterator(dlg.Blocks())) + if err != nil { + errors = append(errors, NewProofError(dlg.Link(), err)) + return + } + + dlgs, failedprf := ResolveProofs( + delegation.NewProofsView(dlg.Proofs(), br), + context, + ) + + // All the proofs that failed to resolve are saved as proof errors. + for _, err := range failedprf { + errors = append(errors, NewProofError(err.Link(), err)) + } + + // All the proofs that resolved are checked for principal alignment. Ones that + // do not align are saved as proof errors. + for _, prf := range dlgs { + // If proof does not delegate to a matching audience save an proof error. + if dlg.Issuer().DID() != prf.Audience().DID() { + errors = append(errors, NewProofError(prf.Link(), NewPrincipalAlignmentError(dlg.Issuer(), prf))) + } else { + prfs = append(prfs, prf) + } + } + // In the second pass we attempt to proofs that were resolved and are aligned. + for _, prf := range prfs { + _, err := Validate(prf, prfs, context) + + // If proof is not valid (expired, not active yet or has incorrect + // signature) save a corresponding proof error. + if err != nil { + errors = append(errors, NewProofError(prf.Link(), err)) + continue + } + + // Otherwise create source objects for it's capabilities, so we could + // track which proof in which capability the are from. + for _, cap := range prf.Capabilities() { + sources = append(sources, NewSource(cap, prf)) + } + } + return +} diff --git a/validator/lib_test.go b/validator/lib_test.go index c64b66e..e24f1f5 100644 --- a/validator/lib_test.go +++ b/validator/lib_test.go @@ -1,30 +1,1019 @@ package validator import ( + "fmt" + "strings" "testing" + "time" - "github.com/storacha-network/go-ucanto/principal/ed25519/signer" + "github.com/ipfs/go-cid" + "github.com/ipld/go-ipld-prime" + cidlink "github.com/ipld/go-ipld-prime/linking/cid" + "github.com/ipld/go-ipld-prime/node/basicnode" + "github.com/storacha-network/go-ucanto/core/dag/blockstore" + "github.com/storacha-network/go-ucanto/core/delegation" + "github.com/storacha-network/go-ucanto/core/invocation" + "github.com/storacha-network/go-ucanto/core/ipld/block" + "github.com/storacha-network/go-ucanto/core/ipld/codec/cbor" + "github.com/storacha-network/go-ucanto/core/ipld/hash/sha256" + "github.com/storacha-network/go-ucanto/core/result/failure" + "github.com/storacha-network/go-ucanto/core/schema" + "github.com/storacha-network/go-ucanto/did" + "github.com/storacha-network/go-ucanto/principal" + "github.com/storacha-network/go-ucanto/principal/ed25519/verifier" + "github.com/storacha-network/go-ucanto/principal/signer" + "github.com/storacha-network/go-ucanto/testing/fixtures" + "github.com/storacha-network/go-ucanto/testing/helpers" "github.com/storacha-network/go-ucanto/ucan" + udm "github.com/storacha-network/go-ucanto/ucan/datamodel/ucan" + "github.com/stretchr/testify/require" ) -func TestIsSelfIssued(t *testing.T) { - alice, err := signer.Generate() - if err != nil { - t.Fatalf("generating key: %v", err) +type storeAddCaveats struct { + Link ipld.Link + Origin ipld.Link +} + +func (c storeAddCaveats) Build() (ipld.Node, error) { + np := basicnode.Prototype.Any + nb := np.NewBuilder() + ma, _ := nb.BeginMap(2) + if c != (storeAddCaveats{}) { + ma.AssembleKey().AssignString("link") + ma.AssembleValue().AssignLink(c.Link) + if c.Origin != nil { + ma.AssembleKey().AssignString("origin") + ma.AssembleValue().AssignLink(c.Origin) + } } - bob, err := signer.Generate() - if err != nil { - t.Fatalf("generating key: %v", err) + ma.Finish() + return nb.Build(), nil +} + +var storeAddTyp = helpers.Must(ipld.LoadSchemaBytes([]byte(` + type StoreAddCaveats struct { + link Link + origin optional Link } +`))) + +var storeAdd = NewCapability( + "store/add", + schema.DIDString(), + schema.Struct[storeAddCaveats](storeAddTyp.TypeByName("StoreAddCaveats"), nil), + func(claimed, delegated ucan.Capability[storeAddCaveats]) failure.Failure { + if claimed.With() != delegated.With() { + err := fmt.Errorf("Expected 'with: \"%s\"' instead got '%s'", delegated.With(), claimed.With()) + return failure.FromError(err) + } + if delegated.Nb().Link != nil && delegated.Nb().Link != claimed.Nb().Link { + var err error + if claimed.Nb().Link == nil { + err = fmt.Errorf("Link violates imposed %s constraint", delegated.Nb().Link) + } else { + err = fmt.Errorf("Link %s violates imposed %s constraint", claimed.Nb().Link, delegated.Nb().Link) + } + return failure.FromError(err) + } + return nil + }, +) + +var testLink = cidlink.Link{Cid: cid.MustParse("bafkqaaa")} +var validateAuthOk = func(auth Authorization[any]) Revoked { return nil } +var parseEdPrincipal = func(str string) (principal.Verifier, error) { + return verifier.Parse(str) +} + +func TestAccess(t *testing.T) { + t.Run("authorized", func(t *testing.T) { + t.Run("self-issued invocation", func(t *testing.T) { + inv, err := storeAdd.Invoke( + fixtures.Alice, + fixtures.Bob, + fixtures.Alice.DID().String(), + storeAddCaveats{Link: testLink}, + ) + require.NoError(t, err) + + context := NewValidationContext( + fixtures.Service.Verifier(), + storeAdd, + IsSelfIssued, + validateAuthOk, + ProofUnavailable, + parseEdPrincipal, + FailDIDKeyResolution, + ) + + a, x := Access(inv, context) + require.NoError(t, x) + require.Equal(t, storeAdd.Can(), a.Capability().Can()) + require.Equal(t, fixtures.Alice.DID().String(), a.Capability().With()) + require.Equal(t, fixtures.Alice.DID(), a.Issuer().DID()) + require.Equal(t, fixtures.Bob.DID(), a.Audience().DID()) + }) + + t.Run("delegated invocation", func(t *testing.T) { + dlg, err := storeAdd.Delegate( + fixtures.Alice, + fixtures.Bob, + fixtures.Alice.DID().String(), + storeAddCaveats{}, + ) + require.NoError(t, err) + + inv, err := storeAdd.Invoke( + fixtures.Bob, + fixtures.Service, + fixtures.Alice.DID().String(), + storeAddCaveats{Link: testLink}, + delegation.WithProof(delegation.FromDelegation(dlg)), + ) + require.NoError(t, err) + + context := NewValidationContext( + fixtures.Service.Verifier(), + storeAdd, + IsSelfIssued, + validateAuthOk, + ProofUnavailable, + parseEdPrincipal, + FailDIDKeyResolution, + ) + + a, x := Access(inv, context) + require.NoError(t, x) + require.Equal(t, storeAdd.Can(), a.Capability().Can()) + require.Equal(t, fixtures.Alice.DID().String(), a.Capability().With()) + require.Equal(t, fixtures.Bob.DID(), a.Issuer().DID()) + require.Equal(t, fixtures.Service.DID(), a.Audience().DID()) + }) + + t.Run("delegation chain", func(t *testing.T) { + alice2bob, err := storeAdd.Delegate( + fixtures.Alice, + fixtures.Bob, + fixtures.Alice.DID().String(), + storeAddCaveats{}, + ) + require.NoError(t, err) + + bob2mallory, err := storeAdd.Delegate( + fixtures.Bob, + fixtures.Mallory, + fixtures.Alice.DID().String(), + storeAddCaveats{}, + delegation.WithProof(delegation.FromDelegation(alice2bob)), + ) + require.NoError(t, err) + + inv, err := storeAdd.Invoke( + fixtures.Mallory, + fixtures.Service, + fixtures.Alice.DID().String(), + storeAddCaveats{Link: testLink}, + delegation.WithProof(delegation.FromDelegation(bob2mallory)), + ) + require.NoError(t, err) + + context := NewValidationContext( + fixtures.Service.Verifier(), + storeAdd, + IsSelfIssued, + validateAuthOk, + ProofUnavailable, + parseEdPrincipal, + FailDIDKeyResolution, + ) + + a, x := Access(inv, context) + require.NoError(t, x) + require.Equal(t, storeAdd.Can(), a.Capability().Can()) + require.Equal(t, fixtures.Alice.DID().String(), a.Capability().With()) + require.Equal(t, fixtures.Mallory.DID(), a.Issuer().DID()) + require.Equal(t, fixtures.Service.DID(), a.Audience().DID()) + + require.Equal(t, storeAdd.Can(), a.Proofs()[0].Capability().Can()) + require.Equal(t, fixtures.Alice.DID().String(), a.Proofs()[0].Capability().With()) + require.Equal(t, fixtures.Bob.DID(), a.Proofs()[0].Issuer().DID()) + require.Equal(t, fixtures.Mallory.DID(), a.Proofs()[0].Audience().DID()) + + require.Equal(t, storeAdd.Can(), a.Proofs()[0].Proofs()[0].Capability().Can()) + require.Equal(t, fixtures.Alice.DID().String(), a.Proofs()[0].Proofs()[0].Capability().With()) + require.Equal(t, fixtures.Alice.DID(), a.Proofs()[0].Proofs()[0].Issuer().DID()) + require.Equal(t, fixtures.Bob.DID(), a.Proofs()[0].Proofs()[0].Audience().DID()) + }) + + t.Run("resolve external proof", func(t *testing.T) { + dlg, err := storeAdd.Delegate( + fixtures.Alice, + fixtures.Bob, + fixtures.Alice.DID().String(), + storeAddCaveats{}, + ) + require.NoError(t, err) + + inv, err := storeAdd.Invoke( + fixtures.Bob, + fixtures.Service, + fixtures.Alice.DID().String(), + storeAddCaveats{Link: testLink}, + delegation.WithProof(delegation.FromDelegation(dlg)), + ) + require.NoError(t, err) + + context := NewValidationContext( + fixtures.Service.Verifier(), + storeAdd, + IsSelfIssued, + validateAuthOk, + func(p ucan.Link) (delegation.Delegation, UnavailableProof) { + if p == dlg.Link() { + return dlg, nil + } + return nil, NewUnavailableProofError(p, fmt.Errorf("no proof resolver configured")) + }, + parseEdPrincipal, + FailDIDKeyResolution, + ) + + a, x := Access(inv, context) + require.NoError(t, x) + require.Equal(t, storeAdd.Can(), a.Capability().Can()) + require.Equal(t, fixtures.Alice.DID().String(), a.Capability().With()) + require.Equal(t, fixtures.Bob.DID(), a.Issuer().DID()) + require.Equal(t, fixtures.Service.DID(), a.Audience().DID()) + + require.Equal(t, storeAdd.Can(), a.Proofs()[0].Capability().Can()) + require.Equal(t, fixtures.Alice.DID().String(), a.Proofs()[0].Capability().With()) + require.Equal(t, fixtures.Alice.DID(), a.Proofs()[0].Issuer().DID()) + require.Equal(t, fixtures.Bob.DID(), a.Proofs()[0].Audience().DID()) + }) + }) + + t.Run("unauthorized", func(t *testing.T) { + t.Run("expired invocation", func(t *testing.T) { + exp := ucan.Now() - 5 + inv, err := storeAdd.Invoke( + fixtures.Alice, + fixtures.Service, + fixtures.Alice.DID().String(), + storeAddCaveats{Link: testLink}, + delegation.WithExpiration(exp), + ) + require.NoError(t, err) + + context := NewValidationContext( + fixtures.Service.Verifier(), + storeAdd, + IsSelfIssued, + validateAuthOk, + ProofUnavailable, + parseEdPrincipal, + FailDIDKeyResolution, + ) + + a, x := Access(inv, context) + require.Nil(t, a) + require.Error(t, x) + require.Equal(t, x.Name(), "Unauthorized") + msg := strings.Join([]string{ + fmt.Sprintf("Claim %s is not authorized", storeAdd), + fmt.Sprintf(" - Proof %s has expired on %s", inv.Link(), time.Unix(int64(exp), 0).Format(time.RFC3339)), + }, "\n") + require.Equal(t, msg, x.Error()) + }) + + t.Run("not valid before", func(t *testing.T) { + nbf := ucan.Now() + 500 + inv, err := storeAdd.Invoke( + fixtures.Alice, + fixtures.Service, + fixtures.Alice.DID().String(), + storeAddCaveats{Link: testLink}, + delegation.WithNotBefore(nbf), + ) + require.NoError(t, err) + + context := NewValidationContext( + fixtures.Service.Verifier(), + storeAdd, + IsSelfIssued, + validateAuthOk, + ProofUnavailable, + parseEdPrincipal, + FailDIDKeyResolution, + ) + + a, x := Access(inv, context) + require.Nil(t, a) + require.Error(t, x) + require.Equal(t, x.Name(), "Unauthorized") + msg := strings.Join([]string{ + fmt.Sprintf("Claim %s is not authorized", storeAdd), + fmt.Sprintf(" - Proof %s is not valid before %s", inv.Link(), time.Unix(int64(nbf), 0).Format(time.RFC3339)), + }, "\n") + require.Equal(t, msg, x.Error()) + }) + + t.Run("invalid signature", func(t *testing.T) { + inv, err := storeAdd.Invoke( + fixtures.Alice, + fixtures.Service, + fixtures.Alice.DID().String(), + storeAddCaveats{Link: testLink}, + ) + require.NoError(t, err) + + inv.Data().Model().S = fixtures.Bob.Sign(inv.Root().Bytes()).Bytes() + + context := NewValidationContext( + fixtures.Service.Verifier(), + storeAdd, + IsSelfIssued, + validateAuthOk, + ProofUnavailable, + parseEdPrincipal, + FailDIDKeyResolution, + ) + + a, x := Access(inv, context) + require.Nil(t, a) + require.Error(t, x) + require.Equal(t, x.Name(), "Unauthorized") + msg := strings.Join([]string{ + fmt.Sprintf("Claim %s is not authorized", storeAdd), + fmt.Sprintf(" - Proof %s does not have a valid signature from %s", inv.Link(), fixtures.Alice.DID()), + }, "\n") + require.Equal(t, msg, x.Error()) + }) + + t.Run("unknown capability", func(t *testing.T) { + inv, err := invocation.Invoke( + fixtures.Alice, + fixtures.Service, + ucan.NewCapability( + "store/write", + fixtures.Alice.DID().String(), + ucan.NoCaveats{}, + ), + ) + require.NoError(t, err) + + context := NewValidationContext( + fixtures.Service.Verifier(), + storeAdd, + IsSelfIssued, + validateAuthOk, + ProofUnavailable, + parseEdPrincipal, + FailDIDKeyResolution, + ) + + a, x := Access(inv, context) + require.Nil(t, a) + require.Error(t, x) + require.Equal(t, x.Name(), "Unauthorized") + msg := strings.Join([]string{ + fmt.Sprintf("Claim %s is not authorized", storeAdd), + " - No matching delegated capability found", + " - Encountered unknown capabilities", + fmt.Sprintf(" - {\"can\":\"store/write\",\"with\":\"%s\",\"nb\":{}}", fixtures.Alice.DID()), + }, "\n") + require.Equal(t, msg, x.Error()) + }) + }) + + t.Run("invalid claim", func(t *testing.T) { + t.Run("no proofs", func(t *testing.T) { + inv, err := storeAdd.Invoke( + fixtures.Alice, + fixtures.Bob, + fixtures.Bob.DID().String(), + storeAddCaveats{Link: testLink}, + ) + require.NoError(t, err) + + context := NewValidationContext( + fixtures.Service.Verifier(), + storeAdd, + IsSelfIssued, + validateAuthOk, + ProofUnavailable, + parseEdPrincipal, + FailDIDKeyResolution, + ) + + a, x := Access(inv, context) + require.Nil(t, a) + require.Error(t, x) + require.Equal(t, x.Name(), "Unauthorized") + msg := strings.Join([]string{ + fmt.Sprintf("Claim %s is not authorized", storeAdd), + fmt.Sprintf(` - Capability {"can":"%s","with":"%s","nb":{"Link":{"/":"%s"},"Origin":null}} is not authorized because:`, storeAdd.Can(), fixtures.Bob.DID(), testLink), + fmt.Sprintf(" - Capability can not be (self) issued by '%s'", fixtures.Alice.DID()), + " - Delegated capability not found", + }, "\n") + require.Equal(t, msg, x.Error()) + }) + + t.Run("expired", func(t *testing.T) { + exp := ucan.Now() - 5 + dlg, err := storeAdd.Delegate( + fixtures.Alice, + fixtures.Bob, + fixtures.Alice.DID().String(), + storeAddCaveats{}, + delegation.WithExpiration(exp), + ) + require.NoError(t, err) + + inv, err := storeAdd.Invoke( + fixtures.Bob, + fixtures.Service, + fixtures.Alice.DID().String(), + storeAddCaveats{Link: testLink}, + delegation.WithProof(delegation.FromDelegation(dlg)), + ) + require.NoError(t, err) + + context := NewValidationContext( + fixtures.Service.Verifier(), + storeAdd, + IsSelfIssued, + validateAuthOk, + ProofUnavailable, + parseEdPrincipal, + FailDIDKeyResolution, + ) + + a, x := Access(inv, context) + require.Nil(t, a) + require.Error(t, x) + require.Equal(t, x.Name(), "Unauthorized") + msg := strings.Join([]string{ + fmt.Sprintf("Claim %s is not authorized", storeAdd), + fmt.Sprintf(` - Capability {"can":"%s","with":"%s","nb":{"Link":{"/":"%s"},"Origin":null}} is not authorized because:`, storeAdd.Can(), fixtures.Alice.DID(), testLink), + fmt.Sprintf(" - Capability can not be (self) issued by '%s'", fixtures.Bob.DID()), + fmt.Sprintf(" - Capability can not be derived from prf: %s because:", dlg.Link()), + fmt.Sprintf(" - Proof %s has expired on %s", dlg.Link(), time.Unix(int64(exp), 0).Format(time.RFC3339)), + }, "\n") + require.Equal(t, msg, x.Error()) + }) + + t.Run("not valid before", func(t *testing.T) { + nbf := ucan.Now() + 60*60 + dlg, err := storeAdd.Delegate( + fixtures.Alice, + fixtures.Bob, + fixtures.Alice.DID().String(), + storeAddCaveats{}, + delegation.WithNotBefore(nbf), + ) + require.NoError(t, err) + + inv, err := storeAdd.Invoke( + fixtures.Bob, + fixtures.Service, + fixtures.Alice.DID().String(), + storeAddCaveats{Link: testLink}, + delegation.WithProof(delegation.FromDelegation(dlg)), + ) + require.NoError(t, err) + + context := NewValidationContext( + fixtures.Service.Verifier(), + storeAdd, + IsSelfIssued, + validateAuthOk, + ProofUnavailable, + parseEdPrincipal, + FailDIDKeyResolution, + ) + + a, x := Access(inv, context) + require.Nil(t, a) + require.Error(t, x) + require.Equal(t, x.Name(), "Unauthorized") + msg := strings.Join([]string{ + fmt.Sprintf("Claim %s is not authorized", storeAdd), + fmt.Sprintf(` - Capability {"can":"%s","with":"%s","nb":{"Link":{"/":"%s"},"Origin":null}} is not authorized because:`, storeAdd.Can(), fixtures.Alice.DID(), testLink), + fmt.Sprintf(" - Capability can not be (self) issued by '%s'", fixtures.Bob.DID()), + fmt.Sprintf(" - Capability can not be derived from prf: %s because:", dlg.Link()), + fmt.Sprintf(" - Proof %s is not valid before %s", dlg.Link(), time.Unix(int64(nbf), 0).Format(time.RFC3339)), + }, "\n") + require.Equal(t, msg, x.Error()) + }) + + t.Run("invalid signature", func(t *testing.T) { + // In order to mess up the signature we need to reach deep in UCAN library + // to create a UCAN model, manually setting the signature to something bad + // and then encode it as the root block of the delegation. + nb, _ := storeAddCaveats{Link: testLink}.Build() + exp := ucan.Now() + 30 + model := udm.UCANModel{ + V: "0.9.1", + S: fixtures.Alice.Sign([]byte{}).Bytes(), + Iss: fixtures.Alice.DID().Bytes(), + Aud: fixtures.Bob.DID().Bytes(), + Att: []udm.CapabilityModel{ + { + Can: storeAdd.Can(), + With: fixtures.Alice.DID().String(), + Nb: nb, + }, + }, + Exp: &exp, + } - cap := ucan.NewCapability("upload/add", alice.DID().String(), struct{}{}) + rt, err := block.Encode(&model, udm.Type(), cbor.Codec, sha256.Hasher) + require.NoError(t, err) + + bs, err := blockstore.NewBlockStore(blockstore.WithBlocks([]block.Block{rt})) + require.NoError(t, err) + + dlg := delegation.NewDelegation(rt, bs) + + inv, err := storeAdd.Invoke( + fixtures.Bob, + fixtures.Service, + fixtures.Alice.DID().String(), + storeAddCaveats{Link: testLink}, + delegation.WithProof(delegation.FromDelegation(dlg)), + ) + require.NoError(t, err) + + context := NewValidationContext( + fixtures.Service.Verifier(), + storeAdd, + IsSelfIssued, + validateAuthOk, + ProofUnavailable, + parseEdPrincipal, + FailDIDKeyResolution, + ) + + a, x := Access(inv, context) + require.Nil(t, a) + require.Error(t, x) + require.Equal(t, x.Name(), "Unauthorized") + msg := strings.Join([]string{ + fmt.Sprintf("Claim %s is not authorized", storeAdd), + fmt.Sprintf(` - Capability {"can":"%s","with":"%s","nb":{"Link":{"/":"%s"},"Origin":null}} is not authorized because:`, storeAdd.Can(), fixtures.Alice.DID(), testLink), + fmt.Sprintf(" - Capability can not be (self) issued by '%s'", fixtures.Bob.DID()), + fmt.Sprintf(" - Capability can not be derived from prf: %s because:", dlg.Link()), + fmt.Sprintf(" - Proof %s does not have a valid signature from %s", dlg.Link(), fixtures.Alice.DID()), + }, "\n") + require.Equal(t, msg, x.Error()) + }) + + t.Run("unknown capability", func(t *testing.T) { + dlg, err := delegation.Delegate( + fixtures.Alice, + fixtures.Bob, + []ucan.Capability[ucan.NoCaveats]{ + ucan.NewCapability("store/pin", fixtures.Alice.DID().String(), ucan.NoCaveats{}), + }, + ) + require.NoError(t, err) + + inv, err := storeAdd.Invoke( + fixtures.Bob, + fixtures.Service, + fixtures.Alice.DID().String(), + storeAddCaveats{Link: testLink}, + delegation.WithProof(delegation.FromDelegation(dlg)), + ) + require.NoError(t, err) + + context := NewValidationContext( + fixtures.Service.Verifier(), + storeAdd, + IsSelfIssued, + validateAuthOk, + ProofUnavailable, + parseEdPrincipal, + FailDIDKeyResolution, + ) + + a, x := Access(inv, context) + require.Nil(t, a) + require.Error(t, x) + require.Equal(t, x.Name(), "Unauthorized") + msg := strings.Join([]string{ + fmt.Sprintf("Claim %s is not authorized", storeAdd), + fmt.Sprintf(` - Capability {"can":"%s","with":"%s","nb":{"Link":{"/":"%s"},"Origin":null}} is not authorized because:`, storeAdd.Can(), fixtures.Alice.DID(), testLink), + fmt.Sprintf(" - Capability can not be (self) issued by '%s'", fixtures.Bob.DID()), + " - Delegated capability not found", + " - Encountered unknown capabilities", + fmt.Sprintf(` - {"can":"store/pin","with":"%s","nb":{}}`, fixtures.Alice.DID()), + }, "\n") + require.Equal(t, msg, x.Error()) + }) + + t.Run("malformed capability", func(t *testing.T) { + badDID := fmt.Sprintf("bib:%s", fixtures.Alice.DID().String()[4:]) + dlg, err := storeAdd.Delegate( + fixtures.Alice, + fixtures.Bob, + badDID, + storeAddCaveats{}, + ) + require.NoError(t, err) + + inv, err := storeAdd.Invoke( + fixtures.Bob, + fixtures.Service, + fixtures.Alice.DID().String(), + storeAddCaveats{Link: testLink}, + delegation.WithProof(delegation.FromDelegation(dlg)), + ) + require.NoError(t, err) + + context := NewValidationContext( + fixtures.Service.Verifier(), + storeAdd, + IsSelfIssued, + validateAuthOk, + ProofUnavailable, + parseEdPrincipal, + FailDIDKeyResolution, + ) + + a, x := Access(inv, context) + require.Nil(t, a) + require.Error(t, x) + require.Equal(t, x.Name(), "Unauthorized") + msg := strings.Join([]string{ + fmt.Sprintf("Claim %s is not authorized", storeAdd), + fmt.Sprintf(` - Capability {"can":"%s","with":"%s","nb":{"Link":{"/":"%s"},"Origin":null}} is not authorized because:`, storeAdd.Can(), fixtures.Alice.DID(), testLink), + fmt.Sprintf(" - Capability can not be (self) issued by '%s'", fixtures.Bob.DID()), + fmt.Sprintf(` - Cannot derive {"can":"%s","with":"%s","nb":{"Link":{"/":"%s"},"Origin":null}} from delegated capabilities:`, storeAdd.Can(), fixtures.Alice.DID(), testLink), + fmt.Sprintf(` - Encountered malformed '%s' capability: {"can":"%s","with":"%s","nb":{}}`, storeAdd.Can(), storeAdd.Can(), badDID), + fmt.Sprintf(` - Expected a "did:" but got "%s" instead`, badDID), + }, "\n") + require.Equal(t, msg, x.Error()) + }) + + t.Run("unavailable proof", func(t *testing.T) { + dlg, err := storeAdd.Delegate( + fixtures.Alice, + fixtures.Bob, + fixtures.Alice.DID().String(), + storeAddCaveats{}, + ) + require.NoError(t, err) + + inv, err := storeAdd.Invoke( + fixtures.Bob, + fixtures.Service, + fixtures.Alice.DID().String(), + storeAddCaveats{Link: testLink}, + delegation.WithProof(delegation.FromLink(dlg.Link())), + ) + require.NoError(t, err) + + context := NewValidationContext( + fixtures.Service.Verifier(), + storeAdd, + IsSelfIssued, + validateAuthOk, + ProofUnavailable, + parseEdPrincipal, + FailDIDKeyResolution, + ) + + a, x := Access(inv, context) + require.Nil(t, a) + require.Error(t, x) + require.Equal(t, x.Name(), "Unauthorized") + msg := strings.Join([]string{ + fmt.Sprintf("Claim %s is not authorized", storeAdd), + fmt.Sprintf(` - Capability {"can":"%s","with":"%s","nb":{"Link":{"/":"%s"},"Origin":null}} is not authorized because:`, storeAdd.Can(), fixtures.Alice.DID(), testLink), + fmt.Sprintf(" - Capability can not be (self) issued by '%s'", fixtures.Bob.DID()), + fmt.Sprintf(` - Capability can not be derived from prf: %s because:`, dlg.Link()), + fmt.Sprintf(` - Linked proof "%s" is not included and could not be resolved`, dlg.Link()), + ` - Proof resolution failed with: no proof resolver configured`, + }, "\n") + require.Equal(t, msg, x.Error()) + }) + + t.Run("invalid audience", func(t *testing.T) { + dlg, err := storeAdd.Delegate( + fixtures.Alice, + fixtures.Bob, + fixtures.Alice.DID().String(), + storeAddCaveats{}, + ) + require.NoError(t, err) + + inv, err := storeAdd.Invoke( + fixtures.Mallory, + fixtures.Service, + fixtures.Alice.DID().String(), + storeAddCaveats{Link: testLink}, + delegation.WithProof(delegation.FromDelegation(dlg)), + ) + require.NoError(t, err) + + context := NewValidationContext( + fixtures.Service.Verifier(), + storeAdd, + IsSelfIssued, + validateAuthOk, + ProofUnavailable, + parseEdPrincipal, + FailDIDKeyResolution, + ) + + a, x := Access(inv, context) + require.Nil(t, a) + require.Error(t, x) + require.Equal(t, x.Name(), "Unauthorized") + msg := strings.Join([]string{ + fmt.Sprintf("Claim %s is not authorized", storeAdd), + fmt.Sprintf(` - Capability {"can":"%s","with":"%s","nb":{"Link":{"/":"%s"},"Origin":null}} is not authorized because:`, storeAdd.Can(), fixtures.Alice.DID(), testLink), + fmt.Sprintf(" - Capability can not be (self) issued by '%s'", fixtures.Mallory.DID()), + fmt.Sprintf(` - Capability can not be derived from prf: %s because:`, dlg.Link()), + fmt.Sprintf(` - Delegation audience is '%s' instead of '%s'`, fixtures.Bob.DID(), fixtures.Mallory.DID()), + }, "\n") + require.Equal(t, msg, x.Error()) + }) + + t.Run("invalid claim", func(t *testing.T) { + dlg, err := storeAdd.Delegate( + fixtures.Alice, + fixtures.Bob, + fixtures.Mallory.DID().String(), + storeAddCaveats{}, + ) + require.NoError(t, err) + + nb := storeAddCaveats{Link: testLink} + inv, err := storeAdd.Invoke( + fixtures.Bob, + fixtures.Service, + fixtures.Alice.DID().String(), + nb, + delegation.WithProof(delegation.FromDelegation(dlg)), + ) + require.NoError(t, err) + + context := NewValidationContext( + fixtures.Service.Verifier(), + storeAdd, + IsSelfIssued, + validateAuthOk, + ProofUnavailable, + parseEdPrincipal, + FailDIDKeyResolution, + ) + + a, x := Access(inv, context) + require.Nil(t, a) + require.Error(t, x) + require.Equal(t, x.Name(), "Unauthorized") + msg := strings.Join([]string{ + fmt.Sprintf("Claim %s is not authorized", storeAdd), + fmt.Sprintf(` - Capability {"can":"%s","with":"%s","nb":{"Link":{"/":"%s"},"Origin":null}} is not authorized because:`, storeAdd.Can(), fixtures.Alice.DID(), testLink), + fmt.Sprintf(" - Capability can not be (self) issued by '%s'", fixtures.Bob.DID()), + fmt.Sprintf(` - Cannot derive {"can":"%s","with":"%s","nb":{"Link":{"/":"%s"},"Origin":null}} from delegated capabilities:`, storeAdd.Can(), fixtures.Alice.DID(), testLink), + fmt.Sprintf(` - Constraint violation: Expected 'with: "%s"' instead got '%s'`, fixtures.Mallory.DID(), fixtures.Alice.DID()), + }, "\n") + require.Equal(t, msg, x.Error()) + }) + + t.Run("invalid sub delegation", func(t *testing.T) { + prf, err := storeAdd.Delegate( + fixtures.Alice, + fixtures.Bob, + fixtures.Service.DID().String(), + storeAddCaveats{}, + ) + require.NoError(t, err) + + dlg, err := storeAdd.Delegate( + fixtures.Bob, + fixtures.Mallory, + fixtures.Service.DID().String(), + storeAddCaveats{}, + delegation.WithProof(delegation.FromDelegation(prf)), + ) + require.NoError(t, err) + + nb := storeAddCaveats{Link: testLink} + inv, err := storeAdd.Invoke( + fixtures.Mallory, + fixtures.Service, + fixtures.Service.DID().String(), + nb, + delegation.WithProof(delegation.FromDelegation(dlg)), + ) + require.NoError(t, err) + + context := NewValidationContext( + fixtures.Service.Verifier(), + storeAdd, + IsSelfIssued, + validateAuthOk, + ProofUnavailable, + parseEdPrincipal, + FailDIDKeyResolution, + ) + + cstr := fmt.Sprintf(`{"can":"%s","with":"%s","nb":{"Link":{"/":"%s"},"Origin":null}}`, storeAdd.Can(), fixtures.Service.DID(), testLink) + a, x := Access(inv, context) + require.Nil(t, a) + require.Error(t, x) + require.Equal(t, x.Name(), "Unauthorized") + msg := strings.Join([]string{ + fmt.Sprintf("Claim %s is not authorized", storeAdd), + fmt.Sprintf(` - Capability %s is not authorized because:`, cstr), + fmt.Sprintf(" - Capability can not be (self) issued by '%s'", fixtures.Mallory.DID()), + fmt.Sprintf(` - Capability %s is not authorized because:`, cstr), + fmt.Sprintf(` - Capability can not be (self) issued by '%s'`, fixtures.Bob.DID()), + fmt.Sprintf(` - Capability %s is not authorized because:`, cstr), + fmt.Sprintf(` - Capability can not be (self) issued by '%s'`, fixtures.Alice.DID()), + " - Delegated capability not found", + }, "\n") + require.Equal(t, msg, x.Error()) + }) + + t.Run("principal alignment", func(t *testing.T) { + prf, err := storeAdd.Delegate( + fixtures.Alice, + fixtures.Bob, + fixtures.Alice.DID().String(), + storeAddCaveats{}, + ) + require.NoError(t, err) + + nb := storeAddCaveats{Link: testLink} + inv, err := storeAdd.Invoke( + fixtures.Mallory, + fixtures.Service, + fixtures.Alice.DID().String(), + nb, + delegation.WithProof(delegation.FromDelegation(prf)), + ) + require.NoError(t, err) + + context := NewValidationContext( + fixtures.Service.Verifier(), + storeAdd, + IsSelfIssued, + validateAuthOk, + ProofUnavailable, + parseEdPrincipal, + FailDIDKeyResolution, + ) + + cstr := fmt.Sprintf(`{"can":"%s","with":"%s","nb":{"Link":{"/":"%s"},"Origin":null}}`, storeAdd.Can(), fixtures.Alice.DID(), testLink) + a, x := Access(inv, context) + require.Nil(t, a) + require.Error(t, x) + require.Equal(t, x.Name(), "Unauthorized") + msg := strings.Join([]string{ + fmt.Sprintf("Claim %s is not authorized", storeAdd), + fmt.Sprintf(` - Capability %s is not authorized because:`, cstr), + fmt.Sprintf(" - Capability can not be (self) issued by '%s'", fixtures.Mallory.DID()), + fmt.Sprintf(` - Capability can not be derived from prf: %s because:`, prf.Link()), + fmt.Sprintf(` - Delegation audience is '%s' instead of '%s'`, fixtures.Bob.DID(), fixtures.Mallory.DID()), + }, "\n") + require.Equal(t, msg, x.Error()) + }) + + t.Run("invalid delegation chain", func(t *testing.T) { + space := fixtures.Alice + + prf, err := storeAdd.Delegate( + space, + fixtures.Service, + space.DID().String(), + storeAddCaveats{}, + ) + require.NoError(t, err) + + nb := storeAddCaveats{Link: testLink} + inv, err := storeAdd.Invoke( + fixtures.Bob, + fixtures.Service, + space.DID().String(), + nb, + delegation.WithProof(delegation.FromDelegation(prf)), + ) + require.NoError(t, err) + + context := NewValidationContext( + fixtures.Service.Verifier(), + storeAdd, + IsSelfIssued, + validateAuthOk, + ProofUnavailable, + parseEdPrincipal, + FailDIDKeyResolution, + ) + + cstr := fmt.Sprintf(`{"can":"%s","with":"%s","nb":{"Link":{"/":"%s"},"Origin":null}}`, storeAdd.Can(), space.DID(), testLink) + a, x := Access(inv, context) + require.Nil(t, a) + require.Error(t, x) + require.Equal(t, x.Name(), "Unauthorized") + msg := strings.Join([]string{ + fmt.Sprintf("Claim %s is not authorized", storeAdd), + fmt.Sprintf(` - Capability %s is not authorized because:`, cstr), + fmt.Sprintf(" - Capability can not be (self) issued by '%s'", fixtures.Bob.DID()), + fmt.Sprintf(` - Capability can not be derived from prf: %s because:`, prf.Link()), + fmt.Sprintf(` - Delegation audience is '%s' instead of '%s'`, fixtures.Service.DID(), fixtures.Bob.DID()), + }, "\n") + require.Equal(t, msg, x.Error()) + }) + }) +} + +func TestClaim(t *testing.T) { + t.Run("without a proof", func(t *testing.T) { + dlg, err := storeAdd.Delegate( + fixtures.Alice, + fixtures.Service, + fixtures.Alice.DID().String(), + storeAddCaveats{}, + ) + require.NoError(t, err) + + context := NewValidationContext( + fixtures.Service.Verifier(), + storeAdd, + IsSelfIssued, + validateAuthOk, + ProofUnavailable, + parseEdPrincipal, + FailDIDKeyResolution, + ) + + a, x := Claim(storeAdd, []delegation.Proof{delegation.FromLink(dlg.Link())}, context) + require.Nil(t, a) + require.Error(t, x) + require.Equal(t, x.Name(), "Unauthorized") + msg := strings.Join([]string{ + fmt.Sprintf("Claim %s is not authorized", storeAdd), + fmt.Sprintf(` - Linked proof "%s" is not included and could not be resolved`, dlg.Link()), + ` - Proof resolution failed with: no proof resolver configured`, + }, "\n") + require.Equal(t, msg, x.Error()) + }) + + t.Run("mismatched signature", func(t *testing.T) { + svcdid, err := did.Parse("did:web:w3.storage") + require.NoError(t, err) + + old, err := signer.Wrap(fixtures.Alice, svcdid) + require.NoError(t, err) + + new, err := signer.Wrap(fixtures.Bob, svcdid) + require.NoError(t, err) + + dlg, err := storeAdd.Delegate( + old, + old, + old.DID().String(), + storeAddCaveats{Link: testLink}, + ) + require.NoError(t, err) + + context := NewValidationContext( + new.Verifier(), + storeAdd, + IsSelfIssued, + validateAuthOk, + ProofUnavailable, + parseEdPrincipal, + FailDIDKeyResolution, + ) + + a, x := Claim(storeAdd, []delegation.Proof{delegation.FromDelegation(dlg)}, context) + require.Nil(t, a) + require.Error(t, x) + require.Equal(t, x.Name(), "Unauthorized") + msg := strings.Join([]string{ + fmt.Sprintf("Claim %s is not authorized", storeAdd), + fmt.Sprintf(` - Proof %s issued by %s does not have a valid signature from %s`, dlg.Link(), new.DID(), new.DID()), + ` ℹ️ Issuer probably signed with a different key, which got rotated, invalidating delegations that were issued with prior keys`, + }, "\n") + require.Equal(t, msg, x.Error()) + }) +} + +func TestIsSelfIssued(t *testing.T) { + cap := ucan.NewCapability("upload/add", fixtures.Alice.DID().String(), struct{}{}) - canIssue := IsSelfIssued(cap, alice.DID()) + canIssue := IsSelfIssued(cap, fixtures.Alice.DID()) if canIssue == false { t.Fatal("capability self issued by alice") } - canIssue = IsSelfIssued(cap, bob.DID()) + canIssue = IsSelfIssued(cap, fixtures.Bob.DID()) if canIssue == true { t.Fatal("capability not self issued by bob") } diff --git a/validator/session_test.go b/validator/session_test.go new file mode 100644 index 0000000..5b2556a --- /dev/null +++ b/validator/session_test.go @@ -0,0 +1,264 @@ +package validator + +import ( + "encoding/base64" + "fmt" + "strings" + "testing" + + "github.com/ipld/go-ipld-prime" + "github.com/ipld/go-ipld-prime/node/basicnode" + "github.com/storacha-network/go-ucanto/core/delegation" + "github.com/storacha-network/go-ucanto/core/result/failure" + "github.com/storacha-network/go-ucanto/core/schema" + "github.com/storacha-network/go-ucanto/did" + "github.com/storacha-network/go-ucanto/principal/absentee" + "github.com/storacha-network/go-ucanto/principal/ed25519/signer" + "github.com/storacha-network/go-ucanto/testing/fixtures" + "github.com/storacha-network/go-ucanto/testing/helpers" + "github.com/storacha-network/go-ucanto/ucan" + "github.com/stretchr/testify/require" +) + +type debugEchoCaveats struct { + Message *string +} + +func (c debugEchoCaveats) Build() (ipld.Node, error) { + np := basicnode.Prototype.Any + nb := np.NewBuilder() + ma, _ := nb.BeginMap(1) + if c.Message != nil { + ma.AssembleKey().AssignString("message") + ma.AssembleValue().AssignString(*c.Message) + } + ma.Finish() + return nb.Build(), nil +} + +var debugEchoTyp = helpers.Must(ipld.LoadSchemaBytes([]byte(` + type DebugEchoCaveats struct { + message optional String + } +`))) + +var debugEcho = NewCapability( + "debug/echo", + schema.DIDString(schema.WithMethod("mailto")), + schema.Struct[debugEchoCaveats](debugEchoTyp.TypeByName("DebugEchoCaveats"), nil), + func(claimed, delegated ucan.Capability[debugEchoCaveats]) failure.Failure { + if claimed.With() != delegated.With() { + err := fmt.Errorf("Expected 'with: \"%s\"' instead got '%s'", delegated.With(), claimed.With()) + return failure.FromError(err) + } + return nil + }, +) + +type attestCaveats struct { + Proof ipld.Link +} + +func (c attestCaveats) Build() (ipld.Node, error) { + np := basicnode.Prototype.Any + nb := np.NewBuilder() + ma, _ := nb.BeginMap(1) + ma.AssembleKey().AssignString("proof") + ma.AssembleValue().AssignLink(c.Proof) + ma.Finish() + return nb.Build(), nil +} + +var attestTyp = helpers.Must(ipld.LoadSchemaBytes([]byte(` + type AttestCaveats struct { + proof Link + } +`))) + +var attest = NewCapability( + "ucan/attest", + schema.DIDString(), + schema.Struct[attestCaveats](attestTyp.TypeByName("AttestCaveats"), nil), + func(claimed, delegated ucan.Capability[attestCaveats]) failure.Failure { + if claimed.With() != delegated.With() { + err := fmt.Errorf("Expected 'with: \"%s\"' instead got '%s'", delegated.With(), claimed.With()) + return failure.FromError(err) + } + return nil + }, +) + +func TestSession(t *testing.T) { + t.Run("validate mailto", func(t *testing.T) { + agent := fixtures.Alice + account := absentee.From(helpers.Must(did.Parse("did:mailto:web.mail:alice"))) + + prf, err := debugEcho.Delegate( + account, + agent, + account.DID().String(), + debugEchoCaveats{}, + ) + require.NoError(t, err) + + session, err := attest.Delegate( + fixtures.Service, + agent, + fixtures.Service.DID().String(), + attestCaveats{Proof: prf.Link()}, + ) + require.NoError(t, err) + + msg := "Hello World" + nb := debugEchoCaveats{Message: &msg} + inv, err := debugEcho.Invoke( + agent, + fixtures.Service, + account.DID().String(), + nb, + delegation.WithProofs(delegation.Proofs{ + delegation.FromDelegation(prf), + delegation.FromDelegation(session), + }), + ) + require.NoError(t, err) + + context := NewValidationContext( + fixtures.Service.Verifier(), + debugEcho, + IsSelfIssued, + validateAuthOk, + ProofUnavailable, + parseEdPrincipal, + FailDIDKeyResolution, + ) + + a, x := Access(inv, context) + require.NoError(t, x) + require.Equal(t, debugEcho.Can(), a.Capability().Can()) + require.Equal(t, account.DID().String(), a.Capability().With()) + require.Equal(t, nb, a.Capability().Nb()) + }) + + t.Run("delegated ucan attest", func(t *testing.T) { + agent := fixtures.Alice + account := absentee.From(helpers.Must(did.Parse("did:mailto:web.mail:alice"))) + + manager, err := signer.Generate() + require.NoError(t, err) + worker, err := signer.Generate() + require.NoError(t, err) + + authority, err := delegation.Delegate( + manager, + worker, + []ucan.Capability[ucan.NoCaveats]{ + ucan.NewCapability("*", fixtures.Service.DID().String(), ucan.NoCaveats{}), + }, + delegation.WithNoExpiration(), + delegation.WithProof( + delegation.FromDelegation( + helpers.Must( + delegation.Delegate( + fixtures.Service, + manager, + []ucan.Capability[ucan.NoCaveats]{ + ucan.NewCapability("*", fixtures.Service.DID().String(), ucan.NoCaveats{}), + }, + ), + ), + ), + ), + ) + require.NoError(t, err) + + prf, err := debugEcho.Delegate( + account, + agent, + account.DID().String(), + debugEchoCaveats{}, + delegation.WithNoExpiration(), + ) + require.NoError(t, err) + + require.Equal( + t, + helpers.Must(base64.RawStdEncoding.DecodeString("gKADAA")), + prf.Signature().Bytes(), + "should have blank signature", + ) + + session, err := attest.Delegate( + worker, + agent, + fixtures.Service.DID().String(), + attestCaveats{Proof: prf.Link()}, + delegation.WithProof(delegation.FromDelegation(authority)), + ) + require.NoError(t, err) + + msg := "Hello World" + nb := debugEchoCaveats{Message: &msg} + inv, err := debugEcho.Invoke( + agent, + fixtures.Service, + account.DID().String(), + nb, + delegation.WithProofs(delegation.Proofs{ + delegation.FromDelegation(session), + delegation.FromDelegation(prf), + }), + ) + require.NoError(t, err) + + context := NewValidationContext( + fixtures.Service.Verifier(), + debugEcho, + IsSelfIssued, + validateAuthOk, + ProofUnavailable, + parseEdPrincipal, + FailDIDKeyResolution, + ) + + a, x := Access(inv, context) + require.NoError(t, x) + require.Equal(t, debugEcho.Can(), a.Capability().Can()) + require.Equal(t, account.DID().String(), a.Capability().With()) + require.Equal(t, nb, a.Capability().Nb()) + }) + + t.Run("fail without proofs", func(t *testing.T) { + account := absentee.From(helpers.Must(did.Parse("did:mailto:web.mail:alice"))) + + msg := "Hello World" + nb := debugEchoCaveats{Message: &msg} + inv, err := debugEcho.Invoke( + account, + fixtures.Service, + account.DID().String(), + nb, + ) + require.NoError(t, err) + + context := NewValidationContext( + fixtures.Service.Verifier(), + debugEcho, + IsSelfIssued, + validateAuthOk, + ProofUnavailable, + parseEdPrincipal, + FailDIDKeyResolution, + ) + + a, x := Access(inv, context) + require.Nil(t, a) + require.Error(t, x) + require.Equal(t, x.Name(), "Unauthorized") + errmsg := strings.Join([]string{ + fmt.Sprintf("Claim %s is not authorized", debugEcho), + fmt.Sprintf(` - Unable to resolve '%s' key`, account.DID()), + }, "\n") + require.Equal(t, errmsg, x.Error()) + }) +} diff --git a/validator/util.go b/validator/util.go new file mode 100644 index 0000000..f5e2969 --- /dev/null +++ b/validator/util.go @@ -0,0 +1,38 @@ +package validator + +func combine[T any](dataset [][]T) [][]T { + first, rest := dataset[0], dataset[1:] + results := make([][]T, 0, len(first)) + for _, value := range first { + results = append(results, []T{value}) + } + for _, values := range rest { + tuples := results + results = make([][]T, 0, len(tuples)) + for _, value := range values { + for _, tuple := range tuples { + newTuple := make([]T, len(tuple), len(tuple)+1) + _ = copy(newTuple, tuple) + results = append(results, append(newTuple, value)) + } + } + } + return results +} + +func intersection[T comparable](left []T, right []T) []T { + set := make([]T, 0) + hash := make(map[T]struct{}) + + for _, v := range left { + hash[v] = struct{}{} + } + + for _, v := range right { + if _, ok := hash[v]; ok { + set = append(set, v) + } + } + + return set +}