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 }