Compare commits
80 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e218b49577 | ||
|
|
4c81ac778e | ||
|
|
7ae65e7c8e | ||
|
|
7f9cb6426c | ||
|
|
9c141029c3 | ||
|
|
0c6717cfbc | ||
|
|
6d7fd28324 | ||
|
|
ccc0120e78 | ||
|
|
bf27b97a57 | ||
|
|
10dd4fa6d1 | ||
|
|
5695609f8b | ||
|
|
b7ddead7c8 | ||
|
|
9cbec37685 | ||
|
|
00dbd975b1 | ||
|
|
c792a4cce5 | ||
|
|
8f3f1c775e | ||
|
|
704ed25768 | ||
|
|
95bdbc4fc5 | ||
|
|
416345dba9 | ||
|
|
042d6dc52f | ||
|
|
8bb3a4f4d0 | ||
|
|
47156a8ad6 | ||
|
|
ce6d163627 | ||
|
|
c3c2c96008 | ||
|
|
903632695f | ||
|
|
f2d75b7815 | ||
|
|
4f09829abe | ||
|
|
5660df32b5 | ||
|
|
2f2a74c7ec | ||
|
|
0592717637 | ||
|
|
80c2d60ab3 | ||
|
|
c518c6657a | ||
|
|
78825f4f55 | ||
|
|
7f1adbd945 | ||
|
|
0f59088d0b | ||
|
|
72e0f353e7 | ||
|
|
d0d4ec3abe | ||
|
|
bb24081b28 | ||
|
|
3688ccea01 | ||
|
|
e9105896d7 | ||
|
|
15751c7362 | ||
|
|
d52218fa5a | ||
|
|
64d3024dec | ||
|
|
78d37d92ef | ||
|
|
da806b1bc5 | ||
|
|
311b942a6d | ||
|
|
56eab758ed | ||
|
|
105323b989 | ||
|
|
5b816ccc62 | ||
|
|
28272e6900 | ||
|
|
a854389e32 | ||
|
|
117a75e2c4 | ||
|
|
a25bfbaf45 | ||
|
|
bff482f73b | ||
|
|
ff79bbb443 | ||
|
|
3997a86184 | ||
|
|
200d6a8ae2 | ||
|
|
0349e7e463 | ||
|
|
dff52f80c4 | ||
|
|
5b7a63a2c6 | ||
|
|
66675f7030 | ||
|
|
7e54be49e1 | ||
|
|
15535d3474 | ||
|
|
170e597e71 | ||
|
|
ce1a4b6e32 | ||
|
|
d1d047cd9e | ||
|
|
3680637090 | ||
|
|
1166a68e5c | ||
|
|
ba4db9bce8 | ||
|
|
20369dba49 | ||
|
|
ade2c7f858 | ||
|
|
943a318b26 | ||
|
|
2d79cdc54e | ||
|
|
820057e41e | ||
|
|
ba0038b0ae | ||
|
|
4a4b200312 | ||
|
|
caae2f58bf | ||
|
|
ec627138cb | ||
|
|
4ec409edc6 | ||
|
|
c61fc8d8b3 |
4
.github/workflows/bench.yml
vendored
4
.github/workflows/bench.yml
vendored
@@ -2,7 +2,7 @@ name: go continuous benchmark
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- v1
|
||||
- main
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "stable"
|
||||
- name: Run benchmark
|
||||
|
||||
4
.github/workflows/gotest.yml
vendored
4
.github/workflows/gotest.yml
vendored
@@ -13,8 +13,8 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}-latest
|
||||
name: ${{ matrix.os}} (go ${{ matrix.go }})
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-go@v2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
- name: Go information
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -102,6 +102,9 @@ func (d DID) PubKey() (crypto.PubKey, error) {
|
||||
|
||||
// String formats the decentralized identity document (DID) as a string.
|
||||
func (d DID) String() string {
|
||||
if d == Undef {
|
||||
return "(undefined)"
|
||||
}
|
||||
key, _ := mbase.Encode(mbase.Base58BTC, []byte(d.bytes))
|
||||
return "did:key:" + key
|
||||
}
|
||||
|
||||
@@ -5,10 +5,8 @@ package didtest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/libp2p/go-libp2p/core/crypto"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/did"
|
||||
)
|
||||
@@ -30,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
|
||||
@@ -85,7 +83,19 @@ func (p Persona) Name() string {
|
||||
|
||||
// PrivKey returns the Ed25519 private key for the Persona.
|
||||
func (p Persona) PrivKey() crypto.PrivKey {
|
||||
return privKeys[p]
|
||||
res, ok := privKeys[p]
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("Unknown persona: %v", p))
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func (p Persona) PrivKeyConfig() string {
|
||||
res, ok := privKeyB64()[p]
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("Unknown persona: %v", p))
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// PubKey returns the Ed25519 public key for the Persona.
|
||||
@@ -95,10 +105,11 @@ func (p Persona) PubKey() crypto.PubKey {
|
||||
|
||||
// PubKeyConfig returns the marshaled and encoded Ed25519 public key
|
||||
// for the Persona.
|
||||
func (p Persona) PubKeyConfig(t *testing.T) string {
|
||||
func (p Persona) PubKeyConfig() string {
|
||||
pubKeyMar, err := crypto.MarshalPublicKey(p.PrivKey().GetPublic())
|
||||
require.NoError(t, err)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return crypto.ConfigEncodeKey(pubKeyMar)
|
||||
}
|
||||
|
||||
@@ -125,3 +136,15 @@ func Personas() []Persona {
|
||||
PersonaFrank,
|
||||
}
|
||||
}
|
||||
|
||||
// DidToName retrieve the persona's name from its DID.
|
||||
func DidToName(d did.DID) string {
|
||||
return map[did.DID]string{
|
||||
PersonaAlice.DID(): "Alice",
|
||||
PersonaBob.DID(): "Bob",
|
||||
PersonaCarol.DID(): "Carol",
|
||||
PersonaDan.DID(): "Dan",
|
||||
PersonaErin.DID(): "Erin",
|
||||
PersonaFrank.DID(): "Frank",
|
||||
}[d]
|
||||
}
|
||||
|
||||
7
go.mod
7
go.mod
@@ -3,7 +3,6 @@ module github.com/ucan-wg/go-ucan
|
||||
go 1.23
|
||||
|
||||
require (
|
||||
github.com/dave/jennifer v1.7.1
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0
|
||||
github.com/ipfs/go-cid v0.4.1
|
||||
github.com/ipld/go-ipld-prime v0.21.0
|
||||
@@ -14,7 +13,8 @@ require (
|
||||
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
|
||||
)
|
||||
|
||||
@@ -35,8 +35,7 @@ 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
|
||||
golang.org/x/sys v0.29.0 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
|
||||
14
go.sum
14
go.sum
@@ -1,7 +1,5 @@
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/dave/jennifer v1.7.1 h1:B4jJJDHelWcDhlRQxWeo0Npa/pYKBLrirAQoTN45txo=
|
||||
github.com/dave/jennifer v1.7.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc=
|
||||
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=
|
||||
@@ -83,21 +81,21 @@ 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/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-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
||||
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=
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
package args
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"iter"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
@@ -14,9 +16,12 @@ import (
|
||||
"github.com/ipld/go-ipld-prime/node/basicnode"
|
||||
"github.com/ipld/go-ipld-prime/printer"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy/limits"
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
|
||||
)
|
||||
|
||||
var ErrNotFound = errors.New("key not found in meta")
|
||||
|
||||
// Args are the Command's arguments when an invocation Token is processed by the executor.
|
||||
// This also serves as a way to construct the underlying IPLD data with minimum allocations
|
||||
// and transformations, while hiding the IPLD complexity from the caller.
|
||||
@@ -35,6 +40,16 @@ func New() *Args {
|
||||
}
|
||||
}
|
||||
|
||||
// GetNode retrieves a value as a raw IPLD node.
|
||||
// Returns ErrNotFound if the given key is missing.
|
||||
func (a *Args) GetNode(key string) (ipld.Node, error) {
|
||||
v, ok := a.Values[key]
|
||||
if !ok {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// Add inserts a key/value pair in the Args set.
|
||||
//
|
||||
// Accepted types for val are any CBOR compatible type, or directly IPLD values.
|
||||
@@ -48,27 +63,51 @@ func (a *Args) Add(key string, val any) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := limits.ValidateIntegerBoundsIPLD(node); err != nil {
|
||||
return fmt.Errorf("value for key %q: %w", key, err)
|
||||
}
|
||||
|
||||
a.Values[key] = node
|
||||
a.Keys = append(a.Keys, key)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type Iterator interface {
|
||||
Iter() iter.Seq2[string, ipld.Node]
|
||||
}
|
||||
|
||||
// Include merges the provided arguments into the existing arguments.
|
||||
//
|
||||
// If duplicate keys are encountered, the new value is silently dropped
|
||||
// without causing an error.
|
||||
func (a *Args) Include(other *Args) {
|
||||
for _, key := range other.Keys {
|
||||
func (a *Args) Include(other Iterator) {
|
||||
for key, value := range other.Iter() {
|
||||
if _, ok := a.Values[key]; ok {
|
||||
// don't overwrite
|
||||
continue
|
||||
}
|
||||
a.Values[key] = other.Values[key]
|
||||
a.Values[key] = value
|
||||
a.Keys = append(a.Keys, key)
|
||||
}
|
||||
}
|
||||
|
||||
// Len return the number of arguments.
|
||||
func (a *Args) Len() int {
|
||||
return len(a.Keys)
|
||||
}
|
||||
|
||||
// Iter iterates over the args key/values
|
||||
func (a *Args) Iter() iter.Seq2[string, ipld.Node] {
|
||||
return func(yield func(string, ipld.Node) bool) {
|
||||
for _, key := range a.Keys {
|
||||
if !yield(key, a.Values[key]) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ToIPLD wraps an instance of an Args with an ipld.Node.
|
||||
func (a *Args) ToIPLD() (ipld.Node, error) {
|
||||
sort.Strings(a.Keys)
|
||||
@@ -135,3 +174,14 @@ func (a *Args) Clone() *Args {
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// Validate checks that all values in the Args are valid according to UCAN specs
|
||||
func (a *Args) Validate() error {
|
||||
for key, value := range a.Values {
|
||||
if err := limits.ValidateIntegerBoundsIPLD(value); err != nil {
|
||||
return fmt.Errorf("value for key %q: %w", key, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package args_test
|
||||
|
||||
import (
|
||||
"maps"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
@@ -8,11 +9,13 @@ import (
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
"github.com/ipld/go-ipld-prime/codec/dagcbor"
|
||||
"github.com/ipld/go-ipld-prime/datamodel"
|
||||
"github.com/ipld/go-ipld-prime/node/basicnode"
|
||||
"github.com/ipld/go-ipld-prime/schema"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/pkg/args"
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy/limits"
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
|
||||
)
|
||||
|
||||
@@ -133,23 +136,138 @@ func TestArgs_Include(t *testing.T) {
|
||||
assert.Equal(t, "val4", must(argsIn.Values["key4"].AsString()))
|
||||
}
|
||||
|
||||
func TestIterCloneEquals(t *testing.T) {
|
||||
a := args.New()
|
||||
|
||||
require.NoError(t, a.Add("foo", "bar"))
|
||||
require.NoError(t, a.Add("baz", 1234))
|
||||
|
||||
expected := map[string]ipld.Node{
|
||||
"foo": basicnode.NewString("bar"),
|
||||
"baz": basicnode.NewInt(1234),
|
||||
}
|
||||
|
||||
// args -> iter
|
||||
require.Equal(t, expected, maps.Collect(a.Iter()))
|
||||
|
||||
// readonly -> iter
|
||||
ro := a.ReadOnly()
|
||||
require.Equal(t, expected, maps.Collect(ro.Iter()))
|
||||
|
||||
// args -> clone -> iter
|
||||
clone := a.Clone()
|
||||
require.Equal(t, expected, maps.Collect(clone.Iter()))
|
||||
|
||||
// readonly -> WriteableClone -> iter
|
||||
wclone := ro.WriteableClone()
|
||||
require.Equal(t, expected, maps.Collect(wclone.Iter()))
|
||||
|
||||
require.True(t, a.Equals(wclone))
|
||||
require.True(t, ro.Equals(wclone.ReadOnly()))
|
||||
}
|
||||
|
||||
func TestInclude(t *testing.T) {
|
||||
a1 := args.New()
|
||||
|
||||
require.NoError(t, a1.Add("samekey", "bar"))
|
||||
require.NoError(t, a1.Add("baz", 1234))
|
||||
|
||||
a2 := args.New()
|
||||
|
||||
require.NoError(t, a2.Add("samekey", "othervalue")) // check no overwrite
|
||||
require.NoError(t, a2.Add("otherkey", 1234))
|
||||
|
||||
a1.Include(a2)
|
||||
|
||||
require.Equal(t, map[string]ipld.Node{
|
||||
"samekey": basicnode.NewString("bar"),
|
||||
"baz": basicnode.NewInt(1234),
|
||||
"otherkey": basicnode.NewInt(1234),
|
||||
}, maps.Collect(a1.Iter()))
|
||||
}
|
||||
|
||||
func TestArgsIntegerBounds(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
key string
|
||||
val int64
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "valid int",
|
||||
key: "valid",
|
||||
val: 42,
|
||||
},
|
||||
{
|
||||
name: "max safe integer",
|
||||
key: "max",
|
||||
val: limits.MaxInt53,
|
||||
},
|
||||
{
|
||||
name: "min safe integer",
|
||||
key: "min",
|
||||
val: limits.MinInt53,
|
||||
},
|
||||
{
|
||||
name: "exceeds max safe integer",
|
||||
key: "tooBig",
|
||||
val: limits.MaxInt53 + 1,
|
||||
wantErr: "exceeds safe integer bounds",
|
||||
},
|
||||
{
|
||||
name: "below min safe integer",
|
||||
key: "tooSmall",
|
||||
val: limits.MinInt53 - 1,
|
||||
wantErr: "exceeds safe integer bounds",
|
||||
},
|
||||
{
|
||||
name: "duplicate key",
|
||||
key: "duplicate",
|
||||
val: 42,
|
||||
wantErr: "duplicate key",
|
||||
},
|
||||
}
|
||||
|
||||
a := args.New()
|
||||
require.NoError(t, a.Add("duplicate", 1)) // tests duplicate key
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := a.Add(tt.key, tt.val)
|
||||
if tt.wantErr != "" {
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), tt.wantErr)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
val, err := a.GetNode(tt.key)
|
||||
require.NoError(t, err)
|
||||
i, err := val.AsInt()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.val, i)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
argsSchema = "type Args { String : Any }"
|
||||
argsName = "Args"
|
||||
)
|
||||
|
||||
var (
|
||||
once sync.Once
|
||||
ts *schema.TypeSystem
|
||||
err error
|
||||
once sync.Once
|
||||
ts *schema.TypeSystem
|
||||
errSchema error
|
||||
)
|
||||
|
||||
func argsType() schema.Type {
|
||||
once.Do(func() {
|
||||
ts, err = ipld.LoadSchemaBytes([]byte(argsSchema))
|
||||
ts, errSchema = ipld.LoadSchemaBytes([]byte(argsSchema))
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
if errSchema != nil {
|
||||
panic(errSchema)
|
||||
}
|
||||
|
||||
return ts.TypeByName(argsName)
|
||||
|
||||
71
pkg/args/builder.go
Normal file
71
pkg/args/builder.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package args
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
)
|
||||
|
||||
// Builder allows the fluid construction of an Args.
|
||||
type Builder struct {
|
||||
args *Args
|
||||
errs error
|
||||
}
|
||||
|
||||
// NewBuilder returns a Builder which will assemble the Args.
|
||||
func NewBuilder() *Builder {
|
||||
return &Builder{
|
||||
args: New(),
|
||||
}
|
||||
}
|
||||
|
||||
// Add inserts a new key/val into the Args being assembled while collecting
|
||||
// any errors caused by duplicate keys.
|
||||
func (b *Builder) Add(key string, val any) *Builder {
|
||||
b.errs = errors.Join(b.errs, b.args.Add(key, val))
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
// Build returns the assembled Args or an error containing a list of
|
||||
// errors encountered while trying to build the Args.
|
||||
func (b *Builder) Build() (*Args, error) {
|
||||
if b.errs != nil {
|
||||
return nil, b.errs
|
||||
}
|
||||
|
||||
return b.args, nil
|
||||
}
|
||||
|
||||
// BuildIPLD is the same as Build except it takes the additional step of
|
||||
// converting the Args to an ipld.Node.
|
||||
func (b *Builder) BuildIPLD() (ipld.Node, error) {
|
||||
args, err := b.Build()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return args.ToIPLD()
|
||||
}
|
||||
|
||||
// MustBuild is the same as Build except it panics if an error occurs.
|
||||
func (b *Builder) MustBuild() *Args {
|
||||
args, err := b.Build()
|
||||
|
||||
if err != nil {
|
||||
panic(b.errs)
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
// MustBuildIPLD is the same as BuildIPLD except it panics if an error
|
||||
// occurs.
|
||||
func (b *Builder) MustBuildIPLD() ipld.Node {
|
||||
node, err := b.BuildIPLD()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return node
|
||||
}
|
||||
81
pkg/args/builder_test.go
Normal file
81
pkg/args/builder_test.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package args_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/ucan-wg/go-ucan/pkg/args"
|
||||
)
|
||||
|
||||
func TestBuilder_XXX(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const (
|
||||
keyOne = "key1"
|
||||
valOne = "string"
|
||||
keyTwo = "key2"
|
||||
valTwo = 42
|
||||
)
|
||||
|
||||
exp := args.New()
|
||||
exp.Add(keyOne, valOne)
|
||||
exp.Add(keyTwo, valTwo)
|
||||
|
||||
expNode, err := exp.ToIPLD()
|
||||
require.NoError(t, err)
|
||||
|
||||
disjointKeys := args.NewBuilder().
|
||||
Add(keyOne, valOne).
|
||||
Add(keyTwo, valTwo)
|
||||
|
||||
duplicateKeys := args.NewBuilder().
|
||||
Add(keyOne, valOne).
|
||||
Add(keyTwo, valTwo).
|
||||
Add(keyOne, "oh no!")
|
||||
|
||||
t.Run("MustBuild succeeds with disjoint keys", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var act *args.Args
|
||||
|
||||
require.NotPanics(t, func() {
|
||||
act = disjointKeys.MustBuild()
|
||||
})
|
||||
assert.Equal(t, exp, act)
|
||||
})
|
||||
|
||||
t.Run("MustBuild fails with duplicate keys", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var act *args.Args
|
||||
|
||||
require.Panics(t, func() {
|
||||
act = duplicateKeys.MustBuild()
|
||||
})
|
||||
assert.Nil(t, act)
|
||||
})
|
||||
|
||||
t.Run("MustBuildIPLD succeeds with disjoint keys", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var act ipld.Node
|
||||
|
||||
require.NotPanics(t, func() {
|
||||
act = disjointKeys.MustBuildIPLD()
|
||||
})
|
||||
assert.Equal(t, expNode, act)
|
||||
})
|
||||
|
||||
t.Run("MustBuildIPLD fails with duplicate keys", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var act ipld.Node
|
||||
|
||||
require.Panics(t, func() {
|
||||
act = duplicateKeys.MustBuildIPLD()
|
||||
})
|
||||
assert.Nil(t, act)
|
||||
})
|
||||
}
|
||||
@@ -1,17 +1,33 @@
|
||||
package args
|
||||
|
||||
import "github.com/ipld/go-ipld-prime"
|
||||
import (
|
||||
"iter"
|
||||
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
)
|
||||
|
||||
type ReadOnly struct {
|
||||
args *Args
|
||||
}
|
||||
|
||||
func (r ReadOnly) GetNode(key string) (ipld.Node, error) {
|
||||
return r.args.GetNode(key)
|
||||
}
|
||||
|
||||
func (r ReadOnly) Len() int {
|
||||
return r.args.Len()
|
||||
}
|
||||
|
||||
func (r ReadOnly) Iter() iter.Seq2[string, ipld.Node] {
|
||||
return r.args.Iter()
|
||||
}
|
||||
|
||||
func (r ReadOnly) ToIPLD() (ipld.Node, error) {
|
||||
return r.args.ToIPLD()
|
||||
}
|
||||
|
||||
func (r ReadOnly) Equals(other *Args) bool {
|
||||
return r.args.Equals(other)
|
||||
func (r ReadOnly) Equals(other ReadOnly) bool {
|
||||
return r.args.Equals(other.args)
|
||||
}
|
||||
|
||||
func (r ReadOnly) String() string {
|
||||
|
||||
109
pkg/container/SPEC.md
Normal file
109
pkg/container/SPEC.md
Normal 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
|
||||
118
pkg/container/packaging.go
Normal file
118
pkg/container/packaging.go
Normal 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
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"iter"
|
||||
"strings"
|
||||
|
||||
"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"
|
||||
@@ -18,87 +18,29 @@ import (
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
// 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
|
||||
// FromBytes decodes a container from a []byte
|
||||
func FromBytes(data []byte) (Reader, error) {
|
||||
return FromReader(bytes.NewReader(data))
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tkn, ok := tkn.(*delegation.Token); ok {
|
||||
return tkn, nil
|
||||
}
|
||||
return nil, delegation.ErrDelegationNotFound
|
||||
// FromString decodes a container from a string
|
||||
func FromString(s string) (Reader, error) {
|
||||
return FromReader(strings.NewReader(s))
|
||||
}
|
||||
|
||||
// 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 the first found invocation.Token.
|
||||
// If none are found, ErrNotFound is returned.
|
||||
func (ctn Reader) GetInvocation() (*invocation.Token, error) {
|
||||
for _, t := range ctn {
|
||||
if inv, ok := t.(*invocation.Token); ok {
|
||||
return inv, nil
|
||||
}
|
||||
}
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
|
||||
func FromCar(r io.Reader) (Reader, error) {
|
||||
_, it, err := readCar(r)
|
||||
// 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
|
||||
}
|
||||
|
||||
ctn := make(Reader)
|
||||
|
||||
for block, err := range it {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = ctn.addToken(block.data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return ctn, nil
|
||||
}
|
||||
|
||||
func FromCarBase64(r io.Reader) (Reader, error) {
|
||||
return FromCar(base64.NewDecoder(base64.StdEncoding, r))
|
||||
}
|
||||
|
||||
func FromCbor(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
|
||||
}
|
||||
@@ -120,7 +62,7 @@ func FromCbor(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)
|
||||
}
|
||||
|
||||
@@ -147,8 +89,72 @@ func FromCbor(r io.Reader) (Reader, error) {
|
||||
return ctn, nil
|
||||
}
|
||||
|
||||
func FromCborBase64(r io.Reader) (Reader, error) {
|
||||
return FromCbor(base64.NewDecoder(base64.StdEncoding, r))
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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 exists, 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ctn Reader) addToken(data []byte) error {
|
||||
|
||||
@@ -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.ToCar, FromCar},
|
||||
{"carBase64", Writer.ToCarBase64, FromCarBase64},
|
||||
{"cbor", Writer.ToCbor, FromCbor},
|
||||
{"cborBase64", Writer.ToCborBase64, FromCborBase64},
|
||||
{"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},
|
||||
{"Base64URLGzip", headerBase64URLGzip, Writer.ToBase64URLGzip},
|
||||
{"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.ToCar, FromCar},
|
||||
{"carBase64", Writer.ToCarBase64, FromCarBase64},
|
||||
{"cbor", Writer.ToCbor, FromCbor},
|
||||
{"cborBase64", Writer.ToCborBase64, FromCborBase64},
|
||||
{"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)
|
||||
@@ -160,13 +202,12 @@ func randToken() (*delegation.Token, cid.Cid, []byte) {
|
||||
|
||||
opts := []delegation.Option{
|
||||
delegation.WithExpiration(time.Now().Add(time.Hour)),
|
||||
delegation.WithSubject(iss),
|
||||
}
|
||||
for i := 0; i < 3; i++ {
|
||||
opts = append(opts, delegation.WithMeta(randomString(8), randomString(10)))
|
||||
}
|
||||
|
||||
t, err := delegation.New(iss, aud, cmd, pol, opts...)
|
||||
t, err := delegation.Root(iss, aud, cmd, pol, opts...)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -182,21 +223,20 @@ 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)
|
||||
}
|
||||
buf := bytes.NewBuffer(nil)
|
||||
err := writer.ToCbor(buf)
|
||||
data, err := writer.ToBytes()
|
||||
require.NoError(f, err)
|
||||
|
||||
f.Add(buf.Bytes())
|
||||
f.Add(data)
|
||||
}
|
||||
|
||||
f.Fuzz(func(t *testing.T, data []byte) {
|
||||
start := time.Now()
|
||||
|
||||
// search for panics
|
||||
_, _ = FromCbor(bytes.NewReader(data))
|
||||
_, _ = FromBytes(data)
|
||||
|
||||
if time.Since(start) > 100*time.Millisecond {
|
||||
panic("too long")
|
||||
|
||||
1
pkg/container/testvectors/Base64StdPadding
Normal file
1
pkg/container/testvectors/Base64StdPadding
Normal file
File diff suppressed because one or more lines are too long
1
pkg/container/testvectors/Base64StdPaddingGzipped
Normal file
1
pkg/container/testvectors/Base64StdPaddingGzipped
Normal file
@@ -0,0 +1 @@
|
||||
OH4sIAAAAAAAA/5zX+ZOUxR3HcY7FwhKToFJGFAU1ghzL8/RztpxzrzsHszuzcy1Ru5/unmd6dubZnXsGE9EtFVnFSCiNEFfFiCSKqIVI8ECBKGiJiMSTFdCYeGDK6GpMWcSUtUOq8ts++Qfev7zq+6nu+5lRys+riKuTYwdvSCw9NrDty6c/Wf668n73nFsqQ988uh1Z8rTNu9X6hkO33S/vG1i92+//ontN6/BubU54fntiUnXqrPNfo9p5bnTWd3PP3IhMt/z52L5i2UD5+aQnvVRsFVqFeQWjVdxsoDKp6SRDrsjS+hUNNZjNtFdJsa1CzUwCxSK8imjO7curtFHwBqy8US3hlBT08XQlkWeGkSPmfGZZ8zEqGLTWOzX9yyemGZli8X+b1XAxESdxALIVX6E3xkiY1coeswLDMOp1BVRgOrGnqxEtukoJo9fqub7fQD09Rmv3z/vRYtZaQT1lOs4olvH/nyU5WkIPmEAQkAYg5lhDqigxjeimJoqEMgC4JipMViURiCYTBQHLBHGZCkyRNcgYzVt5gwauDa67+uiirx+KuQZnjgB9/2H3noUzrj79rQdv27HpuSufnOrfvu3Myf98rfv5Tfds2OX9de9w92Mfm/m9s2eOe/etzPnd/1jdeqI8vvuVbYOb8Q2Ltky0CZRDlAfcJKqHGSklXclQxR+mPKbgcM5n5awkRYmwM2SltSQqjRYo3RFD/lDOnW/TYrxd7ayVQaQme10dVl3RPCardrQHkFJUvTUT2QCyk20CaQpUFF1CnFJJpDqVoGYSDKhuMJVDlaoqpIIMTIZVhcoMcCYAnSgC1GkT6BF9x2d9X19nTBw/+cgI0KO+/W+sk4cmDz7/7AM7D7s2+BdIFw0vGLv9yzm7Vv3h1QN/WZHwxy9Qj0evv2ftkX891BLy7V2cWjY+sGV9f+zYHh4eYxOoXE8E3BFPNB+q0Kw7XexLpfXeXiPrd5WC3N0Wj7iBFfeQMqi06aMFKtcMf9wRrbN4EKOa7KxYfRkQ0/3ecJ/LV/dnkl2OgEPSvIViNmQDyE62CSRDgWEBilwECpWwADVsKkQnhgQZ13QZMVWVITGRCLCmI8ANCWuiSiSqNIF+ExuqnnSds6b7zv4XRoDWzN08c6H1GByE86wXj+561L/q9pc+G1jy4/ya71uPb3kXLJmwe/jlHFu3+NhFK2/8Sce4sZtiLQezq/uVBx6OXnv3n0+zC+T1F5MuENTKFW8u0ld3GcVQNl0gZodSiCk01UkMV9SVauRNXR0tEOtkic5YKtXlDgKt4VFKuhWoGiku417g6wuUimZ7DbTJDAGv2waQnWwTCIka0XVJ4JCKVJGQLGqmIamaYaiQKwpVIFQMgZgUCQLSicqxTKCuyxLATaD4JZf/4mhvbf19y15uGwFyrodHo9vOP7kzObhklvuTfQ/j3IQLn5nz6dKNn0xo7H/nkQs+HT70VvLwmWzvm3c/Ofn7g3f91jvkb7/5vpWHHl8EX3/B7sSxiq9R5bkIsAJKJurhPj9z1GgDh3gy18ViyOP2sa5oUso4C9aoJ84fq/fKKm3LtRe8gWTe568V8z7sMCU1FnA7gJnOhXq9Lk+XaHTamTgb2SaQQIFEEaJcgLIqQEEQgaloGsKIMs5ESRYFQVYlU2UEEVnEXCUSA0zTBLUJJM/nb351ecu/h6dLlREg67y3b3be96Ky/p7L7jzeO9s5Y9Ka5fNxZHc8cVC6+K83iXPfO/GjL5ybrbZrWq4QPu64rfu7adMP7v/uyt/f0r7yJjBpkk2gvgQPOTusdDGqhOuKNxROw0ba29lFUjCbqNV1MSDWZZxgViRkjBbI8jUa1aKl0Y42qz3sTVt11dQSeo5WqsGKD1NY0sN6QS8aUhXaALKTPQWEFU0nKuMi0aGCDayoJjIwZjrGHMoEAUI1QTINxUAShCo3DCwTWdOZ0QRaAhdf1yIf7unm5atGgObsH8rs5FO2wH1r/zb84c8WrNLL179/za+G51WvkvaKXjguP3XmXWetHTix59zd32Te9h2ftGTbhU/dv3PvjcvAlBnjzrAJVOjMUKhKznS4jZR10ZWMVz1JZ5zWHbmuSqnglzpSMqPVuiPgkkcL1Butd1DQVQmGgByp0S5Xo5SLKXlfvD1QDolQZ2oxXIwFk42AUrEBZCfbBBJlSZFFReIIQZlqhgR1U0MSlilROBAMTACBAjYZMyjUich1w1B+2Dd4auIOnLPCY3wFNzxIc+tGgKqX5an27Qtn37C2d+ykS7XPnz30TSi9onDd2eeMK7N4/6x78XLQMnnlm6/mFlbIeRee9qWybfCjBceSi6YVdg2s7ZxgE6ga7sp29QGTJ+LOWAooJcsXJ9loQgvFmZOXOgNt7nox1jBRyAVGC8SNbK0zCyKmrDsDRrrhShLqqGQ1DKJyJVMvknY/jNV52hQbThtAdrJNIIgVSdBljUuCwCSgMxGbBhENJBLIBWYgAzCsAZNAwdAFXeZMVVX4wywKTaBZTxz9E9y6b0JPgu8YAZo9A258I6l75KfJM2Omb+T3zjDPTW393SUXnHjswB3DY05a7wQ7ux9fXjtjx4QxS6fPfmns3598sWf9lkvB83sOXrxytt1XXK5IHCHk1Tx6sc0fTPdlCjTBYmWHHNH8XR2s3khVjKqeFD085R71BeXrdVytWKqSqCaDMiehTm/Q4jCVrUcdrBKTOfR4vEGto01N2bkgG9lTF6RBwFQicAqJALGiaropCwggCWMuAyJThAxETUIA0CVV57qkUEVlOtSbQGMGJuaOTFzyx/FHjj81AvTqwIExd3vB9FuXrdHmrp7106G33/vgo9tP3zjF8t5ZOLz52q2bjt1x6+yTH6zQbmGpiasOJ5Xw+oU7n3v8YMuH77Vs/bbF7kcVmjBmuhuhVMyVUNWiSXg9XMs2yvUkpCnma8BKxlK0eqXT8o0WqBgpmI5YKuLvyLiUeqTsp42S6qiasUCjlFG9Xk1pr3ujpN0BLWoDyE62CSRpMtBFTeaipKqYyZJKTEwURGWkcVHAWDcY0TSTYipjjFUu6YL6w2P8v0AnP45M2du/fWgHeOWD/wQAAP//IuQ6Y1MQAAA=
|
||||
1
pkg/container/testvectors/Base64URL
Normal file
1
pkg/container/testvectors/Base64URL
Normal file
File diff suppressed because one or more lines are too long
1
pkg/container/testvectors/Base64URLGzip
Normal file
1
pkg/container/testvectors/Base64URLGzip
Normal 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
|
||||
BIN
pkg/container/testvectors/Bytes
Normal file
BIN
pkg/container/testvectors/Bytes
Normal file
Binary file not shown.
BIN
pkg/container/testvectors/BytesGzipped
Normal file
BIN
pkg/container/testvectors/BytesGzipped
Normal file
Binary file not shown.
@@ -1,65 +1,122 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"bytes"
|
||||
"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"
|
||||
)
|
||||
|
||||
// TODO: should we have a multibase to wrap the cbor? but there is no reader/write in go-multibase :-(
|
||||
|
||||
const currentContainerVersion = "ctn-v1"
|
||||
|
||||
// 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{}{}
|
||||
}
|
||||
|
||||
func (ctn Writer) ToCar(w io.Writer) error {
|
||||
return writeCar(w, nil, func(yield func(carBlock, error) bool) {
|
||||
for c, bytes := range ctn {
|
||||
if !yield(carBlock{c: c, data: bytes}, nil) {
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
// ToBytes encode the container into raw bytes.
|
||||
func (ctn Writer) ToBytes() ([]byte, error) {
|
||||
return ctn.toBytes(headerRawBytes)
|
||||
}
|
||||
|
||||
func (ctn Writer) ToCarBase64(w io.Writer) error {
|
||||
w2 := base64.NewEncoder(base64.StdEncoding, w)
|
||||
defer w2.Close()
|
||||
return ctn.ToCar(w2)
|
||||
// ToBytesWriter is the same as ToBytes, but with an io.Writer.
|
||||
func (ctn Writer) ToBytesWriter(w io.Writer) error {
|
||||
return ctn.toWriter(headerRawBytes, w)
|
||||
}
|
||||
|
||||
func (ctn Writer) ToCbor(w io.Writer) error {
|
||||
// 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) ToBase64URLGzip() (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.toWriter(header, &buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
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 _, bytes := range ctn {
|
||||
qp.ListEntry(la, qp.Bytes(bytes))
|
||||
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)
|
||||
}
|
||||
|
||||
func (ctn Writer) ToCborBase64(w io.Writer) error {
|
||||
w2 := base64.NewEncoder(base64.StdEncoding, w)
|
||||
defer w2.Close()
|
||||
return ctn.ToCbor(w2)
|
||||
return ipld.EncodeStreaming(encoder, node, cbor.Encode)
|
||||
}
|
||||
|
||||
18
pkg/container/writer_test.go
Normal file
18
pkg/container/writer_test.go
Normal 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)
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
// KeySize represents valid AES key sizes
|
||||
type KeySize int
|
||||
|
||||
const (
|
||||
KeySize128 KeySize = 16 // AES-128
|
||||
KeySize192 KeySize = 24 // AES-192
|
||||
KeySize256 KeySize = 32 // AES-256 (recommended)
|
||||
)
|
||||
|
||||
// IsValid returns true if the key size is valid for AES
|
||||
func (ks KeySize) IsValid() bool {
|
||||
switch ks {
|
||||
case KeySize128, KeySize192, KeySize256:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var ErrShortCipherText = errors.New("ciphertext too short")
|
||||
var ErrNoEncryptionKey = errors.New("encryption key is required")
|
||||
var ErrInvalidKeySize = errors.New("invalid key size: must be 16, 24, or 32 bytes")
|
||||
var ErrZeroKey = errors.New("encryption key cannot be all zeros")
|
||||
|
||||
// GenerateKey generates a random AES key of default size KeySize256 (32 bytes).
|
||||
// Returns an error if the specified size is invalid or if key generation fails.
|
||||
func GenerateKey() ([]byte, error) {
|
||||
return GenerateKeyWithSize(KeySize256)
|
||||
}
|
||||
|
||||
// GenerateKeyWithSize generates a random AES key of the specified size.
|
||||
// Returns an error if the specified size is invalid or if key generation fails.
|
||||
func GenerateKeyWithSize(size KeySize) ([]byte, error) {
|
||||
if !size.IsValid() {
|
||||
return nil, ErrInvalidKeySize
|
||||
}
|
||||
|
||||
key := make([]byte, size)
|
||||
if _, err := io.ReadFull(rand.Reader, key); err != nil {
|
||||
return nil, fmt.Errorf("failed to generate AES key: %w", err)
|
||||
}
|
||||
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// EncryptWithAESKey encrypts data using AES-GCM with the provided key.
|
||||
// The key must be 16, 24, or 32 bytes long (for AES-128, AES-192, or AES-256).
|
||||
// Returns the encrypted data with the nonce prepended, or an error if encryption fails.
|
||||
func EncryptWithAESKey(data, key []byte) ([]byte, error) {
|
||||
if err := validateAESKey(key); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nonce := make([]byte, gcm.NonceSize())
|
||||
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return gcm.Seal(nonce, nonce, data, nil), nil
|
||||
}
|
||||
|
||||
// DecryptStringWithAESKey decrypts data that was encrypted with EncryptWithAESKey.
|
||||
// The key must match the one used for encryption.
|
||||
// Expects the input to have a prepended nonce.
|
||||
// Returns the decrypted data or an error if decryption fails.
|
||||
func DecryptStringWithAESKey(data, key []byte) ([]byte, error) {
|
||||
if err := validateAESKey(key); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(data) < gcm.NonceSize() {
|
||||
return nil, ErrShortCipherText
|
||||
}
|
||||
|
||||
nonce, ciphertext := data[:gcm.NonceSize()], data[gcm.NonceSize():]
|
||||
decrypted, err := gcm.Open(nil, nonce, ciphertext, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return decrypted, nil
|
||||
}
|
||||
|
||||
func validateAESKey(key []byte) error {
|
||||
if key == nil {
|
||||
return ErrNoEncryptionKey
|
||||
}
|
||||
|
||||
if !KeySize(len(key)).IsValid() {
|
||||
return ErrInvalidKeySize
|
||||
}
|
||||
|
||||
// check if key is all zeros
|
||||
for _, b := range key {
|
||||
if b != 0 {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return ErrZeroKey
|
||||
}
|
||||
@@ -3,13 +3,15 @@ package meta
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"iter"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
"github.com/ipld/go-ipld-prime/printer"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/pkg/meta/internal/crypto"
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
|
||||
"github.com/ucan-wg/go-ucan/pkg/secretbox"
|
||||
)
|
||||
|
||||
var ErrNotFound = errors.New("key not found in meta")
|
||||
@@ -61,7 +63,7 @@ func (m *Meta) GetEncryptedString(key string, encryptionKey []byte) (string, err
|
||||
return "", err
|
||||
}
|
||||
|
||||
decrypted, err := crypto.DecryptStringWithAESKey(v, encryptionKey)
|
||||
decrypted, err := secretbox.DecryptStringWithKey(v, encryptionKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -109,7 +111,7 @@ func (m *Meta) GetEncryptedBytes(key string, encryptionKey []byte) ([]byte, erro
|
||||
return nil, err
|
||||
}
|
||||
|
||||
decrypted, err := crypto.DecryptStringWithAESKey(v, encryptionKey)
|
||||
decrypted, err := secretbox.DecryptStringWithKey(v, encryptionKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -119,7 +121,6 @@ func (m *Meta) GetEncryptedBytes(key string, encryptionKey []byte) ([]byte, erro
|
||||
|
||||
// GetNode retrieves a value as a raw IPLD node.
|
||||
// Returns ErrNotFound if the given key is missing.
|
||||
// Returns datamodel.ErrWrongKind if the value has the wrong type.
|
||||
func (m *Meta) GetNode(key string) (ipld.Node, error) {
|
||||
v, ok := m.Values[key]
|
||||
if !ok {
|
||||
@@ -149,18 +150,19 @@ func (m *Meta) Add(key string, val any) error {
|
||||
// AddEncrypted adds a key/value pair in the meta set.
|
||||
// The value is encrypted with the given encryptionKey.
|
||||
// Accepted types for the value are: string, []byte.
|
||||
// The ciphertext will be 40 bytes larger than the plaintext due to encryption overhead.
|
||||
func (m *Meta) AddEncrypted(key string, val any, encryptionKey []byte) error {
|
||||
var encrypted []byte
|
||||
var err error
|
||||
|
||||
switch val := val.(type) {
|
||||
case string:
|
||||
encrypted, err = crypto.EncryptWithAESKey([]byte(val), encryptionKey)
|
||||
encrypted, err = secretbox.EncryptWithKey([]byte(val), encryptionKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case []byte:
|
||||
encrypted, err = crypto.EncryptWithAESKey(val, encryptionKey)
|
||||
encrypted, err = secretbox.EncryptWithKey(val, encryptionKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -171,6 +173,41 @@ func (m *Meta) AddEncrypted(key string, val any, encryptionKey []byte) error {
|
||||
return m.Add(key, encrypted)
|
||||
}
|
||||
|
||||
type Iterator interface {
|
||||
Iter() iter.Seq2[string, ipld.Node]
|
||||
}
|
||||
|
||||
// Include merges the provided meta into the existing one.
|
||||
//
|
||||
// If duplicate keys are encountered, the new value is silently dropped
|
||||
// without causing an error.
|
||||
func (m *Meta) Include(other Iterator) {
|
||||
for key, value := range other.Iter() {
|
||||
if _, ok := m.Values[key]; ok {
|
||||
// don't overwrite
|
||||
continue
|
||||
}
|
||||
m.Values[key] = value
|
||||
m.Keys = append(m.Keys, key)
|
||||
}
|
||||
}
|
||||
|
||||
// Len returns the number of key/values.
|
||||
func (m *Meta) Len() int {
|
||||
return len(m.Values)
|
||||
}
|
||||
|
||||
// Iter iterates over the meta key/values
|
||||
func (m *Meta) Iter() iter.Seq2[string, ipld.Node] {
|
||||
return func(yield func(string, ipld.Node) bool) {
|
||||
for _, key := range m.Keys {
|
||||
if !yield(key, m.Values[key]) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Equals tells if two Meta hold the same key/values.
|
||||
func (m *Meta) Equals(other *Meta) bool {
|
||||
if len(m.Keys) != len(other.Keys) {
|
||||
@@ -188,6 +225,8 @@ func (m *Meta) Equals(other *Meta) bool {
|
||||
}
|
||||
|
||||
func (m *Meta) String() string {
|
||||
sort.Strings(m.Keys)
|
||||
|
||||
buf := strings.Builder{}
|
||||
buf.WriteString("{")
|
||||
|
||||
@@ -209,5 +248,18 @@ func (m *Meta) String() string {
|
||||
|
||||
// ReadOnly returns a read-only version of Meta.
|
||||
func (m *Meta) ReadOnly() ReadOnly {
|
||||
return ReadOnly{m: m}
|
||||
return ReadOnly{meta: m}
|
||||
}
|
||||
|
||||
// Clone makes a deep copy.
|
||||
func (m *Meta) Clone() *Meta {
|
||||
res := &Meta{
|
||||
Keys: make([]string, len(m.Keys)),
|
||||
Values: make(map[string]ipld.Node, len(m.Values)),
|
||||
}
|
||||
copy(res.Keys, m.Keys)
|
||||
for k, v := range m.Values {
|
||||
res.Values[k] = v
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
@@ -2,8 +2,11 @@ package meta_test
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"maps"
|
||||
"testing"
|
||||
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
"github.com/ipld/go-ipld-prime/node/basicnode"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/pkg/meta"
|
||||
@@ -75,3 +78,53 @@ func TestMeta_Add(t *testing.T) {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestIterCloneEquals(t *testing.T) {
|
||||
m := meta.NewMeta()
|
||||
|
||||
require.NoError(t, m.Add("foo", "bar"))
|
||||
require.NoError(t, m.Add("baz", 1234))
|
||||
|
||||
expected := map[string]ipld.Node{
|
||||
"foo": basicnode.NewString("bar"),
|
||||
"baz": basicnode.NewInt(1234),
|
||||
}
|
||||
|
||||
// meta -> iter
|
||||
require.Equal(t, expected, maps.Collect(m.Iter()))
|
||||
|
||||
// readonly -> iter
|
||||
ro := m.ReadOnly()
|
||||
require.Equal(t, expected, maps.Collect(ro.Iter()))
|
||||
|
||||
// meta -> clone -> iter
|
||||
clone := m.Clone()
|
||||
require.Equal(t, expected, maps.Collect(clone.Iter()))
|
||||
|
||||
// readonly -> WriteableClone -> iter
|
||||
wclone := ro.WriteableClone()
|
||||
require.Equal(t, expected, maps.Collect(wclone.Iter()))
|
||||
|
||||
require.True(t, m.Equals(wclone))
|
||||
require.True(t, ro.Equals(wclone.ReadOnly()))
|
||||
}
|
||||
|
||||
func TestInclude(t *testing.T) {
|
||||
m1 := meta.NewMeta()
|
||||
|
||||
require.NoError(t, m1.Add("samekey", "bar"))
|
||||
require.NoError(t, m1.Add("baz", 1234))
|
||||
|
||||
m2 := meta.NewMeta()
|
||||
|
||||
require.NoError(t, m2.Add("samekey", "othervalue")) // check no overwrite
|
||||
require.NoError(t, m2.Add("otherkey", 1234))
|
||||
|
||||
m1.Include(m2)
|
||||
|
||||
require.Equal(t, map[string]ipld.Node{
|
||||
"samekey": basicnode.NewString("bar"),
|
||||
"baz": basicnode.NewInt(1234),
|
||||
"otherkey": basicnode.NewInt(1234),
|
||||
}, maps.Collect(m1.Iter()))
|
||||
}
|
||||
|
||||
@@ -1,50 +1,64 @@
|
||||
package meta
|
||||
|
||||
import (
|
||||
"iter"
|
||||
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
)
|
||||
|
||||
// ReadOnly wraps a Meta into a read-only facade.
|
||||
type ReadOnly struct {
|
||||
m *Meta
|
||||
meta *Meta
|
||||
}
|
||||
|
||||
func (r ReadOnly) GetBool(key string) (bool, error) {
|
||||
return r.m.GetBool(key)
|
||||
return r.meta.GetBool(key)
|
||||
}
|
||||
|
||||
func (r ReadOnly) GetString(key string) (string, error) {
|
||||
return r.m.GetString(key)
|
||||
return r.meta.GetString(key)
|
||||
}
|
||||
|
||||
func (r ReadOnly) GetEncryptedString(key string, encryptionKey []byte) (string, error) {
|
||||
return r.m.GetEncryptedString(key, encryptionKey)
|
||||
return r.meta.GetEncryptedString(key, encryptionKey)
|
||||
}
|
||||
|
||||
func (r ReadOnly) GetInt64(key string) (int64, error) {
|
||||
return r.m.GetInt64(key)
|
||||
return r.meta.GetInt64(key)
|
||||
}
|
||||
|
||||
func (r ReadOnly) GetFloat64(key string) (float64, error) {
|
||||
return r.m.GetFloat64(key)
|
||||
return r.meta.GetFloat64(key)
|
||||
}
|
||||
|
||||
func (r ReadOnly) GetBytes(key string) ([]byte, error) {
|
||||
return r.m.GetBytes(key)
|
||||
return r.meta.GetBytes(key)
|
||||
}
|
||||
|
||||
func (r ReadOnly) GetEncryptedBytes(key string, encryptionKey []byte) ([]byte, error) {
|
||||
return r.m.GetEncryptedBytes(key, encryptionKey)
|
||||
return r.meta.GetEncryptedBytes(key, encryptionKey)
|
||||
}
|
||||
|
||||
func (r ReadOnly) GetNode(key string) (ipld.Node, error) {
|
||||
return r.m.GetNode(key)
|
||||
return r.meta.GetNode(key)
|
||||
}
|
||||
|
||||
func (r ReadOnly) Len() int {
|
||||
return r.meta.Len()
|
||||
}
|
||||
|
||||
func (r ReadOnly) Iter() iter.Seq2[string, ipld.Node] {
|
||||
return r.meta.Iter()
|
||||
}
|
||||
|
||||
func (r ReadOnly) Equals(other ReadOnly) bool {
|
||||
return r.m.Equals(other.m)
|
||||
return r.meta.Equals(other.meta)
|
||||
}
|
||||
|
||||
func (r ReadOnly) String() string {
|
||||
return r.m.String()
|
||||
return r.meta.String()
|
||||
}
|
||||
|
||||
func (r ReadOnly) WriteableClone() *Meta {
|
||||
return r.meta.Clone()
|
||||
}
|
||||
|
||||
@@ -9,10 +9,15 @@ import (
|
||||
"github.com/ipld/go-ipld-prime/must"
|
||||
"github.com/ipld/go-ipld-prime/node/basicnode"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy/limits"
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy/selector"
|
||||
)
|
||||
|
||||
func FromIPLD(node datamodel.Node) (Policy, error) {
|
||||
if err := limits.ValidateIntegerBoundsIPLD(node); err != nil {
|
||||
return nil, fmt.Errorf("policy contains integer values outside safe bounds: %w", err)
|
||||
}
|
||||
|
||||
return statementsFromIPLD("/", node)
|
||||
}
|
||||
|
||||
@@ -76,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
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
package policy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
"github.com/ipld/go-ipld-prime/codec/dagjson"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
|
||||
)
|
||||
|
||||
func TestIpldRoundTrip(t *testing.T) {
|
||||
@@ -21,10 +24,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)
|
||||
@@ -41,3 +65,19 @@ func TestIpldRoundTrip(t *testing.T) {
|
||||
require.JSONEq(t, tc.dagJsonStr, string(wroteAsDagJson))
|
||||
}
|
||||
}
|
||||
|
||||
func TestFoo(t *testing.T) {
|
||||
fmt.Println(MustConstruct(
|
||||
And(
|
||||
Equal(".foo1", literal.String(".bar1")),
|
||||
NotEqual(".foo2", literal.String(".bar2")),
|
||||
),
|
||||
Or(
|
||||
GreaterThan(".foo5", literal.Float(5.2)),
|
||||
GreaterThanOrEqual(".foo6", literal.Float(6.2)),
|
||||
),
|
||||
Not(Like(".foo7", "*@example.com")),
|
||||
All(".foo8", LessThan(".foo3", literal.Int(3))),
|
||||
Any(".foo9", LessThanOrEqual(".foo4", literal.Int(4))),
|
||||
))
|
||||
}
|
||||
|
||||
49
pkg/policy/limits/int.go
Normal file
49
pkg/policy/limits/int.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package limits
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
"github.com/ipld/go-ipld-prime/must"
|
||||
)
|
||||
|
||||
const (
|
||||
// MaxInt53 represents the maximum safe integer in JavaScript (2^53 - 1)
|
||||
MaxInt53 = 9007199254740991
|
||||
// MinInt53 represents the minimum safe integer in JavaScript (-2^53 + 1)
|
||||
MinInt53 = -9007199254740991
|
||||
)
|
||||
|
||||
func ValidateIntegerBoundsIPLD(node ipld.Node) error {
|
||||
switch node.Kind() {
|
||||
case ipld.Kind_Int:
|
||||
val := must.Int(node)
|
||||
if val > MaxInt53 || val < MinInt53 {
|
||||
return fmt.Errorf("integer value %d exceeds safe bounds", val)
|
||||
}
|
||||
case ipld.Kind_List:
|
||||
it := node.ListIterator()
|
||||
for !it.Done() {
|
||||
_, v, err := it.Next()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ValidateIntegerBoundsIPLD(v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case ipld.Kind_Map:
|
||||
it := node.MapIterator()
|
||||
for !it.Done() {
|
||||
_, v, err := it.Next()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ValidateIntegerBoundsIPLD(v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
82
pkg/policy/limits/int_test.go
Normal file
82
pkg/policy/limits/int_test.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package limits
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/ipld/go-ipld-prime/datamodel"
|
||||
"github.com/ipld/go-ipld-prime/fluent/qp"
|
||||
"github.com/ipld/go-ipld-prime/node/basicnode"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestValidateIntegerBoundsIPLD(t *testing.T) {
|
||||
buildMap := func() datamodel.Node {
|
||||
nb := basicnode.Prototype.Any.NewBuilder()
|
||||
qp.Map(1, func(ma datamodel.MapAssembler) {
|
||||
qp.MapEntry(ma, "foo", qp.Int(MaxInt53+1))
|
||||
})(nb)
|
||||
return nb.Build()
|
||||
}
|
||||
|
||||
buildList := func() datamodel.Node {
|
||||
nb := basicnode.Prototype.Any.NewBuilder()
|
||||
qp.List(1, func(la datamodel.ListAssembler) {
|
||||
qp.ListEntry(la, qp.Int(MinInt53-1))
|
||||
})(nb)
|
||||
return nb.Build()
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input datamodel.Node
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid int",
|
||||
input: basicnode.NewInt(42),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "max safe int",
|
||||
input: basicnode.NewInt(MaxInt53),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "min safe int",
|
||||
input: basicnode.NewInt(MinInt53),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "above MaxInt53",
|
||||
input: basicnode.NewInt(MaxInt53 + 1),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "below MinInt53",
|
||||
input: basicnode.NewInt(MinInt53 - 1),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "nested map with invalid int",
|
||||
input: buildMap(),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "nested list with invalid int",
|
||||
input: buildList(),
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := ValidateIntegerBoundsIPLD(tt.input)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "exceeds safe bounds")
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,8 @@ import (
|
||||
"github.com/ipld/go-ipld-prime/fluent/qp"
|
||||
cidlink "github.com/ipld/go-ipld-prime/linking/cid"
|
||||
"github.com/ipld/go-ipld-prime/node/basicnode"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy/limits"
|
||||
)
|
||||
|
||||
var Bool = basicnode.NewBool
|
||||
@@ -58,8 +60,6 @@ func List[T any](l []T) (ipld.Node, error) {
|
||||
// Any creates an IPLD node from any value
|
||||
// If possible, use another dedicated function for your type for performance.
|
||||
func Any(v any) (res ipld.Node, err error) {
|
||||
// TODO: handle uint overflow below
|
||||
|
||||
// some fast path
|
||||
switch val := v.(type) {
|
||||
case bool:
|
||||
@@ -67,7 +67,11 @@ func Any(v any) (res ipld.Node, err error) {
|
||||
case string:
|
||||
return basicnode.NewString(val), nil
|
||||
case int:
|
||||
return basicnode.NewInt(int64(val)), nil
|
||||
i := int64(val)
|
||||
if i > limits.MaxInt53 || i < limits.MinInt53 {
|
||||
return nil, fmt.Errorf("integer value %d exceeds safe integer bounds", i)
|
||||
}
|
||||
return basicnode.NewInt(i), nil
|
||||
case int8:
|
||||
return basicnode.NewInt(int64(val)), nil
|
||||
case int16:
|
||||
@@ -75,6 +79,9 @@ func Any(v any) (res ipld.Node, err error) {
|
||||
case int32:
|
||||
return basicnode.NewInt(int64(val)), nil
|
||||
case int64:
|
||||
if val > limits.MaxInt53 || val < limits.MinInt53 {
|
||||
return nil, fmt.Errorf("integer value %d exceeds safe integer bounds", val)
|
||||
}
|
||||
return basicnode.NewInt(val), nil
|
||||
case uint:
|
||||
return basicnode.NewInt(int64(val)), nil
|
||||
@@ -85,6 +92,9 @@ func Any(v any) (res ipld.Node, err error) {
|
||||
case uint32:
|
||||
return basicnode.NewInt(int64(val)), nil
|
||||
case uint64:
|
||||
if val > uint64(limits.MaxInt53) {
|
||||
return nil, fmt.Errorf("unsigned integer value %d exceeds safe integer bounds", val)
|
||||
}
|
||||
return basicnode.NewInt(int64(val)), nil
|
||||
case float32:
|
||||
return basicnode.NewFloat(float64(val)), nil
|
||||
@@ -168,9 +178,17 @@ func anyAssemble(val any) qp.Assemble {
|
||||
case reflect.Bool:
|
||||
return qp.Bool(rv.Bool())
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return qp.Int(rv.Int())
|
||||
i := rv.Int()
|
||||
if i > limits.MaxInt53 || i < limits.MinInt53 {
|
||||
panic(fmt.Sprintf("integer %d exceeds safe bounds", i))
|
||||
}
|
||||
return qp.Int(i)
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return qp.Int(int64(rv.Uint()))
|
||||
u := rv.Uint()
|
||||
if u > limits.MaxInt53 {
|
||||
panic(fmt.Sprintf("unsigned integer %d exceeds safe bounds", u))
|
||||
}
|
||||
return qp.Int(int64(u))
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return qp.Float(rv.Float())
|
||||
case reflect.String:
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
cidlink "github.com/ipld/go-ipld-prime/linking/cid"
|
||||
"github.com/ipld/go-ipld-prime/printer"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy/limits"
|
||||
)
|
||||
|
||||
func TestList(t *testing.T) {
|
||||
@@ -214,7 +216,7 @@ func TestAny(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.True(t, asLink.(cidlink.Link).Equals(cid.MustParse("bafzbeigai3eoy2ccc7ybwjfz5r3rdxqrinwi4rwytly24tdbh6yk7zslrm")))
|
||||
|
||||
v, err = Any(data["func"])
|
||||
_, err = Any(data["func"])
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
@@ -254,6 +256,56 @@ func BenchmarkAny(b *testing.B) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestAnyAssembleIntegerOverflow(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input interface{}
|
||||
shouldErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid int",
|
||||
input: 42,
|
||||
shouldErr: false,
|
||||
},
|
||||
{
|
||||
name: "max safe int",
|
||||
input: limits.MaxInt53,
|
||||
shouldErr: false,
|
||||
},
|
||||
{
|
||||
name: "min safe int",
|
||||
input: limits.MinInt53,
|
||||
shouldErr: false,
|
||||
},
|
||||
{
|
||||
name: "overflow int",
|
||||
input: int64(limits.MaxInt53 + 1),
|
||||
shouldErr: true,
|
||||
},
|
||||
{
|
||||
name: "underflow int",
|
||||
input: int64(limits.MinInt53 - 1),
|
||||
shouldErr: true,
|
||||
},
|
||||
{
|
||||
name: "overflow uint",
|
||||
input: uint64(limits.MaxInt53 + 1),
|
||||
shouldErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := Any(tt.input)
|
||||
if tt.shouldErr {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func must[T any](t T, err error) T {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
||||
@@ -3,6 +3,7 @@ package policy
|
||||
import (
|
||||
"cmp"
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
"github.com/ipld/go-ipld-prime/datamodel"
|
||||
@@ -81,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)
|
||||
@@ -249,10 +261,22 @@ func matchStatement(cur Statement, node ipld.Node) (_ matchResult, leafMost Stat
|
||||
panic(fmt.Errorf("unimplemented statement kind: %s", cur.Kind()))
|
||||
}
|
||||
|
||||
// isOrdered compares two IPLD nodes and returns true if they satisfy the given ordering function.
|
||||
// It supports comparison of integers and floats, returning false for:
|
||||
// - Nodes of different or unsupported kinds
|
||||
// - Integer values outside JavaScript's safe integer bounds (±2^53-1)
|
||||
// - Non-finite floating point values (NaN or ±Inf)
|
||||
//
|
||||
// The satisfies parameter is a function that interprets the comparison result:
|
||||
// - For ">" it returns true when order is 1
|
||||
// - For ">=" it returns true when order is 0 or 1
|
||||
// - For "<" it returns true when order is -1
|
||||
// - For "<=" it returns true when order is -1 or 0
|
||||
func isOrdered(expected ipld.Node, actual ipld.Node, satisfies func(order int) bool) bool {
|
||||
if expected.Kind() == ipld.Kind_Int && actual.Kind() == ipld.Kind_Int {
|
||||
a := must.Int(actual)
|
||||
b := must.Int(expected)
|
||||
|
||||
return satisfies(cmp.Compare(a, b))
|
||||
}
|
||||
|
||||
@@ -265,6 +289,11 @@ func isOrdered(expected ipld.Node, actual ipld.Node, satisfies func(order int) b
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("extracting selector float: %w", err))
|
||||
}
|
||||
|
||||
if math.IsInf(a, 0) || math.IsNaN(a) || math.IsInf(b, 0) || math.IsNaN(b) {
|
||||
return false
|
||||
}
|
||||
|
||||
return satisfies(cmp.Compare(a, b))
|
||||
}
|
||||
|
||||
|
||||
@@ -7,11 +7,8 @@ import (
|
||||
"github.com/ipfs/go-cid"
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
"github.com/ipld/go-ipld-prime/codec/dagjson"
|
||||
"github.com/ipld/go-ipld-prime/datamodel"
|
||||
"github.com/ipld/go-ipld-prime/fluent/qp"
|
||||
cidlink "github.com/ipld/go-ipld-prime/linking/cid"
|
||||
"github.com/ipld/go-ipld-prime/node/basicnode"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
|
||||
@@ -19,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")))
|
||||
@@ -38,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)))
|
||||
@@ -57,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)))
|
||||
@@ -76,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")}
|
||||
|
||||
@@ -98,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",
|
||||
})
|
||||
@@ -124,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")))
|
||||
@@ -137,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) {
|
||||
@@ -248,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) {
|
||||
@@ -485,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"}`))
|
||||
@@ -904,55 +1069,3 @@ func TestPartialMatch(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestInvocationValidation applies the example policy to the second
|
||||
// example arguments as defined in the [Validation] section of the
|
||||
// invocation specification.
|
||||
//
|
||||
// [Validation]: https://github.com/ucan-wg/delegation/tree/v1_ipld#validation
|
||||
func TestInvocationValidationSpecExamples(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
pol := MustConstruct(
|
||||
Equal(".from", literal.String("alice@example.com")),
|
||||
Any(".to", Like(".", "*@example.com")),
|
||||
)
|
||||
|
||||
t.Run("with passing args", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
argsNode, err := qp.BuildMap(basicnode.Prototype.Any, 2, func(ma datamodel.MapAssembler) {
|
||||
qp.MapEntry(ma, "from", qp.String("alice@example.com"))
|
||||
qp.MapEntry(ma, "to", qp.List(2, func(la datamodel.ListAssembler) {
|
||||
qp.ListEntry(la, qp.String("bob@example.com"))
|
||||
qp.ListEntry(la, qp.String("carol@not.example.com"))
|
||||
}))
|
||||
qp.MapEntry(ma, "title", qp.String("Coffee"))
|
||||
qp.MapEntry(ma, "body", qp.String("Still on for coffee"))
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
exec, stmt := pol.Match(argsNode)
|
||||
assert.True(t, exec)
|
||||
assert.Nil(t, stmt)
|
||||
})
|
||||
|
||||
t.Run("fails on recipients (second statement)", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
argsNode, err := qp.BuildMap(basicnode.Prototype.Any, 2, func(ma datamodel.MapAssembler) {
|
||||
qp.MapEntry(ma, "from", qp.String("alice@example.com"))
|
||||
qp.MapEntry(ma, "to", qp.List(2, func(la datamodel.ListAssembler) {
|
||||
qp.ListEntry(la, qp.String("bob@null.com"))
|
||||
qp.ListEntry(la, qp.String("carol@elsewhere.example.com"))
|
||||
}))
|
||||
qp.MapEntry(ma, "title", qp.String("Coffee"))
|
||||
qp.MapEntry(ma, "body", qp.String("Still on for coffee"))
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
exec, stmt := pol.Match(argsNode)
|
||||
assert.False(t, exec)
|
||||
assert.NotNil(t, stmt)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"]
|
||||
// ]]
|
||||
// ]
|
||||
// ]
|
||||
}
|
||||
|
||||
|
||||
67
pkg/policy/policytest/spec.go
Normal file
67
pkg/policy/policytest/spec.go
Normal file
@@ -0,0 +1,67 @@
|
||||
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"
|
||||
)
|
||||
|
||||
// EmptyPolicy provides a Policy with no statements.
|
||||
var EmptyPolicy = policy.Policy{}
|
||||
|
||||
// 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(
|
||||
policy.Equal(".from", literal.String("alice@example.com")),
|
||||
policy.Any(".to", policy.Like(".", "*@example.com")),
|
||||
)
|
||||
|
||||
// TODO: Replace the URL for [Validation] above when the delegation
|
||||
// specification has been finished/merged.
|
||||
|
||||
// SpecValidArguments provides valid, instantiated Arguments containing
|
||||
// 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().
|
||||
Add("from", "alice@example.com").
|
||||
Add("to", []string{
|
||||
"bob@example.com",
|
||||
"carol@not.example.com",
|
||||
}).
|
||||
Add("title", "Coffee").
|
||||
Add("body", "Still on for coffee").
|
||||
MustBuild()
|
||||
|
||||
var specValidArgumentsIPLD = mustIPLD(SpecValidArguments)
|
||||
|
||||
// SpecInvalidArguments provides invalid, instantiated Arguments containing
|
||||
// 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().
|
||||
Add("from", "alice@example.com").
|
||||
Add("to", []string{
|
||||
"bob@null.com",
|
||||
"carol@elsewhere.example.com",
|
||||
}).
|
||||
Add("title", "Coffee").
|
||||
Add("body", "Still on for coffee").
|
||||
MustBuild()
|
||||
|
||||
var specInvalidArgumentsIPLD = mustIPLD(SpecInvalidArguments)
|
||||
|
||||
func mustIPLD(args *args.Args) ipld.Node {
|
||||
node, err := args.ToIPLD()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return node
|
||||
}
|
||||
32
pkg/policy/policytest/spec_test.go
Normal file
32
pkg/policy/policytest/spec_test.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package policytest
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestInvocationValidation applies the example policy to the second
|
||||
// example arguments as defined in the [Validation] section of the
|
||||
// invocation specification.
|
||||
//
|
||||
// [Validation]: https://github.com/ucan-wg/delegation/tree/v1_ipld#validation
|
||||
func TestInvocationValidationSpecExamples(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("with passing args", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
exec, stmt := SpecPolicy.Match(specValidArgumentsIPLD)
|
||||
assert.True(t, exec)
|
||||
assert.Nil(t, stmt)
|
||||
})
|
||||
|
||||
t.Run("fails on recipients (second statement)", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
exec, stmt := SpecPolicy.Match(specInvalidArgumentsIPLD)
|
||||
assert.False(t, exec)
|
||||
assert.NotNil(t, stmt)
|
||||
})
|
||||
}
|
||||
@@ -6,12 +6,24 @@ import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy/limits"
|
||||
)
|
||||
|
||||
var (
|
||||
indexRegex = regexp.MustCompile(`^-?\d+$`)
|
||||
sliceRegex = regexp.MustCompile(`^((\-?\d+:\-?\d*)|(\-?\d*:\-?\d+))$`)
|
||||
fieldRegex = regexp.MustCompile(`^\.[a-zA-Z_]*?$`)
|
||||
|
||||
// Field name requirements:
|
||||
// - Must start with ASCII letter, Unicode letter, or underscore
|
||||
// - Can contain:
|
||||
// - ASCII letters (a-z, A-Z)
|
||||
// - ASCII digits (0-9)
|
||||
// - Unicode letters (\p{L})
|
||||
// - Dollar sign ($)
|
||||
// - Underscore (_)
|
||||
// - Hyphen (-)
|
||||
fieldRegex = regexp.MustCompile(`^\.[a-zA-Z_\p{L}][a-zA-Z0-9$_\p{L}\-]*$`)
|
||||
)
|
||||
|
||||
func Parse(str string) (Selector, error) {
|
||||
@@ -56,6 +68,9 @@ func Parse(str string) (Selector, error) {
|
||||
if err != nil {
|
||||
return nil, newParseError("invalid index", str, col, tok)
|
||||
}
|
||||
if idx > limits.MaxInt53 || 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})
|
||||
|
||||
// explicit field, ["abcd"]
|
||||
@@ -77,6 +92,9 @@ func Parse(str string) (Selector, error) {
|
||||
if err != nil {
|
||||
return nil, newParseError("invalid slice index", str, col, tok)
|
||||
}
|
||||
if i > limits.MaxInt53 || i < limits.MinInt53 {
|
||||
return nil, newParseError(fmt.Sprintf("slice index %d exceeds safe integer bounds", i), str, col, tok)
|
||||
}
|
||||
rng[0] = i
|
||||
}
|
||||
if splt[1] == "" {
|
||||
@@ -86,6 +104,9 @@ func Parse(str string) (Selector, error) {
|
||||
if err != nil {
|
||||
return nil, newParseError("invalid slice index", str, col, tok)
|
||||
}
|
||||
if i > limits.MaxInt53 || i < limits.MinInt53 {
|
||||
return nil, newParseError(fmt.Sprintf("slice index %d exceeds safe integer bounds", i), str, col, tok)
|
||||
}
|
||||
rng[1] = i
|
||||
}
|
||||
sel = append(sel, segment{str: tok, optional: opt, slice: rng[:]})
|
||||
@@ -152,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}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package selector
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy/limits"
|
||||
)
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
@@ -30,6 +33,29 @@ func TestParse(t *testing.T) {
|
||||
require.Empty(t, sel[0].Slice())
|
||||
require.Equal(t, sel[0].Field(), "foo")
|
||||
require.Empty(t, sel[0].Index())
|
||||
|
||||
sel, err = Parse(".foo_bar")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(sel))
|
||||
require.False(t, sel[0].Identity())
|
||||
require.False(t, sel[0].Optional())
|
||||
require.False(t, sel[0].Iterator())
|
||||
require.Empty(t, sel[0].Slice())
|
||||
require.Equal(t, sel[0].Field(), "foo_bar")
|
||||
require.Empty(t, sel[0].Index())
|
||||
|
||||
sel, err = Parse(".foo-bar")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(sel))
|
||||
require.False(t, sel[0].Identity())
|
||||
require.False(t, sel[0].Optional())
|
||||
require.False(t, sel[0].Iterator())
|
||||
require.Empty(t, sel[0].Slice())
|
||||
require.Equal(t, sel[0].Field(), "foo-bar")
|
||||
require.Empty(t, sel[0].Index())
|
||||
|
||||
sel, err = Parse(".foo*bar")
|
||||
require.ErrorContains(t, err, "invalid segment")
|
||||
})
|
||||
|
||||
t.Run("explicit field", func(t *testing.T) {
|
||||
@@ -549,4 +575,67 @@ func TestParse(t *testing.T) {
|
||||
_, err := Parse(".[foo]")
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("extended field names", func(t *testing.T) {
|
||||
validFields := []string{
|
||||
".basic",
|
||||
".user_name",
|
||||
".user-name",
|
||||
".userName$special",
|
||||
".αβγ", // Greek letters
|
||||
".użytkownik", // Polish characters
|
||||
".用户", // Chinese characters
|
||||
".사용자", // Korean characters
|
||||
"._private",
|
||||
".number123",
|
||||
".camelCase",
|
||||
".snake_case",
|
||||
".kebab-case",
|
||||
".mixed_kebab-case",
|
||||
".with$dollar",
|
||||
".MIXED_Case_123",
|
||||
".unicodeø",
|
||||
}
|
||||
|
||||
for _, field := range validFields {
|
||||
sel, err := Parse(field)
|
||||
require.NoError(t, err, "field: %s", field)
|
||||
require.NotNil(t, sel)
|
||||
}
|
||||
|
||||
invalidFields := []string{
|
||||
".123number", // Can't start with digit
|
||||
".@special", // @ not allowed
|
||||
".space name", // No spaces
|
||||
".#hashtag", // No #
|
||||
".name!", // No !
|
||||
".{brackets}", // No brackets
|
||||
".name/with/slashes", // No slashes
|
||||
}
|
||||
|
||||
for _, field := range invalidFields {
|
||||
sel, err := Parse(field)
|
||||
require.Error(t, err, "field: %s", field)
|
||||
require.Nil(t, sel)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("integer overflow", func(t *testing.T) {
|
||||
sel, err := Parse(fmt.Sprintf(".[%d]", limits.MaxInt53+1))
|
||||
require.Error(t, err)
|
||||
require.Nil(t, sel)
|
||||
|
||||
sel, err = Parse(fmt.Sprintf(".[%d]", limits.MinInt53-1))
|
||||
require.Error(t, err)
|
||||
require.Nil(t, sel)
|
||||
|
||||
// Test slice overflow
|
||||
sel, err = Parse(fmt.Sprintf(".[%d:42]", limits.MaxInt53+1))
|
||||
require.Error(t, err)
|
||||
require.Nil(t, sel)
|
||||
|
||||
sel, err = Parse(fmt.Sprintf(".[1:%d]", limits.MaxInt53+1))
|
||||
require.Error(t, err)
|
||||
require.Nil(t, sel)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -266,19 +266,32 @@ func resolveSliceIndices(slice []int64, length int64) (start int64, end int64) {
|
||||
case slice[0] == math.MinInt:
|
||||
start = 0
|
||||
case slice[0] < 0:
|
||||
start = length + slice[0]
|
||||
// Check for potential overflow before adding
|
||||
if -slice[0] > length {
|
||||
start = 0
|
||||
} else {
|
||||
start = length + slice[0]
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case slice[1] == math.MaxInt:
|
||||
end = length
|
||||
case slice[1] < 0:
|
||||
end = length + slice[1]
|
||||
// Check for potential overflow before adding
|
||||
if -slice[1] > length {
|
||||
end = 0
|
||||
} else {
|
||||
end = length + slice[1]
|
||||
}
|
||||
}
|
||||
|
||||
// backward iteration is not allowed, shortcut to an empty result
|
||||
if start >= end {
|
||||
start, end = 0, 0
|
||||
return
|
||||
}
|
||||
|
||||
// clamp out of bound
|
||||
if start < 0 {
|
||||
start = 0
|
||||
@@ -286,11 +299,14 @@ func resolveSliceIndices(slice []int64, length int64) (start int64, end int64) {
|
||||
if start > length {
|
||||
start = length
|
||||
}
|
||||
if end < 0 {
|
||||
end = 0
|
||||
}
|
||||
if end > length {
|
||||
end = length
|
||||
}
|
||||
|
||||
return start, end
|
||||
return
|
||||
}
|
||||
|
||||
func kindString(n datamodel.Node) string {
|
||||
@@ -300,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}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package selector
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -132,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) {
|
||||
@@ -350,9 +351,63 @@ 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestResolveSliceIndices(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
slice []int64
|
||||
length int64
|
||||
wantStart int64
|
||||
wantEnd int64
|
||||
}{
|
||||
{
|
||||
name: "normal case",
|
||||
slice: []int64{1, 3},
|
||||
length: 5,
|
||||
wantStart: 1,
|
||||
wantEnd: 3,
|
||||
},
|
||||
{
|
||||
name: "negative indices",
|
||||
slice: []int64{-2, -1},
|
||||
length: 5,
|
||||
wantStart: 3,
|
||||
wantEnd: 4,
|
||||
},
|
||||
{
|
||||
name: "overflow protection negative start",
|
||||
slice: []int64{math.MinInt64, 3},
|
||||
length: 5,
|
||||
wantStart: 0,
|
||||
wantEnd: 3,
|
||||
},
|
||||
{
|
||||
name: "overflow protection negative end",
|
||||
slice: []int64{0, math.MinInt64},
|
||||
length: 5,
|
||||
wantStart: 0,
|
||||
wantEnd: 0,
|
||||
},
|
||||
{
|
||||
name: "max bounds",
|
||||
slice: []int64{0, math.MaxInt64},
|
||||
length: 5,
|
||||
wantStart: 0,
|
||||
wantEnd: 5,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
start, end := resolveSliceIndices(tt.slice, tt.length)
|
||||
require.Equal(t, tt.wantStart, start)
|
||||
require.Equal(t, tt.wantEnd, end)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
90
pkg/secretbox/secretbox.go
Normal file
90
pkg/secretbox/secretbox.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package secretbox
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"golang.org/x/crypto/nacl/secretbox"
|
||||
)
|
||||
|
||||
const keySize = 32 // secretbox allows only 32-byte keys
|
||||
|
||||
var ErrShortCipherText = errors.New("ciphertext too short")
|
||||
var ErrNoEncryptionKey = errors.New("encryption key is required")
|
||||
var ErrInvalidKeySize = errors.New("invalid key size: must be 32 bytes")
|
||||
var ErrZeroKey = errors.New("encryption key cannot be all zeros")
|
||||
|
||||
// GenerateKey generates a random 32-byte key to be used by EncryptWithKey and DecryptWithKey
|
||||
func GenerateKey() ([]byte, error) {
|
||||
key := make([]byte, keySize)
|
||||
if _, err := io.ReadFull(rand.Reader, key); err != nil {
|
||||
return nil, fmt.Errorf("failed to generate key: %w", err)
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// EncryptWithKey encrypts data using NaCl's secretbox with the provided key.
|
||||
// 40 bytes of overhead (24-byte nonce + 16-byte MAC) are added to the plaintext size.
|
||||
func EncryptWithKey(data, key []byte) ([]byte, error) {
|
||||
if err := validateKey(key); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var secretKey [keySize]byte
|
||||
copy(secretKey[:], key)
|
||||
|
||||
// Generate 24 bytes of random data as nonce
|
||||
var nonce [24]byte
|
||||
if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Encrypt and authenticate data
|
||||
encrypted := secretbox.Seal(nonce[:], data, &nonce, &secretKey)
|
||||
return encrypted, nil
|
||||
}
|
||||
|
||||
// DecryptStringWithKey decrypts data using secretbox with the provided key
|
||||
func DecryptStringWithKey(data, key []byte) ([]byte, error) {
|
||||
if err := validateKey(key); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(data) < 24 {
|
||||
return nil, ErrShortCipherText
|
||||
}
|
||||
|
||||
var secretKey [keySize]byte
|
||||
copy(secretKey[:], key)
|
||||
|
||||
var nonce [24]byte
|
||||
copy(nonce[:], data[:24])
|
||||
|
||||
decrypted, ok := secretbox.Open(nil, data[24:], &nonce, &secretKey)
|
||||
if !ok {
|
||||
return nil, errors.New("decryption failed")
|
||||
}
|
||||
|
||||
return decrypted, nil
|
||||
}
|
||||
|
||||
func validateKey(key []byte) error {
|
||||
if key == nil {
|
||||
return ErrNoEncryptionKey
|
||||
}
|
||||
|
||||
if len(key) != keySize {
|
||||
return ErrInvalidKeySize
|
||||
}
|
||||
|
||||
// check if key is all zeros
|
||||
for _, b := range key {
|
||||
if b != 0 {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return ErrZeroKey
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package crypto
|
||||
package secretbox
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -8,10 +8,10 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAESEncryption(t *testing.T) {
|
||||
func TestSecretBoxEncryption(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
key := make([]byte, 32) // generated random 32-byte key
|
||||
key := make([]byte, keySize) // generate random 32-byte key
|
||||
_, errKey := rand.Read(key)
|
||||
require.NoError(t, errKey)
|
||||
|
||||
@@ -40,13 +40,13 @@ func TestAESEncryption(t *testing.T) {
|
||||
{
|
||||
name: "invalid key size",
|
||||
data: []byte("hello world"),
|
||||
key: make([]byte, 31),
|
||||
key: make([]byte, 16), // Only 32 bytes allowed now
|
||||
wantErr: ErrInvalidKeySize,
|
||||
},
|
||||
{
|
||||
name: "zero key returns error",
|
||||
data: []byte("hello world"),
|
||||
key: make([]byte, 32),
|
||||
key: make([]byte, keySize),
|
||||
wantErr: ErrZeroKey,
|
||||
},
|
||||
}
|
||||
@@ -56,24 +56,22 @@ func TestAESEncryption(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
encrypted, err := EncryptWithAESKey(tt.data, tt.key)
|
||||
encrypted, err := EncryptWithKey(tt.data, tt.key)
|
||||
if tt.wantErr != nil {
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
decrypted, err := DecryptStringWithAESKey(encrypted, tt.key)
|
||||
require.NoError(t, err)
|
||||
|
||||
if tt.key == nil {
|
||||
require.Equal(t, tt.data, encrypted)
|
||||
require.Equal(t, tt.data, decrypted)
|
||||
} else {
|
||||
require.NotEqual(t, tt.data, encrypted)
|
||||
require.True(t, bytes.Equal(tt.data, decrypted))
|
||||
// Verify encrypted data is different and includes nonce
|
||||
require.Greater(t, len(encrypted), 24) // At least nonce size
|
||||
if len(tt.data) > 0 {
|
||||
require.NotEqual(t, tt.data, encrypted[24:]) // Ignore nonce prefix
|
||||
}
|
||||
|
||||
decrypted, err := DecryptStringWithKey(encrypted, tt.key)
|
||||
require.NoError(t, err)
|
||||
require.True(t, bytes.Equal(tt.data, decrypted))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -81,10 +79,15 @@ func TestAESEncryption(t *testing.T) {
|
||||
func TestDecryptionErrors(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
key := make([]byte, 32)
|
||||
key := make([]byte, keySize)
|
||||
_, err := rand.Read(key)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create valid encrypted data for tampering tests
|
||||
validData := []byte("test message")
|
||||
encrypted, err := EncryptWithKey(validData, key)
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
data []byte
|
||||
@@ -93,19 +96,25 @@ func TestDecryptionErrors(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "short ciphertext",
|
||||
data: []byte("short"),
|
||||
data: make([]byte, 23), // Less than nonce size
|
||||
key: key,
|
||||
errMsg: "ciphertext too short",
|
||||
},
|
||||
{
|
||||
name: "invalid ciphertext",
|
||||
data: make([]byte, 16), // just nonce size
|
||||
data: make([]byte, 24), // Just nonce size
|
||||
key: key,
|
||||
errMsg: "message authentication failed",
|
||||
errMsg: "decryption failed",
|
||||
},
|
||||
{
|
||||
name: "tampered ciphertext",
|
||||
data: tamperWithBytes(encrypted),
|
||||
key: key,
|
||||
errMsg: "decryption failed",
|
||||
},
|
||||
{
|
||||
name: "missing key",
|
||||
data: []byte("<22>`M<><4D><EFBFBD>l\u001AIF<49>\u0012<31><32><EFBFBD>=h<>?<3F>c<EFBFBD> <20><>\u0012<31><32><EFBFBD><EFBFBD>\u001C<31>\u0018Ƽ(g"),
|
||||
data: encrypted,
|
||||
key: nil,
|
||||
errMsg: "encryption key is required",
|
||||
},
|
||||
@@ -116,9 +125,20 @@ func TestDecryptionErrors(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := DecryptStringWithAESKey(tt.data, tt.key)
|
||||
_, err := DecryptStringWithKey(tt.data, tt.key)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), tt.errMsg)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// tamperWithBytes modifies a byte in the encrypted data to simulate tampering
|
||||
func tamperWithBytes(data []byte) []byte {
|
||||
if len(data) < 25 { // Need at least nonce + 1 byte
|
||||
return data
|
||||
}
|
||||
tampered := make([]byte, len(data))
|
||||
copy(tampered, data)
|
||||
tampered[24] ^= 0x01 // Modify first byte after nonce
|
||||
return tampered
|
||||
}
|
||||
@@ -10,8 +10,10 @@ package delegation
|
||||
// TODO: change the "delegation" link above when the specification is merged
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/did"
|
||||
@@ -44,16 +46,15 @@ type Token struct {
|
||||
expiration *time.Time
|
||||
}
|
||||
|
||||
// New creates a validated Token from the provided parameters and options.
|
||||
// New creates a validated delegation Token from the provided parameters and options.
|
||||
// This is typically used to delegate a given power to another agent.
|
||||
//
|
||||
// When creating a delegated token, the Issuer's (iss) DID is assembled
|
||||
// using the public key associated with the private key sent as the first
|
||||
// parameter.
|
||||
func New(iss, aud did.DID, cmd command.Command, pol policy.Policy, opts ...Option) (*Token, error) {
|
||||
// You can read it as "(issuer) allows (audience) to perform (cmd+pol) on (subject)".
|
||||
func New(iss did.DID, aud did.DID, cmd command.Command, pol policy.Policy, sub did.DID, opts ...Option) (*Token, error) {
|
||||
tkn := &Token{
|
||||
issuer: iss,
|
||||
audience: aud,
|
||||
subject: did.Undef,
|
||||
subject: sub,
|
||||
command: cmd,
|
||||
policy: pol,
|
||||
meta: meta.NewMeta(),
|
||||
@@ -81,16 +82,27 @@ func New(iss, aud did.DID, cmd command.Command, pol policy.Policy, opts ...Optio
|
||||
return tkn, nil
|
||||
}
|
||||
|
||||
// Root creates a validated UCAN delegation Token from the provided
|
||||
// parameters and options.
|
||||
// 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.
|
||||
//
|
||||
// When creating a root token, both the Issuer's (iss) and Subject's
|
||||
// (sub) DIDs are assembled from the public key associated with the
|
||||
// private key passed as the first argument.
|
||||
func Root(iss, aud did.DID, cmd command.Command, pol policy.Policy, opts ...Option) (*Token, error) {
|
||||
opts = append(opts, WithSubject(iss))
|
||||
// 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) {
|
||||
return New(iss, aud, cmd, pol, iss, opts...)
|
||||
}
|
||||
|
||||
return New(iss, aud, cmd, pol, opts...)
|
||||
// Powerline creates a validated UCAN delegation Token from the provided parameters and options.
|
||||
//
|
||||
// Powerline is a pattern for automatically delegating all future delegations to another agent regardless of Subject.
|
||||
// This is a very powerful pattern, use it only if you understand it.
|
||||
// Powerline delegations MUST NOT be used as the root delegation to a resource
|
||||
//
|
||||
// A very common use case for Powerline is providing a stable DID across multiple agents (e.g. representing a user with
|
||||
// multiple devices). This enables the automatic sharing of authority across their devices without needing to share keys
|
||||
// or set up a threshold scheme. It is also flexible, since a Powerline delegation MAY be revoked.
|
||||
//
|
||||
// You can read it as "(issuer) allows (audience) to perform (cmd+pol) on anything".
|
||||
func Powerline(iss did.DID, aud did.DID, cmd command.Command, pol policy.Policy, opts ...Option) (*Token, error) {
|
||||
return New(iss, aud, cmd, pol, did.Undef, opts...)
|
||||
}
|
||||
|
||||
// Issuer returns the did.DID representing the Token's issuer.
|
||||
@@ -160,6 +172,32 @@ func (t *Token) IsValidAt(ti time.Time) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (t *Token) String() string {
|
||||
var res strings.Builder
|
||||
|
||||
var kind string
|
||||
switch {
|
||||
case t.issuer == t.subject:
|
||||
kind = " (root delegation)"
|
||||
case t.subject == did.Undef:
|
||||
kind = " (powerline delegation)"
|
||||
default:
|
||||
kind = " (normal delegation)"
|
||||
}
|
||||
|
||||
res.WriteString(fmt.Sprintf("Issuer: %s\n", t.Issuer()))
|
||||
res.WriteString(fmt.Sprintf("Audience: %s\n", t.Audience()))
|
||||
res.WriteString(fmt.Sprintf("Subject: %s%s\n", t.Subject(), kind))
|
||||
res.WriteString(fmt.Sprintf("Command: %s\n", t.Command()))
|
||||
res.WriteString(fmt.Sprintf("Policy: %s\n", t.Policy()))
|
||||
res.WriteString(fmt.Sprintf("Nonce: %s\n", base64.StdEncoding.EncodeToString(t.Nonce())))
|
||||
res.WriteString(fmt.Sprintf("Meta: %s\n", t.Meta()))
|
||||
res.WriteString(fmt.Sprintf("NotBefore: %v\n", t.NotBefore()))
|
||||
res.WriteString(fmt.Sprintf("Expiration: %v", t.Expiration()))
|
||||
|
||||
return res.String()
|
||||
}
|
||||
|
||||
func (t *Token) validate() error {
|
||||
var errs error
|
||||
|
||||
@@ -214,9 +252,19 @@ func tokenFromModel(m tokenPayloadModel) (*Token, error) {
|
||||
tkn.nonce = m.Nonce
|
||||
|
||||
tkn.meta = m.Meta
|
||||
if tkn.meta == nil {
|
||||
tkn.meta = meta.NewMeta()
|
||||
}
|
||||
|
||||
tkn.notBefore = parse.OptionalTimestamp(m.Nbf)
|
||||
tkn.expiration = parse.OptionalTimestamp(m.Exp)
|
||||
tkn.notBefore, err = parse.OptionalTimestamp(m.Nbf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse notBefore: %w", err)
|
||||
}
|
||||
|
||||
tkn.expiration, err = parse.OptionalTimestamp(m.Exp)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse expiration: %w", err)
|
||||
}
|
||||
|
||||
if err := tkn.validate(); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -56,9 +56,6 @@ const (
|
||||
]
|
||||
`
|
||||
|
||||
newCID = "zdpuAwa4qv3ncMDPeDoqVxjZy3JoyWsbqUzm94rdA1AvRFkkw"
|
||||
rootCID = "zdpuAkgGmUp5JrXvehGuuw9JA8DLQKDaxtK3R8brDQQVC2i5X"
|
||||
|
||||
aesKey = "xQklMmNTnVrmaPBq/0pwV5fEwuv/iClF5HWak9MsgI8="
|
||||
)
|
||||
|
||||
@@ -75,9 +72,8 @@ func TestConstructors(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("New", func(t *testing.T) {
|
||||
tkn, err := delegation.New(didtest.PersonaAlice.DID(), didtest.PersonaBob.DID(), cmd, pol,
|
||||
tkn, err := delegation.New(didtest.PersonaAlice.DID(), didtest.PersonaBob.DID(), cmd, pol, didtest.PersonaCarol.DID(),
|
||||
delegation.WithNonce([]byte(nonce)),
|
||||
delegation.WithSubject(didtest.PersonaAlice.DID()),
|
||||
delegation.WithExpiration(exp),
|
||||
delegation.WithMeta("foo", "fooo"),
|
||||
delegation.WithMeta("bar", "barr"),
|
||||
@@ -106,6 +102,23 @@ func TestConstructors(t *testing.T) {
|
||||
|
||||
golden.Assert(t, string(data), "root.dagjson")
|
||||
})
|
||||
|
||||
t.Run("Powerline", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tkn, err := delegation.Powerline(didtest.PersonaAlice.DID(), didtest.PersonaBob.DID(), cmd, pol,
|
||||
delegation.WithNonce([]byte(nonce)),
|
||||
delegation.WithExpiration(exp),
|
||||
delegation.WithMeta("foo", "fooo"),
|
||||
delegation.WithMeta("bar", "barr"),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
data, err := tkn.ToDagJson(didtest.PersonaAlice.PrivKey())
|
||||
require.NoError(t, err)
|
||||
|
||||
golden.Assert(t, string(data), "powerline.dagjson")
|
||||
})
|
||||
}
|
||||
|
||||
func TestEncryptedMeta(t *testing.T) {
|
||||
@@ -153,7 +166,7 @@ func TestEncryptedMeta(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tkn, err := delegation.New(didtest.PersonaAlice.DID(), didtest.PersonaBob.DID(), cmd, pol,
|
||||
tkn, err := delegation.Root(didtest.PersonaAlice.DID(), didtest.PersonaBob.DID(), cmd, pol,
|
||||
delegation.WithEncryptedMetaString(tt.key, tt.value, encryptionKey),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
@@ -191,7 +204,7 @@ func TestEncryptedMeta(t *testing.T) {
|
||||
}
|
||||
|
||||
// Create token with multiple encrypted values
|
||||
tkn, err := delegation.New(didtest.PersonaAlice.DID(), didtest.PersonaBob.DID(), cmd, pol, opts...)
|
||||
tkn, err := delegation.Root(didtest.PersonaAlice.DID(), didtest.PersonaBob.DID(), cmd, pol, opts...)
|
||||
require.NoError(t, err)
|
||||
|
||||
data, err := tkn.ToDagCbor(didtest.PersonaAlice.PrivKey())
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,12 +1,14 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go/format"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/dave/jennifer/jen"
|
||||
"github.com/ipfs/go-cid"
|
||||
"github.com/libp2p/go-libp2p/core/crypto"
|
||||
|
||||
@@ -14,6 +16,7 @@ import (
|
||||
"github.com/ucan-wg/go-ucan/did/didtest"
|
||||
"github.com/ucan-wg/go-ucan/pkg/command"
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy"
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy/policytest"
|
||||
"github.com/ucan-wg/go-ucan/token/delegation"
|
||||
"github.com/ucan-wg/go-ucan/token/delegation/delegationtest"
|
||||
)
|
||||
@@ -27,11 +30,11 @@ const (
|
||||
var constantNonce = []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b}
|
||||
|
||||
type newDelegationParams struct {
|
||||
privKey crypto.PrivKey
|
||||
privKey crypto.PrivKey // iss
|
||||
aud did.DID
|
||||
sub did.DID
|
||||
cmd command.Command
|
||||
pol policy.Policy
|
||||
sub did.DID
|
||||
opts []delegation.Option
|
||||
}
|
||||
|
||||
@@ -86,9 +89,9 @@ func (g *generator) chainPersonas(personas []didtest.Persona, acc acc, vari vari
|
||||
privKey: personas[0].PrivKey(),
|
||||
aud: personas[1].DID(),
|
||||
cmd: delegationtest.NominalCommand,
|
||||
pol: policy.Policy{},
|
||||
pol: policytest.EmptyPolicy,
|
||||
sub: didtest.PersonaAlice.DID(),
|
||||
opts: []delegation.Option{
|
||||
delegation.WithSubject(didtest.PersonaAlice.DID()),
|
||||
delegation.WithNonce(constantNonce),
|
||||
},
|
||||
}
|
||||
@@ -115,7 +118,7 @@ func (g *generator) chainPersonas(personas []didtest.Persona, acc acc, vari vari
|
||||
p.cmd = delegationtest.AttenuatedCommand
|
||||
}},
|
||||
{name: "InvalidSubject", variant: func(p *newDelegationParams) {
|
||||
p.opts = append(p.opts, delegation.WithSubject(didtest.PersonaBob.DID()))
|
||||
p.sub = didtest.PersonaBob.DID()
|
||||
}},
|
||||
{name: "InvalidExpired", variant: func(p *newDelegationParams) {
|
||||
// Note: this makes the generator not deterministic
|
||||
@@ -128,6 +131,9 @@ func (g *generator) chainPersonas(personas []didtest.Persona, acc acc, vari vari
|
||||
}
|
||||
p.opts = append(p.opts, delegation.WithNotBefore(nbf))
|
||||
}},
|
||||
{name: "ValidExamplePolicy", variant: func(p *newDelegationParams) {
|
||||
p.pol = policytest.SpecPolicy
|
||||
}},
|
||||
}
|
||||
|
||||
// Start a branch in the recursion for each of the variants
|
||||
@@ -156,7 +162,7 @@ func (g *generator) createDelegation(params newDelegationParams, name string, va
|
||||
return cid.Undef, err
|
||||
}
|
||||
|
||||
tkn, err := delegation.New(issDID, params.aud, params.cmd, params.pol, params.opts...)
|
||||
tkn, err := delegation.New(issDID, params.aud, params.cmd, params.pol, params.sub, params.opts...)
|
||||
if err != nil {
|
||||
return cid.Undef, err
|
||||
}
|
||||
@@ -199,36 +205,71 @@ func (g *generator) createProofChain(name string, prf []cid.Cid) {
|
||||
}
|
||||
|
||||
func (g *generator) writeGoFile() error {
|
||||
file := jen.NewFile("delegationtest")
|
||||
file.HeaderComment("Code generated by delegationtest - DO NOT EDIT.")
|
||||
buf := bytes.NewBuffer(nil)
|
||||
|
||||
refs := map[cid.Cid]string{}
|
||||
Println := func(a ...any) { _, _ = fmt.Fprintln(buf, a...) }
|
||||
Printf := func(format string, a ...any) { _, _ = fmt.Fprintf(buf, format, a...) }
|
||||
|
||||
Println("// Code generated by delegationtest - DO NOT EDIT.")
|
||||
Println()
|
||||
Println("package delegationtest")
|
||||
Println()
|
||||
Println("import (")
|
||||
Println("\t\"github.com/ipfs/go-cid\"")
|
||||
Println()
|
||||
Println("\t\"github.com/ucan-wg/go-ucan/token/delegation\"")
|
||||
Println(")")
|
||||
|
||||
refs := make(map[cid.Cid]string, len(g.dlgs))
|
||||
|
||||
for _, d := range g.dlgs {
|
||||
refs[d.id] = d.name + "CID"
|
||||
|
||||
file.Var().Defs(
|
||||
jen.Id(d.name+"CID").Op("=").Qual("github.com/ipfs/go-cid", "MustParse").Call(jen.Lit(d.id.String())),
|
||||
jen.Id(d.name).Op("=").Id("mustGetDelegation").Call(jen.Id(d.name+"CID")),
|
||||
)
|
||||
file.Line()
|
||||
Println()
|
||||
Println("var (")
|
||||
Printf("\t%sCID = cid.MustParse(\"%s\")\n", d.name, d.id.String())
|
||||
Printf("\t%sSealed = mustGetBundle(%s).Sealed\n", d.name, d.name+"CID")
|
||||
Printf("\t%sBundle = mustGetBundle(%s)\n", d.name, d.name+"CID")
|
||||
Printf("\t%s = mustGetBundle(%s).Decoded\n", d.name, d.name+"CID")
|
||||
Println(")")
|
||||
}
|
||||
|
||||
Println()
|
||||
Println("var AllTokens = []*delegation.Token{")
|
||||
for _, d := range g.dlgs {
|
||||
Printf("\t%s,\n", d.name)
|
||||
}
|
||||
Println("}")
|
||||
|
||||
Println()
|
||||
Println("var AllBundles = []*delegation.Bundle{")
|
||||
for _, d := range g.dlgs {
|
||||
Printf("\t%sBundle,\n", d.name)
|
||||
}
|
||||
Println("}")
|
||||
|
||||
Println()
|
||||
Println("var cidToName = map[cid.Cid]string{")
|
||||
for _, d := range g.dlgs {
|
||||
Printf("\t%sCID: \"%s\",\n", d.name, d.name)
|
||||
}
|
||||
Println("}")
|
||||
|
||||
for _, c := range g.chains {
|
||||
g := jen.CustomFunc(jen.Options{
|
||||
Multi: true,
|
||||
Separator: ",",
|
||||
Close: "\n",
|
||||
}, func(g *jen.Group) {
|
||||
slices.Reverse(c.prf)
|
||||
for _, p := range c.prf {
|
||||
g.Id(refs[p])
|
||||
}
|
||||
})
|
||||
Println()
|
||||
Printf("var %s = []cid.Cid{\n", c.name)
|
||||
|
||||
file.Var().Id(c.name).Op("=").Index().Qual("github.com/ipfs/go-cid", "Cid").Values(g)
|
||||
file.Line()
|
||||
for _, d := range slices.Backward(c.prf) {
|
||||
Printf("\t%s,\n", refs[d])
|
||||
}
|
||||
|
||||
Println("}")
|
||||
}
|
||||
|
||||
return file.Save("../token_gen.go")
|
||||
out, err := format.Source(buf.Bytes())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile("../token_gen.go", out, 0666)
|
||||
}
|
||||
|
||||
@@ -18,12 +18,12 @@ var (
|
||||
// Execution of this command is generally prohibited in tests.
|
||||
ExpandedCommand = command.MustParse("/expanded")
|
||||
|
||||
// NominalCommand is the command used for most test tokens and proof-
|
||||
// chains. Execution of this command is generally allowed in tests.
|
||||
// NominalCommand is the command used for most test tokens and proof-chains.
|
||||
// Execution of this command is generally allowed in tests.
|
||||
NominalCommand = ExpandedCommand.Join("nominal")
|
||||
|
||||
// AttenuatedCommand is a sub-command of the NominalCommand. Execution
|
||||
// of this command is generally allowed in tests.
|
||||
// AttenuatedCommand is a sub-command of the NominalCommand.
|
||||
// Execution of this command is generally allowed in tests.
|
||||
AttenuatedCommand = NominalCommand.Join("attenuated")
|
||||
)
|
||||
|
||||
@@ -35,21 +35,21 @@ const TokenDir = "data"
|
||||
//go:embed data
|
||||
var fs embed.FS
|
||||
|
||||
var _ delegation.Loader = (*delegationLoader)(nil)
|
||||
var _ delegation.Loader = (*DelegationLoader)(nil)
|
||||
|
||||
type delegationLoader struct {
|
||||
tokens map[cid.Cid]*delegation.Token
|
||||
type DelegationLoader struct {
|
||||
bundles map[cid.Cid]*delegation.Bundle
|
||||
}
|
||||
|
||||
var (
|
||||
once sync.Once
|
||||
ldr delegation.Loader
|
||||
ldr *DelegationLoader
|
||||
)
|
||||
|
||||
// GetDelegationLoader returns a singleton instance of a test
|
||||
// DelegationLoader containing all the tokens present in the data/
|
||||
// directory.
|
||||
func GetDelegationLoader() delegation.Loader {
|
||||
func GetDelegationLoader() *DelegationLoader {
|
||||
once.Do(func() {
|
||||
var err error
|
||||
ldr, err = loadDelegations()
|
||||
@@ -61,22 +61,21 @@ func GetDelegationLoader() delegation.Loader {
|
||||
}
|
||||
|
||||
// GetDelegation implements invocation.DelegationLoader.
|
||||
func (l *delegationLoader) GetDelegation(id cid.Cid) (*delegation.Token, error) {
|
||||
tkn, ok := l.tokens[id]
|
||||
func (l *DelegationLoader) GetDelegation(id cid.Cid) (*delegation.Token, error) {
|
||||
bundle, ok := l.bundles[id]
|
||||
if !ok {
|
||||
return nil, delegation.ErrDelegationNotFound
|
||||
}
|
||||
|
||||
return tkn, nil
|
||||
return bundle.Decoded, nil
|
||||
}
|
||||
|
||||
func loadDelegations() (delegation.Loader, error) {
|
||||
func loadDelegations() (*DelegationLoader, error) {
|
||||
dirEntries, err := fs.ReadDir(TokenDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tkns := make(map[cid.Cid]*delegation.Token, len(dirEntries))
|
||||
bundles := make(map[cid.Cid]*delegation.Bundle, len(dirEntries))
|
||||
|
||||
for _, dirEntry := range dirEntries {
|
||||
data, err := fs.ReadFile(filepath.Join(TokenDir, dirEntry.Name()))
|
||||
@@ -89,11 +88,11 @@ func loadDelegations() (delegation.Loader, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tkns[id] = tkn
|
||||
bundles[id] = &delegation.Bundle{Cid: id, Decoded: tkn, Sealed: data}
|
||||
}
|
||||
|
||||
return &delegationLoader{
|
||||
tokens: tkns,
|
||||
return &DelegationLoader{
|
||||
bundles: bundles,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -103,10 +102,14 @@ func GetDelegation(id cid.Cid) (*delegation.Token, error) {
|
||||
return GetDelegationLoader().GetDelegation(id)
|
||||
}
|
||||
|
||||
func mustGetDelegation(id cid.Cid) *delegation.Token {
|
||||
tkn, err := GetDelegation(id)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return tkn
|
||||
func CidToName(id cid.Cid) string {
|
||||
return cidToName[id]
|
||||
}
|
||||
|
||||
func mustGetBundle(id cid.Cid) *delegation.Bundle {
|
||||
bundle, ok := GetDelegationLoader().bundles[id]
|
||||
if !ok {
|
||||
panic(delegation.ErrDelegationNotFound)
|
||||
}
|
||||
return bundle
|
||||
}
|
||||
|
||||
@@ -2,131 +2,274 @@
|
||||
|
||||
package delegationtest
|
||||
|
||||
import gocid "github.com/ipfs/go-cid"
|
||||
import (
|
||||
"github.com/ipfs/go-cid"
|
||||
|
||||
var (
|
||||
TokenAliceBobCID = gocid.MustParse("bafyreicidrwvmac5lvjypucgityrtjsknojraio7ujjli4r5eyby66wjzm")
|
||||
TokenAliceBob = mustGetDelegation(TokenAliceBobCID)
|
||||
"github.com/ucan-wg/go-ucan/token/delegation"
|
||||
)
|
||||
|
||||
var (
|
||||
TokenBobCarolCID = gocid.MustParse("bafyreihxv2uhq43oxllzs2xfvxst7wtvvvl7pohb2chcz6hjvfv2ntea5u")
|
||||
TokenBobCarol = mustGetDelegation(TokenBobCarolCID)
|
||||
TokenAliceBobCID = cid.MustParse("bafyreicidrwvmac5lvjypucgityrtjsknojraio7ujjli4r5eyby66wjzm")
|
||||
TokenAliceBobSealed = mustGetBundle(TokenAliceBobCID).Sealed
|
||||
TokenAliceBobBundle = mustGetBundle(TokenAliceBobCID)
|
||||
TokenAliceBob = mustGetBundle(TokenAliceBobCID).Decoded
|
||||
)
|
||||
|
||||
var (
|
||||
TokenCarolDanCID = gocid.MustParse("bafyreihclsgiroazq3heqdswvj2cafwqbpboicq7immo65scl7ahktpsdq")
|
||||
TokenCarolDan = mustGetDelegation(TokenCarolDanCID)
|
||||
TokenBobCarolCID = cid.MustParse("bafyreihxv2uhq43oxllzs2xfvxst7wtvvvl7pohb2chcz6hjvfv2ntea5u")
|
||||
TokenBobCarolSealed = mustGetBundle(TokenBobCarolCID).Sealed
|
||||
TokenBobCarolBundle = mustGetBundle(TokenBobCarolCID)
|
||||
TokenBobCarol = mustGetBundle(TokenBobCarolCID).Decoded
|
||||
)
|
||||
|
||||
var (
|
||||
TokenDanErinCID = gocid.MustParse("bafyreicja6ihewy64p3ake56xukotafjlkh4uqep2qhj52en46zzfwby3e")
|
||||
TokenDanErin = mustGetDelegation(TokenDanErinCID)
|
||||
TokenCarolDanCID = cid.MustParse("bafyreihclsgiroazq3heqdswvj2cafwqbpboicq7immo65scl7ahktpsdq")
|
||||
TokenCarolDanSealed = mustGetBundle(TokenCarolDanCID).Sealed
|
||||
TokenCarolDanBundle = mustGetBundle(TokenCarolDanCID)
|
||||
TokenCarolDan = mustGetBundle(TokenCarolDanCID).Decoded
|
||||
)
|
||||
|
||||
var (
|
||||
TokenErinFrankCID = gocid.MustParse("bafyreicjlx3lobxm6hl5s4htd4ydwkkqeiou6rft4rnvulfdyoew565vka")
|
||||
TokenErinFrank = mustGetDelegation(TokenErinFrankCID)
|
||||
TokenDanErinCID = cid.MustParse("bafyreicja6ihewy64p3ake56xukotafjlkh4uqep2qhj52en46zzfwby3e")
|
||||
TokenDanErinSealed = mustGetBundle(TokenDanErinCID).Sealed
|
||||
TokenDanErinBundle = mustGetBundle(TokenDanErinCID)
|
||||
TokenDanErin = mustGetBundle(TokenDanErinCID).Decoded
|
||||
)
|
||||
|
||||
var (
|
||||
TokenCarolDan_InvalidExpandedCommandCID = gocid.MustParse("bafyreid3m3pk53gqgp5rlzqhvpedbwsqbidqlp4yz64vknwbzj7bxrmsr4")
|
||||
TokenCarolDan_InvalidExpandedCommand = mustGetDelegation(TokenCarolDan_InvalidExpandedCommandCID)
|
||||
TokenErinFrankCID = cid.MustParse("bafyreicjlx3lobxm6hl5s4htd4ydwkkqeiou6rft4rnvulfdyoew565vka")
|
||||
TokenErinFrankSealed = mustGetBundle(TokenErinFrankCID).Sealed
|
||||
TokenErinFrankBundle = mustGetBundle(TokenErinFrankCID)
|
||||
TokenErinFrank = mustGetBundle(TokenErinFrankCID).Decoded
|
||||
)
|
||||
|
||||
var (
|
||||
TokenDanErin_InvalidExpandedCommandCID = gocid.MustParse("bafyreifn4sy5onwajx3kqvot5mib6m6xarzrqjozqbzgmzpmc5ox3g2uzm")
|
||||
TokenDanErin_InvalidExpandedCommand = mustGetDelegation(TokenDanErin_InvalidExpandedCommandCID)
|
||||
TokenCarolDan_InvalidExpandedCommandCID = cid.MustParse("bafyreid3m3pk53gqgp5rlzqhvpedbwsqbidqlp4yz64vknwbzj7bxrmsr4")
|
||||
TokenCarolDan_InvalidExpandedCommandSealed = mustGetBundle(TokenCarolDan_InvalidExpandedCommandCID).Sealed
|
||||
TokenCarolDan_InvalidExpandedCommandBundle = mustGetBundle(TokenCarolDan_InvalidExpandedCommandCID)
|
||||
TokenCarolDan_InvalidExpandedCommand = mustGetBundle(TokenCarolDan_InvalidExpandedCommandCID).Decoded
|
||||
)
|
||||
|
||||
var (
|
||||
TokenErinFrank_InvalidExpandedCommandCID = gocid.MustParse("bafyreidmpgd36jznmq42bs34o4qi3fcbrsh4idkg6ejahudejzwb76fwxe")
|
||||
TokenErinFrank_InvalidExpandedCommand = mustGetDelegation(TokenErinFrank_InvalidExpandedCommandCID)
|
||||
TokenDanErin_InvalidExpandedCommandCID = cid.MustParse("bafyreifn4sy5onwajx3kqvot5mib6m6xarzrqjozqbzgmzpmc5ox3g2uzm")
|
||||
TokenDanErin_InvalidExpandedCommandSealed = mustGetBundle(TokenDanErin_InvalidExpandedCommandCID).Sealed
|
||||
TokenDanErin_InvalidExpandedCommandBundle = mustGetBundle(TokenDanErin_InvalidExpandedCommandCID)
|
||||
TokenDanErin_InvalidExpandedCommand = mustGetBundle(TokenDanErin_InvalidExpandedCommandCID).Decoded
|
||||
)
|
||||
|
||||
var (
|
||||
TokenCarolDan_ValidAttenuatedCommandCID = gocid.MustParse("bafyreiekhtm237vyapk3c6voeb5lnz54crebqdqi3x4wn4u4cbrrhzsqfe")
|
||||
TokenCarolDan_ValidAttenuatedCommand = mustGetDelegation(TokenCarolDan_ValidAttenuatedCommandCID)
|
||||
TokenErinFrank_InvalidExpandedCommandCID = cid.MustParse("bafyreidmpgd36jznmq42bs34o4qi3fcbrsh4idkg6ejahudejzwb76fwxe")
|
||||
TokenErinFrank_InvalidExpandedCommandSealed = mustGetBundle(TokenErinFrank_InvalidExpandedCommandCID).Sealed
|
||||
TokenErinFrank_InvalidExpandedCommandBundle = mustGetBundle(TokenErinFrank_InvalidExpandedCommandCID)
|
||||
TokenErinFrank_InvalidExpandedCommand = mustGetBundle(TokenErinFrank_InvalidExpandedCommandCID).Decoded
|
||||
)
|
||||
|
||||
var (
|
||||
TokenDanErin_ValidAttenuatedCommandCID = gocid.MustParse("bafyreicrvzqferyy7rgo75l5rn6r2nl7zyeexxjmu3dm4ff7rn2coblj4y")
|
||||
TokenDanErin_ValidAttenuatedCommand = mustGetDelegation(TokenDanErin_ValidAttenuatedCommandCID)
|
||||
TokenCarolDan_ValidAttenuatedCommandCID = cid.MustParse("bafyreiekhtm237vyapk3c6voeb5lnz54crebqdqi3x4wn4u4cbrrhzsqfe")
|
||||
TokenCarolDan_ValidAttenuatedCommandSealed = mustGetBundle(TokenCarolDan_ValidAttenuatedCommandCID).Sealed
|
||||
TokenCarolDan_ValidAttenuatedCommandBundle = mustGetBundle(TokenCarolDan_ValidAttenuatedCommandCID)
|
||||
TokenCarolDan_ValidAttenuatedCommand = mustGetBundle(TokenCarolDan_ValidAttenuatedCommandCID).Decoded
|
||||
)
|
||||
|
||||
var (
|
||||
TokenErinFrank_ValidAttenuatedCommandCID = gocid.MustParse("bafyreie6fhspk53kplcc2phla3e7z7fzldlbmmpuwk6nbow5q6s2zjmw2q")
|
||||
TokenErinFrank_ValidAttenuatedCommand = mustGetDelegation(TokenErinFrank_ValidAttenuatedCommandCID)
|
||||
TokenDanErin_ValidAttenuatedCommandCID = cid.MustParse("bafyreicrvzqferyy7rgo75l5rn6r2nl7zyeexxjmu3dm4ff7rn2coblj4y")
|
||||
TokenDanErin_ValidAttenuatedCommandSealed = mustGetBundle(TokenDanErin_ValidAttenuatedCommandCID).Sealed
|
||||
TokenDanErin_ValidAttenuatedCommandBundle = mustGetBundle(TokenDanErin_ValidAttenuatedCommandCID)
|
||||
TokenDanErin_ValidAttenuatedCommand = mustGetBundle(TokenDanErin_ValidAttenuatedCommandCID).Decoded
|
||||
)
|
||||
|
||||
var (
|
||||
TokenCarolDan_InvalidSubjectCID = gocid.MustParse("bafyreifgksz6756if42tnc6rqsnbaa2u3fdrveo7ek44lnj2d64d5sw26u")
|
||||
TokenCarolDan_InvalidSubject = mustGetDelegation(TokenCarolDan_InvalidSubjectCID)
|
||||
TokenErinFrank_ValidAttenuatedCommandCID = cid.MustParse("bafyreie6fhspk53kplcc2phla3e7z7fzldlbmmpuwk6nbow5q6s2zjmw2q")
|
||||
TokenErinFrank_ValidAttenuatedCommandSealed = mustGetBundle(TokenErinFrank_ValidAttenuatedCommandCID).Sealed
|
||||
TokenErinFrank_ValidAttenuatedCommandBundle = mustGetBundle(TokenErinFrank_ValidAttenuatedCommandCID)
|
||||
TokenErinFrank_ValidAttenuatedCommand = mustGetBundle(TokenErinFrank_ValidAttenuatedCommandCID).Decoded
|
||||
)
|
||||
|
||||
var (
|
||||
TokenDanErin_InvalidSubjectCID = gocid.MustParse("bafyreibdwew5nypsxrm4fq73wu6hw3lgwwiolj3bi33xdrbgcf3ogm6fty")
|
||||
TokenDanErin_InvalidSubject = mustGetDelegation(TokenDanErin_InvalidSubjectCID)
|
||||
TokenCarolDan_InvalidSubjectCID = cid.MustParse("bafyreifgksz6756if42tnc6rqsnbaa2u3fdrveo7ek44lnj2d64d5sw26u")
|
||||
TokenCarolDan_InvalidSubjectSealed = mustGetBundle(TokenCarolDan_InvalidSubjectCID).Sealed
|
||||
TokenCarolDan_InvalidSubjectBundle = mustGetBundle(TokenCarolDan_InvalidSubjectCID)
|
||||
TokenCarolDan_InvalidSubject = mustGetBundle(TokenCarolDan_InvalidSubjectCID).Decoded
|
||||
)
|
||||
|
||||
var (
|
||||
TokenErinFrank_InvalidSubjectCID = gocid.MustParse("bafyreicr364mj3n7x4iyhcksxypelktcqkkw3ptg7ggxtqegw3p3mr6zc4")
|
||||
TokenErinFrank_InvalidSubject = mustGetDelegation(TokenErinFrank_InvalidSubjectCID)
|
||||
TokenDanErin_InvalidSubjectCID = cid.MustParse("bafyreibdwew5nypsxrm4fq73wu6hw3lgwwiolj3bi33xdrbgcf3ogm6fty")
|
||||
TokenDanErin_InvalidSubjectSealed = mustGetBundle(TokenDanErin_InvalidSubjectCID).Sealed
|
||||
TokenDanErin_InvalidSubjectBundle = mustGetBundle(TokenDanErin_InvalidSubjectCID)
|
||||
TokenDanErin_InvalidSubject = mustGetBundle(TokenDanErin_InvalidSubjectCID).Decoded
|
||||
)
|
||||
|
||||
var (
|
||||
TokenCarolDan_InvalidExpiredCID = gocid.MustParse("bafyreigenypixaxvhzlry5rjnywvjyl4xvzlzxz2ui74uzys7qdhos4bbu")
|
||||
TokenCarolDan_InvalidExpired = mustGetDelegation(TokenCarolDan_InvalidExpiredCID)
|
||||
TokenErinFrank_InvalidSubjectCID = cid.MustParse("bafyreicr364mj3n7x4iyhcksxypelktcqkkw3ptg7ggxtqegw3p3mr6zc4")
|
||||
TokenErinFrank_InvalidSubjectSealed = mustGetBundle(TokenErinFrank_InvalidSubjectCID).Sealed
|
||||
TokenErinFrank_InvalidSubjectBundle = mustGetBundle(TokenErinFrank_InvalidSubjectCID)
|
||||
TokenErinFrank_InvalidSubject = mustGetBundle(TokenErinFrank_InvalidSubjectCID).Decoded
|
||||
)
|
||||
|
||||
var (
|
||||
TokenDanErin_InvalidExpiredCID = gocid.MustParse("bafyreifvnfb7zqocpdysedcvjkb4y7tqfuziuqjhbbdoay4zg33pwpbzqi")
|
||||
TokenDanErin_InvalidExpired = mustGetDelegation(TokenDanErin_InvalidExpiredCID)
|
||||
TokenCarolDan_InvalidExpiredCID = cid.MustParse("bafyreifyzm5jkx2sfu5awyndg3dn5zlg7sq5hssgfatafk62kiiilapnqe")
|
||||
TokenCarolDan_InvalidExpiredSealed = mustGetBundle(TokenCarolDan_InvalidExpiredCID).Sealed
|
||||
TokenCarolDan_InvalidExpiredBundle = mustGetBundle(TokenCarolDan_InvalidExpiredCID)
|
||||
TokenCarolDan_InvalidExpired = mustGetBundle(TokenCarolDan_InvalidExpiredCID).Decoded
|
||||
)
|
||||
|
||||
var (
|
||||
TokenErinFrank_InvalidExpiredCID = gocid.MustParse("bafyreicvydzt3obkqx7krmoi3zu4tlirlksibxfks5jc7vlvjxjamv2764")
|
||||
TokenErinFrank_InvalidExpired = mustGetDelegation(TokenErinFrank_InvalidExpiredCID)
|
||||
TokenDanErin_InvalidExpiredCID = cid.MustParse("bafyreihhnisabmkofuk3qaw37leijxqjaz5or6v2cufjxwzdkvuvv2dzbq")
|
||||
TokenDanErin_InvalidExpiredSealed = mustGetBundle(TokenDanErin_InvalidExpiredCID).Sealed
|
||||
TokenDanErin_InvalidExpiredBundle = mustGetBundle(TokenDanErin_InvalidExpiredCID)
|
||||
TokenDanErin_InvalidExpired = mustGetBundle(TokenDanErin_InvalidExpiredCID).Decoded
|
||||
)
|
||||
|
||||
var (
|
||||
TokenCarolDan_InvalidInactiveCID = gocid.MustParse("bafyreicea5y2nvlitvxijkupeavtg23i7ktjk3uejnaquguurzptiabk4u")
|
||||
TokenCarolDan_InvalidInactive = mustGetDelegation(TokenCarolDan_InvalidInactiveCID)
|
||||
TokenErinFrank_InvalidExpiredCID = cid.MustParse("bafyreigeokaziviwm5kzmkpwesj3gta5k7zrd62x4a746fnrnkhvatwbna")
|
||||
TokenErinFrank_InvalidExpiredSealed = mustGetBundle(TokenErinFrank_InvalidExpiredCID).Sealed
|
||||
TokenErinFrank_InvalidExpiredBundle = mustGetBundle(TokenErinFrank_InvalidExpiredCID)
|
||||
TokenErinFrank_InvalidExpired = mustGetBundle(TokenErinFrank_InvalidExpiredCID).Decoded
|
||||
)
|
||||
|
||||
var (
|
||||
TokenDanErin_InvalidInactiveCID = gocid.MustParse("bafyreifsgqzkmxj2vexuts3z766mwcjreiisjg2jykyzf7tbj5sclutpvq")
|
||||
TokenDanErin_InvalidInactive = mustGetDelegation(TokenDanErin_InvalidInactiveCID)
|
||||
TokenCarolDan_InvalidInactiveCID = cid.MustParse("bafyreicea5y2nvlitvxijkupeavtg23i7ktjk3uejnaquguurzptiabk4u")
|
||||
TokenCarolDan_InvalidInactiveSealed = mustGetBundle(TokenCarolDan_InvalidInactiveCID).Sealed
|
||||
TokenCarolDan_InvalidInactiveBundle = mustGetBundle(TokenCarolDan_InvalidInactiveCID)
|
||||
TokenCarolDan_InvalidInactive = mustGetBundle(TokenCarolDan_InvalidInactiveCID).Decoded
|
||||
)
|
||||
|
||||
var (
|
||||
TokenErinFrank_InvalidInactiveCID = gocid.MustParse("bafyreifbfegon24c6dndiqyktahzs65vhyasrygbw7nhsvojn6distsdre")
|
||||
TokenErinFrank_InvalidInactive = mustGetDelegation(TokenErinFrank_InvalidInactiveCID)
|
||||
TokenDanErin_InvalidInactiveCID = cid.MustParse("bafyreifsgqzkmxj2vexuts3z766mwcjreiisjg2jykyzf7tbj5sclutpvq")
|
||||
TokenDanErin_InvalidInactiveSealed = mustGetBundle(TokenDanErin_InvalidInactiveCID).Sealed
|
||||
TokenDanErin_InvalidInactiveBundle = mustGetBundle(TokenDanErin_InvalidInactiveCID)
|
||||
TokenDanErin_InvalidInactive = mustGetBundle(TokenDanErin_InvalidInactiveCID).Decoded
|
||||
)
|
||||
|
||||
var ProofAliceBob = []gocid.Cid{
|
||||
var (
|
||||
TokenErinFrank_InvalidInactiveCID = cid.MustParse("bafyreifbfegon24c6dndiqyktahzs65vhyasrygbw7nhsvojn6distsdre")
|
||||
TokenErinFrank_InvalidInactiveSealed = mustGetBundle(TokenErinFrank_InvalidInactiveCID).Sealed
|
||||
TokenErinFrank_InvalidInactiveBundle = mustGetBundle(TokenErinFrank_InvalidInactiveCID)
|
||||
TokenErinFrank_InvalidInactive = mustGetBundle(TokenErinFrank_InvalidInactiveCID).Decoded
|
||||
)
|
||||
|
||||
var (
|
||||
TokenCarolDan_ValidExamplePolicyCID = cid.MustParse("bafyreibtfrp2njnkjrcuhxd4ebaecmpcql5knek2h2j2fjzu2sij2tv6ei")
|
||||
TokenCarolDan_ValidExamplePolicySealed = mustGetBundle(TokenCarolDan_ValidExamplePolicyCID).Sealed
|
||||
TokenCarolDan_ValidExamplePolicyBundle = mustGetBundle(TokenCarolDan_ValidExamplePolicyCID)
|
||||
TokenCarolDan_ValidExamplePolicy = mustGetBundle(TokenCarolDan_ValidExamplePolicyCID).Decoded
|
||||
)
|
||||
|
||||
var (
|
||||
TokenDanErin_ValidExamplePolicyCID = cid.MustParse("bafyreidxfwbkzujpu7ivulkc7b6ff4cpbzrkeklmxqvyhhmkmym5b45e2e")
|
||||
TokenDanErin_ValidExamplePolicySealed = mustGetBundle(TokenDanErin_ValidExamplePolicyCID).Sealed
|
||||
TokenDanErin_ValidExamplePolicyBundle = mustGetBundle(TokenDanErin_ValidExamplePolicyCID)
|
||||
TokenDanErin_ValidExamplePolicy = mustGetBundle(TokenDanErin_ValidExamplePolicyCID).Decoded
|
||||
)
|
||||
|
||||
var (
|
||||
TokenErinFrank_ValidExamplePolicyCID = cid.MustParse("bafyreiatkvtvgakqcrdk6vgrv7tbq5rbeiqct52ep4plcftp2agffjyvp4")
|
||||
TokenErinFrank_ValidExamplePolicySealed = mustGetBundle(TokenErinFrank_ValidExamplePolicyCID).Sealed
|
||||
TokenErinFrank_ValidExamplePolicyBundle = mustGetBundle(TokenErinFrank_ValidExamplePolicyCID)
|
||||
TokenErinFrank_ValidExamplePolicy = mustGetBundle(TokenErinFrank_ValidExamplePolicyCID).Decoded
|
||||
)
|
||||
|
||||
var AllTokens = []*delegation.Token{
|
||||
TokenAliceBob,
|
||||
TokenBobCarol,
|
||||
TokenCarolDan,
|
||||
TokenDanErin,
|
||||
TokenErinFrank,
|
||||
TokenCarolDan_InvalidExpandedCommand,
|
||||
TokenDanErin_InvalidExpandedCommand,
|
||||
TokenErinFrank_InvalidExpandedCommand,
|
||||
TokenCarolDan_ValidAttenuatedCommand,
|
||||
TokenDanErin_ValidAttenuatedCommand,
|
||||
TokenErinFrank_ValidAttenuatedCommand,
|
||||
TokenCarolDan_InvalidSubject,
|
||||
TokenDanErin_InvalidSubject,
|
||||
TokenErinFrank_InvalidSubject,
|
||||
TokenCarolDan_InvalidExpired,
|
||||
TokenDanErin_InvalidExpired,
|
||||
TokenErinFrank_InvalidExpired,
|
||||
TokenCarolDan_InvalidInactive,
|
||||
TokenDanErin_InvalidInactive,
|
||||
TokenErinFrank_InvalidInactive,
|
||||
TokenCarolDan_ValidExamplePolicy,
|
||||
TokenDanErin_ValidExamplePolicy,
|
||||
TokenErinFrank_ValidExamplePolicy,
|
||||
}
|
||||
|
||||
var AllBundles = []*delegation.Bundle{
|
||||
TokenAliceBobBundle,
|
||||
TokenBobCarolBundle,
|
||||
TokenCarolDanBundle,
|
||||
TokenDanErinBundle,
|
||||
TokenErinFrankBundle,
|
||||
TokenCarolDan_InvalidExpandedCommandBundle,
|
||||
TokenDanErin_InvalidExpandedCommandBundle,
|
||||
TokenErinFrank_InvalidExpandedCommandBundle,
|
||||
TokenCarolDan_ValidAttenuatedCommandBundle,
|
||||
TokenDanErin_ValidAttenuatedCommandBundle,
|
||||
TokenErinFrank_ValidAttenuatedCommandBundle,
|
||||
TokenCarolDan_InvalidSubjectBundle,
|
||||
TokenDanErin_InvalidSubjectBundle,
|
||||
TokenErinFrank_InvalidSubjectBundle,
|
||||
TokenCarolDan_InvalidExpiredBundle,
|
||||
TokenDanErin_InvalidExpiredBundle,
|
||||
TokenErinFrank_InvalidExpiredBundle,
|
||||
TokenCarolDan_InvalidInactiveBundle,
|
||||
TokenDanErin_InvalidInactiveBundle,
|
||||
TokenErinFrank_InvalidInactiveBundle,
|
||||
TokenCarolDan_ValidExamplePolicyBundle,
|
||||
TokenDanErin_ValidExamplePolicyBundle,
|
||||
TokenErinFrank_ValidExamplePolicyBundle,
|
||||
}
|
||||
|
||||
var cidToName = map[cid.Cid]string{
|
||||
TokenAliceBobCID: "TokenAliceBob",
|
||||
TokenBobCarolCID: "TokenBobCarol",
|
||||
TokenCarolDanCID: "TokenCarolDan",
|
||||
TokenDanErinCID: "TokenDanErin",
|
||||
TokenErinFrankCID: "TokenErinFrank",
|
||||
TokenCarolDan_InvalidExpandedCommandCID: "TokenCarolDan_InvalidExpandedCommand",
|
||||
TokenDanErin_InvalidExpandedCommandCID: "TokenDanErin_InvalidExpandedCommand",
|
||||
TokenErinFrank_InvalidExpandedCommandCID: "TokenErinFrank_InvalidExpandedCommand",
|
||||
TokenCarolDan_ValidAttenuatedCommandCID: "TokenCarolDan_ValidAttenuatedCommand",
|
||||
TokenDanErin_ValidAttenuatedCommandCID: "TokenDanErin_ValidAttenuatedCommand",
|
||||
TokenErinFrank_ValidAttenuatedCommandCID: "TokenErinFrank_ValidAttenuatedCommand",
|
||||
TokenCarolDan_InvalidSubjectCID: "TokenCarolDan_InvalidSubject",
|
||||
TokenDanErin_InvalidSubjectCID: "TokenDanErin_InvalidSubject",
|
||||
TokenErinFrank_InvalidSubjectCID: "TokenErinFrank_InvalidSubject",
|
||||
TokenCarolDan_InvalidExpiredCID: "TokenCarolDan_InvalidExpired",
|
||||
TokenDanErin_InvalidExpiredCID: "TokenDanErin_InvalidExpired",
|
||||
TokenErinFrank_InvalidExpiredCID: "TokenErinFrank_InvalidExpired",
|
||||
TokenCarolDan_InvalidInactiveCID: "TokenCarolDan_InvalidInactive",
|
||||
TokenDanErin_InvalidInactiveCID: "TokenDanErin_InvalidInactive",
|
||||
TokenErinFrank_InvalidInactiveCID: "TokenErinFrank_InvalidInactive",
|
||||
TokenCarolDan_ValidExamplePolicyCID: "TokenCarolDan_ValidExamplePolicy",
|
||||
TokenDanErin_ValidExamplePolicyCID: "TokenDanErin_ValidExamplePolicy",
|
||||
TokenErinFrank_ValidExamplePolicyCID: "TokenErinFrank_ValidExamplePolicy",
|
||||
}
|
||||
|
||||
var ProofAliceBob = []cid.Cid{
|
||||
TokenAliceBobCID,
|
||||
}
|
||||
|
||||
var ProofAliceBobCarol = []gocid.Cid{
|
||||
var ProofAliceBobCarol = []cid.Cid{
|
||||
TokenBobCarolCID,
|
||||
TokenAliceBobCID,
|
||||
}
|
||||
|
||||
var ProofAliceBobCarolDan = []gocid.Cid{
|
||||
var ProofAliceBobCarolDan = []cid.Cid{
|
||||
TokenCarolDanCID,
|
||||
TokenBobCarolCID,
|
||||
TokenAliceBobCID,
|
||||
}
|
||||
|
||||
var ProofAliceBobCarolDanErin = []gocid.Cid{
|
||||
var ProofAliceBobCarolDanErin = []cid.Cid{
|
||||
TokenDanErinCID,
|
||||
TokenCarolDanCID,
|
||||
TokenBobCarolCID,
|
||||
TokenAliceBobCID,
|
||||
}
|
||||
|
||||
var ProofAliceBobCarolDanErinFrank = []gocid.Cid{
|
||||
var ProofAliceBobCarolDanErinFrank = []cid.Cid{
|
||||
TokenErinFrankCID,
|
||||
TokenDanErinCID,
|
||||
TokenCarolDanCID,
|
||||
@@ -134,20 +277,20 @@ var ProofAliceBobCarolDanErinFrank = []gocid.Cid{
|
||||
TokenAliceBobCID,
|
||||
}
|
||||
|
||||
var ProofAliceBobCarolDan_InvalidExpandedCommand = []gocid.Cid{
|
||||
var ProofAliceBobCarolDan_InvalidExpandedCommand = []cid.Cid{
|
||||
TokenCarolDan_InvalidExpandedCommandCID,
|
||||
TokenBobCarolCID,
|
||||
TokenAliceBobCID,
|
||||
}
|
||||
|
||||
var ProofAliceBobCarolDanErin_InvalidExpandedCommand = []gocid.Cid{
|
||||
var ProofAliceBobCarolDanErin_InvalidExpandedCommand = []cid.Cid{
|
||||
TokenDanErin_InvalidExpandedCommandCID,
|
||||
TokenCarolDan_InvalidExpandedCommandCID,
|
||||
TokenBobCarolCID,
|
||||
TokenAliceBobCID,
|
||||
}
|
||||
|
||||
var ProofAliceBobCarolDanErinFrank_InvalidExpandedCommand = []gocid.Cid{
|
||||
var ProofAliceBobCarolDanErinFrank_InvalidExpandedCommand = []cid.Cid{
|
||||
TokenErinFrank_InvalidExpandedCommandCID,
|
||||
TokenDanErin_InvalidExpandedCommandCID,
|
||||
TokenCarolDan_InvalidExpandedCommandCID,
|
||||
@@ -155,20 +298,20 @@ var ProofAliceBobCarolDanErinFrank_InvalidExpandedCommand = []gocid.Cid{
|
||||
TokenAliceBobCID,
|
||||
}
|
||||
|
||||
var ProofAliceBobCarolDan_ValidAttenuatedCommand = []gocid.Cid{
|
||||
var ProofAliceBobCarolDan_ValidAttenuatedCommand = []cid.Cid{
|
||||
TokenCarolDan_ValidAttenuatedCommandCID,
|
||||
TokenBobCarolCID,
|
||||
TokenAliceBobCID,
|
||||
}
|
||||
|
||||
var ProofAliceBobCarolDanErin_ValidAttenuatedCommand = []gocid.Cid{
|
||||
var ProofAliceBobCarolDanErin_ValidAttenuatedCommand = []cid.Cid{
|
||||
TokenDanErin_ValidAttenuatedCommandCID,
|
||||
TokenCarolDan_ValidAttenuatedCommandCID,
|
||||
TokenBobCarolCID,
|
||||
TokenAliceBobCID,
|
||||
}
|
||||
|
||||
var ProofAliceBobCarolDanErinFrank_ValidAttenuatedCommand = []gocid.Cid{
|
||||
var ProofAliceBobCarolDanErinFrank_ValidAttenuatedCommand = []cid.Cid{
|
||||
TokenErinFrank_ValidAttenuatedCommandCID,
|
||||
TokenDanErin_ValidAttenuatedCommandCID,
|
||||
TokenCarolDan_ValidAttenuatedCommandCID,
|
||||
@@ -176,20 +319,20 @@ var ProofAliceBobCarolDanErinFrank_ValidAttenuatedCommand = []gocid.Cid{
|
||||
TokenAliceBobCID,
|
||||
}
|
||||
|
||||
var ProofAliceBobCarolDan_InvalidSubject = []gocid.Cid{
|
||||
var ProofAliceBobCarolDan_InvalidSubject = []cid.Cid{
|
||||
TokenCarolDan_InvalidSubjectCID,
|
||||
TokenBobCarolCID,
|
||||
TokenAliceBobCID,
|
||||
}
|
||||
|
||||
var ProofAliceBobCarolDanErin_InvalidSubject = []gocid.Cid{
|
||||
var ProofAliceBobCarolDanErin_InvalidSubject = []cid.Cid{
|
||||
TokenDanErin_InvalidSubjectCID,
|
||||
TokenCarolDan_InvalidSubjectCID,
|
||||
TokenBobCarolCID,
|
||||
TokenAliceBobCID,
|
||||
}
|
||||
|
||||
var ProofAliceBobCarolDanErinFrank_InvalidSubject = []gocid.Cid{
|
||||
var ProofAliceBobCarolDanErinFrank_InvalidSubject = []cid.Cid{
|
||||
TokenErinFrank_InvalidSubjectCID,
|
||||
TokenDanErin_InvalidSubjectCID,
|
||||
TokenCarolDan_InvalidSubjectCID,
|
||||
@@ -197,20 +340,20 @@ var ProofAliceBobCarolDanErinFrank_InvalidSubject = []gocid.Cid{
|
||||
TokenAliceBobCID,
|
||||
}
|
||||
|
||||
var ProofAliceBobCarolDan_InvalidExpired = []gocid.Cid{
|
||||
var ProofAliceBobCarolDan_InvalidExpired = []cid.Cid{
|
||||
TokenCarolDan_InvalidExpiredCID,
|
||||
TokenBobCarolCID,
|
||||
TokenAliceBobCID,
|
||||
}
|
||||
|
||||
var ProofAliceBobCarolDanErin_InvalidExpired = []gocid.Cid{
|
||||
var ProofAliceBobCarolDanErin_InvalidExpired = []cid.Cid{
|
||||
TokenDanErin_InvalidExpiredCID,
|
||||
TokenCarolDan_InvalidExpiredCID,
|
||||
TokenBobCarolCID,
|
||||
TokenAliceBobCID,
|
||||
}
|
||||
|
||||
var ProofAliceBobCarolDanErinFrank_InvalidExpired = []gocid.Cid{
|
||||
var ProofAliceBobCarolDanErinFrank_InvalidExpired = []cid.Cid{
|
||||
TokenErinFrank_InvalidExpiredCID,
|
||||
TokenDanErin_InvalidExpiredCID,
|
||||
TokenCarolDan_InvalidExpiredCID,
|
||||
@@ -218,23 +361,44 @@ var ProofAliceBobCarolDanErinFrank_InvalidExpired = []gocid.Cid{
|
||||
TokenAliceBobCID,
|
||||
}
|
||||
|
||||
var ProofAliceBobCarolDan_InvalidInactive = []gocid.Cid{
|
||||
var ProofAliceBobCarolDan_InvalidInactive = []cid.Cid{
|
||||
TokenCarolDan_InvalidInactiveCID,
|
||||
TokenBobCarolCID,
|
||||
TokenAliceBobCID,
|
||||
}
|
||||
|
||||
var ProofAliceBobCarolDanErin_InvalidInactive = []gocid.Cid{
|
||||
var ProofAliceBobCarolDanErin_InvalidInactive = []cid.Cid{
|
||||
TokenDanErin_InvalidInactiveCID,
|
||||
TokenCarolDan_InvalidInactiveCID,
|
||||
TokenBobCarolCID,
|
||||
TokenAliceBobCID,
|
||||
}
|
||||
|
||||
var ProofAliceBobCarolDanErinFrank_InvalidInactive = []gocid.Cid{
|
||||
var ProofAliceBobCarolDanErinFrank_InvalidInactive = []cid.Cid{
|
||||
TokenErinFrank_InvalidInactiveCID,
|
||||
TokenDanErin_InvalidInactiveCID,
|
||||
TokenCarolDan_InvalidInactiveCID,
|
||||
TokenBobCarolCID,
|
||||
TokenAliceBobCID,
|
||||
}
|
||||
|
||||
var ProofAliceBobCarolDan_ValidExamplePolicy = []cid.Cid{
|
||||
TokenCarolDan_ValidExamplePolicyCID,
|
||||
TokenBobCarolCID,
|
||||
TokenAliceBobCID,
|
||||
}
|
||||
|
||||
var ProofAliceBobCarolDanErin_ValidExamplePolicy = []cid.Cid{
|
||||
TokenDanErin_ValidExamplePolicyCID,
|
||||
TokenCarolDan_ValidExamplePolicyCID,
|
||||
TokenBobCarolCID,
|
||||
TokenAliceBobCID,
|
||||
}
|
||||
|
||||
var ProofAliceBobCarolDanErinFrank_ValidExamplePolicy = []cid.Cid{
|
||||
TokenErinFrank_ValidExamplePolicyCID,
|
||||
TokenDanErin_ValidExamplePolicyCID,
|
||||
TokenCarolDan_ValidExamplePolicyCID,
|
||||
TokenBobCarolCID,
|
||||
TokenAliceBobCID,
|
||||
}
|
||||
|
||||
@@ -41,8 +41,7 @@ func ExampleNew() {
|
||||
)),
|
||||
)
|
||||
|
||||
tkn, err := delegation.New(didtest.PersonaBob.DID(), didtest.PersonaCarol.DID(), cmd, pol,
|
||||
delegation.WithSubject(didtest.PersonaAlice.DID()),
|
||||
tkn, err := delegation.New(didtest.PersonaBob.DID(), didtest.PersonaCarol.DID(), cmd, pol, didtest.PersonaAlice.DID(),
|
||||
delegation.WithExpirationIn(time.Hour),
|
||||
delegation.WithNotBeforeIn(time.Minute),
|
||||
delegation.WithMeta("foo", "bar"),
|
||||
@@ -273,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): {}
|
||||
|
||||
@@ -3,8 +3,6 @@ package delegation
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/did"
|
||||
)
|
||||
|
||||
// Option is a type that allows optional fields to be set during the
|
||||
@@ -45,7 +43,8 @@ func WithMeta(key string, val any) Option {
|
||||
}
|
||||
|
||||
// WithEncryptedMetaString adds a key/value pair in the "meta" field.
|
||||
// The string value is encrypted with the given aesKey.
|
||||
// The string value is encrypted with the given key.
|
||||
// The ciphertext will be 40 bytes larger than the plaintext due to encryption overhead.
|
||||
func WithEncryptedMetaString(key, val string, encryptionKey []byte) Option {
|
||||
return func(t *Token) error {
|
||||
return t.meta.AddEncrypted(key, val, encryptionKey)
|
||||
@@ -53,7 +52,8 @@ func WithEncryptedMetaString(key, val string, encryptionKey []byte) Option {
|
||||
}
|
||||
|
||||
// WithEncryptedMetaBytes adds a key/value pair in the "meta" field.
|
||||
// The []byte value is encrypted with the given aesKey.
|
||||
// The []byte value is encrypted with the given key.
|
||||
// The ciphertext will be 40 bytes larger than the plaintext due to encryption overhead.
|
||||
func WithEncryptedMetaBytes(key string, val, encryptionKey []byte) Option {
|
||||
return func(t *Token) error {
|
||||
return t.meta.AddEncrypted(key, val, encryptionKey)
|
||||
@@ -83,20 +83,6 @@ func WithNotBeforeIn(nbf time.Duration) Option {
|
||||
}
|
||||
}
|
||||
|
||||
// WithSubject sets the Tokens's optional "subject" field to the value of
|
||||
// provided did.DID.
|
||||
//
|
||||
// This Option should only be used with the New constructor - since
|
||||
// Subject is a required parameter when creating a Token via the Root
|
||||
// constructor, any value provided via this Option will be silently
|
||||
// overwritten.
|
||||
func WithSubject(sub did.DID) Option {
|
||||
return func(t *Token) error {
|
||||
t.subject = sub
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithNonce sets the Token's nonce with the given value.
|
||||
// If this option is not used, a random 12-byte nonce is generated for this required field.
|
||||
func WithNonce(nonce []byte) Option {
|
||||
|
||||
@@ -33,9 +33,12 @@ func TestSchemaRoundTrip(t *testing.T) {
|
||||
p1, err := delegation.FromDagJson(delegationJson)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, newCID, err := p1.ToSealed(privKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
cborBytes, id, err := p1.ToSealed(privKey)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, newCID, envelope.CIDToBase58BTC(id))
|
||||
assert.Equal(t, envelope.CIDToBase58BTC(newCID), envelope.CIDToBase58BTC(id))
|
||||
|
||||
p2, c2, err := delegation.FromSealed(cborBytes)
|
||||
require.NoError(t, err)
|
||||
@@ -58,10 +61,13 @@ func TestSchemaRoundTrip(t *testing.T) {
|
||||
p1, err := delegation.FromDagJsonReader(buf)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, newCID, err := p1.ToSealed(privKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
cborBytes := &bytes.Buffer{}
|
||||
id, err := p1.ToSealedWriter(cborBytes, privKey)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, newCID, envelope.CIDToBase58BTC(id))
|
||||
assert.Equal(t, envelope.CIDToBase58BTC(newCID), envelope.CIDToBase58BTC(id))
|
||||
|
||||
// buf = bytes.NewBuffer(cborBytes.Bytes())
|
||||
p2, c2, err := delegation.FromSealedReader(cborBytes)
|
||||
|
||||
2
token/delegation/testdata/new.dagjson
vendored
2
token/delegation/testdata/new.dagjson
vendored
@@ -1 +1 @@
|
||||
[{"/":{"bytes":"BBabgnWqd+cjwG1td0w9BudNocmUwoR89RMZTqZHk3osCXEI/bOkko0zTvlusaE4EMBBeSzZDKzjvunLBfdiBg"}},{"h":{"/":{"bytes":"NO0BcQ"}},"ucan/dlg@1.0.0-rc.1":{"aud":"did:key:z6MkvJPmEZZYbgiw1ouT1oouTsTFBHJSts9ophVsNgcRmYxU","cmd":"/foo/bar","exp":7258118400,"iss":"did:key:z6Mkuukk2skDXLQn7NK3Eh9jMndYfvDBxxktgpidJAqb7M3p","meta":{"bar":"barr","foo":"fooo"},"nonce":{"/":{"bytes":"NnJvRGhHaTBraU5yaVFBejdKM2QrYk9lb0kvdGo4RU5pa21RTmJ0am5EMA"}},"pol":[["==",".status","draft"],["all",".reviewer",["like",".email","*@example.com"]],["any",".tags",["or",[["==",".","news"],["==",".","press"]]]]],"sub":"did:key:z6Mkuukk2skDXLQn7NK3Eh9jMndYfvDBxxktgpidJAqb7M3p"}}]
|
||||
[{"/":{"bytes":"YJsl8EMLnXSFE/nKKjMxz9bHHo+Y7QeLEzukEzW1TB+m53TTiY1aOt+qUO8JaTcOKsOHt/a4Vn+YiOd5CkLdAQ"}},{"h":{"/":{"bytes":"NO0BcQ"}},"ucan/dlg@1.0.0-rc.1":{"aud":"did:key:z6MkvJPmEZZYbgiw1ouT1oouTsTFBHJSts9ophVsNgcRmYxU","cmd":"/foo/bar","exp":7258118400,"iss":"did:key:z6Mkuukk2skDXLQn7NK3Eh9jMndYfvDBxxktgpidJAqb7M3p","meta":{"bar":"barr","foo":"fooo"},"nonce":{"/":{"bytes":"NnJvRGhHaTBraU5yaVFBejdKM2QrYk9lb0kvdGo4RU5pa21RTmJ0am5EMA"}},"pol":[["==",".status","draft"],["all",".reviewer",["like",".email","*@example.com"]],["any",".tags",["or",[["==",".","news"],["==",".","press"]]]]],"sub":"did:key:z6Mkgupchh5HwuHahS7YsyE8bLua1Mr8p2iKNRhyvSvRAs9n"}}]
|
||||
1
token/delegation/testdata/powerline.dagjson
vendored
Normal file
1
token/delegation/testdata/powerline.dagjson
vendored
Normal file
@@ -0,0 +1 @@
|
||||
[{"/":{"bytes":"i3YkPDvNSU4V8XYEluZhLH0b+NDcW/6+PtPSUHC17cmXXqgelG0K4EzWQQkS9UsYCHfkZSCn9NjGSXYMMFhaAQ"}},{"h":{"/":{"bytes":"NO0BcQ"}},"ucan/dlg@1.0.0-rc.1":{"aud":"did:key:z6MkvJPmEZZYbgiw1ouT1oouTsTFBHJSts9ophVsNgcRmYxU","cmd":"/foo/bar","exp":7258118400,"iss":"did:key:z6Mkuukk2skDXLQn7NK3Eh9jMndYfvDBxxktgpidJAqb7M3p","meta":{"bar":"barr","foo":"fooo"},"nonce":{"/":{"bytes":"NnJvRGhHaTBraU5yaVFBejdKM2QrYk9lb0kvdGo4RU5pa21RTmJ0am5EMA"}},"pol":[["==",".status","draft"],["all",".reviewer",["like",".email","*@example.com"]],["any",".tags",["or",[["==",".","news"],["==",".","press"]]]]]}}]
|
||||
@@ -15,3 +15,10 @@ type Loader interface {
|
||||
// If not found, ErrDelegationNotFound is returned.
|
||||
GetDelegation(cid cid.Cid) (*Token, error)
|
||||
}
|
||||
|
||||
// Bundle carries together a decoded delegation with its Cid and raw signed data.
|
||||
type Bundle struct {
|
||||
Cid cid.Cid
|
||||
Decoded *Token
|
||||
Sealed []byte
|
||||
}
|
||||
@@ -41,18 +41,18 @@ const (
|
||||
var schemaBytes []byte
|
||||
|
||||
var (
|
||||
once sync.Once
|
||||
ts *schema.TypeSystem
|
||||
err error
|
||||
once sync.Once
|
||||
ts *schema.TypeSystem
|
||||
errSchema error
|
||||
)
|
||||
|
||||
func mustLoadSchema() *schema.TypeSystem {
|
||||
once.Do(func() {
|
||||
ts, err = ipld.LoadSchemaBytes(schemaBytes)
|
||||
ts, errSchema = ipld.LoadSchemaBytes(schemaBytes)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("failed to load IPLD schema: %s", err))
|
||||
if errSchema != nil {
|
||||
panic(fmt.Errorf("failed to load IPLD schema: %s", errSchema))
|
||||
}
|
||||
|
||||
return ts
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package parse
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/did"
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy/limits"
|
||||
)
|
||||
|
||||
func OptionalDID(s *string) (did.DID, error) {
|
||||
@@ -13,10 +15,15 @@ func OptionalDID(s *string) (did.DID, error) {
|
||||
return did.Parse(*s)
|
||||
}
|
||||
|
||||
func OptionalTimestamp(sec *int64) *time.Time {
|
||||
func OptionalTimestamp(sec *int64) (*time.Time, error) {
|
||||
if sec == nil {
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if *sec > limits.MaxInt53 || *sec < limits.MinInt53 {
|
||||
return nil, fmt.Errorf("timestamp value %d exceeds safe integer bounds", *sec)
|
||||
}
|
||||
|
||||
t := time.Unix(*sec, 0)
|
||||
return &t
|
||||
return &t, nil
|
||||
}
|
||||
|
||||
64
token/internal/parse/parse_test.go
Normal file
64
token/internal/parse/parse_test.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package parse
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy/limits"
|
||||
)
|
||||
|
||||
func TestOptionalTimestamp(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input *int64
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "nil timestamp",
|
||||
input: nil,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid timestamp",
|
||||
input: int64Ptr(1625097600),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "max safe integer",
|
||||
input: int64Ptr(limits.MaxInt53),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "exceeds max safe integer",
|
||||
input: int64Ptr(limits.MaxInt53 + 1),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "below min safe integer",
|
||||
input: int64Ptr(limits.MinInt53 - 1),
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := OptionalTimestamp(tt.input)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "exceeds safe integer bounds")
|
||||
require.Nil(t, result)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
if tt.input == nil {
|
||||
require.Nil(t, result)
|
||||
} else {
|
||||
require.NotNil(t, result)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func int64Ptr(i int64) *int64 {
|
||||
return &i
|
||||
}
|
||||
@@ -35,3 +35,7 @@ var (
|
||||
// next delegation or invocation's command.
|
||||
ErrCommandNotCovered = errors.New("allowed command doesn't cover the next delegation or invocation")
|
||||
)
|
||||
|
||||
// ErrPolicyNotSatisfied is returned when the provided Arguments don't
|
||||
// satisfy one or more of the aggregated Policy Statements
|
||||
var ErrPolicyNotSatisfied = errors.New("the following UCAN policy is not satisfied")
|
||||
|
||||
@@ -27,7 +27,7 @@ func ExampleNew() {
|
||||
return
|
||||
}
|
||||
|
||||
inv, err := invocation.New(iss, sub, cmd, prf,
|
||||
inv, err := invocation.New(iss, cmd, sub, prf,
|
||||
invocation.WithArgument("uri", args["uri"]),
|
||||
invocation.WithArgument("headers", args["headers"]),
|
||||
invocation.WithArgument("payload", args["payload"]),
|
||||
|
||||
@@ -8,8 +8,10 @@
|
||||
package invocation
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ipfs/go-cid"
|
||||
@@ -67,7 +69,9 @@ type Token struct {
|
||||
//
|
||||
// With the exception of the WithMeta option, all others will overwrite
|
||||
// the previous contents of their target field.
|
||||
func New(iss, sub did.DID, cmd command.Command, prf []cid.Cid, opts ...Option) (*Token, error) {
|
||||
//
|
||||
// You can read it as "(Issuer - I) executes (command) on (subject)".
|
||||
func New(iss did.DID, cmd command.Command, sub did.DID, prf []cid.Cid, opts ...Option) (*Token, error) {
|
||||
iat := time.Now()
|
||||
|
||||
tkn := Token{
|
||||
@@ -164,6 +168,8 @@ func (t *Token) Arguments() args.ReadOnly {
|
||||
|
||||
// Proof() returns the ordered list of cid.Cid which reference the
|
||||
// delegation Tokens that authorize this invocation.
|
||||
// Ordering is from the leaf Delegation (with aud matching the invocation's iss)
|
||||
// to the root delegation.
|
||||
func (t *Token) Proof() []cid.Cid {
|
||||
return t.proof
|
||||
}
|
||||
@@ -210,6 +216,24 @@ func (t *Token) IsValidAt(ti time.Time) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (t *Token) String() string {
|
||||
var res strings.Builder
|
||||
|
||||
res.WriteString(fmt.Sprintf("Issuer: %s\n", t.Issuer()))
|
||||
res.WriteString(fmt.Sprintf("Audience: %s\n", t.Audience()))
|
||||
res.WriteString(fmt.Sprintf("Subject: %v\n", t.Subject()))
|
||||
res.WriteString(fmt.Sprintf("Command: %s\n", t.Command()))
|
||||
res.WriteString(fmt.Sprintf("Args: %s\n", t.Arguments()))
|
||||
res.WriteString(fmt.Sprintf("Proof: %v\n", t.Proof()))
|
||||
res.WriteString(fmt.Sprintf("Nonce: %s\n", base64.StdEncoding.EncodeToString(t.Nonce())))
|
||||
res.WriteString(fmt.Sprintf("Meta: %s\n", t.Meta()))
|
||||
res.WriteString(fmt.Sprintf("Expiration: %v\n", t.Expiration()))
|
||||
res.WriteString(fmt.Sprintf("Invoked At: %v\n", t.InvokedAt()))
|
||||
res.WriteString(fmt.Sprintf("Cause: %v", t.Cause()))
|
||||
|
||||
return res.String()
|
||||
}
|
||||
|
||||
func (t *Token) validate() error {
|
||||
var errs error
|
||||
|
||||
@@ -270,11 +294,26 @@ func tokenFromModel(m tokenPayloadModel) (*Token, error) {
|
||||
tkn.nonce = m.Nonce
|
||||
|
||||
tkn.arguments = m.Args
|
||||
tkn.proof = m.Prf
|
||||
tkn.meta = m.Meta
|
||||
if err := tkn.arguments.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("invalid arguments: %w", err)
|
||||
}
|
||||
|
||||
tkn.expiration = parse.OptionalTimestamp(m.Exp)
|
||||
tkn.invokedAt = parse.OptionalTimestamp(m.Iat)
|
||||
tkn.proof = m.Prf
|
||||
|
||||
tkn.meta = m.Meta
|
||||
if tkn.meta == nil {
|
||||
tkn.meta = meta.NewMeta()
|
||||
}
|
||||
|
||||
tkn.expiration, err = parse.OptionalTimestamp(m.Exp)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse expiration: %w", err)
|
||||
}
|
||||
|
||||
tkn.invokedAt, err = parse.OptionalTimestamp(m.Iat)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse invokedAt: %w", err)
|
||||
}
|
||||
|
||||
tkn.cause = m.Cause
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/ucan-wg/go-ucan/did/didtest"
|
||||
"github.com/ucan-wg/go-ucan/pkg/args"
|
||||
"github.com/ucan-wg/go-ucan/pkg/command"
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy/policytest"
|
||||
"github.com/ucan-wg/go-ucan/token/delegation/delegationtest"
|
||||
"github.com/ucan-wg/go-ucan/token/invocation"
|
||||
)
|
||||
@@ -48,6 +49,18 @@ func TestToken_ExecutionAllowed(t *testing.T) {
|
||||
testPasses(t, didtest.PersonaFrank, delegationtest.AttenuatedCommand, emptyArguments, delegationtest.ProofAliceBobCarolDanErinFrank)
|
||||
})
|
||||
|
||||
t.Run("passes - arguments satisfy empty policy", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testPasses(t, didtest.PersonaFrank, delegationtest.NominalCommand, policytest.SpecValidArguments, delegationtest.ProofAliceBobCarolDanErinFrank)
|
||||
})
|
||||
|
||||
t.Run("passes - arguments satify example policy", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testPasses(t, didtest.PersonaFrank, delegationtest.NominalCommand, policytest.SpecValidArguments, delegationtest.ProofAliceBobCarolDanErinFrank_ValidExamplePolicy)
|
||||
})
|
||||
|
||||
t.Run("fails - no proof", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -115,14 +128,21 @@ func TestToken_ExecutionAllowed(t *testing.T) {
|
||||
|
||||
testFails(t, invocation.ErrWrongSub, didtest.PersonaFrank, delegationtest.ExpandedCommand, emptyArguments, delegationtest.ProofAliceBobCarolDanErinFrank_InvalidSubject)
|
||||
})
|
||||
|
||||
t.Run("passes - arguments satisfy example policy", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testFails(t, invocation.ErrPolicyNotSatisfied, didtest.PersonaFrank, delegationtest.NominalCommand, policytest.SpecInvalidArguments, delegationtest.ProofAliceBobCarolDanErinFrank_ValidExamplePolicy)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func test(t *testing.T, persona didtest.Persona, cmd command.Command, args *args.Args, prf []cid.Cid, opts ...invocation.Option) error {
|
||||
t.Helper()
|
||||
|
||||
// TODO: use the args and add minimal test to check that they are verified against the policy
|
||||
opts = append(opts, invocation.WithArguments(args))
|
||||
|
||||
tkn, err := invocation.New(persona.DID(), didtest.PersonaAlice.DID(), cmd, prf, opts...)
|
||||
tkn, err := invocation.New(persona.DID(), cmd, didtest.PersonaAlice.DID(), prf, opts...)
|
||||
require.NoError(t, err)
|
||||
|
||||
return tkn.ExecutionAllowed(delegationtest.GetDelegationLoader())
|
||||
|
||||
@@ -16,7 +16,7 @@ func TestSealUnsealRoundtrip(t *testing.T) {
|
||||
privKey, iss, sub, cmd, args, prf, meta, err := setupExampleNew()
|
||||
require.NoError(t, err)
|
||||
|
||||
tkn1, err := invocation.New(iss, sub, cmd, prf,
|
||||
tkn1, err := invocation.New(iss, cmd, sub, prf,
|
||||
invocation.WithArgument("uri", args["uri"]),
|
||||
invocation.WithArgument("headers", args["headers"]),
|
||||
invocation.WithArgument("payload", args["payload"]),
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"github.com/ipfs/go-cid"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/did"
|
||||
"github.com/ucan-wg/go-ucan/pkg/args"
|
||||
)
|
||||
|
||||
// Option is a type that allows optional fields to be set during the
|
||||
@@ -19,13 +20,33 @@ func WithArgument(key string, val any) Option {
|
||||
}
|
||||
}
|
||||
|
||||
// WithArguments merges the provided arguments into the Token's existing
|
||||
// arguments.
|
||||
//
|
||||
// If duplicate keys are encountered, the new value is silently dropped
|
||||
// without causing an error. Since duplicate keys can only be encountered
|
||||
// due to previous calls to WithArgument or WithArguments, calling only
|
||||
// this function to set the Token's arguments is equivalent to assigning
|
||||
// the arguments to the Token.
|
||||
func WithArguments(args *args.Args) Option {
|
||||
return func(t *Token) error {
|
||||
t.arguments.Include(args)
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithAudience sets the Token's audience to the provided did.DID.
|
||||
//
|
||||
// This can be used if the resource on which the token operates on is different
|
||||
// from the subject. In that situation, the subject is akin to the "service" and
|
||||
// the audience is akin to the resource.
|
||||
//
|
||||
// If the provided did.DID is the same as the Token's subject, the
|
||||
// audience is not set.
|
||||
func WithAudience(aud did.DID) Option {
|
||||
return func(t *Token) error {
|
||||
if t.subject.String() != aud.String() {
|
||||
if t.subject != aud {
|
||||
t.audience = aud
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ import (
|
||||
// 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 Audience field.
|
||||
// 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.
|
||||
//
|
||||
// 5. If steps 1-4 pass:
|
||||
@@ -58,18 +58,18 @@ func (t *Token) verifyProofs(delegations []*delegation.Token) error {
|
||||
|
||||
cmd := t.command
|
||||
iss := t.issuer
|
||||
aud := t.audience
|
||||
if !aud.Defined() {
|
||||
aud = t.subject
|
||||
sub := t.subject
|
||||
if t.audience.Defined() {
|
||||
sub = t.audience
|
||||
}
|
||||
|
||||
// control from the invocation to the root
|
||||
for i, dlgCid := range t.proof {
|
||||
dlg := delegations[i]
|
||||
|
||||
// The Subject of each delegation must equal the invocation's Audience field. - 4f
|
||||
if dlg.Subject() != aud {
|
||||
return fmt.Errorf("%w: delegation %s, expected %s, got %s", ErrWrongSub, dlgCid, aud, dlg.Subject())
|
||||
// The Subject of each delegation must equal the invocation's Subject (or Audience if defined). - 4f
|
||||
if dlg.Subject() != sub {
|
||||
return fmt.Errorf("%w: delegation %s, expected %s, got %s", ErrWrongSub, dlgCid, sub, dlg.Subject())
|
||||
}
|
||||
|
||||
// The first proof must be issued to the Invoker (audience DID). - 4c
|
||||
@@ -134,7 +134,7 @@ func (t *Token) verifyArgs(delegations []*delegation.Token, arguments *args.Args
|
||||
|
||||
ok, statement := policies.Match(argsIpld)
|
||||
if !ok {
|
||||
return fmt.Errorf("the following UCAN policy is not satisfied: %v", statement.String())
|
||||
return fmt.Errorf("%w: %v", ErrPolicyNotSatisfied, statement.String())
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user