github.com/storacha/go-ucanto@v0.7.2/core/delegation/delegation.go (about)

     1  package delegation
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io"
     7  	"iter"
     8  	"slices"
     9  
    10  	"github.com/ipfs/go-cid"
    11  	"github.com/multiformats/go-multibase"
    12  	"github.com/multiformats/go-multicodec"
    13  	"github.com/multiformats/go-multihash"
    14  	"github.com/storacha/go-ucanto/core/car"
    15  	"github.com/storacha/go-ucanto/core/dag/blockstore"
    16  	adm "github.com/storacha/go-ucanto/core/delegation/datamodel"
    17  	"github.com/storacha/go-ucanto/core/ipld"
    18  	"github.com/storacha/go-ucanto/core/ipld/block"
    19  	"github.com/storacha/go-ucanto/core/ipld/codec/cbor"
    20  	"github.com/storacha/go-ucanto/core/ipld/hash/sha256"
    21  	"github.com/storacha/go-ucanto/core/iterable"
    22  	"github.com/storacha/go-ucanto/ucan"
    23  	"github.com/storacha/go-ucanto/ucan/crypto/signature"
    24  	udm "github.com/storacha/go-ucanto/ucan/datamodel/ucan"
    25  )
    26  
    27  type exportConfig struct {
    28  	omitProofs []ipld.Link
    29  }
    30  
    31  type ExportOption func(c *exportConfig)
    32  
    33  // WithOmitProof excludes the specified proof blocks from the export.
    34  func WithOmitProof(proof ...ipld.Link) ExportOption {
    35  	return func(c *exportConfig) {
    36  		c.omitProofs = append(c.omitProofs, proof...)
    37  	}
    38  }
    39  
    40  // Delagation is a materialized view of a UCAN delegation, which can be encoded
    41  // into a UCAN token and used as proof for an invocation or further delegations.
    42  type Delegation interface {
    43  	ipld.View
    44  	ucan.UCAN
    45  	// Data returns the UCAN view of the delegation.
    46  	Data() ucan.View
    47  	// Link returns the IPLD link of the root block of the delegation.
    48  	Link() ucan.Link
    49  	// Archive writes the delegation to a Content Addressed aRchive (CAR).
    50  	Archive() io.Reader
    51  	// Export ONLY the blocks for the delegation and it's proofs, as well
    52  	// as blocks attached using Attach.
    53  	//
    54  	// Note: this differs from calling Blocks - which simply iterates over all
    55  	// blocks in the block reader that backs this [ipld.View].
    56  	Export(options ...ExportOption) iter.Seq2[block.Block, error]
    57  	// Attach a block to the delegation DAG so it will be included in the block
    58  	// iterator. You should only attach blocks that are referenced from
    59  	// `Capabilities` or `Facts`.
    60  	Attach(block block.Block) error
    61  }
    62  
    63  type delegation struct {
    64  	rt       ipld.Block
    65  	blks     blockstore.BlockReader
    66  	atchblks blockstore.BlockStore
    67  	ucan     ucan.View
    68  }
    69  
    70  var _ Delegation = (*delegation)(nil)
    71  
    72  func (d *delegation) Data() ucan.View {
    73  	return d.ucan
    74  }
    75  
    76  func (d *delegation) Root() ipld.Block {
    77  	return d.rt
    78  }
    79  
    80  func (d *delegation) Link() ucan.Link {
    81  	return d.rt.Link()
    82  }
    83  
    84  func (d *delegation) Blocks() iter.Seq2[ipld.Block, error] {
    85  	return iterable.Concat2(d.blks.Iterator(), d.atchblks.Iterator())
    86  }
    87  
    88  func (d *delegation) Export(options ...ExportOption) iter.Seq2[ipld.Block, error] {
    89  	return export(d.ucan, d.rt, d.blks, d.atchblks, options...)
    90  }
    91  
    92  func (d *delegation) Archive() io.Reader {
    93  	return Archive(d)
    94  }
    95  
    96  func (d *delegation) Issuer() ucan.Principal {
    97  	return d.Data().Issuer()
    98  }
    99  
   100  func (d *delegation) Audience() ucan.Principal {
   101  	return d.Data().Audience()
   102  }
   103  
   104  func (d *delegation) Version() ucan.Version {
   105  	return d.Data().Version()
   106  }
   107  
   108  func (d *delegation) Capabilities() []ucan.Capability[any] {
   109  	return d.Data().Capabilities()
   110  }
   111  
   112  func (d *delegation) Expiration() *ucan.UTCUnixTimestamp {
   113  	return d.Data().Expiration()
   114  }
   115  
   116  func (d *delegation) NotBefore() ucan.UTCUnixTimestamp {
   117  	return d.Data().NotBefore()
   118  }
   119  
   120  func (d *delegation) Nonce() ucan.Nonce {
   121  	return d.Data().Nonce()
   122  }
   123  
   124  func (d *delegation) Facts() []ucan.Fact {
   125  	return d.Data().Facts()
   126  }
   127  
   128  func (d *delegation) Proofs() []ucan.Link {
   129  	return d.Data().Proofs()
   130  }
   131  
   132  func (d *delegation) Signature() signature.SignatureView {
   133  	return d.Data().Signature()
   134  }
   135  
   136  func (d *delegation) Attach(b block.Block) error {
   137  	return d.atchblks.Put(b)
   138  }
   139  
   140  func NewDelegation(root ipld.Block, bs blockstore.BlockReader) (Delegation, error) {
   141  	ucan, err := decode(root)
   142  	if err != nil {
   143  		return nil, fmt.Errorf("decoding UCAN: %w", err)
   144  	}
   145  	attachments, err := blockstore.NewBlockStore()
   146  	if err != nil {
   147  		return nil, err
   148  	}
   149  	return &delegation{rt: root, ucan: ucan, blks: bs, atchblks: attachments}, nil
   150  }
   151  
   152  func NewDelegationView(root ipld.Link, bs blockstore.BlockReader) (Delegation, error) {
   153  	blk, ok, err := bs.Get(root)
   154  	if err != nil {
   155  		return nil, fmt.Errorf("getting delegation root block: %w", err)
   156  	}
   157  	if !ok {
   158  		return nil, fmt.Errorf("missing delegation root block: %s", root)
   159  	}
   160  	return NewDelegation(blk, bs)
   161  }
   162  
   163  // export the blocks that comprise the delegation, including all extra attached
   164  // blocks.
   165  func export(rt ucan.View, rtblk ipld.Block, blks blockstore.BlockReader, atchblks blockstore.BlockReader, options ...ExportOption) iter.Seq2[ipld.Block, error] {
   166  	config := exportConfig{}
   167  	for _, o := range options {
   168  		o(&config)
   169  	}
   170  	return func(yield func(ipld.Block, error) bool) {
   171  		for _, p := range rt.Proofs() {
   172  			if slices.ContainsFunc(config.omitProofs, func(link ipld.Link) bool {
   173  				return link.String() == p.String()
   174  			}) {
   175  				continue
   176  			}
   177  			proofblk, ok, err := blks.Get(p)
   178  			if err != nil {
   179  				yield(nil, err)
   180  				return
   181  			}
   182  			if !ok {
   183  				continue
   184  			}
   185  			prf, err := decode(proofblk)
   186  			if err != nil {
   187  				yield(nil, err)
   188  				return
   189  			}
   190  			for b, err := range export(prf, proofblk, blks, nil, options...) {
   191  				if !yield(b, err) {
   192  					return
   193  				}
   194  				if err != nil {
   195  					return
   196  				}
   197  			}
   198  		}
   199  
   200  		if atchblks != nil {
   201  			for b, err := range atchblks.Iterator() {
   202  				if !yield(b, err) {
   203  					return
   204  				}
   205  				if err != nil {
   206  					return
   207  				}
   208  			}
   209  		}
   210  
   211  		yield(rtblk, nil)
   212  	}
   213  }
   214  
   215  func decode(root ipld.Block) (ucan.View, error) {
   216  	data := udm.UCANModel{}
   217  	err := block.Decode(root, &data, udm.Type(), cbor.Codec, sha256.Hasher)
   218  	if err != nil {
   219  		return nil, fmt.Errorf("decoding root block: %w", err)
   220  	}
   221  	ucan, err := ucan.NewUCAN(&data)
   222  	if err != nil {
   223  		return nil, fmt.Errorf("constructing UCAN view: %w", err)
   224  	}
   225  	return ucan, nil
   226  }
   227  
   228  func Archive(d Delegation) io.Reader {
   229  	// We create a descriptor block to describe what this DAG represents
   230  	variant, err := block.Encode(
   231  		&adm.ArchiveModel{Ucan0_9_1: d.Link()},
   232  		adm.Type(),
   233  		cbor.Codec,
   234  		sha256.Hasher,
   235  	)
   236  	if err != nil {
   237  		reader, _ := io.Pipe()
   238  		reader.CloseWithError(fmt.Errorf("hashing variant block bytes: %w", err))
   239  		return reader
   240  	}
   241  	return car.Encode([]ipld.Link{variant.Link()}, func(yield func(ipld.Block, error) bool) {
   242  		for b, err := range d.Export() {
   243  			if !yield(b, err) || err != nil {
   244  				return
   245  			}
   246  		}
   247  		yield(variant, nil)
   248  	})
   249  }
   250  
   251  func Extract(b []byte) (Delegation, error) {
   252  	roots, blks, err := car.Decode(bytes.NewReader(b))
   253  	if err != nil {
   254  		return nil, fmt.Errorf("decoding CAR: %s", err)
   255  	}
   256  	if len(roots) == 0 {
   257  		return nil, fmt.Errorf("missing root CID in delegation archive")
   258  	}
   259  	if len(roots) > 1 {
   260  		return nil, fmt.Errorf("unexpected number of root CIDs in archive: %d", len(roots))
   261  	}
   262  
   263  	br, err := blockstore.NewBlockReader(blockstore.WithBlocksIterator(blks))
   264  	if err != nil {
   265  		return nil, fmt.Errorf("creating block reader: %w", err)
   266  	}
   267  
   268  	rt, ok, err := br.Get(roots[0])
   269  	if err != nil {
   270  		return nil, fmt.Errorf("getting root block: %w", err)
   271  	}
   272  	if !ok {
   273  		return nil, fmt.Errorf("missing root block: %s", roots[0])
   274  	}
   275  
   276  	model := adm.ArchiveModel{}
   277  	err = block.Decode(
   278  		rt,
   279  		&model,
   280  		adm.Type(),
   281  		cbor.Codec,
   282  		sha256.Hasher,
   283  	)
   284  	if err != nil {
   285  		return nil, fmt.Errorf("decoding root block: %w", err)
   286  	}
   287  
   288  	return NewDelegationView(model.Ucan0_9_1, br)
   289  }
   290  
   291  func Format(dlg Delegation) (string, error) {
   292  	bytes, err := io.ReadAll(dlg.Archive())
   293  	if err != nil {
   294  		return "", fmt.Errorf("archiving delegation: %w", err)
   295  	}
   296  	digest, err := multihash.Sum(bytes, uint64(multicodec.Identity), -1)
   297  	if err != nil {
   298  		return "", fmt.Errorf("creating multihash: %w", err)
   299  	}
   300  	return cid.NewCidV1(uint64(multicodec.Car), digest).StringOfBase(multibase.Base64)
   301  }
   302  
   303  func Parse(input string) (Delegation, error) {
   304  	cid, err := cid.Decode(input)
   305  	if err != nil {
   306  		return nil, fmt.Errorf("decoding CID: %w", err)
   307  	}
   308  	codec := multicodec.Code(cid.Prefix().Codec)
   309  	if codec != multicodec.Car {
   310  		return nil, fmt.Errorf("non CAR codec found: %s", codec.String())
   311  	}
   312  	mhinfo, err := multihash.Decode(cid.Hash())
   313  	if err != nil {
   314  		return nil, fmt.Errorf("decoding multihash: %w", err)
   315  	}
   316  	mhcodec := multicodec.Code(mhinfo.Code)
   317  	if mhcodec != multicodec.Identity {
   318  		return nil, fmt.Errorf("non identity multihash: %s", mhcodec.String())
   319  	}
   320  	return Extract(mhinfo.Digest)
   321  }