Compare commits
64 Commits
v1
...
proof-chec
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb97653529 | ||
|
|
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
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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]
|
||||
}
|
||||
|
||||
1
go.mod
1
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
|
||||
|
||||
2
go.sum
2
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=
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"iter"
|
||||
"strings"
|
||||
|
||||
"github.com/ipfs/go-cid"
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
@@ -18,6 +20,7 @@ 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
|
||||
@@ -60,44 +63,45 @@ func (ctn Reader) GetAllDelegations() iter.Seq2[cid.Cid, *delegation.Token] {
|
||||
}
|
||||
}
|
||||
|
||||
// GetInvocation returns the first found invocation.Token.
|
||||
// GetInvocation returns a single invocation.Token.
|
||||
// If none are found, ErrNotFound is returned.
|
||||
// If more than one invocation exist, ErrMultipleInvocations is returned.
|
||||
func (ctn Reader) GetInvocation() (*invocation.Token, error) {
|
||||
var res *invocation.Token
|
||||
for _, t := range ctn {
|
||||
if inv, ok := t.(*invocation.Token); ok {
|
||||
return inv, nil
|
||||
if res != nil {
|
||||
return nil, ErrMultipleInvocations
|
||||
}
|
||||
res = inv
|
||||
}
|
||||
}
|
||||
return nil, ErrNotFound
|
||||
if res == nil {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func FromCar(r io.Reader) (Reader, error) {
|
||||
_, it, err := readCar(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
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ctn, nil
|
||||
}
|
||||
|
||||
func FromCarBase64(r io.Reader) (Reader, error) {
|
||||
return FromCar(base64.NewDecoder(base64.StdEncoding, r))
|
||||
// FromCbor decodes a DAG-CBOR encoded container.
|
||||
func FromCbor(data []byte) (Reader, error) {
|
||||
return FromCborReader(bytes.NewReader(data))
|
||||
}
|
||||
|
||||
func FromCbor(r io.Reader) (Reader, error) {
|
||||
// FromCborReader is the same as FromCbor, but with an io.Reader.
|
||||
func FromCborReader(r io.Reader) (Reader, error) {
|
||||
n, err := ipld.DecodeStreaming(r, dagcbor.Decode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -147,8 +151,52 @@ func FromCbor(r io.Reader) (Reader, error) {
|
||||
return ctn, nil
|
||||
}
|
||||
|
||||
func FromCborBase64(r io.Reader) (Reader, error) {
|
||||
return FromCbor(base64.NewDecoder(base64.StdEncoding, r))
|
||||
// FromCborBase64 decodes a base64 DAG-CBOR encoded container.
|
||||
func FromCborBase64(data string) (Reader, error) {
|
||||
return FromCborBase64Reader(strings.NewReader(data))
|
||||
}
|
||||
|
||||
// FromCborBase64Reader is the same as FromCborBase64, but with an io.Reader.
|
||||
func FromCborBase64Reader(r io.Reader) (Reader, error) {
|
||||
return FromCborReader(base64.NewDecoder(base64.StdEncoding, r))
|
||||
}
|
||||
|
||||
// FromCar decodes a CAR file encoded container.
|
||||
func FromCar(data []byte) (Reader, error) {
|
||||
return FromCarReader(bytes.NewReader(data))
|
||||
}
|
||||
|
||||
// FromCarReader is the same as FromCar, but with an io.Reader.
|
||||
func FromCarReader(r io.Reader) (Reader, error) {
|
||||
_, it, err := readCar(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// FromCarBase64 decodes a base64 CAR file encoded container.
|
||||
func FromCarBase64(data string) (Reader, error) {
|
||||
return FromCarReader(strings.NewReader(data))
|
||||
}
|
||||
|
||||
// FromCarBase64Reader is the same as FromCarBase64, but with an io.Reader.
|
||||
func FromCarBase64Reader(r io.Reader) (Reader, error) {
|
||||
return FromCarReader(base64.NewDecoder(base64.StdEncoding, r))
|
||||
}
|
||||
|
||||
func (ctn Reader) addToken(data []byte) error {
|
||||
|
||||
@@ -26,10 +26,10 @@ func TestContainerRoundTrip(t *testing.T) {
|
||||
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},
|
||||
{"car", Writer.ToCarWriter, FromCarReader},
|
||||
{"carBase64", Writer.ToCarBase64Writer, FromCarBase64Reader},
|
||||
{"cbor", Writer.ToCborWriter, FromCborReader},
|
||||
{"cborBase64", Writer.ToCborBase64Writer, FromCborBase64Reader},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
tokens := make(map[cid.Cid]*delegation.Token)
|
||||
@@ -98,10 +98,10 @@ 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},
|
||||
{"car", Writer.ToCarWriter, FromCarReader},
|
||||
{"carBase64", Writer.ToCarBase64Writer, FromCarBase64Reader},
|
||||
{"cbor", Writer.ToCborWriter, FromCborReader},
|
||||
{"cborBase64", Writer.ToCborBase64Writer, FromCborBase64Reader},
|
||||
} {
|
||||
writer := NewWriter()
|
||||
|
||||
@@ -160,13 +160,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)
|
||||
}
|
||||
@@ -185,18 +184,17 @@ func FuzzContainerRead(f *testing.F) {
|
||||
_, c, data := randToken()
|
||||
writer.AddSealed(c, data)
|
||||
}
|
||||
buf := bytes.NewBuffer(nil)
|
||||
err := writer.ToCbor(buf)
|
||||
data, err := writer.ToCbor()
|
||||
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))
|
||||
_, _ = FromCbor(data)
|
||||
|
||||
if time.Since(start) > 100*time.Millisecond {
|
||||
panic("too long")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
|
||||
@@ -12,10 +13,6 @@ import (
|
||||
"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
|
||||
|
||||
@@ -28,27 +25,24 @@ func (ctn Writer) AddSealed(cid cid.Cid, data []byte) {
|
||||
ctn[cid] = data
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
})
|
||||
const currentContainerVersion = "ctn-v1"
|
||||
|
||||
// ToCbor encode the container into a DAG-CBOR binary format.
|
||||
func (ctn Writer) ToCbor() ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
err := ctn.ToCborWriter(&buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func (ctn Writer) ToCarBase64(w io.Writer) error {
|
||||
w2 := base64.NewEncoder(base64.StdEncoding, w)
|
||||
defer w2.Close()
|
||||
return ctn.ToCar(w2)
|
||||
}
|
||||
|
||||
func (ctn Writer) ToCbor(w io.Writer) error {
|
||||
// ToCborWriter is the same as ToCbor, but with an io.Writer.
|
||||
func (ctn Writer) ToCborWriter(w io.Writer) error {
|
||||
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))
|
||||
for _, data := range ctn {
|
||||
qp.ListEntry(la, qp.Bytes(data))
|
||||
}
|
||||
}))
|
||||
})
|
||||
@@ -58,8 +52,57 @@ func (ctn Writer) ToCbor(w io.Writer) error {
|
||||
return ipld.EncodeStreaming(w, node, dagcbor.Encode)
|
||||
}
|
||||
|
||||
func (ctn Writer) ToCborBase64(w io.Writer) error {
|
||||
// ToCborBase64 encode the container into a base64 encoded DAG-CBOR binary format.
|
||||
func (ctn Writer) ToCborBase64() (string, error) {
|
||||
var buf bytes.Buffer
|
||||
err := ctn.ToCborBase64Writer(&buf)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
// ToCborBase64Writer is the same as ToCborBase64, but with an io.Writer.
|
||||
func (ctn Writer) ToCborBase64Writer(w io.Writer) error {
|
||||
w2 := base64.NewEncoder(base64.StdEncoding, w)
|
||||
defer w2.Close()
|
||||
return ctn.ToCbor(w2)
|
||||
return ctn.ToCborWriter(w2)
|
||||
}
|
||||
|
||||
// ToCar encode the container into a CAR file.
|
||||
func (ctn Writer) ToCar() ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
err := ctn.ToCarWriter(&buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// ToCarWriter is the same as ToCar, but with an io.Writer.
|
||||
func (ctn Writer) ToCarWriter(w io.Writer) error {
|
||||
return writeCar(w, nil, func(yield func(carBlock, error) bool) {
|
||||
for c, data := range ctn {
|
||||
if !yield(carBlock{c: c, data: data}, nil) {
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ToCarBase64 encode the container into a base64 encoded CAR file.
|
||||
func (ctn Writer) ToCarBase64() (string, error) {
|
||||
var buf bytes.Buffer
|
||||
err := ctn.ToCarBase64Writer(&buf)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
// ToCarBase64Writer is the same as ToCarBase64, but with an io.Writer.
|
||||
func (ctn Writer) ToCarBase64Writer(w io.Writer) error {
|
||||
w2 := base64.NewEncoder(base64.StdEncoding, w)
|
||||
defer w2.Close()
|
||||
return ctn.ToCarWriter(w2)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
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"
|
||||
@@ -249,10 +250,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 +278,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"
|
||||
@@ -904,55 +901,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)
|
||||
})
|
||||
}
|
||||
|
||||
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{}
|
||||
|
||||
// ExampleValidationPolicy provides a instantiated SpecPolicy 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 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 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[:]})
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -2,6 +2,7 @@ package selector
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -356,3 +357,57 @@ func FuzzParseAndSelect(f *testing.F) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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,51 @@ func (t *Token) IsValidAt(ti time.Time) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// Covers indicate if this token has the power to allow the given sub-delegation.
|
||||
// This function only verifies the principals alignment
|
||||
func (t *Token) Covers(subDelegation *Token) bool {
|
||||
// The Subject of each delegation must equal the invocation's Subject (or Audience if defined). - 4f
|
||||
if t.Subject() != sub {
|
||||
return fmt.Errorf("%w: delegation %s, expected %s, got %s", ErrWrongSub, dlgCid, sub, dlg.Subject())
|
||||
}
|
||||
|
||||
// The Issuer of each delegation must be the Audience in the next one. - 4d
|
||||
if t.Audience() != subDelegation.Issuer() {
|
||||
return fmt.Errorf("%w: delegation %s, expected %s, got %s", ErrBrokenChain, dlgCid, iss, dlg.Audience())
|
||||
}
|
||||
|
||||
// The command of each delegation must "allow" the one before it. - 4g
|
||||
if !dlg.Command().Covers(cmd) {
|
||||
return fmt.Errorf("%w: delegation %s, %s doesn't cover %s", ErrCommandNotCovered, dlgCid, dlg.Command(), cmd)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Token) String() string {
|
||||
var res strings.Builder
|
||||
|
||||
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 +271,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"),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -37,10 +37,10 @@ import (
|
||||
// 4. When the proof chain is being validated (verifyProofs below):
|
||||
// a. There must be at least one delegation in the proof chain.
|
||||
// b. All referenced delegations must be available.
|
||||
// c. The first proof must be issued to the Invoker (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.
|
||||
// c. The first proof must be issued to the Invoker.
|
||||
// d. The Issuer of each delegation must be the Audience in the parent delegation.
|
||||
// e. The chain must terminate with a root delegation.
|
||||
// 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,21 +58,21 @@ 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
|
||||
// The first proof must be issued to the Invoker. - 4c
|
||||
// The Issuer of each delegation must be the Audience in the next one. - 4d
|
||||
if dlg.Audience() != iss {
|
||||
return fmt.Errorf("%w: delegation %s, expected %s, got %s", ErrBrokenChain, dlgCid, iss, dlg.Audience())
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user