64 Commits

Author SHA1 Message Date
Michael Muré
fb97653529 WIP 2025-01-06 14:24:56 +01:00
Michael Muré
95bdbc4fc5 Merge pull request #95 from ucan-wg/minor-impro
Minor impro
2024-12-12 16:36:17 +01:00
Michael Muré
416345dba9 args,meta: add a Len() 2024-12-12 16:06:01 +01:00
Michael Muré
042d6dc52f didtest: complete the set of function, finish removing the dependency on testing.T 2024-12-12 16:05:13 +01:00
Michael Muré
8bb3a4f4d0 expose secretbox, notably for the GenerateKey() function that should be public 2024-12-12 16:04:31 +01:00
Michael Muré
47156a8ad6 Merge pull request #94 from ucan-wg/base64-string
container: I/O as strings for base64
2024-12-11 16:12:48 +01:00
Michael Muré
ce6d163627 container: I/O as strings for base64 2024-12-11 16:05:16 +01:00
Michael Muré
c3c2c96008 Merge pull request #93 from ucan-wg/proof-rephrase
invocation: rephrase slightly the proof rules to be less confusing down the line
2024-12-10 13:27:12 +01:00
Steve Moyer
903632695f test(delegation): calculate newCID from new.dagjson so tests are not brittle 2024-12-10 07:14:36 -05:00
Steve Moyer
f2d75b7815 test(delegation): fix newCID after new.dagjson update (brittle) 2024-12-10 07:03:06 -05:00
Steve Moyer
4f09829abe feat(delegationtest): update generator to changed delegation.New constructor 2024-12-10 06:53:01 -05:00
Michael Muré
5660df32b5 delegation,invocation: proper String() function 2024-12-10 12:20:18 +01:00
Michael Muré
2f2a74c7ec delegation: fix following constructor updates 2024-12-10 12:19:47 +01:00
Michael Muré
0592717637 (WIP) refine the token constructors:
- for invocation, reorder the parameters for a more "natural language" mental model
- for delegation, make "subject" a required parameter to avoid make powerline by mistake
- for delegation, implement powerline
2024-12-09 20:39:47 +01:00
Michael Muré
80c2d60ab3 invocation: rephrase slightly the proof rules to be less confusing down the line 2024-12-09 18:53:38 +01:00
Michael Muré
c518c6657a Merge pull request #92 from ucan-wg/generator-cleanup
delegationtest: run go fmt, less noisy code
2024-12-05 15:36:23 +01:00
Michael Muré
78825f4f55 delegationtest: run go fmt, less noisy code 2024-12-05 15:30:23 +01:00
Michael Muré
7f1adbd945 Merge pull request #91 from ucan-wg/fix-everything
Fix everything
2024-12-04 20:09:28 +01:00
Michael Muré
0f59088d0b test: improvement on the generator, small fixes 2024-12-04 19:55:29 +01:00
Michael Muré
72e0f353e7 delegation: add a Bundle to carry around decoded, sealed and Cid 2024-12-04 19:54:46 +01:00
Michael Muré
d0d4ec3abe delegation,invocation: meta can be nil in the model, but not in the view 2024-12-04 19:53:05 +01:00
Michael Muré
bb24081b28 Merge pull request #90 from ucan-wg/fix/extended-field-names
Add support for extended field names in selectors
2024-12-03 11:43:38 +01:00
Fabio Bozzo
3688ccea01 fieldRegex to be more restrictive and consistent 2024-12-02 19:18:01 +01:00
Fabio Bozzo
e9105896d7 Merge branch 'main' into fix/extended-field-names
# Conflicts:
#	pkg/policy/selector/parsing_test.go
2024-12-02 19:05:19 +01:00
Fabio Bozzo
15751c7362 regex to be more restrictive and consistent 2024-12-02 18:30:41 +01:00
Michael Muré
d52218fa5a Merge pull request #88 from ucan-wg/feat/secretbox-meta-encryption
feat(meta): secretbox encryption in place of aes-gcm
2024-12-02 17:34:29 +01:00
Fabio Bozzo
64d3024dec remove aes comparison and add ciphertext overhead comments 2024-12-02 17:31:19 +01:00
Michael Muré
78d37d92ef Merge pull request #89 from ucan-wg/fix/prevent-int-overflow
fix: prevent overflow of int values
2024-12-02 14:41:12 +01:00
Fabio Bozzo
da806b1bc5 remove TODO comment 2024-12-02 14:32:15 +01:00
Fabio Bozzo
311b942a6d validate invocation token args 2024-12-02 14:22:42 +01:00
Fabio Bozzo
56eab758ed move args int validation to their creation 2024-12-02 12:24:06 +01:00
Fabio Bozzo
105323b989 moved unit test 2024-12-02 12:13:56 +01:00
Fabio Bozzo
5b816ccc62 streamline int overflow check for token timestamps 2024-12-02 12:06:06 +01:00
Fabio Bozzo
28272e6900 move int validation to where a error can be returned 2024-12-02 11:59:16 +01:00
Fabio Bozzo
a854389e32 validate token timestamps integer limits 2024-12-02 11:37:03 +01:00
Fabio Bozzo
117a75e2c4 cleanup comments 2024-11-29 19:36:00 +01:00
Fabio Bozzo
a25bfbaf45 fix: extended field names 2024-11-29 19:32:31 +01:00
Fabio Bozzo
bff482f73b add constants.go 2024-11-29 13:04:14 +01:00
Fabio Bozzo
ff79bbb443 go fmt 2024-11-29 13:03:48 +01:00
Fabio Bozzo
3997a86184 fix: prevent overflow of int values 2024-11-29 13:00:00 +01:00
Fabio Bozzo
200d6a8ae2 benchmarks vs aes-gcm 2024-11-28 17:17:10 +01:00
Fabio Bozzo
0349e7e463 feat(meta): secretbox encryption in place of aes-gcm 2024-11-28 16:16:04 +01:00
Steve Moyer
dff52f80c4 Merge pull request #87 from ucan-wg/chore-eliminate-jen-dependency
chore(delegationtest): eliminate dependency on dave/jennifer/jen package
2024-11-28 06:06:39 -05:00
Steve Moyer
5b7a63a2c6 style(delegationtest/generator): adopt suggestions from PR 2024-11-28 05:37:05 -05:00
Steve Moyer
66675f7030 build: tidy Go module files 2024-11-27 15:06:32 -05:00
Steve Moyer
7e54be49e1 chore(delegationtest): eliminate dependency on dave/jennifer/jen package
Resolves #68
2024-11-27 15:01:16 -05:00
Steve Moyer
15535d3474 Merge pull request #86 from ucan-wg/test/invocation-verifies-args-vs-pols
test(invocation): verify arguments versus aggregated policies
2024-11-27 14:56:05 -05:00
Steve Moyer
170e597e71 feat(args): export fluent builder 2024-11-27 12:05:00 -05:00
Steve Moyer
ce1a4b6e32 test(invocation): verify arguments versus aggregated policies 2024-11-27 10:20:40 -05:00
Michael Muré
d1d047cd9e Merge pull request #85 from ucan-wg/container-accessor
container: add a way to check for a single invocation, also iterator
2024-11-27 16:13:29 +01:00
Michael Muré
3680637090 container: add a way to check for a single invocation, also iterator 2024-11-27 16:13:15 +01:00
Michael Muré
1166a68e5c Merge pull request #84 from ucan-wg/args-minor
args: add simple value accessor
2024-11-27 12:26:19 +01:00
Michael Muré
ba4db9bce8 args: add simple value accessor 2024-11-27 12:23:10 +01:00
Michael Muré
20369dba49 Merge pull request #83 from ucan-wg/feat/invocation-witharguments
feat(invocation): add WithArguments Option to set all arguments at once
2024-11-27 10:10:19 +01:00
Steve Moyer
ade2c7f858 feat(invocation): add WithArguments Option to set all arguments at once 2024-11-26 14:39:10 -05:00
Michael Muré
943a318b26 Merge commit '60bdc8873b3d259ff297be4873a9623fcebb7f75' 2024-11-26 11:44:27 +01:00
Michael Muré
2d79cdc54e Merge pull request #82 from ucan-wg/cont-io
container: streamed and non-streamed IO, documentation
2024-11-26 11:37:46 +01:00
Michael Muré
820057e41e container: streamed and non-streamed IO, documentation 2024-11-21 15:49:29 +01:00
Michael Muré
ba0038b0ae Merge pull request #79 from ucan-wg/meta-args
meta,args: add missing Include, add iterator to use normal or Readonly the same way
2024-11-21 11:27:37 +01:00
Michael Muré
4a4b200312 Merge pull request #81 from ucan-wg/fix/66/allow-dash-in-policy-segment
fix(selector): allow dashes in selector names
2024-11-21 11:27:14 +01:00
Michael Muré
caae2f58bf meta,args: add missing Include, add iterator to use normal or Readonly the same way 2024-11-21 11:25:15 +01:00
Steve Moyer
ec627138cb fix(selector): allow dashes in selector names
This functionality will be further enhanced in #80.

Resolves #66
2024-11-20 12:37:29 -05:00
Michael Muré
4ec409edc6 Merge pull request #74 from ucan-wg/ci
ci: fix after merging to main
2024-11-20 16:11:07 +01:00
Michael Muré
c61fc8d8b3 ci: fix after merging to main 2024-11-20 16:07:49 +01:00
60 changed files with 1952 additions and 516 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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
}

View File

@@ -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
View File

@@ -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
View File

@@ -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=

View File

@@ -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
}

View File

@@ -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
View 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
View 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)
})
}

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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")

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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()))
}

View File

@@ -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()
}

View File

@@ -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
View 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
}

View 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)
}
})
}
}

View File

@@ -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:

View File

@@ -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)

View File

@@ -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))
}

View File

@@ -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)
})
}

View 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
}

View 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)
})
}

View File

@@ -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[:]})

View File

@@ -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)
})
}

View File

@@ -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 {

View File

@@ -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)
})
}
}

View 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
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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())

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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,
}

View File

@@ -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"),

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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"}}]

View 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"]]]]]}}]

View File

@@ -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
}

View File

@@ -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

View File

@@ -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
}

View 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
}

View File

@@ -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")

View File

@@ -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"]),

View File

@@ -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

View File

@@ -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())

View File

@@ -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"]),

View File

@@ -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
}

View File

@@ -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