From e980d6c0b969c84a3500a565f2ef50091493379a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Wed, 20 Nov 2024 12:34:24 +0100 Subject: [PATCH] various sanding everywhere towards building the tookit --- pkg/args/args.go | 43 +++++++++++++++++++ pkg/args/readonly.go | 23 ++++++++++ pkg/container/reader.go | 6 ++- pkg/meta/meta.go | 19 ++++---- pkg/policy/policy_test.go | 31 +++++++++++++ token/delegation/delegation.go | 2 +- token/delegation/delegationtest/token.go | 13 +++--- token/delegation/delegationtest/token_test.go | 6 +-- token/delegation/loader.go | 17 ++++++++ token/internal/nonce/nonce.go | 19 +++++++- token/invocation/invocation.go | 28 ++++++------ token/invocation/invocation_test.go | 11 ++--- token/invocation/proof.go | 6 --- 13 files changed, 176 insertions(+), 48 deletions(-) create mode 100644 pkg/args/readonly.go create mode 100644 token/delegation/loader.go diff --git a/pkg/args/args.go b/pkg/args/args.go index 4ac9d94..d046d93 100644 --- a/pkg/args/args.go +++ b/pkg/args/args.go @@ -6,11 +6,13 @@ package args import ( "fmt" "sort" + "strings" "github.com/ipld/go-ipld-prime" "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/ipld/go-ipld-prime/printer" "github.com/ucan-wg/go-ucan/pkg/policy/literal" ) @@ -70,6 +72,7 @@ func (a *Args) Include(other *Args) { // ToIPLD wraps an instance of an Args with an ipld.Node. func (a *Args) ToIPLD() (ipld.Node, error) { sort.Strings(a.Keys) + return qp.BuildMap(basicnode.Prototype.Any, int64(len(a.Keys)), func(ma datamodel.MapAssembler) { for _, key := range a.Keys { qp.MapEntry(ma, key, qp.Node(a.Values[key])) @@ -92,3 +95,43 @@ func (a *Args) Equals(other *Args) bool { } return true } + +func (a *Args) String() string { + sort.Strings(a.Keys) + + buf := strings.Builder{} + buf.WriteString("{") + + for _, key := range a.Keys { + buf.WriteString("\n\t") + buf.WriteString(key) + buf.WriteString(": ") + buf.WriteString(strings.ReplaceAll(printer.Sprint(a.Values[key]), "\n", "\n\t")) + buf.WriteString(",") + } + + if len(a.Keys) > 0 { + buf.WriteString("\n") + } + buf.WriteString("}") + + return buf.String() +} + +// ReadOnly returns a read-only version of Args. +func (a *Args) ReadOnly() ReadOnly { + return ReadOnly{args: a} +} + +// Clone makes a deep copy. +func (a *Args) Clone() *Args { + res := &Args{ + Keys: make([]string, len(a.Keys)), + Values: make(map[string]ipld.Node, len(a.Values)), + } + copy(res.Keys, a.Keys) + for k, v := range a.Values { + res.Values[k] = v + } + return res +} diff --git a/pkg/args/readonly.go b/pkg/args/readonly.go new file mode 100644 index 0000000..a708807 --- /dev/null +++ b/pkg/args/readonly.go @@ -0,0 +1,23 @@ +package args + +import "github.com/ipld/go-ipld-prime" + +type ReadOnly struct { + args *Args +} + +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) String() string { + return r.args.String() +} + +func (r ReadOnly) WriteableClone() *Args { + return r.args.Clone() +} diff --git a/pkg/container/reader.go b/pkg/container/reader.go index db1e145..984b152 100644 --- a/pkg/container/reader.go +++ b/pkg/container/reader.go @@ -2,6 +2,7 @@ package container import ( "encoding/base64" + "errors" "fmt" "io" "iter" @@ -34,13 +35,16 @@ func (ctn Reader) GetToken(cid cid.Cid) (token.Token, error) { // GetDelegation is the same as GetToken but only return a delegation.Token, with the right type. func (ctn Reader) GetDelegation(cid cid.Cid) (*delegation.Token, error) { tkn, err := ctn.GetToken(cid) + if errors.Is(err, ErrNotFound) { + return nil, delegation.ErrDelegationNotFound + } if err != nil { return nil, err } if tkn, ok := tkn.(*delegation.Token); ok { return tkn, nil } - return nil, fmt.Errorf("not a delegation token") + return nil, delegation.ErrDelegationNotFound } // GetAllDelegations returns all the delegation.Token in the container. diff --git a/pkg/meta/meta.go b/pkg/meta/meta.go index a08d97c..913530f 100644 --- a/pkg/meta/meta.go +++ b/pkg/meta/meta.go @@ -12,9 +12,7 @@ import ( "github.com/ucan-wg/go-ucan/pkg/policy/literal" ) -var ErrUnsupported = errors.New("failure adding unsupported type to meta") - -var ErrNotFound = errors.New("key-value not found in meta") +var ErrNotFound = errors.New("key not found in meta") var ErrNotEncryptable = errors.New("value of this type cannot be encrypted") @@ -193,18 +191,19 @@ func (m *Meta) String() string { buf := strings.Builder{} buf.WriteString("{") - var i int for key, node := range m.Values { - if i > 0 { - buf.WriteString(", ") - } - i++ + buf.WriteString("\n\t") buf.WriteString(key) - buf.WriteString(":") - buf.WriteString(printer.Sprint(node)) + buf.WriteString(": ") + buf.WriteString(strings.ReplaceAll(printer.Sprint(node), "\n", "\n\t")) + buf.WriteString(",") } + if len(m.Values) > 0 { + buf.WriteString("\n") + } buf.WriteString("}") + return buf.String() } diff --git a/pkg/policy/policy_test.go b/pkg/policy/policy_test.go index 26ed239..c7f8e9f 100644 --- a/pkg/policy/policy_test.go +++ b/pkg/policy/policy_test.go @@ -37,6 +37,37 @@ func ExamplePolicy() { // ] } +func ExamplePolicy_accumulate() { + var statements []policy.Constructor + + statements = append(statements, policy.Equal(".status", literal.String("draft"))) + + statements = append(statements, policy.All(".reviewer", + policy.Like(".email", "*@example.com"), + )) + + statements = append(statements, policy.Any(".tags", policy.Or( + policy.Equal(".", literal.String("news")), + policy.Equal(".", literal.String("press")), + ))) + + pol := policy.MustConstruct(statements...) + + fmt.Println(pol) + + // Output: + // [ + // ["==", ".status", "draft"], + // ["all", ".reviewer", + // ["like", ".email", "*@example.com"]], + // ["any", ".tags", + // ["or", [ + // ["==", ".", "news"], + // ["==", ".", "press"]]] + // ] + // ] +} + func TestConstruct(t *testing.T) { pol, err := policy.Construct( policy.Equal(".status", literal.String("draft")), diff --git a/token/delegation/delegation.go b/token/delegation/delegation.go index 1f19f42..6ab32c6 100644 --- a/token/delegation/delegation.go +++ b/token/delegation/delegation.go @@ -48,7 +48,7 @@ type Token struct { // New creates a validated Token from the provided parameters and options. // -// When creating a delegated token, the Issuer's (iss) DID is assembed +// 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(privKey crypto.PrivKey, aud did.DID, cmd command.Command, pol policy.Policy, opts ...Option) (*Token, error) { diff --git a/token/delegation/delegationtest/token.go b/token/delegation/delegationtest/token.go index 07c27e6..47a77fd 100644 --- a/token/delegation/delegationtest/token.go +++ b/token/delegation/delegationtest/token.go @@ -2,14 +2,13 @@ package delegationtest import ( "embed" - "fmt" "path/filepath" "sync" "github.com/ipfs/go-cid" + "github.com/ucan-wg/go-ucan/pkg/command" "github.com/ucan-wg/go-ucan/token/delegation" - "github.com/ucan-wg/go-ucan/token/invocation" ) const ( @@ -41,11 +40,11 @@ var fs embed.FS var ( once sync.Once - ldr invocation.DelegationLoader + ldr delegation.Loader err error ) -var _ invocation.DelegationLoader = (*delegationLoader)(nil) +var _ delegation.Loader = (*delegationLoader)(nil) type delegationLoader struct { tokens map[cid.Cid]*delegation.Token @@ -54,7 +53,7 @@ type delegationLoader struct { // GetDelegationLoader returns a singleton instance of a test // DelegationLoader containing all the tokens present in the data/ // directory. -func GetDelegationLoader() (invocation.DelegationLoader, error) { +func GetDelegationLoader() (delegation.Loader, error) { once.Do(func() { ldr, err = loadDelegations() }) @@ -66,13 +65,13 @@ func GetDelegationLoader() (invocation.DelegationLoader, error) { func (l *delegationLoader) GetDelegation(id cid.Cid) (*delegation.Token, error) { tkn, ok := l.tokens[id] if !ok { - return nil, fmt.Errorf("%w: CID %s", invocation.ErrMissingDelegation, id.String()) + return nil, delegation.ErrDelegationNotFound } return tkn, nil } -func loadDelegations() (invocation.DelegationLoader, error) { +func loadDelegations() (delegation.Loader, error) { dirEntries, err := fs.ReadDir("data") if err != nil { return nil, err diff --git a/token/delegation/delegationtest/token_test.go b/token/delegation/delegationtest/token_test.go index e9518ac..fde6749 100644 --- a/token/delegation/delegationtest/token_test.go +++ b/token/delegation/delegationtest/token_test.go @@ -6,8 +6,9 @@ import ( "github.com/ipfs/go-cid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/ucan-wg/go-ucan/token/delegation" "github.com/ucan-wg/go-ucan/token/delegation/delegationtest" - "github.com/ucan-wg/go-ucan/token/invocation" ) func TestGetDelegation(t *testing.T) { @@ -25,8 +26,7 @@ func TestGetDelegation(t *testing.T) { t.Parallel() tkn, err := delegationtest.GetDelegation(cid.Undef) - require.ErrorIs(t, err, invocation.ErrMissingDelegation) - require.ErrorContains(t, err, "CID b") + require.ErrorIs(t, err, delegation.ErrDelegationNotFound) assert.Nil(t, tkn) }) } diff --git a/token/delegation/loader.go b/token/delegation/loader.go new file mode 100644 index 0000000..13dd81d --- /dev/null +++ b/token/delegation/loader.go @@ -0,0 +1,17 @@ +package delegation + +import ( + "fmt" + + "github.com/ipfs/go-cid" +) + +// ErrDelegationNotFound is returned if a delegation token is not found +var ErrDelegationNotFound = fmt.Errorf("delegation not found") + +// Loader is a delegation token loader. +type Loader interface { + // GetDelegation returns the delegation.Token matching the given CID. + // If not found, ErrDelegationNotFound is returned. + GetDelegation(cid cid.Cid) (*Token, error) +} diff --git a/token/internal/nonce/nonce.go b/token/internal/nonce/nonce.go index 3bda21b..f4928a8 100644 --- a/token/internal/nonce/nonce.go +++ b/token/internal/nonce/nonce.go @@ -2,8 +2,25 @@ package nonce import "crypto/rand" -// Generate creates a 12-byte random nonce. // TODO: some crypto scheme require more, is that our case? +// +// The spec mention: +// The REQUIRED nonce parameter nonce MAY be any value. +// A randomly generated string is RECOMMENDED to provide a unique UCAN, though it MAY +// also be a monotonically increasing count of the number of links in the hash chain. +// This field helps prevent replay attacks and ensures a unique CID per delegation. +// The iss, aud, and exp fields together will often ensure that UCANs are unique, +// but adding the nonce ensures uniqueness. +// +// The recommended size of the nonce differs by key type. In many cases, a random +// 12-byte nonce is sufficient. If uncertain, check the nonce in your DID's crypto suite. +// +// 12 bytes is 10^28, 16 bytes is 10^38. Both sounds like a lot of random to achieve +// those goals, but maybe the crypto voodoo require more. +// +// The rust implementation use 16 bytes nonce. + +// Generate creates a 12-byte random nonce. func Generate() ([]byte, error) { res := make([]byte, 12) _, err := rand.Read(res) diff --git a/token/invocation/invocation.go b/token/invocation/invocation.go index 3faa0d3..12f0a08 100644 --- a/token/invocation/invocation.go +++ b/token/invocation/invocation.go @@ -102,34 +102,38 @@ func New(iss, sub did.DID, cmd command.Command, prf []cid.Cid, opts ...Option) ( return &tkn, nil } -func (t *Token) ExecutionAllowed(loader DelegationLoader) (bool, error) { +func (t *Token) ExecutionAllowed(loader delegation.Loader) error { return t.executionAllowed(loader, t.arguments) } -func (t *Token) ExecutionAllowedWithArgsHook(loader DelegationLoader, hook func(*args.Args) *args.Args) (bool, error) { - return t.executionAllowed(loader, hook(t.arguments)) +func (t *Token) ExecutionAllowedWithArgsHook(loader delegation.Loader, hook func(args args.ReadOnly) (*args.Args, error)) error { + newArgs, err := hook(t.arguments.ReadOnly()) + if err != nil { + return err + } + return t.executionAllowed(loader, newArgs) } -func (t *Token) executionAllowed(loader DelegationLoader, arguments *args.Args) (bool, error) { +func (t *Token) executionAllowed(loader delegation.Loader, arguments *args.Args) error { delegations, err := t.loadProofs(loader) if err != nil { // All referenced delegations must be available - 4b - return false, err + return err } if err := t.verifyProofs(delegations); err != nil { - return false, err + return err } if err := t.verifyTimeBound(delegations); err != nil { - return false, err + return err } if err := t.verifyArgs(delegations, arguments); err != nil { - return false, err + return err } - return true, nil + return nil } // Issuer returns the did.DID representing the Token's issuer. @@ -154,8 +158,8 @@ func (t *Token) Command() command.Command { // Arguments returns the arguments to be used when the command is // invoked. -func (t *Token) Arguments() *args.Args { - return t.arguments +func (t *Token) Arguments() args.ReadOnly { + return t.arguments.ReadOnly() } // Proof() returns the ordered list of cid.Cid which reference the @@ -225,7 +229,7 @@ func (t *Token) validate() error { return errs } -func (t *Token) loadProofs(loader DelegationLoader) (res []*delegation.Token, err error) { +func (t *Token) loadProofs(loader delegation.Loader) (res []*delegation.Token, err error) { res = make([]*delegation.Token, len(t.proof)) for i, c := range t.proof { res[i], err = loader.GetDelegation(c) diff --git a/token/invocation/invocation_test.go b/token/invocation/invocation_test.go index 6482606..ea0ba2a 100644 --- a/token/invocation/invocation_test.go +++ b/token/invocation/invocation_test.go @@ -5,13 +5,12 @@ import ( "github.com/ipfs/go-cid" "github.com/stretchr/testify/require" + "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/token/delegation/delegationtest" "github.com/ucan-wg/go-ucan/token/invocation" - - "github.com/stretchr/testify/assert" ) const ( @@ -118,7 +117,7 @@ func TestToken_ExecutionAllowed(t *testing.T) { }) } -func test(t *testing.T, persona didtest.Persona, cmd command.Command, args *args.Args, prf []cid.Cid, opts ...invocation.Option) (bool, error) { +func test(t *testing.T, persona didtest.Persona, cmd command.Command, args *args.Args, prf []cid.Cid, opts ...invocation.Option) error { t.Helper() tkn, err := invocation.New(persona.DID(t), didtest.PersonaAlice.DID(t), cmd, prf, opts...) @@ -131,13 +130,11 @@ func test(t *testing.T, persona didtest.Persona, cmd command.Command, args *args } func testFails(t *testing.T, expErr error, persona didtest.Persona, cmd command.Command, args *args.Args, prf []cid.Cid, opts ...invocation.Option) { - ok, err := test(t, persona, cmd, args, prf, opts...) + err := test(t, persona, cmd, args, prf, opts...) require.ErrorIs(t, err, expErr) - assert.False(t, ok) } func testPasses(t *testing.T, persona didtest.Persona, cmd command.Command, args *args.Args, prf []cid.Cid, opts ...invocation.Option) { - ok, err := test(t, persona, cmd, args, prf, opts...) + err := test(t, persona, cmd, args, prf, opts...) require.NoError(t, err) - assert.True(t, ok) } diff --git a/token/invocation/proof.go b/token/invocation/proof.go index a644c17..a61e022 100644 --- a/token/invocation/proof.go +++ b/token/invocation/proof.go @@ -4,8 +4,6 @@ import ( "fmt" "time" - "github.com/ipfs/go-cid" - "github.com/ucan-wg/go-ucan/pkg/args" "github.com/ucan-wg/go-ucan/pkg/policy" "github.com/ucan-wg/go-ucan/token/delegation" @@ -49,10 +47,6 @@ import ( // a. The policy must "match" the arguments. (verifyArgs below) // b. The nonce (if present) is not reused. (out of scope for go-ucan) -type DelegationLoader interface { - GetDelegation(cid cid.Cid) (*delegation.Token, error) -} - // verifyProofs controls that the proof chain allows the invocation: // - principal alignment // - command alignment