37 Commits

Author SHA1 Message Date
Michael Muré
a82bce556f Merge pull request #108 from ucan-wg/cont-test
container: add a helper for test only
2025-02-27 15:00:47 +01:00
Michael Muré
a54d66afe5 Merge pull request #110 from ucan-wg/cleanups
minor cleanups
2025-02-27 14:58:24 +01:00
Michael Muré
14a57d7391 Merge pull request #109 from ucan-wg/downgrade-libp2p
deps: downgrade libp2p
2025-02-27 14:58:17 +01:00
Michael Muré
68469db91a minor cleanups 2025-02-27 14:35:53 +01:00
Michael Muré
cc1d68be0c deps: downgrade libp2p
We only use go-libp2p for the crypto wrappers (github.com/libp2p/go-libp2p/core/crypto), and those have not changed in a while.

We don't need the most recent version of go-libp2p. On the other hand, a too recent version can easily mess with project that aim for an older version of libp2p (often transitively). This PR gives some leeway for version resolution.
2025-02-27 14:28:23 +01:00
Michael Muré
6d3846ac62 container: add a helper for test only 2025-02-27 14:27:02 +01:00
Steve Moyer
6aa33b1547 Merge pull request #107 from ucan-wg/tinygo
fix(policy): make 53-bit limits portable across platforms
2025-02-24 06:25:06 -05:00
Steve Moyer
9589cc8b44 feat(tinygo): make 53-bit limits portable across platforms 2025-02-21 11:03:33 -05:00
Michael Muré
879c0ab03b Merge pull request #106 from ucan-wg/cont-type
container: use the correct type in GetAllDelegations
2025-02-03 15:13:05 +01:00
Michael Muré
fe14765c8d container: use the correct type in GetAllDelegations 2025-02-03 15:02:31 +01:00
Michael Muré
1b28cb49bf Merge pull request #105 from ucan-wg/containertest
rename testvectors into containertest to match previous namings
2025-01-29 15:04:33 +01:00
Michael Muré
e1fc838caf rename testvectors into containertest to match previous namings 2025-01-29 14:46:01 +01:00
Michael Muré
f29b9e94fc Merge pull request #104 from ucan-wg/bundle
token: ditch the generic bundle in favor of specialized struct
2025-01-29 14:37:29 +01:00
Michael Muré
506ed21b94 token: ditch the generic bundle in favor of specialized struct
It's kust cleaner that way, the generic has no upside.
2025-01-29 14:28:13 +01:00
Michael Muré
126177b9e5 Merge pull request #103 from ucan-wg/dlg-is
delegation: add predicates to check if a delegation is a root or powe…
2025-01-29 14:11:29 +01:00
Michael Muré
2bddab8b0c delegation: add predicates to check if a delegation is a root or powerline 2025-01-29 14:07:49 +01:00
Michael Muré
45ead12131 Merge pull request #102 from ucan-wg/cont-keep-sealed
container: Reader should keep around and expose the sealed bytes
2025-01-23 18:06:41 +01:00
Michael Muré
9d5e170409 Merge pull request #101 from ucan-wg/cont-testing
container: expose test vectors for easier testing
2025-01-23 18:06:27 +01:00
Michael Muré
9d047f038d container: Reader should keep around and expose the sealed bytes 2025-01-23 17:13:10 +01:00
Michael Muré
4c5afcb084 container: expose test vectors for easier testing 2025-01-23 17:10:34 +01:00
Michael Muré
e218b49577 Merge pull request #100 from ucan-wg/pol-neq
policy: implement the missing '!='
2025-01-23 12:38:17 +01:00
Michael Muré
4c81ac778e policy: fix tests now that selector evaluation is clearer
https://github.com/ucan-wg/delegation/pull/15
https://github.com/ucan-wg/delegation/pull/16
2025-01-23 12:28:02 +01:00
Michael Muré
7ae65e7c8e policy: fix some code style 2025-01-22 17:30:28 +01:00
Michael Muré
7f9cb6426c policy: implement the missing '!='
https://github.com/ucan-wg/delegation/pull/15
2025-01-22 14:12:41 +01:00
Michael Muré
9c141029c3 container: better naming 2025-01-20 12:14:45 +01:00
Michael Muré
0c6717cfbc Merge pull request #98 from ucan-wg/container-err
container: clarify returned error
2025-01-13 13:54:06 +01:00
Michael Muré
6d7fd28324 container: clarify returned error 2025-01-13 13:50:05 +01:00
Michael Muré
ccc0120e78 Merge pull request #99 from ucan-wg/persona-fix
didtest: offset persona constants
2025-01-13 13:44:18 +01:00
Michael Muré
bf27b97a57 didtest: offset persona constants
It seems like having a persona with a zero value confuses some static checker into warning against a possible nil pointer deref.

An easy fix is to just not have zero.
2025-01-13 13:30:03 +01:00
Michael Muré
10dd4fa6d1 Merge pull request #97 from ucan-wg/ctn-writer-no-cid
container: the writer should not ask for a CID, as its discarded
2025-01-13 12:41:10 +01:00
Michael Muré
5695609f8b container: the writer should not ask for a CID, as its discarded 2025-01-13 12:24:04 +01:00
Michael Muré
b7ddead7c8 Merge pull request #96 from ucan-wg/container-spec
container: add a specification
2025-01-09 13:30:48 +01:00
Michael Muré
9cbec37685 container: no extra keys in the packaging allowed 2025-01-08 21:14:30 +01:00
Michael Muré
00dbd975b1 container: add test vectors 2025-01-08 18:59:30 +01:00
Michael Muré
c792a4cce5 container: typo and clarity in the spec 2025-01-08 18:55:08 +01:00
Michael Muré
8f3f1c775e container: implement header based serialization, also add SPEC test vectors 2025-01-08 17:31:04 +01:00
Michael Muré
704ed25768 container: add a specification 2025-01-07 18:16:27 +01:00
41 changed files with 911 additions and 355 deletions

View File

@@ -57,7 +57,7 @@ Not implemented yet:
Besides that, `go-ucan` also includes:
- a simplified [DID](https://www.w3.org/TR/did-core/) and [did-key](https://w3c-ccg.github.io/did-method-key/) implementation
- a [token container](https://github.com/ucan-wg/go-ucan/tree/v1/pkg/container) with CBOR and CAR format, to package and carry tokens together
- a [token container](https://github.com/ucan-wg/go-ucan/tree/v1/pkg/container) with CBOR and CAR format, to package and carry tokens together, see [SPEC](pkg/container/SPEC.md)
- support for encrypted values in token's metadata
## Getting Help

View File

@@ -28,7 +28,7 @@ type Persona int
//
// [table]: https://en.wikipedia.org/wiki/Alice_and_Bob#Cryptographic_systems
const (
PersonaAlice Persona = iota
PersonaAlice Persona = iota + 1
PersonaBob
PersonaCarol
PersonaDan

16
go.mod
View File

@@ -4,16 +4,17 @@ go 1.23
require (
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0
github.com/ipfs/go-cid v0.4.1
github.com/ipfs/go-cid v0.5.0
github.com/ipld/go-ipld-prime v0.21.0
github.com/lestrrat-go/jwx/v2 v2.1.1
github.com/libp2p/go-libp2p v0.36.3
github.com/lestrrat-go/jwx/v2 v2.1.3
github.com/libp2p/go-libp2p v0.33.0
github.com/mr-tron/base58 v1.2.0
github.com/multiformats/go-multibase v0.2.0
github.com/multiformats/go-multicodec v0.9.0
github.com/multiformats/go-multihash v0.2.3
github.com/multiformats/go-varint v0.0.7
github.com/stretchr/testify v1.9.0
github.com/stretchr/testify v1.10.0
golang.org/x/crypto v0.32.0
gotest.tools/v3 v3.5.1
)
@@ -21,7 +22,7 @@ require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/goccy/go-json v0.10.3 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/httprc v1.0.6 // indirect
@@ -34,9 +35,8 @@ require (
github.com/polydawn/refmt v0.89.0 // indirect
github.com/segmentio/asm v1.2.0 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect
golang.org/x/crypto v0.25.0 // indirect
golang.org/x/sys v0.22.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
golang.org/x/sys v0.29.0 // indirect
google.golang.org/protobuf v1.36.0 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/blake3 v1.3.0 // indirect

41
go.sum
View File

@@ -16,14 +16,14 @@ 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/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/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s=
github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk=
github.com/ipfs/go-cid v0.5.0 h1:goEKKhaGm0ul11IHA7I6p1GmKz8kEYniqFopaB5Otwg=
github.com/ipfs/go-cid v0.5.0/go.mod h1:0L7vmeNXpQpUS9vt+yEARkJ8rOg43DF3iPgn4GIN0mk=
github.com/ipld/go-ipld-prime v0.21.0 h1:n4JmcpOlPDIxBcY037SVfpd1G+Sj1nKZah0m6QH9C2E=
github.com/ipld/go-ipld-prime v0.21.0/go.mod h1:3RLqy//ERg/y5oShXXdx5YIp50cFGOanyMctpPjsvxQ=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
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/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -36,14 +36,14 @@ github.com/lestrrat-go/httprc v1.0.6 h1:qgmgIRhpvBqexMJjA/PmwSvhNk679oqD1RbovdCG
github.com/lestrrat-go/httprc v1.0.6/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
github.com/lestrrat-go/jwx/v2 v2.1.1 h1:Y2ltVl8J6izLYFs54BVcpXLv5msSW4o8eXwnzZLI32E=
github.com/lestrrat-go/jwx/v2 v2.1.1/go.mod h1:4LvZg7oxu6Q5VJwn7Mk/UwooNRnTHUpXBj2C4j3HNx0=
github.com/lestrrat-go/jwx/v2 v2.1.3 h1:Ud4lb2QuxRClYAmRleF50KrbKIoM1TddXgBrneT5/Jo=
github.com/lestrrat-go/jwx/v2 v2.1.3/go.mod h1:q6uFgbgZfEmQrfJfrCo90QcQOcXFMfbI/fO0NqRtvZo=
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8=
github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg=
github.com/libp2p/go-libp2p v0.36.3 h1:NHz30+G7D8Y8YmznrVZZla0ofVANrvBl2c+oARfMeDQ=
github.com/libp2p/go-libp2p v0.36.3/go.mod h1:4Y5vFyCUiJuluEPmpnKYf6WFx5ViKPUYs/ixe9ANFZ8=
github.com/libp2p/go-libp2p v0.33.0 h1:yTPSr8sJRbfeEYXyeN8VPVSlTlFjtMUwGDRniwaf/xQ=
github.com/libp2p/go-libp2p v0.33.0/go.mod h1:RIJFRQVUBKy82dnW7J5f1homqqv6NcsDJAl3e7CRGfE=
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.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
@@ -52,8 +52,8 @@ github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aG
github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI=
github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0=
github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4=
github.com/multiformats/go-multiaddr v0.13.0 h1:BCBzs61E3AGHcYYTv8dqRH43ZfyrqM8RXVPT8t13tLQ=
github.com/multiformats/go-multiaddr v0.13.0/go.mod h1:sBXrNzucqkFJhvKOiwwLyqamGa/P5EIXNPLovyhQCII=
github.com/multiformats/go-multiaddr v0.12.2 h1:9G9sTY/wCYajKa9lyfWPmpZAwe6oV+Wb1zcmMS1HG24=
github.com/multiformats/go-multiaddr v0.12.2/go.mod h1:GKyaTYjZRdcUhyOetrxTk9z0cW+jA/YrnqTOvKgi44M=
github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g=
github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk=
github.com/multiformats/go-multicodec v0.9.0 h1:pb/dlPnzee/Sxv/j4PmkDRxCOi3hXTz3IbPKOXWJkmg=
@@ -81,25 +81,24 @@ github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
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/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ=
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/exp v0.0.0-20240213143201-ec583247a57a h1:HinSgX1tJRX3KsL//Gxynpw5CTOAIPhgL4W8PNiIpVE=
golang.org/x/exp v0.0.0-20240213143201-ec583247a57a/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
google.golang.org/protobuf v1.36.0 h1:mjIs9gYtt56AzC4ZaffQuh88TZurBGhIJMBZGSxNerQ=
google.golang.org/protobuf v1.36.0/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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=

109
pkg/container/SPEC.md Normal file
View File

@@ -0,0 +1,109 @@
# UCAN container Specification v0.1.0
## Editors
* [Michael Muré], [Consensys]
## Authors
* [Michael Muré], [Consensys]
* [Hugo Dias]
## Language
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in [BCP 14] when, and only when, they appear in all capitals, as shown here.
# 0 Abstract
[User-Controlled Authorization Network (UCAN)][UCAN] is a trustless, secure, local-first, user-originated authorization and revocation scheme. This document describes a container format for transmitting one or more UCAN tokens as bytes, regardless of the transport.
# 1 Introduction
The UCAN spec itself is transport agnostic. This specification describes how to transfer one or more [UCAN] tokens bundled together, regardless of the transport.
# 2 Container format
## 2.1 Inner structure
UCAN tokens, regardless of their kind ([Delegation], [Invocation], [Revocation], [Promise]) MUST be first signed and serialized into DAG-CBOR bytes according to their respective specification. As the token's CID is not part of the serialized container, any CID returned by this operation is to be ignored.
All the tokens' bytes MUST be assembled in a [CBOR] array. The ordering of tokens in the array MUST NOT matter. This array SHOULD NOT have duplicate entries.
That array is then inserted as the value under the `ctn-v1` string key, in a CBOR map. There MUST NOT be other keys.
For clarity, the CBOR shape is given below:
```json
{
"ctn-v1": [
<token1 bytes>,
<token2 bytes>,
<token3 bytes>,
]
}
```
## 2.2 Serialisation
To serialize the container into bytes, the inner CBOR structure MUST then be serialized into bytes according to the CBOR specification. The resulting bytes MAY be compressed by a supported algorithm, then MAY be encoded with a supported base encoding.
The following compression algorithms are REQUIRED to be supported:
- [GZIP]
The following base encoding combinations are REQUIRED to be supported:
- base64, standard alphabet, padding
- base64, URL alphabet, no padding
The CBOR bytes MUST be prepended by a single byte header to indicate the selection combination of base encoding and compression. This header value MUST be set according to the following table:
| Header as hex | Header as ASCII | Base encoding | Compression |
|---------------|-----------------|-------------------------|----------------|
| 0x40 | @ | raw bytes | no compression |
| 0x42 | B | base64 std padding | no compression |
| 0x43 | C | base64 url (no padding) | no compression |
| 0x4D | M | raw bytes | gzip |
| 0x4F | O | base64 std padding | gzip |
| 0x50 | P | base64 url (no padding) | gzip |
For clarity, the resulting serialisation is in the form of `<header byte><cbor bytes, optionally compressed, optionally encoded>`.
# 3 FAQ
## 3.1 Why not include the UCAN CIDs?
Several attacks are possible if UCAN tokens aren't validated. If CIDs aren't validated, at least two attacks are possible: [privilege escalation] and [cache poisoning], as UCAN delegation proofs depends on a correct hash-linked structure.
By not including the CID in the container, the recipient is forced to hash (and thus validate) the CIDs for each token. If presented with a claimed CID paired with the token bytes, implementers could ignore CID validation, breaking a core part of the proof chain security model. Hash functions are very fast on a couple kilobytes of data so the overhead is still very low. It also significantly reduces the size of the container.
## 3.2 Why compress? Why not always compress?
Compression is a relatively demanding operation. As such, using it is a tradeoff between size on the wire and CPU/memory usage, both when writing and reading a container. The transport itself can make compression worthwhile or not: for example, HTTP/2 and HTTP/3 headers are already compressed, but HTTP/1 headers are not. This being highly contextual, the choice is left to the final implementer.
# 4 Implementation recommendations
## 4.1 Dissociate reader and writer
While it is tempting to write a single implementation to read and write a container, it is RECOMMENDED to separate the implementation into a reader and a writer. The writer can simply accept arbitrary tokens as bytes, while the reader provides a read-only view with convenient access functions.
# 5 Acknowledgments
Many thanks to all the [Fission] team and in particular to [Brooklyn Zelenka] for creating and pushing [UCAN] and other critical pieces like [WNFS], and generally being awesome and supportive people.
<!-- External Links -->
[BCP 14]: https://www.rfc-editor.org/info/bcp14
[Brooklyn Zelenka]: https://github.com/expede
[CBOR]: https://www.rfc-editor.org/rfc/rfc8949.html
[Consensys]: https://consensys.io/
[Delegation]: https://github.com/ucan-wg/delegation/tree/v1_ipld
[Fission]: https://fission.codes
[GZIP]: https://datatracker.ietf.org/doc/html/rfc1952
[Hugo Dias]: https://github.com/hugomrdias
[Invocation]: https://github.com/ucan-wg/invocation
[Michael Muré]: https://github.com/MichaelMure/
[Promise]: https://github.com/ucan-wg/promise/tree/v1-rc1
[Revocation]: https://github.com/ucan-wg/revocation/tree/first-draft
[UCAN]: https://github.com/ucan-wg/spec
[WNFS]: https://github.com/wnfs-wg
[cache poisoning]: https://en.wikipedia.org/wiki/Cache_poisoning
[privilede escalation]: https://en.wikipedia.org/wiki/Privilege_escalation

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
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

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
PH4sIAAAAAAAA_5zWa5cUxR3HcVcRiejmuCII3siKwgnubnd1dVW1eIS5rzM9w8zs3HZETFd19_T07PTcZ2cH1KMgoB41YgIo3nM0G1GCRhCRiILIEaIhIEo0Bi9c9ewaFfEazYMd8iCPdvIGvk8-p_71e1RnZaujyt_e2_LQLYk5Fbb1QefFR9-_5LzWO59-9PfWoWV_nN_f177z06eH31j13Je8-Wpk35bTccKjtV_22MRfnz9DufuSg8Ntn02KzZtw83sLxj-mGE441FIoVZhidal9qTl8J9fJdRRZJz_IlIpaI2pavTKjDVxZR_5MQXa4aFIFgr-gMCrrOTWUqWNajNZERzin0GLRz7IxsTcfAgnGsqrRpedyXVQpMq2Wn5K68dmLWLpU-p9msRbQ-usuHEsGorZK0BbwxWzxvCdrhDK2vL23UMCRTM0RlStVxPK5vpsXMaWvj3Vee90i5Wq9s6r0VbRTWalC__-smtXKyu8MERNOwJSZkCACeAFgzkC6xGkYAhOLqkJEiBXJkJAuCUBUTMIA4KEgAEWzchbT5DXHfxwc7LiqNnPmRwdHgBa8NO6jDQ9uvW6wa7DHYetfP3DxO4s7JhybLa1YcerUV-Yae7aDww_P2_zjvH9euWqOOW3yjBWtO2__bKzLvfnR_enEpmaBMtFuy4qEsgrlqRMpPm86mql6apjI4V63V_bFgl5WT-USpQhDowVK2amiFmM8tEUsiRJcCeowHM9AvVQreLGE83LMkcBBI99dAE0ANZNtAGFewJjwkilBJPG8TphoKEQhmihKpsgBXuIVhXEGRRQpRGAmhzkRqIpOaQNo4afuoTWTj60cfjw5fQQoxy98ZPGmF3YsWLJo8voJ587Y9-X38geyLBq_nHXZIy9fPun5m1pebqVzv7J-NuaM08XEzYl3v3kmtfuCr2Yc-VAa3vtxS7NAnpKSKsBEzVaMphWdFB0kgNSEgHpwiPe7a0qaxYtVG_SVq_7RAhkey-asYmfUZ1XKMbGSr-Vo1UZ60rZyNV6Uy8l8MY_iES1ic0abAGom2wCCEqcLVFRNpEKBaghAaCBJUYgKOVNVGMQCUyXRQBqFTJWwKQkIIoZFQW8AvZV64bRDez_6flYbbmu8oDa_euu2yJB9w1vX3uO-YrZvobj2yK07V7-1YebjrRcgbsvS4fiyJ9_gpx4KvT5n_A7PTz-cqF_asuuaoTFnn2N-bbU2CaSHi0bJlrEi3kyh6NFrgqOu59VKOJ2t9Xj4sllT4wXqJdFQfzI2aiBvIGSG_Zls2IsDSdKblRV3QlQz2X7Z2evudVBk16QIM1yiCZsBaiLbAOIEnROQppu8AjgKiKqIhgQVwDQCTCKJkqCIBIqGpoicCilvcoxDWJewcPLE3TD0fHzb0y_BrtRd2gjQN3fd--36ju_CYzuV8adcsOeA95UvVv-09N1t05_bN7DllouWTN-18r4nTm-9Y8uZ2L02fj1XLizHlzyfrZ-x65Pt503b0uyJs0oDhpWuSh4vwFpJjlcDciKRCbtQTzmvxkWrKIWCUQm51FomO1ogKyMLyXSPx0xHxKrixwFT7a9XgzUUTuedXgDi8XKmgGJmJOwMNQHUTLYBhERGkE6YCTXEYYAYxxuE5xGnCbopAkWjAtCgbhCMKAUQmQqkvA4wJaQBpNyhPfDyK9Ijs-yOv40ArTv_hoMtSbB12NjtzD1Ef35ow47k_D3frn41f-2LX6YmdTxzzba3t180_7XFV5m-fz304tj2Td-t7Nx_oblszv7ctI2Hmz1xhayl1myOkhEOxUt2S_S4shFvMI7dIbseyPa4AlalJ8q6g2KhGBktUJGVfQEaSsXtsK7EnXnC6zIP84ZHrYUyGR9vcxXj9lwuaIvG7E0ANZM9-QcBHkNCNFMFEKhYFTRiSBoUOV0RTE3DKmWMCJKhijzgeJWakgoIwyLUuAZQSlq-Y177mBOSHt49AjRF-PBAe9d1H3ffcvyZczrHLAhP9X3jC5y797YFa-mNxYl_evDqo4fu_8NZh3d88un1R1f_5cfBf8_76p3tv_psyooll_k-nj2mWaC6b8BCmVrCytYjIFZKF1wytmUFKdijVuoCLIdrBV8dk3AxbIwWqJRAkpyRMnkY9ReLcipX8PR3u6RcrheykFINGCCc7-lJ25LMpjUB1Ez25IljDHFYpCYUiUYR4yVkUMKYIEiSiTDBSFcRgAajPAOU6ibjmShBToOwAXRf-6V31zceuXHdjHV9I0D7Jn7wgXzr4TVX_-PYw_mpLatO69_qW7R1Q-m8o9OcsGP2rM_f3lM_3n0_-aL18zWHV-eHzkik9r4_LriMt16a98Tm889sdiTASDqRwFLW3evj6_mU6i96cqInGEY4Ea0F1brlsPUXJSlCrcpogfRCzGll1YKRsqXrWEuVajq21wK5buKv5FxOV7wQy4Wz5Wwg3NRIaCbbABKopoi6BkymSSrTRCpSQ8UqEEQBmkTTBQp0VcSGBgAvYaiZCgQMUiz89wXdVtm7cNtjqvLmb5ZtHAHq2vfX9Uem2q-ZXTzw3oub0ruemj52f29hwuWeZ2_6_qylidcO5d9p3310-_wptTV3Dv1WXZzbuLsNDU2-dzDy7Sf84MxxTQKZAxWi5wO0Agci0WTOnStHklUpbPaHSi7JcsV9UbvH6_IHzHTVNeqRwLtlj6rV_T3ZehxGex2KLA3EXX4XhNmaGZdj-VjCRgy37HA4mhkJTWQbQBImhOkcbyIeQA1oiOcNBVBEdBWYlOc0IgBAREPnBYoABSavUAI0RhFuAM22T_nzs8NTTyz94YFVI0Bze_9-4utz3dvfW5B7fXL7uJWvr-1rO-uG5evusY_jlk-ynvzheOfE5IFIt4ieunAfO3bK-CWuX2w-0HbQP_fDi-8Y2NPsitNYLCfr2YLGEhrwg7LNoL3ZWKLSAzwDxNvNV_OA1L0oFVdToz5xuURswBupyoo7Ggua8Vg8kIjV5TLvcyCUDJsV7PXKuglTehn5mwBqJnvyBSEd6qpKTV5HCEOeU4mBCJSwpImmIkBd4BRGoUEUHUlMQ6amQVVVJB2CBpBwxRsH-9-cvnYnOfvd_wQAAP__hMJ751MQAAA

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,21 @@
package containertest
import _ "embed"
//go:embed Base64StdPadding
var Base64StdPadding string
//go:embed Base64StdPaddingGzipped
var Base64StdPaddingGzipped string
//go:embed Base64URL
var Base64URL string
//go:embed Base64URLGzipped
var Base64URLGzipped string
//go:embed Bytes
var Bytes []byte
//go:embed BytesGzipped
var BytesGzipped []byte

118
pkg/container/packaging.go Normal file
View File

@@ -0,0 +1,118 @@
package container
import (
"compress/gzip"
"encoding/base64"
"errors"
"fmt"
"io"
)
const containerVersionTag = "ctn-v1"
type header byte
const (
headerRawBytes = header(0x40)
headerBase64StdPadding = header(0x42)
headerBase64URL = header(0x43)
headerRawBytesGzip = header(0x4D)
headerBase64StdPaddingGzip = header(0x4F)
headerBase64URLGzip = header(0x50)
)
func (h header) encoder(w io.Writer) *payloadWriter {
res := &payloadWriter{rawWriter: w, writer: w, header: h}
switch h {
case headerBase64StdPadding, headerBase64StdPaddingGzip:
b64Writer := base64.NewEncoder(base64.StdEncoding, res.writer)
res.writer = b64Writer
res.closers = append([]io.Closer{b64Writer}, res.closers...)
case headerBase64URL, headerBase64URLGzip:
b64Writer := base64.NewEncoder(base64.RawURLEncoding, res.writer)
res.writer = b64Writer
res.closers = append([]io.Closer{b64Writer}, res.closers...)
}
switch h {
case headerRawBytesGzip, headerBase64StdPaddingGzip, headerBase64URLGzip:
gzipWriter := gzip.NewWriter(res.writer)
res.writer = gzipWriter
res.closers = append([]io.Closer{gzipWriter}, res.closers...)
}
return res
}
func payloadDecoder(r io.Reader) (io.Reader, error) {
headerBuf := make([]byte, 1)
_, err := r.Read(headerBuf)
if err != nil {
return nil, err
}
h := header(headerBuf[0])
switch h {
case headerRawBytes,
headerBase64StdPadding,
headerBase64URL,
headerRawBytesGzip,
headerBase64StdPaddingGzip,
headerBase64URLGzip:
default:
return nil, fmt.Errorf("unknown container header")
}
switch h {
case headerBase64StdPadding, headerBase64StdPaddingGzip:
r = base64.NewDecoder(base64.StdEncoding, r)
case headerBase64URL, headerBase64URLGzip:
r = base64.NewDecoder(base64.RawURLEncoding, r)
}
switch h {
case headerRawBytesGzip, headerBase64StdPaddingGzip, headerBase64URLGzip:
gzipReader, err := gzip.NewReader(r)
if err != nil {
return nil, err
}
r = gzipReader
}
return r, nil
}
var _ io.WriteCloser = &payloadWriter{}
// payloadWriter is tasked with two things:
// - prepend the header byte
// - call Close() on all the underlying io.Writer
type payloadWriter struct {
rawWriter io.Writer
writer io.Writer
header header
headerWrote bool
closers []io.Closer
}
func (w *payloadWriter) Write(p []byte) (n int, err error) {
if !w.headerWrote {
_, err := w.rawWriter.Write([]byte{byte(w.header)})
if err != nil {
return 0, err
}
w.headerWrote = true
}
return w.writer.Write(p)
}
func (w *payloadWriter) Close() error {
var errs error
for _, closer := range w.closers {
if err := closer.Close(); err != nil {
errs = errors.Join(errs, err)
}
}
return errs
}

View File

@@ -2,8 +2,6 @@ package container
import (
"bytes"
"encoding/base64"
"errors"
"fmt"
"io"
"iter"
@@ -11,7 +9,7 @@ import (
"github.com/ipfs/go-cid"
"github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/codec/dagcbor"
"github.com/ipld/go-ipld-prime/codec/cbor"
"github.com/ipld/go-ipld-prime/datamodel"
"github.com/ucan-wg/go-ucan/token"
@@ -23,86 +21,31 @@ var ErrNotFound = fmt.Errorf("not found")
var ErrMultipleInvocations = fmt.Errorf("multiple invocations")
// Reader is a token container reader. It exposes the tokens conveniently decoded.
type Reader map[cid.Cid]token.Token
type Reader map[cid.Cid]bundle
// GetToken returns an arbitrary decoded token, from its CID.
// If not found, ErrNotFound is returned.
func (ctn Reader) GetToken(cid cid.Cid) (token.Token, error) {
tkn, ok := ctn[cid]
if !ok {
return nil, ErrNotFound
}
return tkn, nil
type bundle struct {
sealed []byte
token token.Token
}
// GetDelegation is the same as GetToken but only return a delegation.Token, with the right type.
func (ctn Reader) GetDelegation(cid cid.Cid) (*delegation.Token, error) {
tkn, err := ctn.GetToken(cid)
if errors.Is(err, ErrNotFound) {
return nil, delegation.ErrDelegationNotFound
}
// FromBytes decodes a container from a []byte
func FromBytes(data []byte) (Reader, error) {
return FromReader(bytes.NewReader(data))
}
// FromString decodes a container from a string
func FromString(s string) (Reader, error) {
return FromReader(strings.NewReader(s))
}
// FromReader decodes a container from an io.Reader.
func FromReader(r io.Reader) (Reader, error) {
payload, err := payloadDecoder(r)
if err != nil {
return nil, err
}
if tkn, ok := tkn.(*delegation.Token); ok {
return tkn, nil
}
return nil, delegation.ErrDelegationNotFound
}
// GetAllDelegations returns all the delegation.Token in the container.
func (ctn Reader) GetAllDelegations() iter.Seq2[cid.Cid, *delegation.Token] {
return func(yield func(cid.Cid, *delegation.Token) bool) {
for c, t := range ctn {
if t, ok := t.(*delegation.Token); ok {
if !yield(c, t) {
return
}
}
}
}
}
// GetInvocation returns a single invocation.Token.
// If none are found, ErrNotFound is returned.
// If more than one invocation exist, ErrMultipleInvocations is returned.
func (ctn Reader) GetInvocation() (*invocation.Token, error) {
var res *invocation.Token
for _, t := range ctn {
if inv, ok := t.(*invocation.Token); ok {
if res != nil {
return nil, ErrMultipleInvocations
}
res = inv
}
}
if res == nil {
return nil, ErrNotFound
}
return res, nil
}
// GetAllInvocations returns all the invocation.Token in the container.
func (ctn Reader) GetAllInvocations() iter.Seq2[cid.Cid, *invocation.Token] {
return func(yield func(cid.Cid, *invocation.Token) bool) {
for c, t := range ctn {
if t, ok := t.(*invocation.Token); ok {
if !yield(c, t) {
return
}
}
}
}
}
// FromCbor decodes a DAG-CBOR encoded container.
func FromCbor(data []byte) (Reader, error) {
return FromCborReader(bytes.NewReader(data))
}
// FromCborReader is the same as FromCbor, but with an io.Reader.
func FromCborReader(r io.Reader) (Reader, error) {
n, err := ipld.DecodeStreaming(r, dagcbor.Decode)
n, err := ipld.DecodeStreaming(payload, cbor.Decode)
if err != nil {
return nil, err
}
@@ -124,7 +67,7 @@ func FromCborReader(r io.Reader) (Reader, error) {
if err != nil {
return nil, fmt.Errorf("invalid container format: version must be string")
}
if version != currentContainerVersion {
if version != containerVersionTag {
return nil, fmt.Errorf("unsupported container version: %s", version)
}
@@ -151,52 +94,105 @@ func FromCborReader(r io.Reader) (Reader, error) {
return ctn, nil
}
// FromCborBase64 decodes a base64 DAG-CBOR encoded container.
func FromCborBase64(data string) (Reader, error) {
return FromCborBase64Reader(strings.NewReader(data))
}
// FromCborBase64Reader is the same as FromCborBase64, but with an io.Reader.
func FromCborBase64Reader(r io.Reader) (Reader, error) {
return FromCborReader(base64.NewDecoder(base64.StdEncoding, r))
}
// FromCar decodes a CAR file encoded container.
func FromCar(data []byte) (Reader, error) {
return FromCarReader(bytes.NewReader(data))
}
// FromCarReader is the same as FromCar, but with an io.Reader.
func FromCarReader(r io.Reader) (Reader, error) {
_, it, err := readCar(r)
if err != nil {
return nil, err
// GetToken returns an arbitrary decoded token, from its CID.
// If not found, ErrNotFound is returned.
func (ctn Reader) GetToken(cid cid.Cid) (token.Token, error) {
bndl, ok := ctn[cid]
if !ok {
return nil, ErrNotFound
}
return bndl.token, nil
}
ctn := make(Reader)
// GetSealed returns an arbitrary sealed token, from its CID.
// If not found, ErrNotFound is returned.
func (ctn Reader) GetSealed(cid cid.Cid) ([]byte, error) {
bndl, ok := ctn[cid]
if !ok {
return nil, ErrNotFound
}
return bndl.sealed, nil
}
for block, err := range it {
if err != nil {
return nil, err
}
err = ctn.addToken(block.data)
if err != nil {
return nil, err
// GetAllTokens return all the tokens in the container.
func (ctn Reader) GetAllTokens() iter.Seq[token.Bundle] {
return func(yield func(token.Bundle) bool) {
for c, bndl := range ctn {
if !yield(token.Bundle{
Cid: c,
Decoded: bndl.token,
Sealed: bndl.sealed,
}) {
return
}
}
}
return ctn, nil
}
// FromCarBase64 decodes a base64 CAR file encoded container.
func FromCarBase64(data string) (Reader, error) {
return FromCarReader(strings.NewReader(data))
// GetDelegation is the same as GetToken but only return a delegation.Token, with the right type.
// If not found, delegation.ErrDelegationNotFound is returned.
func (ctn Reader) GetDelegation(cid cid.Cid) (*delegation.Token, error) {
tkn, err := ctn.GetToken(cid)
if err != nil { // only ErrNotFound expected
return nil, delegation.ErrDelegationNotFound
}
if tkn, ok := tkn.(*delegation.Token); ok {
return tkn, nil
}
return nil, delegation.ErrDelegationNotFound
}
// FromCarBase64Reader is the same as FromCarBase64, but with an io.Reader.
func FromCarBase64Reader(r io.Reader) (Reader, error) {
return FromCarReader(base64.NewDecoder(base64.StdEncoding, r))
// GetAllDelegations returns all the delegation.Token in the container.
func (ctn Reader) GetAllDelegations() iter.Seq[delegation.Bundle] {
return func(yield func(delegation.Bundle) bool) {
for c, bndl := range ctn {
if t, ok := bndl.token.(*delegation.Token); ok {
if !yield(delegation.Bundle{
Cid: c,
Decoded: t,
Sealed: bndl.sealed,
}) {
return
}
}
}
}
}
// GetInvocation returns a single invocation.Token.
// If none are found, ErrNotFound is returned.
// If more than one invocation exists, ErrMultipleInvocations is returned.
func (ctn Reader) GetInvocation() (*invocation.Token, error) {
var res *invocation.Token
for _, bndl := range ctn {
if inv, ok := bndl.token.(*invocation.Token); ok {
if res != nil {
return nil, ErrMultipleInvocations
}
res = inv
}
}
if res == nil {
return nil, ErrNotFound
}
return res, nil
}
// GetAllInvocations returns all the invocation.Token in the container.
func (ctn Reader) GetAllInvocations() iter.Seq[invocation.Bundle] {
return func(yield func(invocation.Bundle) bool) {
for c, bndl := range ctn {
if t, ok := bndl.token.(*invocation.Token); ok {
if !yield(invocation.Bundle{
Cid: c,
Decoded: t,
Sealed: bndl.sealed,
}) {
return
}
}
}
}
}
func (ctn Reader) addToken(data []byte) error {
@@ -204,6 +200,19 @@ func (ctn Reader) addToken(data []byte) error {
if err != nil {
return err
}
ctn[c] = tkn
ctn[c] = bundle{
sealed: data,
token: tkn,
}
return nil
}
// ToWriter convert a container Reader into a Writer.
// Most likely, you only want to use this in tests for convenience.
func (ctn Reader) ToWriter() Writer {
writer := NewWriter()
for _, bndl := range ctn {
writer.AddSealed(bndl.sealed)
}
return writer
}

View File

@@ -22,14 +22,22 @@ import (
func TestContainerRoundTrip(t *testing.T) {
for _, tc := range []struct {
name string
writer func(ctn Writer, w io.Writer) error
reader func(io.Reader) (Reader, error)
name string
expectedHeader header
writer any
}{
{"car", Writer.ToCarWriter, FromCarReader},
{"carBase64", Writer.ToCarBase64Writer, FromCarBase64Reader},
{"cbor", Writer.ToCborWriter, FromCborReader},
{"cborBase64", Writer.ToCborBase64Writer, FromCborBase64Reader},
{"Bytes", headerRawBytes, Writer.ToBytes},
{"BytesWriter", headerRawBytes, Writer.ToBytesWriter},
{"BytesGzipped", headerRawBytesGzip, Writer.ToBytesGzipped},
{"BytesGzippedWriter", headerRawBytesGzip, Writer.ToBytesGzippedWriter},
{"Base64StdPadding", headerBase64StdPadding, Writer.ToBase64StdPadding},
{"Base64StdPaddingWriter", headerBase64StdPadding, Writer.ToBase64StdPaddingWriter},
{"Base64StdPaddingGzipped", headerBase64StdPaddingGzip, Writer.ToBase64StdPaddingGzipped},
{"Base64StdPaddingGzippedWriter", headerBase64StdPaddingGzip, Writer.ToBase64StdPaddingGzippedWriter},
{"Base64URL", headerBase64URL, Writer.ToBase64URL},
{"Base64URLWriter", headerBase64URL, Writer.ToBase64URLWriter},
{"Base64URLGzipped", headerBase64URLGzip, Writer.ToBase64URLGzipped},
{"Base64URLGzipWriter", headerBase64URLGzip, Writer.ToBase64URLGzipWriter},
} {
t.Run(tc.name, func(t *testing.T) {
tokens := make(map[cid.Cid]*delegation.Token)
@@ -39,21 +47,53 @@ func TestContainerRoundTrip(t *testing.T) {
for i := 0; i < 10; i++ {
dlg, c, data := randToken()
writer.AddSealed(c, data)
writer.AddSealed(data)
tokens[c] = dlg
dataSize += len(data)
}
buf := bytes.NewBuffer(nil)
var reader Reader
var serialLen int
err := tc.writer(writer, buf)
require.NoError(t, err)
switch fn := tc.writer.(type) {
case func(ctn Writer, w io.Writer) error:
buf := bytes.NewBuffer(nil)
err := fn(writer, buf)
require.NoError(t, err)
serialLen = buf.Len()
t.Logf("data size %d", dataSize)
t.Logf("container overhead: %d%%, %d bytes", int(float32(buf.Len()-dataSize)/float32(dataSize)*100.0), buf.Len()-dataSize)
h, err := buf.ReadByte()
require.NoError(t, err)
require.Equal(t, byte(tc.expectedHeader), h)
err = buf.UnreadByte()
require.NoError(t, err)
reader, err := tc.reader(bytes.NewReader(buf.Bytes()))
require.NoError(t, err)
reader, err = FromReader(bytes.NewReader(buf.Bytes()))
require.NoError(t, err)
case func(ctn Writer) ([]byte, error):
b, err := fn(writer)
require.NoError(t, err)
serialLen = len(b)
require.Equal(t, byte(tc.expectedHeader), b[0])
reader, err = FromBytes(b)
require.NoError(t, err)
case func(ctn Writer) (string, error):
s, err := fn(writer)
require.NoError(t, err)
serialLen = len(s)
require.Equal(t, byte(tc.expectedHeader), s[0])
reader, err = FromString(s)
require.NoError(t, err)
}
t.Logf("data size %d, container size %d, overhead: %d%%, %d bytes",
dataSize, serialLen, int(float32(serialLen-dataSize)/float32(dataSize)*100.0), serialLen-dataSize)
for c, dlg := range tokens {
tknRead, err := reader.GetToken(c)
@@ -98,16 +138,18 @@ func BenchmarkContainerSerialisation(b *testing.B) {
writer func(ctn Writer, w io.Writer) error
reader func(io.Reader) (Reader, error)
}{
{"car", Writer.ToCarWriter, FromCarReader},
{"carBase64", Writer.ToCarBase64Writer, FromCarBase64Reader},
{"cbor", Writer.ToCborWriter, FromCborReader},
{"cborBase64", Writer.ToCborBase64Writer, FromCborBase64Reader},
{"Bytes", Writer.ToBytesWriter, FromReader},
{"BytesGzipped", Writer.ToBytesGzippedWriter, FromReader},
{"Base64StdPadding", Writer.ToBase64StdPaddingWriter, FromReader},
{"Base64StdPaddingGzipped", Writer.ToBase64StdPaddingGzippedWriter, FromReader},
{"Base64URL", Writer.ToBase64URLWriter, FromReader},
{"Base64URLGzip", Writer.ToBase64URLGzipWriter, FromReader},
} {
writer := NewWriter()
for i := 0; i < 10; i++ {
_, c, data := randToken()
writer.AddSealed(c, data)
_, _, data := randToken()
writer.AddSealed(data)
}
buf := bytes.NewBuffer(nil)
@@ -181,10 +223,10 @@ func FuzzContainerRead(f *testing.F) {
for tokenCount := 0; tokenCount < 10; tokenCount++ {
writer := NewWriter()
for i := 0; i < tokenCount; i++ {
_, c, data := randToken()
writer.AddSealed(c, data)
_, _, data := randToken()
writer.AddSealed(data)
}
data, err := writer.ToCbor()
data, err := writer.ToBytes()
require.NoError(f, err)
f.Add(data)
@@ -194,7 +236,7 @@ func FuzzContainerRead(f *testing.F) {
start := time.Now()
// search for panics
_, _ = FromCbor(data)
_, _ = FromBytes(data)
if time.Since(start) > 100*time.Millisecond {
panic("too long")

View File

@@ -2,107 +2,138 @@ package container
import (
"bytes"
"encoding/base64"
"io"
"github.com/ipfs/go-cid"
"github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/codec/dagcbor"
"github.com/ipld/go-ipld-prime/codec/cbor"
"github.com/ipld/go-ipld-prime/datamodel"
"github.com/ipld/go-ipld-prime/fluent/qp"
"github.com/ipld/go-ipld-prime/node/basicnode"
)
// Writer is a token container writer. It provides a convenient way to aggregate and serialize tokens together.
type Writer map[cid.Cid][]byte
type Writer map[string]struct{}
func NewWriter() Writer {
return make(Writer)
}
// AddSealed includes a "sealed" token (serialized with a ToSealed* function) in the container.
func (ctn Writer) AddSealed(cid cid.Cid, data []byte) {
ctn[cid] = data
func (ctn Writer) AddSealed(data []byte) {
ctn[string(data)] = struct{}{}
}
const currentContainerVersion = "ctn-v1"
// ToBytes encode the container into raw bytes.
func (ctn Writer) ToBytes() ([]byte, error) {
return ctn.toBytes(headerRawBytes)
}
// ToCbor encode the container into a DAG-CBOR binary format.
func (ctn Writer) ToCbor() ([]byte, error) {
// ToBytesWriter is the same as ToBytes, but with an io.Writer.
func (ctn Writer) ToBytesWriter(w io.Writer) error {
return ctn.toWriter(headerRawBytes, w)
}
// ToBytesGzipped encode the container into gzipped bytes.
func (ctn Writer) ToBytesGzipped() ([]byte, error) {
return ctn.toBytes(headerRawBytesGzip)
}
// ToBytesGzippedWriter is the same as ToBytesGzipped, but with an io.Writer.
func (ctn Writer) ToBytesGzippedWriter(w io.Writer) error {
return ctn.toWriter(headerRawBytesGzip, w)
}
// ToBase64StdPadding encode the container into a base64 string, with standard encoding and padding.
func (ctn Writer) ToBase64StdPadding() (string, error) {
return ctn.toString(headerBase64StdPadding)
}
// ToBase64StdPaddingWriter is the same as ToBase64StdPadding, but with an io.Writer.
func (ctn Writer) ToBase64StdPaddingWriter(w io.Writer) error {
return ctn.toWriter(headerBase64StdPadding, w)
}
// ToBase64StdPaddingGzipped encode the container into a pre-gzipped base64 string, with standard encoding and padding.
func (ctn Writer) ToBase64StdPaddingGzipped() (string, error) {
return ctn.toString(headerBase64StdPaddingGzip)
}
// ToBase64StdPaddingGzippedWriter is the same as ToBase64StdPaddingGzipped, but with an io.Writer.
func (ctn Writer) ToBase64StdPaddingGzippedWriter(w io.Writer) error {
return ctn.toWriter(headerBase64StdPaddingGzip, w)
}
// ToBase64URL encode the container into base64 string, with URL-safe encoding and no padding.
func (ctn Writer) ToBase64URL() (string, error) {
return ctn.toString(headerBase64URL)
}
// ToBase64URLWriter is the same as ToBase64URL, but with an io.Writer.
func (ctn Writer) ToBase64URLWriter(w io.Writer) error {
return ctn.toWriter(headerBase64URL, w)
}
// ToBase64URL encode the container into pre-gzipped base64 string, with URL-safe encoding and no padding.
func (ctn Writer) ToBase64URLGzipped() (string, error) {
return ctn.toString(headerBase64URLGzip)
}
// ToBase64URLWriter is the same as ToBase64URL, but with an io.Writer.
func (ctn Writer) ToBase64URLGzipWriter(w io.Writer) error {
return ctn.toWriter(headerBase64URLGzip, w)
}
func (ctn Writer) toBytes(header header) ([]byte, error) {
var buf bytes.Buffer
err := ctn.ToCborWriter(&buf)
err := ctn.toWriter(header, &buf)
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// ToCborWriter is the same as ToCbor, but with an io.Writer.
func (ctn Writer) ToCborWriter(w io.Writer) error {
func (ctn Writer) toString(header header) (string, error) {
var buf bytes.Buffer
err := ctn.toWriter(header, &buf)
if err != nil {
return "", err
}
return buf.String(), nil
}
func (ctn Writer) toWriter(header header, w io.Writer) (err error) {
encoder := header.encoder(w)
defer func() {
err = encoder.Close()
}()
node, err := qp.BuildMap(basicnode.Prototype.Any, 1, func(ma datamodel.MapAssembler) {
qp.MapEntry(ma, currentContainerVersion, qp.List(int64(len(ctn)), func(la datamodel.ListAssembler) {
for _, data := range ctn {
qp.ListEntry(la, qp.Bytes(data))
qp.MapEntry(ma, containerVersionTag, qp.List(int64(len(ctn)), func(la datamodel.ListAssembler) {
for data, _ := range ctn {
qp.ListEntry(la, qp.Bytes([]byte(data)))
}
}))
})
if err != nil {
return err
}
return ipld.EncodeStreaming(w, node, dagcbor.Encode)
return ipld.EncodeStreaming(encoder, node, cbor.Encode)
}
// ToCborBase64 encode the container into a base64 encoded DAG-CBOR binary format.
func (ctn Writer) ToCborBase64() (string, error) {
var buf bytes.Buffer
err := ctn.ToCborBase64Writer(&buf)
// ToReader convert a container Writer into a Reader.
// Most likely, you only want to use this in tests for convenience.
// This is not optimized and can panic.
func (ctn Writer) ToReader() Reader {
data, err := ctn.ToBytes()
if err != nil {
return "", err
panic(err)
}
return buf.String(), nil
}
// ToCborBase64Writer is the same as ToCborBase64, but with an io.Writer.
func (ctn Writer) ToCborBase64Writer(w io.Writer) error {
w2 := base64.NewEncoder(base64.StdEncoding, w)
defer w2.Close()
return ctn.ToCborWriter(w2)
}
// ToCar encode the container into a CAR file.
func (ctn Writer) ToCar() ([]byte, error) {
var buf bytes.Buffer
err := ctn.ToCarWriter(&buf)
reader, err := FromBytes(data)
if err != nil {
return nil, err
panic(err)
}
return buf.Bytes(), nil
}
// ToCarWriter is the same as ToCar, but with an io.Writer.
func (ctn Writer) ToCarWriter(w io.Writer) error {
return writeCar(w, nil, func(yield func(carBlock, error) bool) {
for c, data := range ctn {
if !yield(carBlock{c: c, data: data}, nil) {
return
}
}
})
}
// ToCarBase64 encode the container into a base64 encoded CAR file.
func (ctn Writer) ToCarBase64() (string, error) {
var buf bytes.Buffer
err := ctn.ToCarBase64Writer(&buf)
if err != nil {
return "", err
}
return buf.String(), nil
}
// ToCarBase64Writer is the same as ToCarBase64, but with an io.Writer.
func (ctn Writer) ToCarBase64Writer(w io.Writer) error {
w2 := base64.NewEncoder(base64.StdEncoding, w)
defer w2.Close()
return ctn.ToCarWriter(w2)
return reader
}

View File

@@ -0,0 +1,18 @@
package container
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestWriterDedup(t *testing.T) {
ctn := NewWriter()
_, _, sealed := randToken()
ctn.AddSealed(sealed)
require.Len(t, ctn, 1)
ctn.AddSealed(sealed)
require.Len(t, ctn, 1)
}

View File

@@ -81,7 +81,7 @@ func statementFromIPLD(path string, node datamodel.Node) (Statement, error) {
}
case 3:
switch op {
case KindEqual, KindLessThan, KindLessThanOrEqual, KindGreaterThan, KindGreaterThanOrEqual:
case KindEqual, KindNotEqual, KindLessThan, KindLessThanOrEqual, KindGreaterThan, KindGreaterThanOrEqual:
sel, err := arg2AsSelector(op)
if err != nil {
return nil, err

View File

@@ -21,10 +21,31 @@ func TestIpldRoundTrip(t *testing.T) {
]
]`
// must contain all the operators
const allOps = `
[
["and", [
["==", ".foo1", ".bar1"],
["!=", ".foo2", ".bar2"]
]],
["or", [
[">", ".foo5", 5.2],
[">=", ".foo6", 6.2]
]],
["not", ["like", ".foo7", "*@example.com"]],
["all", ".foo8",
["<", ".foo3", 3]
],
["any", ".foo9",
["<=", ".foo4", 4]
]
]`
for _, tc := range []struct {
name, dagJsonStr string
}{
{"illustrativeExample", illustrativeExample},
{"allOps", allOps},
} {
nodes, err := ipld.Decode([]byte(tc.dagJsonStr), dagjson.Decode)
require.NoError(t, err)

View File

@@ -9,9 +9,9 @@ import (
const (
// MaxInt53 represents the maximum safe integer in JavaScript (2^53 - 1)
MaxInt53 = 9007199254740991
MaxInt53 int64 = 9007199254740991
// MinInt53 represents the minimum safe integer in JavaScript (-2^53 + 1)
MinInt53 = -9007199254740991
MinInt53 int64 = -9007199254740991
)
func ValidateIntegerBoundsIPLD(node ipld.Node) error {

View File

@@ -185,7 +185,7 @@ func anyAssemble(val any) qp.Assemble {
return qp.Int(i)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
u := rv.Uint()
if u > limits.MaxInt53 {
if u > uint64(limits.MaxInt53) {
panic(fmt.Sprintf("unsigned integer %d exceeds safe bounds", u))
}
return qp.Int(int64(u))

View File

@@ -82,6 +82,17 @@ func matchStatement(cur Statement, node ipld.Node) (_ matchResult, leafMost Stat
}
return boolToRes(datamodel.DeepEqual(s.value, res))
}
case KindNotEqual:
if s, ok := cur.(equality); ok {
res, err := s.selector.Select(node)
if err != nil {
return matchResultNoData, cur
}
if res == nil { // optional selector didn't match
return matchResultOptionalNoData, nil
}
return boolToRes(!datamodel.DeepEqual(s.value, res))
}
case KindGreaterThan:
if s, ok := cur.(equality); ok {
res, err := s.selector.Select(node)

View File

@@ -16,7 +16,7 @@ import (
func TestMatch(t *testing.T) {
t.Run("equality", func(t *testing.T) {
t.Run("string", func(t *testing.T) {
t.Run("eq string", func(t *testing.T) {
nd := literal.String("test")
pol := MustConstruct(Equal(".", literal.String("test")))
@@ -35,7 +35,7 @@ func TestMatch(t *testing.T) {
require.Equal(t, pol[0], leaf)
})
t.Run("int", func(t *testing.T) {
t.Run("eq int", func(t *testing.T) {
nd := literal.Int(138)
pol := MustConstruct(Equal(".", literal.Int(138)))
@@ -54,7 +54,7 @@ func TestMatch(t *testing.T) {
require.Equal(t, pol[0], leaf)
})
t.Run("float", func(t *testing.T) {
t.Run("eq float", func(t *testing.T) {
nd := literal.Float(1.138)
pol := MustConstruct(Equal(".", literal.Float(1.138)))
@@ -73,7 +73,7 @@ func TestMatch(t *testing.T) {
require.Equal(t, pol[0], leaf)
})
t.Run("IPLD Link", func(t *testing.T) {
t.Run("eq IPLD Link", func(t *testing.T) {
l0 := cidlink.Link{Cid: cid.MustParse("bafybeif4owy5gno5lwnixqm52rwqfodklf76hsetxdhffuxnplvijskzqq")}
l1 := cidlink.Link{Cid: cid.MustParse("bafkreifau35r7vi37tvbvfy3hdwvgb4tlflqf7zcdzeujqcjk3rsphiwte")}
@@ -95,7 +95,7 @@ func TestMatch(t *testing.T) {
require.Equal(t, pol[0], leaf)
})
t.Run("string in map", func(t *testing.T) {
t.Run("eq string in map", func(t *testing.T) {
nd, _ := literal.Map(map[string]any{
"foo": "bar",
})
@@ -121,7 +121,7 @@ func TestMatch(t *testing.T) {
require.Equal(t, pol[0], leaf)
})
t.Run("string in list", func(t *testing.T) {
t.Run("eq string in list", func(t *testing.T) {
nd, _ := literal.List([]any{"foo"})
pol := MustConstruct(Equal(".[0]", literal.String("foo")))
@@ -134,6 +134,132 @@ func TestMatch(t *testing.T) {
require.False(t, ok)
require.Equal(t, pol[0], leaf)
})
t.Run("neq string", func(t *testing.T) {
nd := literal.String("test")
pol := MustConstruct(NotEqual(".", literal.String("test")))
ok, leaf := pol.Match(nd)
require.False(t, ok)
require.Equal(t, pol[0], leaf)
pol = MustConstruct(NotEqual(".", literal.String("test2")))
ok, leaf = pol.Match(nd)
require.True(t, ok)
require.Nil(t, leaf)
pol = MustConstruct(NotEqual(".", literal.Int(138)))
ok, leaf = pol.Match(nd)
require.True(t, ok)
require.Nil(t, leaf)
})
t.Run("neq int", func(t *testing.T) {
nd := literal.Int(138)
pol := MustConstruct(NotEqual(".", literal.Int(138)))
ok, leaf := pol.Match(nd)
require.False(t, ok)
require.Equal(t, pol[0], leaf)
pol = MustConstruct(NotEqual(".", literal.Int(1138)))
ok, leaf = pol.Match(nd)
require.True(t, ok)
require.Nil(t, leaf)
pol = MustConstruct(NotEqual(".", literal.String("138")))
ok, leaf = pol.Match(nd)
require.True(t, ok)
require.Nil(t, leaf)
})
t.Run("neq float", func(t *testing.T) {
nd := literal.Float(1.138)
pol := MustConstruct(NotEqual(".", literal.Float(1.138)))
ok, leaf := pol.Match(nd)
require.False(t, ok)
require.Equal(t, pol[0], leaf)
pol = MustConstruct(NotEqual(".", literal.Float(11.38)))
ok, leaf = pol.Match(nd)
require.True(t, ok)
require.Nil(t, leaf)
pol = MustConstruct(NotEqual(".", literal.String("138")))
ok, leaf = pol.Match(nd)
require.True(t, ok)
require.Nil(t, leaf)
})
t.Run("neq IPLD Link", func(t *testing.T) {
l0 := cidlink.Link{Cid: cid.MustParse("bafybeif4owy5gno5lwnixqm52rwqfodklf76hsetxdhffuxnplvijskzqq")}
l1 := cidlink.Link{Cid: cid.MustParse("bafkreifau35r7vi37tvbvfy3hdwvgb4tlflqf7zcdzeujqcjk3rsphiwte")}
nd := literal.Link(l0)
pol := MustConstruct(NotEqual(".", literal.Link(l0)))
ok, leaf := pol.Match(nd)
require.False(t, ok)
require.Equal(t, pol[0], leaf)
pol = MustConstruct(NotEqual(".", literal.Link(l1)))
ok, leaf = pol.Match(nd)
require.True(t, ok)
require.Nil(t, leaf)
pol = MustConstruct(NotEqual(".", literal.String("bafybeif4owy5gno5lwnixqm52rwqfodklf76hsetxdhffuxnplvijskzqq")))
ok, leaf = pol.Match(nd)
require.True(t, ok)
require.Nil(t, leaf)
})
t.Run("neq string in map", func(t *testing.T) {
nd, _ := literal.Map(map[string]any{
"foo": "bar",
})
pol := MustConstruct(NotEqual(".foo", literal.String("bar")))
ok, leaf := pol.Match(nd)
require.False(t, ok)
require.Equal(t, pol[0], leaf)
pol = MustConstruct(NotEqual(".[\"foo\"]", literal.String("bar")))
ok, leaf = pol.Match(nd)
require.False(t, ok)
require.Equal(t, pol[0], leaf)
pol = MustConstruct(NotEqual(".foo", literal.String("baz")))
ok, leaf = pol.Match(nd)
require.True(t, ok)
require.Nil(t, leaf)
// missing data will fail, as not optional
pol = MustConstruct(NotEqual(".foobar", literal.String("bar")))
ok, leaf = pol.Match(nd)
require.False(t, ok)
require.Equal(t, pol[0], leaf)
})
t.Run("neq string in list", func(t *testing.T) {
nd, _ := literal.List([]any{"foo"})
pol := MustConstruct(NotEqual(".[0]", literal.String("foo")))
ok, leaf := pol.Match(nd)
require.False(t, ok)
require.Equal(t, pol[0], leaf)
pol = MustConstruct(NotEqual(".[0]", literal.String("bar")))
ok, leaf = pol.Match(nd)
require.True(t, ok)
require.Nil(t, leaf)
// missing data will fail, as not optional
pol = MustConstruct(NotEqual(".[1]", literal.String("foo")))
ok, leaf = pol.Match(nd)
require.False(t, ok)
require.Equal(t, pol[0], leaf)
})
})
t.Run("inequality", func(t *testing.T) {
@@ -245,20 +371,61 @@ func TestMatch(t *testing.T) {
require.False(t, ok)
require.Equal(t, pol[0], leaf)
})
t.Run("lt float", func(t *testing.T) {
nd := literal.Float(1.38)
pol := MustConstruct(LessThan(".", literal.Float(1)))
ok, leaf := pol.Match(nd)
require.False(t, ok)
require.Equal(t, pol[0], leaf)
pol = MustConstruct(LessThan(".", literal.Float(2)))
ok, leaf = pol.Match(nd)
require.True(t, ok)
require.Nil(t, leaf)
})
t.Run("lte float", func(t *testing.T) {
nd := literal.Float(1.38)
pol := MustConstruct(LessThanOrEqual(".", literal.Float(1)))
ok, leaf := pol.Match(nd)
require.False(t, ok)
require.Equal(t, pol[0], leaf)
pol = MustConstruct(GreaterThanOrEqual(".", literal.Float(1.38)))
ok, leaf = pol.Match(nd)
require.True(t, ok)
require.Nil(t, leaf)
pol = MustConstruct(LessThanOrEqual(".", literal.Float(2)))
ok, leaf = pol.Match(nd)
require.True(t, ok)
require.Nil(t, leaf)
})
})
t.Run("negation", func(t *testing.T) {
nd := literal.Bool(false)
nd, _ := literal.Map(map[string]any{
"foo": false,
})
pol := MustConstruct(Not(Equal(".", literal.Bool(true))))
pol := MustConstruct(Not(Equal(".foo", literal.Bool(true))))
ok, leaf := pol.Match(nd)
require.True(t, ok)
require.Nil(t, leaf)
pol = MustConstruct(Not(Equal(".", literal.Bool(false))))
pol = MustConstruct(Not(Equal(".foo", literal.Bool(false))))
ok, leaf = pol.Match(nd)
require.False(t, ok)
require.Equal(t, pol[0], leaf)
// missing data will fail, as not optional
pol = MustConstruct(Not(Equal(".foobar", literal.Bool(true))))
ok, leaf = pol.Match(nd)
require.False(t, ok)
require.Equal(t, MustConstruct(Equal(".foobar", literal.Bool(true)))[0], leaf)
})
t.Run("conjunction", func(t *testing.T) {
@@ -482,6 +649,7 @@ func FuzzMatch(f *testing.F) {
f.Add([]byte(`[["all", ".reviewer", ["like", ".email", "*@example.com"]]]`), []byte(`{"reviewer": [{"email": "alice@example.com"}, {"email": "bob@example.com"}]}`))
f.Add([]byte(`[["any", ".tags", ["or", [["==", ".", "news"], ["==", ".", "press"]]]]]`), []byte(`{"tags": ["news", "press"]}`))
f.Add([]byte(`[["==", ".name", "Alice"]]`), []byte(`{"name": "Alice"}`))
f.Add([]byte(`[["!=", ".name", "Alice"]]`), []byte(`{"name": "Alice"}`))
f.Add([]byte(`[[">", ".age", 30]]`), []byte(`{"age": 31}`))
f.Add([]byte(`[["<=", ".height", 180]]`), []byte(`{"height": 170}`))
f.Add([]byte(`[["not", ["==", ".status", "inactive"]]]`), []byte(`{"status": "active"}`))

View File

@@ -14,6 +14,7 @@ import (
const (
KindEqual = "==" // implemented by equality
KindNotEqual = "!=" // implemented by equality
KindGreaterThan = ">" // implemented by equality
KindGreaterThanOrEqual = ">=" // implemented by equality
KindLessThan = "<" // implemented by equality
@@ -87,6 +88,13 @@ func Equal(selector string, value ipld.Node) Constructor {
}
}
func NotEqual(selector string, value ipld.Node) Constructor {
return func() (Statement, error) {
sel, err := selpkg.Parse(selector)
return equality{kind: KindNotEqual, selector: sel, value: value}, err
}
}
func GreaterThan(selector string, value ipld.Node) Constructor {
return func() (Statement, error) {
sel, err := selpkg.Parse(selector)
@@ -125,7 +133,7 @@ func (n negation) Kind() string {
func (n negation) String() string {
child := n.statement.String()
return fmt.Sprintf(`["%s", "%s"]`, n.Kind(), strings.ReplaceAll(child, "\n", "\n "))
return fmt.Sprintf(`["%s", %s]`, n.Kind(), strings.ReplaceAll(child, "\n", "\n "))
}
func Not(cstor Constructor) Constructor {
@@ -149,7 +157,7 @@ func (c connective) String() string {
for i, statement := range c.statements {
childs[i] = strings.ReplaceAll(statement.String(), "\n", "\n ")
}
return fmt.Sprintf("[\"%s\", [\n %s]]\n", c.kind, strings.Join(childs, ",\n "))
return fmt.Sprintf("[\"%s\", [\n %s\n]]", c.kind, strings.Join(childs, ",\n "))
}
func And(cstors ...Constructor) Constructor {
@@ -208,7 +216,7 @@ func (n quantifier) Kind() string {
func (n quantifier) String() string {
child := n.statement.String()
return fmt.Sprintf("[\"%s\", \"%s\",\n %s]", n.Kind(), n.selector, strings.ReplaceAll(child, "\n", "\n "))
return fmt.Sprintf("[\"%s\", \"%s\",\n %s\n]", n.Kind(), n.selector, strings.ReplaceAll(child, "\n", "\n "))
}
func All(selector string, cstor Constructor) Constructor {

View File

@@ -28,12 +28,14 @@ func ExamplePolicy() {
// [
// ["==", ".status", "draft"],
// ["all", ".reviewer",
// ["like", ".email", "*@example.com"]],
// ["like", ".email", "*@example.com"]
// ],
// ["any", ".tags",
// ["or", [
// ["==", ".", "news"],
// ["==", ".", "press"]]]
// ]
// ["==", ".", "press"]
// ]]
// ]
// ]
}
@@ -59,12 +61,14 @@ func ExamplePolicy_accumulate() {
// [
// ["==", ".status", "draft"],
// ["all", ".reviewer",
// ["like", ".email", "*@example.com"]],
// ["like", ".email", "*@example.com"]
// ],
// ["any", ".tags",
// ["or", [
// ["==", ".", "news"],
// ["==", ".", "press"]]]
// ]
// ["==", ".", "press"]
// ]]
// ]
// ]
}

View File

@@ -2,6 +2,7 @@ package policytest
import (
"github.com/ipld/go-ipld-prime"
"github.com/ucan-wg/go-ucan/pkg/args"
"github.com/ucan-wg/go-ucan/pkg/policy"
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
@@ -10,9 +11,8 @@ import (
// EmptyPolicy provides a Policy with no statements.
var EmptyPolicy = policy.Policy{}
// ExampleValidationPolicy provides a instantiated SpecPolicy containing the
// statements that are included in the second code block of the [Validation]
// section of the delegation specification.
// SpecPolicy provides a valid Policy containing the statements that are included
// in the second code block of the [Validation] section of the delegation specification.
//
// [Validation]: https://github.com/ucan-wg/delegation/tree/v1_ipld#validation
var SpecPolicy = policy.MustConstruct(
@@ -24,8 +24,8 @@ var SpecPolicy = policy.MustConstruct(
// specification has been finished/merged.
// SpecValidArguments provides valid, instantiated Arguments containing
// the key/value pairs that are included in portion of the the second code
// block of the [Validation] section of the delegation specification.
// the key/value pairs that are included in portion of the second code block
// of the [Validation] section of the delegation specification.
//
// [Validation]: https://github.com/ucan-wg/delegation/tree/v1_ipld#validation
var SpecValidArguments = args.NewBuilder().
@@ -41,8 +41,8 @@ var SpecValidArguments = args.NewBuilder().
var specValidArgumentsIPLD = mustIPLD(SpecValidArguments)
// SpecInvalidArguments provides invalid, instantiated Arguments containing
// the key/value pairs that are included in portion of the the second code
// block of the [Validation] section of the delegation specification.
// the key/value pairs that are included in portion of the second code block
// of the [Validation] section of the delegation specification.
//
// [Validation]: https://github.com/ucan-wg/delegation/tree/v1_ipld#validation
var SpecInvalidArguments = args.NewBuilder().

View File

@@ -68,7 +68,7 @@ func Parse(str string) (Selector, error) {
if err != nil {
return nil, newParseError("invalid index", str, col, tok)
}
if idx > limits.MaxInt53 || idx < limits.MinInt53 {
if int64(idx) > limits.MaxInt53 || int64(idx) < limits.MinInt53 {
return nil, newParseError(fmt.Sprintf("index %d exceeds safe integer bounds", idx), str, col, tok)
}
sel = append(sel, segment{str: tok, optional: opt, index: idx})
@@ -173,37 +173,37 @@ func tokenize(str string) []string {
return toks
}
type parseerr struct {
type parseErr struct {
msg string
src string
col int
tok string
}
func (p parseerr) Name() string {
func (p parseErr) Name() string {
return "ParseError"
}
func (p parseerr) Message() string {
func (p parseErr) Message() string {
return p.msg
}
func (p parseerr) Column() int {
func (p parseErr) Column() int {
return p.col
}
func (p parseerr) Error() string {
func (p parseErr) Error() string {
return p.msg
}
func (p parseerr) Source() string {
func (p parseErr) Source() string {
return p.src
}
func (p parseerr) Token() string {
func (p parseErr) Token() string {
return p.tok
}
func newParseError(message string, source string, column int, token string) error {
return parseerr{message, source, column, token}
return parseErr{message, source, column, token}
}

View File

@@ -19,7 +19,7 @@ type Selector []segment
// Select perform the selection described by the selector on the input IPLD DAG.
// Select can return:
// - exactly one matched IPLD node
// - a resolutionerr error if not being able to resolve to a node
// - a resolutionErr error if not being able to resolve to a node
// - nil and no errors, if the selector couldn't match on an optional segment (with ?).
func (s Selector) Select(subject ipld.Node) (ipld.Node, error) {
return resolve(s, subject, nil)
@@ -316,27 +316,27 @@ func kindString(n datamodel.Node) string {
return n.Kind().String()
}
type resolutionerr struct {
type resolutionErr struct {
msg string
at []string
}
func (r resolutionerr) Name() string {
func (r resolutionErr) Name() string {
return "ResolutionError"
}
func (r resolutionerr) Message() string {
func (r resolutionErr) Message() string {
return fmt.Sprintf("can not resolve path: .%s", strings.Join(r.at, "."))
}
func (r resolutionerr) At() []string {
func (r resolutionErr) At() []string {
return r.at
}
func (r resolutionerr) Error() string {
func (r resolutionErr) Error() string {
return r.Message()
}
func newResolutionError(message string, at []string) error {
return resolutionerr{message, at}
return resolutionErr{message, at}
}

View File

@@ -133,7 +133,7 @@ func TestSelect(t *testing.T) {
require.Error(t, err)
require.Empty(t, res)
require.ErrorAs(t, err, &resolutionerr{}, "error should be a resolution error")
require.ErrorAs(t, err, &resolutionErr{}, "error should be a resolution error")
})
t.Run("optional not exists", func(t *testing.T) {
@@ -351,7 +351,7 @@ func FuzzParseAndSelect(f *testing.F) {
// look for panic()
_, err = sel.Select(node)
if err != nil && !errors.As(err, &resolutionerr{}) {
if err != nil && !errors.As(err, &resolutionErr{}) {
// not normal, we should only have resolution errors
t.Fatal(err)
}

View File

@@ -83,7 +83,7 @@ func New(iss did.DID, aud did.DID, cmd command.Command, pol policy.Policy, sub d
}
// Root creates a validated UCAN delegation Token from the provided parameters and options.
// This is typically used to create and give a power to an agent.
// This is typically used to create and give power to an agent.
//
// You can read it as "(issuer) allows (audience) to perform (cmd+pol) on itself".
func Root(iss did.DID, aud did.DID, cmd command.Command, pol policy.Policy, opts ...Option) (*Token, error) {
@@ -154,6 +154,16 @@ func (t *Token) Expiration() *time.Time {
return t.expiration
}
// IsRoot tells if the token is a root delegation.
func (t *Token) IsRoot() bool {
return t.issuer == t.subject
}
// IsPowerline tells if the token is a powerline delegation.
func (t *Token) IsPowerline() bool {
return t.subject == did.Undef
}
// IsValidNow verifies that the token can be used at the current time, based on expiration or "not before" fields.
// This does NOT do any other kind of verifications.
func (t *Token) IsValidNow() bool {
@@ -172,25 +182,6 @@ func (t *Token) IsValidAt(ti time.Time) bool {
return true
}
// Covers indicate if this token has the power to allow the given sub-delegation.
// This function only verifies the principals alignment
func (t *Token) Covers(subDelegation *Token) bool {
// The Subject of each delegation must equal the invocation's Subject (or Audience if defined). - 4f
if t.Subject() != sub {
return fmt.Errorf("%w: delegation %s, expected %s, got %s", ErrWrongSub, dlgCid, sub, dlg.Subject())
}
// The Issuer of each delegation must be the Audience in the next one. - 4d
if t.Audience() != subDelegation.Issuer() {
return fmt.Errorf("%w: delegation %s, expected %s, got %s", ErrBrokenChain, dlgCid, iss, dlg.Audience())
}
// The command of each delegation must "allow" the one before it. - 4g
if !dlg.Command().Covers(cmd) {
return fmt.Errorf("%w: delegation %s, %s doesn't cover %s", ErrCommandNotCovered, dlgCid, dlg.Command(), cmd)
}
}
func (t *Token) String() string {
var res strings.Builder

View File

@@ -20,39 +20,16 @@ const (
subJectCmd = "/foo/bar"
subjectPol = `
[
[
"==",
".status",
"draft"
],
[
"all",
".reviewer",
[
"like",
".email",
"*@example.com"
]
],
[
"any",
".tags",
[
"or",
[
[
"==",
".",
"news"
],
[
"==",
".",
"press"
]
]
]
]
["==", ".status", "draft"],
["all", ".reviewer",
["like", ".email", "*@example.com"]
],
["any", ".tags",
["or", [
["==", ".", "news"],
["==", ".", "press"]
]]
]
]
`
@@ -80,6 +57,9 @@ func TestConstructors(t *testing.T) {
)
require.NoError(t, err)
require.False(t, tkn.IsRoot())
require.False(t, tkn.IsPowerline())
data, err := tkn.ToDagJson(didtest.PersonaAlice.PrivKey())
require.NoError(t, err)
@@ -97,6 +77,9 @@ func TestConstructors(t *testing.T) {
)
require.NoError(t, err)
require.True(t, tkn.IsRoot())
require.False(t, tkn.IsPowerline())
data, err := tkn.ToDagJson(didtest.PersonaAlice.PrivKey())
require.NoError(t, err)
@@ -114,6 +97,9 @@ func TestConstructors(t *testing.T) {
)
require.NoError(t, err)
require.False(t, tkn.IsRoot())
require.True(t, tkn.IsPowerline())
data, err := tkn.ToDagJson(didtest.PersonaAlice.PrivKey())
require.NoError(t, err)

View File

@@ -242,7 +242,7 @@ func (g *generator) writeGoFile() error {
Println("}")
Println()
Println("var AllBundles = []*delegation.Bundle{")
Println("var AllBundles = []delegation.Bundle{")
for _, d := range g.dlgs {
Printf("\t%sBundle,\n", d.name)
}

View File

@@ -38,7 +38,7 @@ var fs embed.FS
var _ delegation.Loader = (*DelegationLoader)(nil)
type DelegationLoader struct {
bundles map[cid.Cid]*delegation.Bundle
bundles map[cid.Cid]delegation.Bundle
}
var (
@@ -75,7 +75,7 @@ func loadDelegations() (*DelegationLoader, error) {
return nil, err
}
bundles := make(map[cid.Cid]*delegation.Bundle, len(dirEntries))
bundles := make(map[cid.Cid]delegation.Bundle, len(dirEntries))
for _, dirEntry := range dirEntries {
data, err := fs.ReadFile(filepath.Join(TokenDir, dirEntry.Name()))
@@ -88,7 +88,7 @@ func loadDelegations() (*DelegationLoader, error) {
return nil, err
}
bundles[id] = &delegation.Bundle{Cid: id, Decoded: tkn, Sealed: data}
bundles[id] = delegation.Bundle{Cid: id, Decoded: tkn, Sealed: data}
}
return &DelegationLoader{
@@ -106,7 +106,7 @@ func CidToName(id cid.Cid) string {
return cidToName[id]
}
func mustGetBundle(id cid.Cid) *delegation.Bundle {
func mustGetBundle(id cid.Cid) delegation.Bundle {
bundle, ok := GetDelegationLoader().bundles[id]
if !ok {
panic(delegation.ErrDelegationNotFound)

View File

@@ -195,7 +195,7 @@ var AllTokens = []*delegation.Token{
TokenErinFrank_ValidExamplePolicy,
}
var AllBundles = []*delegation.Bundle{
var AllBundles = []delegation.Bundle{
TokenAliceBobBundle,
TokenBobCarolBundle,
TokenCarolDanBundle,

View File

@@ -272,12 +272,14 @@ func ExampleFromSealed() {
// Policy (pol): [
// ["==", ".status", "draft"],
// ["all", ".reviewer",
// ["like", ".email", "*@example.com"]],
// ["like", ".email", "*@example.com"]
// ],
// ["any", ".tags",
// ["or", [
// ["==", ".", "news"],
// ["==", ".", "press"]]]
// ]
// ["==", ".", "press"]
// ]]
// ]
// ]
// Nonce (nonce): 000102030405060708090a0b
// Meta (meta): {}

View File

@@ -16,7 +16,7 @@ type Loader interface {
GetDelegation(cid cid.Cid) (*Token, error)
}
// Bundle carries together a decoded delegation with its Cid and raw signed data.
// Bundle carries together a decoded token with its Cid and raw signed data.
type Bundle struct {
Cid cid.Cid
Decoded *Token

View File

@@ -39,3 +39,10 @@ type Marshaller interface {
// ToDagJsonWriter is the same as ToDagJson, but it accepts an io.Writer.
ToDagJsonWriter(w io.Writer, privKey crypto.PrivKey) error
}
// Bundle carries together a decoded token with its Cid and raw signed data.
type Bundle struct {
Cid cid.Cid
Decoded Token
Sealed []byte
}

View File

@@ -59,6 +59,9 @@ type Token struct {
// New creates an invocation Token with the provided options.
//
// The given proofs MUST be ordered from the leaf (matching the invocation) to
// the root delegation.
//
// If no nonce is provided, a random 12-byte nonce is generated. Use the
// WithNonce or WithEmptyNonce options to specify provide your own nonce
// or to leave the nonce empty respectively.

View File

@@ -37,9 +37,9 @@ import (
// 4. When the proof chain is being validated (verifyProofs below):
// a. There must be at least one delegation in the proof chain.
// b. All referenced delegations must be available.
// c. The first proof must be issued to the Invoker.
// d. The Issuer of each delegation must be the Audience in the parent delegation.
// e. The chain must terminate with a root delegation.
// c. The first proof must be issued to the Invoker (audience DID).
// d. The Issuer of each delegation must be the Audience in the next one.
// e. The last token must be a root delegation.
// f. The Subject of each delegation must equal the invocation's Subject (or Audience if defined)
// g. The command of each delegation must "allow" the one before it.
//
@@ -63,7 +63,7 @@ func (t *Token) verifyProofs(delegations []*delegation.Token) error {
sub = t.audience
}
// control from the invocation to the root
// control from the invocation to the root delegation
for i, dlgCid := range t.proof {
dlg := delegations[i]
@@ -72,7 +72,7 @@ func (t *Token) verifyProofs(delegations []*delegation.Token) error {
return fmt.Errorf("%w: delegation %s, expected %s, got %s", ErrWrongSub, dlgCid, sub, dlg.Subject())
}
// The first proof must be issued to the Invoker. - 4c
// The first proof must be issued to the Invoker (audience DID). - 4c
// The Issuer of each delegation must be the Audience in the next one. - 4d
if dlg.Audience() != iss {
return fmt.Errorf("%w: delegation %s, expected %s, got %s", ErrBrokenChain, dlgCid, iss, dlg.Audience())

View File

@@ -2,7 +2,6 @@ package invocation_test
import (
"bytes"
"fmt"
"testing"
"github.com/libp2p/go-libp2p/core/crypto"
@@ -38,18 +37,13 @@ func TestSchemaRoundTrip(t *testing.T) {
cborBytes, id, err := p1.ToSealed(privKey)
require.NoError(t, err)
assert.Equal(t, newCID, envelope.CIDToBase58BTC(id))
fmt.Println("cborBytes length", len(cborBytes))
fmt.Println("cbor", string(cborBytes))
p2, c2, err := invocation.FromSealed(cborBytes)
require.NoError(t, err)
assert.Equal(t, id, c2)
fmt.Println("read Cbor", p2)
readJson, err := p2.ToDagJson(privKey)
require.NoError(t, err)
fmt.Println("readJson length", len(readJson))
fmt.Println("json: ", string(readJson))
assert.JSONEq(t, string(invocationJson), string(readJson))
})

View File

@@ -0,0 +1,10 @@
package invocation
import "github.com/ipfs/go-cid"
// Bundle carries together a decoded token with its Cid and raw signed data.
type Bundle struct {
Cid cid.Cid
Decoded *Token
Sealed []byte
}