From 89e4d5d4199b00178291cb17397247c48951fbee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Fri, 18 Oct 2024 10:48:47 +0200 Subject: [PATCH 01/13] wip API exploration --- token/delegation/delegation.go | 18 +++++++++++++++ token/interface.go | 15 ++++++++++++- token/internal/envelope/ipld.go | 3 +-- token/invocation/invocation.go | 39 ++++++++++++++++++++++++++++++--- 4 files changed, 69 insertions(+), 6 deletions(-) diff --git a/token/delegation/delegation.go b/token/delegation/delegation.go index 599f773..b2a2ca2 100644 --- a/token/delegation/delegation.go +++ b/token/delegation/delegation.go @@ -153,6 +153,24 @@ func (t *Token) Expiration() *time.Time { return t.expiration } +// IsValidNow verifies that the token can be used at the current time, based on expiration or "not before" fields. +// This does NOT do any other kind of verifications. +func (t *Token) IsValidNow() bool { + return t.IsValidAt(time.Now()) +} + +// IsValidNow verifies that the token can be used at the given time, based on expiration or "not before" fields. +// This does NOT do any other kind of verifications. +func (t *Token) IsValidAt(ti time.Time) bool { + if t.expiration == nil && ti.After(*t.expiration) { + return false + } + if t.notBefore != nil && ti.Before(*t.notBefore) { + return false + } + return true +} + func (t *Token) validate() error { var errs error diff --git a/token/interface.go b/token/interface.go index 3079f56..6865ccf 100644 --- a/token/interface.go +++ b/token/interface.go @@ -2,11 +2,11 @@ package token import ( "io" + "time" "github.com/ipfs/go-cid" "github.com/ipld/go-ipld-prime/codec" "github.com/libp2p/go-libp2p/core/crypto" - "github.com/ucan-wg/go-ucan/did" "github.com/ucan-wg/go-ucan/pkg/meta" ) @@ -18,6 +18,19 @@ type Token interface { Issuer() did.DID // Meta returns the Token's metadata. Meta() meta.ReadOnly + // TODO: not sure we actually need that interface + + // IsValidNow verifies that the token can be used at the current time, based on expiration or "not before" fields. + // This does NOT do any other kind of verifications. + IsValidNow() bool + // IsValidNow verifies that the token can be used at the given time, based on expiration or "not before" fields. + // This does NOT do any other kind of verifications. + IsValidAt(t time.Time) bool + + // // Issuer returns the did.DID representing the Token's issuer. + // Issuer() did.DID + // // Meta returns the Token's metadata. + // Meta() *meta.Meta } type Marshaller interface { diff --git a/token/internal/envelope/ipld.go b/token/internal/envelope/ipld.go index 6e9533d..5b6ed15 100644 --- a/token/internal/envelope/ipld.go +++ b/token/internal/envelope/ipld.go @@ -187,8 +187,7 @@ func FromIPLD[T Tokener](node datamodel.Node) (T, error) { return zero, errors.New("the VarsigHeader key type doesn't match the issuer's key type") } - // TODO: this re-encode the payload! Is there a less wasteful way? - + // TODO: can we use the already serialized CBOR data here, instead of encoding again the payload? data, err := ipld.Encode(info.sigPayloadNode, dagcbor.Encode) if err != nil { return zero, err diff --git a/token/invocation/invocation.go b/token/invocation/invocation.go index c48121f..4b67dfa 100644 --- a/token/invocation/invocation.go +++ b/token/invocation/invocation.go @@ -18,6 +18,7 @@ import ( "github.com/ucan-wg/go-ucan/pkg/args" "github.com/ucan-wg/go-ucan/pkg/command" "github.com/ucan-wg/go-ucan/pkg/meta" + "github.com/ucan-wg/go-ucan/token/delegation" "github.com/ucan-wg/go-ucan/token/internal/nonce" "github.com/ucan-wg/go-ucan/token/internal/parse" ) @@ -33,11 +34,12 @@ type Token struct { // The Command command command.Command - // The Command's Arguments + // The Command's arguments arguments *args.Args - // Delegations that prove the chain of authority + // CIDs of the delegation.Token that prove the chain of authority + // They need to form a strictly linear chain, and being ordered starting from the root Delegation (issued by the Subject), + // in a strict sequence where the aud of the previous Delegation matches the iss of the next Delegation. proof []cid.Cid - // Arbitrary Metadata meta *meta.Meta @@ -98,6 +100,22 @@ func New(iss, sub did.DID, cmd command.Command, prf []cid.Cid, opts ...Option) ( return &tkn, nil } +type DelegationLoader interface { + GetDelegation(cid cid.Cid) (*delegation.Token, error) +} + +func (t *Token) ExecutionAllowed(loader DelegationLoader) bool { + return t.executionAllowed(loader, t.arguments) +} + +func (t *Token) ExecutionAllowedWithArgsHook(loader DelegationLoader, hook func(*args.Args) *args.Args) bool { + return t.executionAllowed(loader, hook(t.arguments)) +} + +func (t *Token) executionAllowed(loader DelegationLoader, arguments *args.Args) bool { + panic("TODO") +} + // Issuer returns the did.DID representing the Token's issuer. func (t *Token) Issuer() did.DID { return t.issuer @@ -157,6 +175,21 @@ func (t *Token) Cause() *cid.Cid { return t.cause } +// IsValidNow verifies that the token can be used at the current time, based on expiration or "not before" fields. +// This does NOT do any other kind of verifications. +func (t *Token) IsValidNow() bool { + return t.IsValidAt(time.Now()) +} + +// IsValidNow verifies that the token can be used at the given time, based on expiration or "not before" fields. +// This does NOT do any other kind of verifications. +func (t *Token) IsValidAt(ti time.Time) bool { + if t.expiration == nil && ti.After(*t.expiration) { + return false + } + return true +} + func (t *Token) validate() error { var errs error From 0f705573092c76723fe234f4beee06099d961b36 Mon Sep 17 00:00:00 2001 From: Steve Moyer Date: Tue, 12 Nov 2024 15:16:43 -0500 Subject: [PATCH 02/13] feat(invocation): validate delegation proof chain --- token/invocation/errors.go | 32 +++ token/invocation/invocation.go | 102 ++++++- token/invocation/invocation_test.go | 334 +++++++++++++++++++++++ token/invocation/invocationtest/field.go | 39 +++ 4 files changed, 503 insertions(+), 4 deletions(-) create mode 100644 token/invocation/errors.go create mode 100644 token/invocation/invocation_test.go create mode 100644 token/invocation/invocationtest/field.go diff --git a/token/invocation/errors.go b/token/invocation/errors.go new file mode 100644 index 0000000..e58a8d1 --- /dev/null +++ b/token/invocation/errors.go @@ -0,0 +1,32 @@ +package invocation + +import "errors" + +var ( + // ErrDelegationExpired is returned if one of the delegations in the + // proof chain has expired. + ErrDelegationExpired = errors.New("delegation in proof chain has expired") + + // ErrDelegationInactive is returned if one of the delegations in the + // proof chain is not yet active. + ErrDelegationInactive = errors.New("delegation in proof chain not yet active") + + // ErrLastNotRoot is returned if the last delegation token in the proof + // chain is not a root delegation token. + ErrLastNotRoot = errors.New("the last delegation token in proof chain must be a root token") + + // ErrMissingDelegation + ErrMissingDelegation = errors.New("loader missing delegation for proof chain") + + // ErrNoProof is returned when no delegations were provided to prove + // that the invocation should be executed. + ErrNoProof = errors.New("at least one delegation must be provided to validate the invocation") + + // ErrNotIssuedToInvoder is returned if the first delegation token's + // Audience DID is not the Invoker's Issuer DID. + ErrNotIssuedToInvoker = errors.New("first delegation token is not issued to invoker") + + // ErrBrokenChain is returned when the Audience of each delegation is + // not the Issuer of the previous one. + ErrBrokenChain = errors.New("delegation proof chain is broken") +) diff --git a/token/invocation/invocation.go b/token/invocation/invocation.go index 4b67dfa..8135229 100644 --- a/token/invocation/invocation.go +++ b/token/invocation/invocation.go @@ -3,6 +3,45 @@ // from the [envelope]-enclosed, signed and DAG-CBOR-encoded form that // should most commonly be used for transport and storage. // +// # Invocation token validation +// +// Per the specification, invocation Tokens must be validated before the +// command is executed. This validation can happen in multiple stages: +// +// 1. When the invocation is unsealed from its containing envelope: +// a. The envelope can be decoded. +// b. The envelope contains a Signature, VarsigHeader and Payload. +// c. The Payload contains an iss field that contains a valid did:key. +// d. The a public key can be extracted from the did:key. +// e. The public key type is supported by go-ucan. +// f. The Signature can be decoded per the VarsigHeader. +// g. The SigPayload can be verified using the Signature and public key. +// h. The field key of the TokenPayload matches the expected tag. +// 2. When the invocation is created or passes step one: +// a. The Issuer field is not empty. +// b. The Subject field is not empty +// c. The Command field is not empty and the Command is not a wildcard. +// d. The Policy field is present (but may be empty). +// e. The Arguments field is present (but may be empty). +// 3. When an unsealed invocation passes steps one and two for execution: +// a. The invocation can not be expired. +// b. Invoked at should not be in the future. +// 4. When the proof chain is being validated: +// 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 token must not be expired (expiration in the future or absent). +// e. The token must be active (nbf in the past or absent). +// f. The Issuer of each delegation must be the Audience in the next +// one. +// g. The last token must be a root delegation. +// h. The Subject of each delegation must equal the invocation's +// Audience field. +// i. The command of each delegation must "allow" the one before it. +// 5. If steps 1-4 pass: +// a. The policy must "match" the arguments. +// b. The nonce (if present) is not reused. +// // [envelope]: https://github.com/ucan-wg/spec#envelope // [invocation]: https://github.com/ucan-wg/invocation package invocation @@ -104,16 +143,71 @@ type DelegationLoader interface { GetDelegation(cid cid.Cid) (*delegation.Token, error) } -func (t *Token) ExecutionAllowed(loader DelegationLoader) bool { +func (t *Token) ExecutionAllowed(loader DelegationLoader) (bool, error) { return t.executionAllowed(loader, t.arguments) } -func (t *Token) ExecutionAllowedWithArgsHook(loader DelegationLoader, hook func(*args.Args) *args.Args) bool { +func (t *Token) ExecutionAllowedWithArgsHook(loader DelegationLoader, hook func(*args.Args) *args.Args) (bool, error) { return t.executionAllowed(loader, hook(t.arguments)) } -func (t *Token) executionAllowed(loader DelegationLoader, arguments *args.Args) bool { - panic("TODO") +func (t *Token) executionAllowed(loader DelegationLoader, arguments *args.Args) (bool, error) { + // There must be at least one delegation referenced - 4a + if len(t.proof) < 1 { + return false, ErrNoProof + } + + type chainer interface { + Issuer() did.DID + Subject() did.DID // TODO: if the invocation token's Audience is nil, copy the subject into it + Command() command.Command + } + + // This starts as the invocation token but will be the root delegation + // after the for loop below completes + var lastChainer chainer = t + + for i, dlgCid := range t.proof { + // The token must be present - 4b + dlg, err := loader.GetDelegation(dlgCid) + if err != nil { + return false, fmt.Errorf("%w: need %s", ErrMissingDelegation, dlgCid) + } + + // No tokens in the proof chain may be expired - 4d + if dlg.Expiration() != nil && dlg.Expiration().Before(time.Now()) { + return false, fmt.Errorf("%w: CID is %s", ErrDelegationExpired, dlgCid) + } + + // No tokens in the proof chain may be inactive - 4e + if dlg.NotBefore() != nil && dlg.NotBefore().After(time.Now()) { + return false, fmt.Errorf("%w: CID is %s", ErrDelegationInactive, dlgCid) + } + + // First proof must have the invoker's Issuer as the Audience - 4c + if i == 0 && dlg.Audience() != t.Issuer() { + return false, fmt.Errorf("%w: expected %s, got %s", ErrNotIssuedToInvoker, t.issuer, dlg.Audience()) + } + + // Tokens must form a chain with current issuer equal to the + // next audience - 4f + if lastChainer.Issuer() != dlg.Audience() { + return false, fmt.Errorf("%w: expected %s, got %s", ErrBrokenChain, lastChainer.Issuer(), dlg.Audience()) + } + + // TODO: Checking the subject consistency can happen here - 4h + // TODO: Checking the command equivalence or attenuation can happen here - 4i + + lastChainer = dlg + } + + // The last prf value must be a root delegation (have the issuer field + // match the Subject field) - 4g + if lastChainer.Issuer() != lastChainer.Subject() { + return false, fmt.Errorf("%w: expected %s, got %s", ErrLastNotRoot, lastChainer.Subject(), lastChainer.Issuer()) + } + + return true, nil } // Issuer returns the did.DID representing the Token's issuer. diff --git a/token/invocation/invocation_test.go b/token/invocation/invocation_test.go new file mode 100644 index 0000000..93e967e --- /dev/null +++ b/token/invocation/invocation_test.go @@ -0,0 +1,334 @@ +package invocation_test + +import ( + "errors" + "fmt" + "testing" + "time" + + "github.com/ipfs/go-cid" + "github.com/libp2p/go-libp2p/core/crypto" + "github.com/stretchr/testify/require" + "github.com/ucan-wg/go-ucan/did" + "github.com/ucan-wg/go-ucan/pkg/args" + "github.com/ucan-wg/go-ucan/pkg/command" + "github.com/ucan-wg/go-ucan/pkg/policy" + "github.com/ucan-wg/go-ucan/token/delegation" + "github.com/ucan-wg/go-ucan/token/invocation" + "github.com/ucan-wg/go-ucan/token/invocation/invocationtest" + + "github.com/stretchr/testify/assert" +) + +const ( + rootPrivKeyCfg = "CAESQNMjVXyblHUrtmvdzjfkdrUlLkxh/cHZMFirki7dJLFjW7cCs74VxjO8Wh04f8Xg0uqyKw7N0wTUBiPy2h4hGPQ=" + rootDIDStr = "did:key:z6MkkdH136Kee5akqx1DiK3hE3WGx4SKmHo11tYw2cWmjjZV" + rootTknCIDStr = "bafyreibk6lkgd32zldpqqqsdn7diyosokelrhder5lic4ujadtxl5blkei" + + rootOnlyTknCIDStr = "bafyreidyknlfnsah63jujdkhe5vvjil2yoznlgoaezeq62qqhghaxzpfya" + + dlg0PrivKeyCfg = "CAESQFIb3aD0lZzidUzTtwdpHtCyx1VJxe+Uq4x/S+XQFDDgz/NZIi/TR3rhgUn550RSBOSxNmw0QnR0FOPmAB7SXAg=" + dlg0DIDStr = "did:key:z6MktT1f5LXQ2MFSUwwTTY9DDU2QdBzZA11V5bjou4YDQY6K" + dlg0TknCIDStr = "bafyreihocbstcdvgyeczjoijiyo2bdppyplm2aglqumwtutyapd2zlp2bi" + + expiredTknDagJson = `[{"/":{"bytes":"U4IH1Q52UrdOyOHkHtXSkH0uf5ouk10k/LTOMz3UvP2k1kqv9/rbGXUwhQCy6JP3s8hc+U3h/lBXYFYzIlULAw"}},{"h":{"/":{"bytes":"NO0BcQ"}},"ucan/dlg@1.0.0-rc.1":{"aud":"did:key:z6MkqTkYrRV6WpFCWRcEHBXnTV3cVqzMtVZREhJKAAswrrGq","cmd":"/seg0","exp":1731439015,"iss":"did:key:z6MktT1f5LXQ2MFSUwwTTY9DDU2QdBzZA11V5bjou4YDQY6K","nonce":{"/":{"bytes":"AAECAwQFBgcICQoL"}},"pol":[],"sub":"did:key:z6MkkdH136Kee5akqx1DiK3hE3WGx4SKmHo11tYw2cWmjjZV"}}]` + expiredDlg0TknCIDStr = "bafyreigezbrohmjmzv5nxdw2nhjabnrvs52vcv4uvbuscnbyr2jzep2vru" + + inactiveDlg0TknCIDStr = "bafyreiffflxvtfiv722i3lomrqcogc6nncxai3voxeltvrphpuwecdfbgq" + + dlg1PrivKeyCfg = "CAESQPHkXp4OatPxpJ1veMAxEYzP4rd3/sPUz9PRQuJaDKTco5DJTdJC5iXxjCe1VDYAmRwlJOBvBdbsvS3qFgV6zoI=" + dlg1DIDStr = "did:key:z6MkqTkYrRV6WpFCWRcEHBXnTV3cVqzMtVZREhJKAAswrrGq" + dlg1TknCIDStr = "bafyreihjbkvom3tdivzolh6aieuwamz4x6bu3dh667bxytdc5vzo37obo4" + + // dlg2PrivKeyCfg = "CAESQJryJWe3uGt+7BTH2doqsN+2H83rNXv7Yw0tMoKE+lBKTqByESll668G1Jiy9gW/8hm9/jVcrFD9Y1BWyokfNBE=" + // dlg2DIDStr = "did:key:z6MkjkBk9hzMhcr6rYMXHPEsXy1LAWK5dHniNdbaEPojGXr8" + + // dlg3PrivKeyCfg = "CAESQPrLMlX2p+Dgz70YCyVXbHAJfVMT7lLUYAbuZ1rBKQmBLiD7WJ4Spc4VFZAsJ7HUnkneJWNTk/FFaN2z3pb/OZI=" + // dlg3DIDStr = "did:key:z6MkhZKy9X4sLtbH1fGQmPVMjXmBAEQ3vAN6DRSfebSBCqpu" + + invPrivKeyCfg = "CAESQHJW8WYTZDRzxjLBjrFN35raIGvVsPoXAJB/5X+J8miboVWVLZFyQmxCAIXOMpwLqWW7R2I98qsCGvxgEJZ5qgY=" + invDIDStr = "did:key:z6MkqK3NgTnZZo77iptQdU9proJn1ozMmcTSKR98t8sZzJAq" + invTknCIDStr = "" + + missingPrivKeyCfg = "CAESQMjRvrEIjpPYRQKmkAGw/pV0XgE958rYa4vlnKJjl1zz/sdnGnyV1xKLJk8D39edyjhHWyqcpgFnozQK62SG16k=" + missingDIDStr = "bafyreigwypmw6eul6vadi6g6lnfbsfo2zck7gfzsbjoroqs3djhnzzc7mm" + missingTknCIDStr = "did:key:z6MkwboxFsH3kEuehBZ5fLkRmxi68yv1u38swA4r9Jm2VRma" +) + +func TestToken_ExecutionAllowed(t *testing.T) { + t.Parallel() + + t.Run("passes - only root", func(t *testing.T) { + t.Parallel() + + args := invocationtest.EmptyArguments + prf := invocationtest.Proof(t, rootOnlyTknCIDStr) + testPasses(t, []string{"seg0"}, args, prf) + }) + + t.Run("passes - valid chain", func(t *testing.T) { + t.Parallel() + + args := invocationtest.EmptyArguments + prf := invocationtest.Proof(t, dlg1TknCIDStr, dlg0TknCIDStr, rootTknCIDStr) + testPasses(t, []string{"seg0"}, args, prf) + }) + + t.Run("fails - no proof", func(t *testing.T) { + t.Parallel() + + args := args.New() + testFails(t, invocation.ErrNoProof, []string{"seg0"}, args, []cid.Cid{}) + }) + + t.Run("fails - missing referenced delegation", func(t *testing.T) { + t.Parallel() + + args := invocationtest.EmptyArguments + prf := invocationtest.Proof(t, missingDIDStr, rootTknCIDStr) + testFails(t, invocation.ErrMissingDelegation, []string{"seg0"}, args, prf) + }) + + t.Run("fails - referenced delegation expired", func(t *testing.T) { + t.Parallel() + + args := invocationtest.EmptyArguments + prf := invocationtest.Proof(t, dlg1TknCIDStr, expiredDlg0TknCIDStr, rootTknCIDStr) + testFails(t, invocation.ErrDelegationExpired, []string{"seg0"}, args, prf) + }) + + t.Run("fails - referenced delegation inactive", func(t *testing.T) { + t.Parallel() + + args := invocationtest.EmptyArguments + prf := invocationtest.Proof(t, dlg1TknCIDStr, inactiveDlg0TknCIDStr, rootTknCIDStr) + testFails(t, invocation.ErrDelegationInactive, []string{"seg0"}, args, prf) + }) + + t.Run("fails - last (or only) delegation not root", func(t *testing.T) { + t.Parallel() + + args := args.New() + prf := invocationtest.Proof(t, dlg1TknCIDStr) + testFails(t, invocation.ErrLastNotRoot, []string{"seg0"}, args, prf) + }) + + t.Run("fails - broken chain", func(t *testing.T) { + t.Parallel() + + args := invocationtest.EmptyArguments + prf := invocationtest.Proof(t, dlg1TknCIDStr, rootTknCIDStr) + testFails(t, invocation.ErrBrokenChain, []string{"seg0"}, args, prf) + }) + + t.Run("fails - first not issued to invoker", func(t *testing.T) { + t.Parallel() + + args := invocationtest.EmptyArguments + prf := invocationtest.Proof(t, dlg0TknCIDStr, rootTknCIDStr) + testFails(t, invocation.ErrNotIssuedToInvoker, []string{"seg0"}, args, prf) + }) +} + +func test(t *testing.T, cmdSegs []string, args *args.Args, prf []cid.Cid, opts ...invocation.Option) (bool, error) { + ldr := newTestDelegationLoader(t) + tkn := testInvocation(t, invPrivKeyCfg, rootDIDStr, rootDIDStr, cmdSegs, args, prf, opts...) + + return tkn.ExecutionAllowed(ldr) +} + +func testFails(t *testing.T, expErr error, cmdSegs []string, args *args.Args, prf []cid.Cid, opts ...invocation.Option) { + t.Helper() + + ok, err := test(t, cmdSegs, args, prf, opts...) + require.ErrorIs(t, err, expErr) + assert.False(t, ok) +} + +func testPasses(t *testing.T, cmdSegs []string, args *args.Args, prf []cid.Cid, opts ...invocation.Option) { + ok, err := test(t, cmdSegs, args, prf, opts...) + require.NoError(t, err) + assert.True(t, ok) +} + +var _ invocation.DelegationLoader = (*testDelegationLoader)(nil) + +type testDelegationLoader struct { + tkns map[cid.Cid]*delegation.Token +} + +func newTestDelegationLoader(t *testing.T) *testDelegationLoader { + t.Helper() + + cmdSegs := []string{"seg0"} + ldr := &testDelegationLoader{ + tkns: map[cid.Cid]*delegation.Token{}, + } + + // aud, err := did.Parse(dlg0DIDStr) + // require.NoError(t, err) + // cmd := command.New(cmdSegs...) + + // pol, err := policy.FromDagJson("[]") + // require.NoError(t, err) + + // rootDlg, err := delegation.Root(rootPrivKey, aud, cmd, pol) + // require.NoError(t, err) + // _, rootCID, err := rootDlg.ToSealed(rootPrivKey) + // require.NoError(t, err) + + // Do not add this one to the loader + _, missingCID := testDelegation(t, missingPrivKeyCfg, dlg1DIDStr, rootDIDStr, cmdSegs, "[]") + t.Log("Missing CID", missingCID) + + rootTkn, rootDlgCID := testDelegation(t, rootPrivKeyCfg, dlg0DIDStr, rootDIDStr, cmdSegs, "[]") + t.Log("Root chain CID:", rootDlgCID) + ldr.tkns[rootDlgCID] = rootTkn + + rootOnlyTkn, rootOnlyDlgCID := testDelegation(t, rootPrivKeyCfg, invDIDStr, rootDIDStr, cmdSegs, "[]") + t.Log("Root only chain CID:", rootOnlyDlgCID) + ldr.tkns[rootOnlyDlgCID] = rootOnlyTkn + + dlg0Tkn, dlg0CID := testDelegation(t, dlg0PrivKeyCfg, dlg1DIDStr, rootDIDStr, cmdSegs, "[]") + t.Log("Dlg0 CID:", dlg0CID) + ldr.tkns[dlg0CID] = dlg0Tkn + + // exp := time.Now().Add(time.Second) + + // expiredDlg0Tkn, expiredDlg0CID := testDelegation(t, dlg0PrivKeyCfg, dlg1DIDStr, rootDIDStr, cmdSegs, "[]", delegation.WithExpiration(exp)) + // t.Log("Expired Dlg0 CID:", expiredDlg0CID) + + // dlg0PrivKeyEnc, err := crypto.ConfigDecodeKey(dlg0PrivKeyCfg) + // require.NoError(t, err) + // dlg0PrivKey, err := crypto.UnmarshalPrivateKey(dlg0PrivKeyEnc) + // require.NoError(t, err) + + // dlg0DagJson, err := expiredDlg0Tkn.ToDagJson(dlg0PrivKey) + // require.NoError(t, err) + + // t.Log("Expired token DAGJSON:", string(dlg0DagJson)) + + expiredDlg0Tkn, err := delegation.FromDagJson([]byte(expiredTknDagJson)) + require.NoError(t, err) + expiredDlg0TknCID, err := cid.Parse(expiredDlg0TknCIDStr) + ldr.tkns[expiredDlg0TknCID] = expiredDlg0Tkn + + nbf, err := time.Parse(time.RFC3339, "2500-01-01T00:00:00Z") + require.NoError(t, err) + + inactiveDlg0Tkn, inactiveDlg0CID := testDelegation(t, dlg0PrivKeyCfg, dlg1DIDStr, rootDIDStr, cmdSegs, "[]", delegation.WithNotBefore(nbf)) + t.Log("Inactive Dlg0 CID:", inactiveDlg0CID) + ldr.tkns[inactiveDlg0CID] = inactiveDlg0Tkn + + dlg1Tkn, dlg1CID := testDelegation(t, dlg1PrivKeyCfg, invDIDStr, rootDIDStr, cmdSegs, "[]") + t.Log("Dlg1 CID:", dlg1CID) + ldr.tkns[dlg1CID] = dlg1Tkn + + // for i := 0; i < 5; i++ { + // privKey, id, err := did.GenerateEd25519() + // require.NoError(t, err) + + // privKeyEnc, err := crypto.MarshalPrivateKey(privKey) + // require.NoError(t, err) + + // t.Log("PrivKey:", crypto.ConfigEncodeKey(privKeyEnc), "DID:", id) + + // } + + t.Log("Delegation loader length:", len(ldr.tkns)) + + return ldr +} + +var ErrUnknownDelegationTokenCID = errors.New("unknown delegation token CID") + +func (l *testDelegationLoader) GetDelegation(c cid.Cid) (*delegation.Token, error) { + tkn, ok := l.tkns[c] + if !ok { + return nil, fmt.Errorf("%w: %s", ErrUnknownDelegationTokenCID, c.String()) + } + + return tkn, nil +} + +func testDelegation(t *testing.T, privKeyCfg string, audStr string, subStr string, cmdSegs []string, polDAGJSON string, opts ...delegation.Option) (*delegation.Token, cid.Cid) { + t.Helper() + + privKey, aud, sub, cmd := parseTokenArgs(t, privKeyCfg, audStr, subStr, cmdSegs) + + pol, err := policy.FromDagJson(polDAGJSON) + require.NoError(t, err) + + opts = append( + opts, + delegation.WithNonce([]byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b}), + delegation.WithSubject(sub), + ) + + tkn, err := delegation.New(privKey, aud, cmd, pol, opts...) + require.NoError(t, err) + + _, id, err := tkn.ToSealed(privKey) + require.NoError(t, err) + + return tkn, id +} + +func testInvocation(t *testing.T, privKeyCfg string, audStr string, subStr string, cmdSegs []string, args *args.Args, prf []cid.Cid, opts ...invocation.Option) *invocation.Token { + t.Helper() + + privKey, _, sub, cmd := parseTokenArgs(t, privKeyCfg, audStr, subStr, cmdSegs) + + iss, err := did.FromPrivKey(privKey) + require.NoError(t, err) + + tkn, err := invocation.New(iss, sub, cmd, prf) + require.NoError(t, err) + + return tkn +} + +func testProof(t *testing.T, cidStrs ...string) []cid.Cid { + var prf []cid.Cid + + for cidStr := range cidStrs { + id, err := cid.Parse(cidStr) + require.NoError(t, err) + + prf = append(prf, id) + } + + return prf +} + +func parseTokenArgs( + t *testing.T, + privKeyCfg string, + audStr string, + subStr string, + cmdSegs []string, +) ( + privKey crypto.PrivKey, + aud did.DID, + sub did.DID, + cmd command.Command, +) { + t.Helper() + + var err error + + privKeyEnc, err := crypto.ConfigDecodeKey(privKeyCfg) + require.NoError(t, err) + privKey, err = crypto.UnmarshalPrivateKey(privKeyEnc) + require.NoError(t, err) + + aud, err = did.Parse(audStr) + require.NoError(t, err) + + sub, err = did.Parse(subStr) + require.NoError(t, err) + + cmd = command.New(cmdSegs...) + + return +} diff --git a/token/invocation/invocationtest/field.go b/token/invocation/invocationtest/field.go new file mode 100644 index 0000000..1d31ccf --- /dev/null +++ b/token/invocation/invocationtest/field.go @@ -0,0 +1,39 @@ +package invocationtest + +import ( + "testing" + + "github.com/ipfs/go-cid" + "github.com/stretchr/testify/require" + "github.com/ucan-wg/go-ucan/pkg/args" + "github.com/ucan-wg/go-ucan/pkg/policy" +) + +var ( + EmptyArguments = args.New() + EmptyPolicy = emptyPolicy() +) + +func emptyPolicy() policy.Policy { + pol, err := policy.FromDagJson("[]") + if err != nil { + panic(err) + } + + return pol +} + +func Proof(t *testing.T, cidStrs ...string) []cid.Cid { + // t.Helper() + + prf := make([]cid.Cid, len(cidStrs)) + + for i, cidStr := range cidStrs { + id, err := cid.Parse(cidStr) + require.NoError(t, err) + + prf[i] = id + } + + return prf +} From 814cec14957e4c1148906c94fff4cf698e9143a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Fri, 18 Oct 2024 10:48:47 +0200 Subject: [PATCH 03/13] wip API exploration --- token/interface.go | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/token/interface.go b/token/interface.go index 6865ccf..b3a8d21 100644 --- a/token/interface.go +++ b/token/interface.go @@ -7,30 +7,17 @@ import ( "github.com/ipfs/go-cid" "github.com/ipld/go-ipld-prime/codec" "github.com/libp2p/go-libp2p/core/crypto" - "github.com/ucan-wg/go-ucan/did" - "github.com/ucan-wg/go-ucan/pkg/meta" ) type Token interface { Marshaller - // Issuer returns the did.DID representing the Token's issuer. - Issuer() did.DID - // Meta returns the Token's metadata. - Meta() meta.ReadOnly - // TODO: not sure we actually need that interface - // IsValidNow verifies that the token can be used at the current time, based on expiration or "not before" fields. // This does NOT do any other kind of verifications. IsValidNow() bool // IsValidNow verifies that the token can be used at the given time, based on expiration or "not before" fields. // This does NOT do any other kind of verifications. IsValidAt(t time.Time) bool - - // // Issuer returns the did.DID representing the Token's issuer. - // Issuer() did.DID - // // Meta returns the Token's metadata. - // Meta() *meta.Meta } type Marshaller interface { From 92065ca0d393aea0a911df41b03a8b385fad37a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Wed, 13 Nov 2024 14:50:59 +0100 Subject: [PATCH 04/13] invocation: fix proof documentation --- token/invocation/invocation.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/token/invocation/invocation.go b/token/invocation/invocation.go index 8135229..816030c 100644 --- a/token/invocation/invocation.go +++ b/token/invocation/invocation.go @@ -76,8 +76,9 @@ type Token struct { // The Command's arguments arguments *args.Args // CIDs of the delegation.Token that prove the chain of authority - // They need to form a strictly linear chain, and being ordered starting from the root Delegation (issued by the Subject), - // in a strict sequence where the aud of the previous Delegation matches the iss of the next Delegation. + // They need to form a strictly linear chain, and being ordered starting from the + // leaf Delegation (with aud matching the invocation's iss), in a strict sequence + // where the iss of the previous Delegation matches the aud of the next Delegation. proof []cid.Cid // Arbitrary Metadata meta *meta.Meta From 64b989452ff9ce9910a209b18d56e7e682a18ef8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Wed, 13 Nov 2024 16:58:48 +0100 Subject: [PATCH 05/13] invocation: split out the delegation chain control --- token/delegation/schema.go | 12 ++-- token/invocation/errors.go | 43 ++++++++------ token/invocation/invocation.go | 72 +++++++--------------- token/invocation/proof.go | 105 +++++++++++++++++++++++++++++++++ token/invocation/schema.go | 12 ++-- 5 files changed, 162 insertions(+), 82 deletions(-) create mode 100644 token/invocation/proof.go diff --git a/token/delegation/schema.go b/token/delegation/schema.go index 1c6873c..13721e9 100644 --- a/token/delegation/schema.go +++ b/token/delegation/schema.go @@ -26,17 +26,17 @@ const Tag = "ucan/dlg@1.0.0-rc.1" 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 } diff --git a/token/invocation/errors.go b/token/invocation/errors.go index e58a8d1..5335763 100644 --- a/token/invocation/errors.go +++ b/token/invocation/errors.go @@ -2,31 +2,36 @@ package invocation import "errors" +// Loading errors var ( - // ErrDelegationExpired is returned if one of the delegations in the - // proof chain has expired. - ErrDelegationExpired = errors.New("delegation in proof chain has expired") + // ErrMissingDelegation + ErrMissingDelegation = errors.New("loader missing delegation for proof chain") +) - // ErrDelegationInactive is returned if one of the delegations in the - // proof chain is not yet active. - ErrDelegationInactive = errors.New("delegation in proof chain not yet active") +// Time bound errors +var ( + // ErrTokenExpired is returned if a token is invalid at execution time + ErrTokenInvalidNow = errors.New("token has expired") +) + +// Principal alignment errors +var ( + // ErrNoProof is returned when no delegations were provided to prove + // that the invocation should be executed. + ErrNoProof = errors.New("at least one delegation must be provided to validate the invocation") // ErrLastNotRoot is returned if the last delegation token in the proof // chain is not a root delegation token. ErrLastNotRoot = errors.New("the last delegation token in proof chain must be a root token") - // ErrMissingDelegation - ErrMissingDelegation = errors.New("loader missing delegation for proof chain") - - // ErrNoProof is returned when no delegations were provided to prove - // that the invocation should be executed. - ErrNoProof = errors.New("at least one delegation must be provided to validate the invocation") - - // ErrNotIssuedToInvoder is returned if the first delegation token's - // Audience DID is not the Invoker's Issuer DID. - ErrNotIssuedToInvoker = errors.New("first delegation token is not issued to invoker") - - // ErrBrokenChain is returned when the Audience of each delegation is + // ErrBrokenChain is returned when the Audience of a delegation is // not the Issuer of the previous one. - ErrBrokenChain = errors.New("delegation proof chain is broken") + ErrBrokenChain = errors.New("delegation proof chain doesn't connect the invocation to the subject") + + // ErrWrongSub is returned when the Subject of a delegation is not the invocation audience. + ErrWrongSub = errors.New("delegation subject need to match the invocation audience") + + // ErrCommandNotCovered is returned when a delegation command doesn't cover (identical or parent of) the + // next delegation or invocation's command. + ErrCommandNotCovered = errors.New("allowed command doesn't cover the next delegation or invocation") ) diff --git a/token/invocation/invocation.go b/token/invocation/invocation.go index 816030c..b5cc5c3 100644 --- a/token/invocation/invocation.go +++ b/token/invocation/invocation.go @@ -126,6 +126,7 @@ func New(iss, sub did.DID, cmd command.Command, prf []cid.Cid, opts ...Option) ( } } + var err error if len(tkn.nonce) == 0 { tkn.nonce, err = nonce.Generate() if err != nil { @@ -140,10 +141,6 @@ func New(iss, sub did.DID, cmd command.Command, prf []cid.Cid, opts ...Option) ( return &tkn, nil } -type DelegationLoader interface { - GetDelegation(cid cid.Cid) (*delegation.Token, error) -} - func (t *Token) ExecutionAllowed(loader DelegationLoader) (bool, error) { return t.executionAllowed(loader, t.arguments) } @@ -153,59 +150,21 @@ func (t *Token) ExecutionAllowedWithArgsHook(loader DelegationLoader, hook func( } func (t *Token) executionAllowed(loader DelegationLoader, arguments *args.Args) (bool, error) { - // There must be at least one delegation referenced - 4a - if len(t.proof) < 1 { - return false, ErrNoProof + delegations, err := t.loadProofs(loader) + if err != nil { + return false, err } - type chainer interface { - Issuer() did.DID - Subject() did.DID // TODO: if the invocation token's Audience is nil, copy the subject into it - Command() command.Command + if err := t.verifyProofs(delegations); err != nil { + return false, err } - // This starts as the invocation token but will be the root delegation - // after the for loop below completes - var lastChainer chainer = t - - for i, dlgCid := range t.proof { - // The token must be present - 4b - dlg, err := loader.GetDelegation(dlgCid) - if err != nil { - return false, fmt.Errorf("%w: need %s", ErrMissingDelegation, dlgCid) - } - - // No tokens in the proof chain may be expired - 4d - if dlg.Expiration() != nil && dlg.Expiration().Before(time.Now()) { - return false, fmt.Errorf("%w: CID is %s", ErrDelegationExpired, dlgCid) - } - - // No tokens in the proof chain may be inactive - 4e - if dlg.NotBefore() != nil && dlg.NotBefore().After(time.Now()) { - return false, fmt.Errorf("%w: CID is %s", ErrDelegationInactive, dlgCid) - } - - // First proof must have the invoker's Issuer as the Audience - 4c - if i == 0 && dlg.Audience() != t.Issuer() { - return false, fmt.Errorf("%w: expected %s, got %s", ErrNotIssuedToInvoker, t.issuer, dlg.Audience()) - } - - // Tokens must form a chain with current issuer equal to the - // next audience - 4f - if lastChainer.Issuer() != dlg.Audience() { - return false, fmt.Errorf("%w: expected %s, got %s", ErrBrokenChain, lastChainer.Issuer(), dlg.Audience()) - } - - // TODO: Checking the subject consistency can happen here - 4h - // TODO: Checking the command equivalence or attenuation can happen here - 4i - - lastChainer = dlg + if err := t.verifyTimeBound(delegations); err != nil { + return false, err } - // The last prf value must be a root delegation (have the issuer field - // match the Subject field) - 4g - if lastChainer.Issuer() != lastChainer.Subject() { - return false, fmt.Errorf("%w: expected %s, got %s", ErrLastNotRoot, lastChainer.Subject(), lastChainer.Issuer()) + if err := t.verifyArgs(delegations, arguments); err != nil { + return false, err } return true, nil @@ -304,6 +263,17 @@ func (t *Token) validate() error { return errs } +func (t *Token) loadProofs(loader DelegationLoader) (res []*delegation.Token, err error) { + res = make([]*delegation.Token, len(t.proof)) + for i, c := range t.proof { + res[i], err = loader.GetDelegation(c) + if err != nil { + return nil, fmt.Errorf("%w: need %s", ErrMissingDelegation, c) + } + } + return res, nil +} + // tokenFromModel build a decoded view of the raw IPLD data. // This function also serves as validation. func tokenFromModel(m tokenPayloadModel) (*Token, error) { diff --git a/token/invocation/proof.go b/token/invocation/proof.go new file mode 100644 index 0000000..e345f3c --- /dev/null +++ b/token/invocation/proof.go @@ -0,0 +1,105 @@ +package invocation + +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" +) + +type DelegationLoader interface { + GetDelegation(cid cid.Cid) (*delegation.Token, error) +} + +// verifyProofs controls that the proof chain allows the invocation: +// - principal alignment +// - command alignment +func (t *Token) verifyProofs(delegations []*delegation.Token) error { + cmd := t.command + iss := t.issuer + aud := t.audience + if !aud.Defined() { + aud = t.subject + } + + var last *delegation.Token + + // control from the invocation to the root + for i, dlgCid := range t.proof { + dlg := delegations[i] + + if dlg.Subject() != aud { + return fmt.Errorf("%w: delegation %s, expected %s, got %s", ErrWrongSub, dlgCid, aud, dlg.Subject()) + } + + if dlg.Audience() != iss { + return fmt.Errorf("%w: delegation %s, expected %s, got %s", ErrBrokenChain, dlgCid, iss, dlg.Audience()) + } + iss = dlg.Audience() + + if !dlg.Command().Covers(cmd) { + return fmt.Errorf("%w: delegation %s, %s doesn't cover %s", ErrCommandNotCovered, dlgCid, dlg.Command(), cmd) + } + cmd = dlg.Command() + + last = dlg + } + + // There must be at least one delegation referenced + // (yes, it's an odd way to test this, but it allows for the static check to not be mad about "last" + // being possibly nil below). + if last == nil { + return ErrNoProof + } + + // The last prf value must be a root delegation (have the issuer field + // match the Subject field) - 4g + if last.Issuer() != last.Subject() { + return fmt.Errorf("%w: expected %s, got %s", ErrLastNotRoot, last.Subject(), last.Issuer()) + } + + return nil +} + +func (t *Token) verifyTimeBound(dlgs []*delegation.Token) error { + return t.verifyTimeBoundAt(time.Now(), dlgs) +} + +func (t *Token) verifyTimeBoundAt(at time.Time, delegations []*delegation.Token) error { + for i, dlgCid := range t.proof { + dlg := delegations[i] + + if !dlg.IsValidAt(at) { + return fmt.Errorf("%w: delegation %s", ErrTokenInvalidNow, dlgCid) + } + } + return nil +} + +func (t *Token) verifyArgs(delegations []*delegation.Token, arguments *args.Args) error { + var count int + for i := range t.proof { + count += len(delegations[i].Policy()) + } + + policies := make(policy.Policy, 0, count) + for i := range t.proof { + policies = append(policies, delegations[i].Policy()...) + } + + argsIpld, err := arguments.ToIPLD() + if err != nil { + return err + } + + ok, statement := policies.Match(argsIpld) + if !ok { + return fmt.Errorf("the following UCAN policy is not satisfied: %v", statement.String()) + } + + return nil +} diff --git a/token/invocation/schema.go b/token/invocation/schema.go index d51cf4f..5a30e66 100644 --- a/token/invocation/schema.go +++ b/token/invocation/schema.go @@ -25,17 +25,17 @@ const Tag = "ucan/inv@1.0.0-rc.1" 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 } From fc4c8f2de12f35cd944aeaa5e807fe97c74276b5 Mon Sep 17 00:00:00 2001 From: Steve Moyer Date: Wed, 13 Nov 2024 12:40:25 -0500 Subject: [PATCH 06/13] fix: issues discovered by invocation validation tests --- did/did.go | 2 +- token/delegation/delegation.go | 2 +- token/invocation/invocation_test.go | 6 +++--- token/invocation/proof.go | 2 ++ 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/did/did.go b/did/did.go index bf92946..19042bf 100644 --- a/did/did.go +++ b/did/did.go @@ -78,7 +78,7 @@ func MustParse(str string) DID { // Defined tells if the DID is defined, not equal to Undef. func (d DID) Defined() bool { - return d.code == 0 || len(d.bytes) > 0 + return d.code != 0 || len(d.bytes) > 0 } // PubKey returns the public key encapsulated by the did:key. diff --git a/token/delegation/delegation.go b/token/delegation/delegation.go index b2a2ca2..1f19f42 100644 --- a/token/delegation/delegation.go +++ b/token/delegation/delegation.go @@ -162,7 +162,7 @@ func (t *Token) IsValidNow() bool { // IsValidNow verifies that the token can be used at the given time, based on expiration or "not before" fields. // This does NOT do any other kind of verifications. func (t *Token) IsValidAt(ti time.Time) bool { - if t.expiration == nil && ti.After(*t.expiration) { + if t.expiration != nil && ti.After(*t.expiration) { return false } if t.notBefore != nil && ti.Before(*t.notBefore) { diff --git a/token/invocation/invocation_test.go b/token/invocation/invocation_test.go index 93e967e..921e085 100644 --- a/token/invocation/invocation_test.go +++ b/token/invocation/invocation_test.go @@ -94,7 +94,7 @@ func TestToken_ExecutionAllowed(t *testing.T) { args := invocationtest.EmptyArguments prf := invocationtest.Proof(t, dlg1TknCIDStr, expiredDlg0TknCIDStr, rootTknCIDStr) - testFails(t, invocation.ErrDelegationExpired, []string{"seg0"}, args, prf) + testFails(t, invocation.ErrTokenInvalidNow, []string{"seg0"}, args, prf) }) t.Run("fails - referenced delegation inactive", func(t *testing.T) { @@ -102,7 +102,7 @@ func TestToken_ExecutionAllowed(t *testing.T) { args := invocationtest.EmptyArguments prf := invocationtest.Proof(t, dlg1TknCIDStr, inactiveDlg0TknCIDStr, rootTknCIDStr) - testFails(t, invocation.ErrDelegationInactive, []string{"seg0"}, args, prf) + testFails(t, invocation.ErrTokenInvalidNow, []string{"seg0"}, args, prf) }) t.Run("fails - last (or only) delegation not root", func(t *testing.T) { @@ -126,7 +126,7 @@ func TestToken_ExecutionAllowed(t *testing.T) { args := invocationtest.EmptyArguments prf := invocationtest.Proof(t, dlg0TknCIDStr, rootTknCIDStr) - testFails(t, invocation.ErrNotIssuedToInvoker, []string{"seg0"}, args, prf) + testFails(t, invocation.ErrBrokenChain, []string{"seg0"}, args, prf) }) } diff --git a/token/invocation/proof.go b/token/invocation/proof.go index e345f3c..67a3364 100644 --- a/token/invocation/proof.go +++ b/token/invocation/proof.go @@ -26,6 +26,8 @@ func (t *Token) verifyProofs(delegations []*delegation.Token) error { aud = t.subject } + fmt.Println("Subject:", t.subject, ", Audience:", aud) + var last *delegation.Token // control from the invocation to the root From 00d2380f14f9d50a8ac1718d0aa97b1c64fae713 Mon Sep 17 00:00:00 2001 From: Steve Moyer Date: Thu, 14 Nov 2024 07:21:27 -0500 Subject: [PATCH 07/13] fix(invocation): change verifyProof to chain the Issuer field --- token/invocation/proof.go | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/token/invocation/proof.go b/token/invocation/proof.go index 67a3364..31f0125 100644 --- a/token/invocation/proof.go +++ b/token/invocation/proof.go @@ -26,10 +26,6 @@ func (t *Token) verifyProofs(delegations []*delegation.Token) error { aud = t.subject } - fmt.Println("Subject:", t.subject, ", Audience:", aud) - - var last *delegation.Token - // control from the invocation to the root for i, dlgCid := range t.proof { dlg := delegations[i] @@ -48,19 +44,19 @@ func (t *Token) verifyProofs(delegations []*delegation.Token) error { } cmd = dlg.Command() - last = dlg + iss = dlg.Issuer() } // There must be at least one delegation referenced // (yes, it's an odd way to test this, but it allows for the static check to not be mad about "last" // being possibly nil below). - if last == nil { + if len(delegations) < 1 { return ErrNoProof } // The last prf value must be a root delegation (have the issuer field // match the Subject field) - 4g - if last.Issuer() != last.Subject() { + if last := delegations[len(delegations)-1]; last.Issuer() != last.Subject() { return fmt.Errorf("%w: expected %s, got %s", ErrLastNotRoot, last.Subject(), last.Issuer()) } From 417ef78570976aee7be792a5f7c1e5ce2daad06b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Thu, 14 Nov 2024 15:03:49 +0100 Subject: [PATCH 08/13] fix(invocation): cleanup proof verification algo --- token/invocation/proof.go | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/token/invocation/proof.go b/token/invocation/proof.go index 31f0125..7791a2b 100644 --- a/token/invocation/proof.go +++ b/token/invocation/proof.go @@ -19,6 +19,11 @@ type DelegationLoader interface { // - principal alignment // - command alignment func (t *Token) verifyProofs(delegations []*delegation.Token) error { + // There must be at least one delegation referenced + if len(delegations) < 1 { + return ErrNoProof + } + cmd := t.command iss := t.issuer aud := t.audience @@ -37,21 +42,12 @@ func (t *Token) verifyProofs(delegations []*delegation.Token) error { if dlg.Audience() != iss { return fmt.Errorf("%w: delegation %s, expected %s, got %s", ErrBrokenChain, dlgCid, iss, dlg.Audience()) } - iss = dlg.Audience() + iss = dlg.Issuer() if !dlg.Command().Covers(cmd) { return fmt.Errorf("%w: delegation %s, %s doesn't cover %s", ErrCommandNotCovered, dlgCid, dlg.Command(), cmd) } cmd = dlg.Command() - - iss = dlg.Issuer() - } - - // There must be at least one delegation referenced - // (yes, it's an odd way to test this, but it allows for the static check to not be mad about "last" - // being possibly nil below). - if len(delegations) < 1 { - return ErrNoProof } // The last prf value must be a root delegation (have the issuer field From bb36d61d93d97f7e6f34c6ca5159ecfa08ba150c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Thu, 14 Nov 2024 16:44:32 +0100 Subject: [PATCH 09/13] invocation: rework the validation doc, fix missing invocation time check --- token/invocation/invocation.go | 42 ++------------------------ token/invocation/proof.go | 54 ++++++++++++++++++++++++++++++++-- 2 files changed, 53 insertions(+), 43 deletions(-) diff --git a/token/invocation/invocation.go b/token/invocation/invocation.go index b5cc5c3..3faa0d3 100644 --- a/token/invocation/invocation.go +++ b/token/invocation/invocation.go @@ -3,45 +3,6 @@ // from the [envelope]-enclosed, signed and DAG-CBOR-encoded form that // should most commonly be used for transport and storage. // -// # Invocation token validation -// -// Per the specification, invocation Tokens must be validated before the -// command is executed. This validation can happen in multiple stages: -// -// 1. When the invocation is unsealed from its containing envelope: -// a. The envelope can be decoded. -// b. The envelope contains a Signature, VarsigHeader and Payload. -// c. The Payload contains an iss field that contains a valid did:key. -// d. The a public key can be extracted from the did:key. -// e. The public key type is supported by go-ucan. -// f. The Signature can be decoded per the VarsigHeader. -// g. The SigPayload can be verified using the Signature and public key. -// h. The field key of the TokenPayload matches the expected tag. -// 2. When the invocation is created or passes step one: -// a. The Issuer field is not empty. -// b. The Subject field is not empty -// c. The Command field is not empty and the Command is not a wildcard. -// d. The Policy field is present (but may be empty). -// e. The Arguments field is present (but may be empty). -// 3. When an unsealed invocation passes steps one and two for execution: -// a. The invocation can not be expired. -// b. Invoked at should not be in the future. -// 4. When the proof chain is being validated: -// 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 token must not be expired (expiration in the future or absent). -// e. The token must be active (nbf in the past or absent). -// f. The Issuer of each delegation must be the Audience in the next -// one. -// g. The last token must be a root delegation. -// h. The Subject of each delegation must equal the invocation's -// Audience field. -// i. The command of each delegation must "allow" the one before it. -// 5. If steps 1-4 pass: -// a. The policy must "match" the arguments. -// b. The nonce (if present) is not reused. -// // [envelope]: https://github.com/ucan-wg/spec#envelope // [invocation]: https://github.com/ucan-wg/invocation package invocation @@ -152,6 +113,7 @@ func (t *Token) ExecutionAllowedWithArgsHook(loader DelegationLoader, hook func( func (t *Token) executionAllowed(loader DelegationLoader, arguments *args.Args) (bool, error) { delegations, err := t.loadProofs(loader) if err != nil { + // All referenced delegations must be available - 4b return false, err } @@ -238,7 +200,7 @@ func (t *Token) IsValidNow() bool { // IsValidNow verifies that the token can be used at the given time, based on expiration or "not before" fields. // This does NOT do any other kind of verifications. func (t *Token) IsValidAt(ti time.Time) bool { - if t.expiration == nil && ti.After(*t.expiration) { + if t.expiration != nil && ti.After(*t.expiration) { return false } return true diff --git a/token/invocation/proof.go b/token/invocation/proof.go index 7791a2b..a644c17 100644 --- a/token/invocation/proof.go +++ b/token/invocation/proof.go @@ -11,6 +11,44 @@ import ( "github.com/ucan-wg/go-ucan/token/delegation" ) +// # Invocation token validation +// +// Per the specification, invocation Tokens must be validated before the command is executed. +// This validation effectively happens in multiple places in the codebase. +// Steps 1 and 2 are the same for all token types. +// +// 1. When a token is read/unsealed from its containing envelope (`envelope` package): +// a. The envelope can be decoded. +// b. The envelope contains a Signature, VarsigHeader and Payload. +// c. The Payload contains an iss field that contains a valid `did:key`. +// d. The public key can be extracted from the `did:key`. +// e. The public key type is supported by go-ucan. +// f. The Signature can be decoded per the VarsigHeader. +// g. The SigPayload can be verified using the Signature and public key. +// h. The field key of the TokenPayload matches the expected tag. +// +// 2. When the token is created or passes step one (token constructor or decoder): +// a. All required fields are present +// b. All populated fields respect their own rules (example: a policy is legal) +// +// 3. When an unsealed invocation passes steps one and two for execution (verifyTimeBound below): +// a. The invocation cannot be expired (expiration in the future or absent). +// b. All the delegation must not be expired (expiration in the future or absent). +// c. All the delegation must be active (nbf in the past or absent). +// +// 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. +// g. The command of each delegation must "allow" the one before it. +// +// 5. If steps 1-4 pass: +// 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) } @@ -19,7 +57,7 @@ type DelegationLoader interface { // - principal alignment // - command alignment func (t *Token) verifyProofs(delegations []*delegation.Token) error { - // There must be at least one delegation referenced + // There must be at least one delegation referenced - 4a if len(delegations) < 1 { return ErrNoProof } @@ -35,23 +73,26 @@ func (t *Token) verifyProofs(delegations []*delegation.Token) error { 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 first proof must be issued to the Invoker (audience DID). - 4c + // The Issuer of each delegation must be the Audience in the next one. - 4d if dlg.Audience() != iss { return fmt.Errorf("%w: delegation %s, expected %s, got %s", ErrBrokenChain, dlgCid, iss, dlg.Audience()) } iss = dlg.Issuer() + // 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) } cmd = dlg.Command() } - // The last prf value must be a root delegation (have the issuer field - // match the Subject field) - 4g + // The last prf value must be a root delegation (have the issuer field match the Subject field) - 4e if last := delegations[len(delegations)-1]; last.Issuer() != last.Subject() { return fmt.Errorf("%w: expected %s, got %s", ErrLastNotRoot, last.Subject(), last.Issuer()) } @@ -64,9 +105,16 @@ func (t *Token) verifyTimeBound(dlgs []*delegation.Token) error { } func (t *Token) verifyTimeBoundAt(at time.Time, delegations []*delegation.Token) error { + // The invocation cannot be expired (expiration in the future or absent). - 3a + if !t.IsValidAt(at) { + return fmt.Errorf("%w: invocation", ErrTokenInvalidNow) + } + for i, dlgCid := range t.proof { dlg := delegations[i] + // All the delegation must not be expired (expiration in the future or absent). - 3b + // All the delegation must be active (nbf in the past or absent). - 3c if !dlg.IsValidAt(at) { return fmt.Errorf("%w: delegation %s", ErrTokenInvalidNow, dlgCid) } From 1098e76cba8cab2d44909dcee411a5e35ba779e4 Mon Sep 17 00:00:00 2001 From: Steve Moyer Date: Tue, 19 Nov 2024 14:35:46 -0500 Subject: [PATCH 10/13] test(invocation): add command.Covers and subject consistency tests Also improve the maintainability of the tests by a) providing a set of fixed Personas and then generating a slew of valid delegation tokens, invalid delegation tokens and proof-chains thereof. --- did/didtest/crypto.go | 149 ++++++++ go.mod | 1 + go.sum | 2 + pkg/policy/policytest/policy.go | 20 ++ token/delegation/delegationtest/README.md | 5 + .../delegationtest/data/TokenAliceBob.dagcbor | Bin 0 -> 333 bytes .../delegationtest/data/TokenBobCarol.dagcbor | Bin 0 -> 333 bytes .../delegationtest/data/TokenCarolDan.dagcbor | Bin 0 -> 333 bytes ...okenCarolDanInvalidExpandedCommand.dagcbor | Bin 0 -> 325 bytes .../data/TokenCarolDanInvalidExpired.dagcbor | Bin 0 -> 337 bytes .../data/TokenCarolDanInvalidInactive.dagcbor | Bin 0 -> 342 bytes .../data/TokenCarolDanInvalidSubject.dagcbor | Bin 0 -> 333 bytes ...okenCarolDanValidAttenuatedCommand.dagcbor | Bin 0 -> 345 bytes .../delegationtest/data/TokenDanErin.dagcbor | Bin 0 -> 333 bytes ...TokenDanErinInvalidExpandedCommand.dagcbor | Bin 0 -> 325 bytes .../data/TokenDanErinInvalidExpired.dagcbor | Bin 0 -> 337 bytes .../data/TokenDanErinInvalidInactive.dagcbor | Bin 0 -> 342 bytes .../data/TokenDanErinInvalidSubject.dagcbor | Bin 0 -> 333 bytes ...TokenDanErinValidAttenuatedCommand.dagcbor | Bin 0 -> 345 bytes .../data/TokenErinFrank.dagcbor | Bin 0 -> 333 bytes ...kenErinFrankInvalidExpandedCommand.dagcbor | Bin 0 -> 325 bytes .../data/TokenErinFrankInvalidExpired.dagcbor | Bin 0 -> 337 bytes .../TokenErinFrankInvalidInactive.dagcbor | Bin 0 -> 342 bytes .../data/TokenErinFrankInvalidSubject.dagcbor | Bin 0 -> 333 bytes ...kenErinFrankValidAttenuatedCommand.dagcbor | Bin 0 -> 345 bytes token/delegation/delegationtest/doc.go | 33 ++ .../delegationtest/generator_test.go | 224 ++++++++++++ token/delegation/delegationtest/token.go | 120 +++++++ token/delegation/delegationtest/token_gen.go | 240 +++++++++++++ token/delegation/delegationtest/token_test.go | 32 ++ token/invocation/invocation_test.go | 321 ++++-------------- token/invocation/invocationtest/field.go | 39 --- 32 files changed, 891 insertions(+), 295 deletions(-) create mode 100644 did/didtest/crypto.go create mode 100644 pkg/policy/policytest/policy.go create mode 100644 token/delegation/delegationtest/README.md create mode 100644 token/delegation/delegationtest/data/TokenAliceBob.dagcbor create mode 100644 token/delegation/delegationtest/data/TokenBobCarol.dagcbor create mode 100644 token/delegation/delegationtest/data/TokenCarolDan.dagcbor create mode 100644 token/delegation/delegationtest/data/TokenCarolDanInvalidExpandedCommand.dagcbor create mode 100644 token/delegation/delegationtest/data/TokenCarolDanInvalidExpired.dagcbor create mode 100644 token/delegation/delegationtest/data/TokenCarolDanInvalidInactive.dagcbor create mode 100644 token/delegation/delegationtest/data/TokenCarolDanInvalidSubject.dagcbor create mode 100644 token/delegation/delegationtest/data/TokenCarolDanValidAttenuatedCommand.dagcbor create mode 100644 token/delegation/delegationtest/data/TokenDanErin.dagcbor create mode 100644 token/delegation/delegationtest/data/TokenDanErinInvalidExpandedCommand.dagcbor create mode 100644 token/delegation/delegationtest/data/TokenDanErinInvalidExpired.dagcbor create mode 100644 token/delegation/delegationtest/data/TokenDanErinInvalidInactive.dagcbor create mode 100644 token/delegation/delegationtest/data/TokenDanErinInvalidSubject.dagcbor create mode 100644 token/delegation/delegationtest/data/TokenDanErinValidAttenuatedCommand.dagcbor create mode 100644 token/delegation/delegationtest/data/TokenErinFrank.dagcbor create mode 100644 token/delegation/delegationtest/data/TokenErinFrankInvalidExpandedCommand.dagcbor create mode 100644 token/delegation/delegationtest/data/TokenErinFrankInvalidExpired.dagcbor create mode 100644 token/delegation/delegationtest/data/TokenErinFrankInvalidInactive.dagcbor create mode 100644 token/delegation/delegationtest/data/TokenErinFrankInvalidSubject.dagcbor create mode 100644 token/delegation/delegationtest/data/TokenErinFrankValidAttenuatedCommand.dagcbor create mode 100644 token/delegation/delegationtest/doc.go create mode 100644 token/delegation/delegationtest/generator_test.go create mode 100644 token/delegation/delegationtest/token.go create mode 100644 token/delegation/delegationtest/token_gen.go create mode 100644 token/delegation/delegationtest/token_test.go delete mode 100644 token/invocation/invocationtest/field.go diff --git a/did/didtest/crypto.go b/did/didtest/crypto.go new file mode 100644 index 0000000..b06928e --- /dev/null +++ b/did/didtest/crypto.go @@ -0,0 +1,149 @@ +// Package didtest provides Personas that can be used for testing. Each +// Persona has a name, crypto.PrivKey and associated crypto.PubKey and +// did.DID. +package didtest + +import ( + "sync" + "testing" + + "github.com/libp2p/go-libp2p/core/crypto" + "github.com/stretchr/testify/require" + "github.com/ucan-wg/go-ucan/did" +) + +const ( + alicePrivKeyCfg = "CAESQHdNJLBBiuc1AdwPHBkubB2KS1p0cv2JEF7m8tfwtrcm5ajaYPm+XmVCmtcHOF2lGDlmaiDA7emfwD3IrcyES0M=" + bobPrivKeyCfg = "CAESQHBz+AIop1g+9iBDj+ufUc/zm9/ry7c6kDFO8Wl/D0+H63V9hC6s9l4npf3pYEFCjBtlR0AMNWMoFQKSlYNKo20=" + carolPrivKeyCfg = "CAESQPrCgkcHnYFXDT9AlAydhPECBEivEuuVx9dJxLjVvDTmJIVNivfzg6H4mAiPfYS+5ryVVUZTHZBzvMuvvvG/Ks0=" + danPrivKeyCfg = "CAESQCgNhzofKhC+7hW6x+fNd7iMPtQHeEmKRhhlduf/I7/TeOEFYAEflbJ0sAhMeDJ/HQXaAvsWgHEbJ3ZLhP8q2B0=" + erinPrivKeyCfg = "CAESQKhCJo5UBpQcthko8DKMFsbdZ+qqQ5oc01CtLCqrE90dF2GfRlrMmot3WPHiHGCmEYi5ZMEHuiSI095e/6O4Bpw=" + frankPrivKeyCfg = "CAESQDlXPKsy3jHh7OWTWQqyZF95Ueac5DKo7xD0NOBE5F2BNr1ZVxRmJ2dBELbOt8KP9sOACcO9qlCB7uMA1UQc7sk=" +) + +// Persona is a generic participant used for cryptographic testing. +type Persona int + +// The provided Personas were selected from the first few generic +// participants listed in this [table]. +// +// [table]: https://en.wikipedia.org/wiki/Alice_and_Bob#Cryptographic_systems +const ( + PersonaAlice Persona = iota + PersonaBob + PersonaCarol + PersonaDan + PersonaErin + PersonaFrank +) + +var ( + once sync.Once + privKeys = make(map[Persona]crypto.PrivKey, 6) + err error +) + +// DID returns a did.DID based on the Persona's Ed25519 public key. +func (p Persona) DID(t *testing.T) did.DID { + t.Helper() + + did, err := did.FromPrivKey(p.PrivKey(t)) + require.NoError(t, err) + + return did +} + +// Name returns the username of the Persona. +func (p Persona) Name(t *testing.T) string { + t.Helper() + + name, ok := map[Persona]string{ + PersonaAlice: "Alice", + PersonaBob: "Bob", + PersonaCarol: "Carol", + PersonaDan: "Dan", + PersonaErin: "Erin", + PersonaFrank: "Frank", + }[p] + if !ok { + t.Fatal("Unknown persona:", p) + } + + return name +} + +// PrivKey returns the Ed25519 private key for the Persona. +func (p Persona) PrivKey(t *testing.T) crypto.PrivKey { + t.Helper() + + once.Do(func() { + for persona, privKeyCfg := range privKeyCfgs(t) { + privKeyMar, err := crypto.ConfigDecodeKey(privKeyCfg) + if err != nil { + return + } + + privKey, err := crypto.UnmarshalPrivateKey(privKeyMar) + if err != nil { + return + } + + privKeys[persona] = privKey + } + }) + require.NoError(t, err) + + return privKeys[p] +} + +// PrivKeyConfig returns the marshaled and encoded Ed25519 private key +// for the Persona. +func (p Persona) PrivKeyConfig(t *testing.T) string { + t.Helper() + + return privKeyCfgs(t)[p] +} + +// PubKey returns the Ed25519 public key for the Persona. +func (p Persona) PubKey(t *testing.T) crypto.PubKey { + t.Helper() + + return p.PrivKey(t).GetPublic() +} + +// PubKeyConfig returns the marshaled and encoded Ed25519 public key +// for the Persona. +func (p Persona) PubKeyConfig(t *testing.T) string { + pubKeyMar, err := crypto.MarshalPublicKey(p.PrivKey(t).GetPublic()) + require.NoError(t, err) + + return crypto.ConfigEncodeKey(pubKeyMar) +} + +func privKeyCfgs(t *testing.T) map[Persona]string { + t.Helper() + + return map[Persona]string{ + PersonaAlice: alicePrivKeyCfg, + PersonaBob: bobPrivKeyCfg, + PersonaCarol: carolPrivKeyCfg, + PersonaDan: danPrivKeyCfg, + PersonaErin: erinPrivKeyCfg, + PersonaFrank: frankPrivKeyCfg, + } +} + +// Personas returns an (alphabetically) ordered list of the defined +// Persona values. +func Personas(t *testing.T) []Persona { + t.Helper() + + return []Persona{ + PersonaAlice, + PersonaBob, + PersonaCarol, + PersonaDan, + PersonaErin, + PersonaFrank, + } +} diff --git a/go.mod b/go.mod index 2193ff4..51d0d0e 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ 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 diff --git a/go.sum b/go.sum index 82932d5..b7534ef 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ 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= diff --git a/pkg/policy/policytest/policy.go b/pkg/policy/policytest/policy.go new file mode 100644 index 0000000..4afd5f3 --- /dev/null +++ b/pkg/policy/policytest/policy.go @@ -0,0 +1,20 @@ +// Package policytest provides values and functions that are useful when +// testing code that relies on Policies. +package policytest + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/ucan-wg/go-ucan/pkg/policy" +) + +// EmptyPolicy provides a policy with no statements for testing purposes. +func EmptyPolicy(t *testing.T) policy.Policy { + t.Helper() + + pol, err := policy.FromDagJson("[]") + require.NoError(t, err) + + return pol +} diff --git a/token/delegation/delegationtest/README.md b/token/delegation/delegationtest/README.md new file mode 100644 index 0000000..406875d --- /dev/null +++ b/token/delegation/delegationtest/README.md @@ -0,0 +1,5 @@ +# delegationtest + +See the package documentation for instructions on how to use the generated +tokens as well as information on how to regenerate the code if changes have +been made. diff --git a/token/delegation/delegationtest/data/TokenAliceBob.dagcbor b/token/delegation/delegationtest/data/TokenAliceBob.dagcbor new file mode 100644 index 0000000000000000000000000000000000000000..2122c4d06d05135f9e6fe8dc1190f77b9d9c26c4 GIT binary patch literal 333 zcmZpQa7bSe8vXk4wZ~;2uPrZLpkRK=FL;;onM*%!$uC>-bD_n)!c7Yt=F6Jilj43` z{WtBdInP-iKK7cs@3x*utP5Bd&AKQt!^Pw+V_|V=a$=r-N=~|ip`L-Bfo@T@u%_T-T_m$fWela>M-65W{>RQ5@ptE2mt$d)xvz0SazTDhLvnFx68T1?=H=%lr}{83 PGBLBTvaxe;a&ZFydZ2+A literal 0 HcmV?d00001 diff --git a/token/delegation/delegationtest/data/TokenBobCarol.dagcbor b/token/delegation/delegationtest/data/TokenBobCarol.dagcbor new file mode 100644 index 0000000000000000000000000000000000000000..8d68e2bd1b8a69edd800e8ae047f5bd0ad66da11 GIT binary patch literal 333 zcmZpQa0s1sLC5DL=fWd_tYY6Jg*}4S_6r%A7Mrd97uqj$<7>cawPz(4q&KW9ykvN- z_V!zosfnrW^98>e{JXaQ*38>inHMEyxR|_UEG#ZfPR!F!$w_xG)HBdC&@D>VGhCjW zSejB{k&>BWm7Q8?Rb}RzonBgyoRMMbQC{kim=SCqSzPIAk>pdFXy{vHQDBtm?H80$ zSr%Lti-LN%haG&|dVGhCjW zSejB{k&>BWm7Q8?Rb}RzotI=@ndOt8SZV5>=;f1bQEBLEW|kk2?d)ilnq{2rXr7vF zk!6}2WLl7&oSdASQmCI=QIMFIlA5BQm!F%Nmza|b6!?~$SzL^2aC&J$az=)!M|r77 zVn(oeWO1ddMUqcxqM>h*MS)SKw_i|3Wm#}pkYllBUUETxPD65WX%eatrKQ=~M#b4K z5k7%==6>GBt{IkDzIiE;X=N@>6&2Yf=>?f7UXF!H=Dx-Ssd@Q%$*DdJj7-cdtZeKY HoLt-h`u%%n literal 0 HcmV?d00001 diff --git a/token/delegation/delegationtest/data/TokenCarolDanInvalidExpandedCommand.dagcbor b/token/delegation/delegationtest/data/TokenCarolDanInvalidExpandedCommand.dagcbor new file mode 100644 index 0000000000000000000000000000000000000000..c8bb4b36b745345ce736269161e913a34c90a5df GIT binary patch literal 325 zcmZpQaQJDs=h*EN!AI;r?oeH~nrXx2`{ADgrtdCY*pj2Kw(Y&Zu021Ua@YU=r_xj1 zaCXOvRJSGCzRYT$1hZamHu@Mak$q8OhKtEt#=_##-2Lp=jM1KpxzJ;UY6 ziKQtO7Acu2R@teQR#j%c*?CFkm03RdiIt}AiC#Y07L|ssW@h;T+0Kq;saeL^j^?S^ z7FnjbL8b-S$;rvNDVh4I6$OcTDXA&RK*qP^%;I8H1Jg?jk~1<)J<3Zx5;KC$Ba16t zEs}gn6AgWfEDDS=z5RkRD$9b)f*gx2^O6hla~hJ1OOsHIC@sy-HY(0`iSP-`Gxzg0 zcFnNN^36+$Oe=G7s;I~=NiWDu@p3FoGWRtuNX^U7OHTD+U}R!uVP#|I;N;>40H(Ek A=Kufz literal 0 HcmV?d00001 diff --git a/token/delegation/delegationtest/data/TokenCarolDanInvalidExpired.dagcbor b/token/delegation/delegationtest/data/TokenCarolDanInvalidExpired.dagcbor new file mode 100644 index 0000000000000000000000000000000000000000..09d95dcca90d7ecd2474493649d8c9d752dc9e7e GIT binary patch literal 337 zcmZpQaF}#CL`ctjOY+TQU8>tEK6312Ub@t<=;>Pb@7o^NF8WzyI8&ne-1oM{n)?kB zjV6Em^~`qJ3grWOpS7GHz21I*3g@E43>TBPjD^Le$%%RTDLLs5hI$5i2D(MbdWOrB z6H8MnEK)L4tg=%pt*Xp?v-6V7E3vularHkQwsG{D+&_xQc_d&^YU{u^AdBCfdW$LHkb60GmDE+4NosENY2PG z^(Zg(NX!T}k1VcqwMg$S z+o(9(CBi2#&)m=3*fqm4%Qr72GOf(TsiGpgB)uRr#mliU$=uhtAT=*PFFDnRfsu)s Mg_Vt+gOiIJ0Ajs?3IG5A literal 0 HcmV?d00001 diff --git a/token/delegation/delegationtest/data/TokenCarolDanInvalidInactive.dagcbor b/token/delegation/delegationtest/data/TokenCarolDanInvalidInactive.dagcbor new file mode 100644 index 0000000000000000000000000000000000000000..fae7e2195ab6cd4f0c8dcea375c30e475209abc0 GIT binary patch literal 342 zcmZpQaEQ#{w0wBm`kazpY+dK?GQl31T#q2%vI#~@nw z2sV!_u5`6X@+nO;^ewU|Fv|4y3(BZ03oZ+CEVj%`&Pz&@+9N64kX(?T(~w+TnuKar zX=!%0QE|3Qgim0exu3VOYldZ(Z(d4dTA7PeMMZW=dO>E2mt$d)xvz0SYF>U`a;gsl PBNH)^cTzb<{EJJ^*&Gqe_&Nc}tkJ35stT9q_zOY?% z&bRsRd*7B-O6}ZUq93Sk`(xv2{zZuyE+%go3yVvW6Z7;_a?%|P^$he3bc>Sp43{S- zmZnr#q-3U8Wv5nJRhju_=Ovj}X8GhNR+_pedii8qR2sUPndJv$J3E@CW*KKYnx|%4 zWSQm$nHFRxCnx8o6zZo|6eQ-Qq^9WS<>zMRCFUdp1->O`78j!$oL*XxoRMMbQC{ki zm=SCqSzPIAk>pdFXy{vHQDBtm?H80$Sr%Lt;kaJ1@z+GRr4FvC`B%(aR^>qSDaS%q%}3+u6}9HOn~L(L6QV zBFi*4$h06kIXO8ur9wtOwW1&~FC{faKQBKwGcPemKe41FHLoBHLT{A4ReDhKw)5=_&Dk`!|(hD+Eyc`RY%zcduQuFfj Tl2d&c7@3$^SlQS)IJvk1e6fRG literal 0 HcmV?d00001 diff --git a/token/delegation/delegationtest/data/TokenDanErin.dagcbor b/token/delegation/delegationtest/data/TokenDanErin.dagcbor new file mode 100644 index 0000000000000000000000000000000000000000..baaac2e0e316a0ff2af3946baf9adf359c462c6f GIT binary patch literal 333 zcmZpQaL8MHZ_(ouQZCn2UYJ+3wK6!*J=XQ~1n)d;4=91``l!{+#zQum7 zXq~x|*ZJH0{qh?Gt+;0^oxiJF%eE*n!^Pw+V_|V=a$=r-N=~|ip`L-Bfo@T~y1uTx0V}r?67r-26P(h|m7imD3VsQf6C zz+@Mn{0vL4q~zq>ltTT~ih{(vl++acy!_nEyu_Sjpuo4}%;I8HgY%NiE3vulamYba~hJ1OOsHIC@sy-HY(0` ziSP-`Gxzg0cFnNN^36+$Oe=G7s;I~=NiWDu@p3FoGWRtuNX^U7OHTD+U}R!uVP#|I I;N;>40O9a{)c^nh literal 0 HcmV?d00001 diff --git a/token/delegation/delegationtest/data/TokenDanErinInvalidExpandedCommand.dagcbor b/token/delegation/delegationtest/data/TokenDanErinInvalidExpandedCommand.dagcbor new file mode 100644 index 0000000000000000000000000000000000000000..777c0aaee08ed24ea1908cd3bbeb8d511994fafe GIT binary patch literal 325 zcmZpQaCrXegM19b{ux_~Ki+Ozm#}(i=3M`%q;k7_kF6Jk_PjqAzdG`>&!6C|>UAtE z>4EYL_Zt=+esIuQk2l5bM{n#r?nQ|iE+%go3yVvW6Z7;_a?%|P^$he3bc>Sp43{S- zmZnr#q-3U8Wv5nJRhju_ryE7&8k<)-g_Zi|=I6Oaltw0-o0KL+dOPM;R8{y!mWa_6@6eQ-Qq^2YT8Q+pKi;GbW%u6z_%<{=ktTc5`^zzBJs5Epn zGs_Ric6KyN%`(n*G*8X8$TH0hGA+nXPA0s5sjt!Y44#+|S$C zHN!H?H!meJt<1%#q9VH_y&yBi%ds%Y+}F4uH7`FeIn{@Ok%^gwm5rT)lZzVwM$CF( literal 0 HcmV?d00001 diff --git a/token/delegation/delegationtest/data/TokenDanErinInvalidExpired.dagcbor b/token/delegation/delegationtest/data/TokenDanErinInvalidExpired.dagcbor new file mode 100644 index 0000000000000000000000000000000000000000..ba90840528194bed2e6f34278b25790b0b0cf819 GIT binary patch literal 337 zcmZpQaHz{%VJ!2j-chUWZP($%8&5b5rG8zSQ2$l5BW+#9p44?umWx)`-kJV*`6N~E z_%~nosRw=U)A?FIMe+1g8>60h&P9nCE+%go3yVvW6Z7;_a?%|P^$he3bc>Sp43{S- zmZnr#q-3U8Wv5nJRhju_ryE7&8k<)-g_Zi|=I6Oaltw0-o0KL+dOPM;R8{y!m6zZo|6eQ-Qq^9WS<>zMRCFUdp1*Fn#F6kv_78j!$o|j}^ndOt8 zSZV5>=;f1bQEBLEW|kk2?d)ilnq{2rXr7vFk!6}2WLl7&oLrEf(~w+TnuKaZX=!%0 zQE|3Qgim0exu3VOYldZ(Z(d4dTA7PeMMZW=dO>E2mt$d)xvz0SYF>U`a;gslBNH%eckj||u6+fr-}N)PcRb!A7Fx@2pSxzscLs*i*DuJKf#)l zbF_BiE7mVxoU`{{I_2|hi_(OJ+=~)3Tuk0F78aK#C+6v=KW)6=oTgG8Lmi9 zEKRAfNXbmG%1*7csxtG0vADb!D`C`impNlnqu%g@csOUy|I3Vch>EG|YhI4{Y(GRr4FvC`B% z(aR^>qSDaS%q%}3+u6}9HOn~L(L6QVBFi*4$h06kIXN#WO=^#%a6@uIeojMjacL5& zS*4}f*+#|LE)hO~dFFoJ#;zHbS-yEGk!fWvP8Ai|CFup3DPE3+N#?%B1*v)YdC93h Q42(?7EUawo9GqO-0K>C@tN;K2 literal 0 HcmV?d00001 diff --git a/token/delegation/delegationtest/data/TokenDanErinInvalidSubject.dagcbor b/token/delegation/delegationtest/data/TokenDanErinInvalidSubject.dagcbor new file mode 100644 index 0000000000000000000000000000000000000000..ba61f8d33054241db7356a140880ce0cd925d912 GIT binary patch literal 333 zcmZpQa9H@^-s{+&!WT(7n8S)g~g@GiFx`dIq43DdIow1x<$!)hRc%^ zOH(Q=QZiGlvQsOqs?2<|(~Tl>jm;~a!b*K}^YdIIN+XlaO-hp@y&ZEasw#Y=@}o=w zlU;oBGc3K5l9O{&3iVSf3KH{DQd9Kv@^dru5_6J)0^gD|i;GbW&Py_{%<{=ktTc5` z^zzBJs5EpnGs_Ric6KyN%`(n*G*8X8$TH0hGA+nXPAw`*`Y8 GaRUHNG=8lB literal 0 HcmV?d00001 diff --git a/token/delegation/delegationtest/data/TokenDanErinValidAttenuatedCommand.dagcbor b/token/delegation/delegationtest/data/TokenDanErinValidAttenuatedCommand.dagcbor new file mode 100644 index 0000000000000000000000000000000000000000..639958545ec1698075a0c3b7c53f4427bad73755 GIT binary patch literal 345 zcmZpQaOh3!N=fh4xiMcd)k?rbKjQ8FYx_C){(hcgsJz13JN23S|C(pSp43{S- zmZnr#q-3U8Wv5nJRhju_ryE7&8k<)-g_Zi|=I6Oaltw0-o0KL+dOPM;R8{y!mRLJP3Rum-WrKG0l=jG>S<|XFnCzh0?=9MOvq^2YT<-R3n78j#h zl9yy&ndOt8SZV5>=;f1bQEBLEW|kk2?d)ilnq{2rXr7vFk!6}2WLl7&oLrEf(~w+T znuKaZX=!%0QE|3Qgim0exu3VOYldZ(Z(d4dTA7PeMMZW=dO>E2mt$d)xvz0SYF>U` Ta;gslBNHm;lGi0(MoQrt*-xYMVcq2;5(D%Cl$ft;X#_D*~k~#Md0}UCRH{ z%vf)R^{E@nms@PmOxUj!cc#gWVNqg+i^*HY!s628#610!oOB06Jp(-h-J)bY!{y0| zr70B_DVZr&*{PLQRc5}~nHJ>{ZlU3Ymd2q;Ie{fyNf>}xZ93N{j|DwbU7n8S)g~g@GiFx`dIq43DdIow1x<$!)hRc%^ zOH(Q=QZiGlvQsOqs?2<|GcC#^+(N?(EsaBy%CiC!3o9a>-Q1FsOH(6K%DjDY%Y!}A zDtx_tj04=Alaq5(GWAm{3KH{DQd5$FjBm-A#l@%wrW-}%8k<)-g_Zi|=I6Oaltw0- zo0KL+dOPM;R8{y!VGhCjW zSejB{k&>BWm7Q8?Rb}RzooP`X;T9TRXlWdpRGt->SXdG1?BGBt{IkDzIiE;X=N@>6&2Yf=>?f7UXF!H=Dx-Ssd@Q%$*DdJj7-cd LtZeKYoLt-h2%UcO literal 0 HcmV?d00001 diff --git a/token/delegation/delegationtest/data/TokenErinFrankInvalidInactive.dagcbor b/token/delegation/delegationtest/data/TokenErinFrankInvalidInactive.dagcbor new file mode 100644 index 0000000000000000000000000000000000000000..ae90da155a2303bfdc6eb65e89aa1a30a3f21975 GIT binary patch literal 342 zcmZpQa8UX&XMIVGhC6J zSejB{k&>BWm7Q8?Rb}RzooP`X;T9TRXlWdpRGt->SXdG1?B|vyF|H@D>E($t8QGH;*U@?ej& z3SVy@;{bQ(T(~Tl>jm;~a!b*K} z^YdIIN+XlaO-hp@y&ZEasw#Y=@}o=wlU;oBGc3K5k_+;48j_1klTeK)^9smyjf#p) zO3y4e%r6Zw%m)(1A#P3{Ucn{BmiYx4Va0yw$w9f16``ql`FY8yJ`9XZ%q*;I>>Qk2 F+yFQid$j-n literal 0 HcmV?d00001 diff --git a/token/delegation/delegationtest/data/TokenErinFrankValidAttenuatedCommand.dagcbor b/token/delegation/delegationtest/data/TokenErinFrankValidAttenuatedCommand.dagcbor new file mode 100644 index 0000000000000000000000000000000000000000..822aa18f24481a40f1359ccd5ecac1704f3fd8d7 GIT binary patch literal 345 zcmZpQaL94-jnm*PR#YmAJ`t(0OeyfQ(y4sb>AdWbevepH_p0o+P-T~pSKc1#;m0JY zaLagA$*+`ok6xE2ZO!}i>h=}pMTr?MCT|%Fi%XLe^Yl}4(j5%-4D<|ii<0#WmnSEd zrc_v@WTseUr&d~3nfYdCT9ik)g@zYe8iyv8X9Xq}Rzy0xxg{r;rbeWcdHdv+2YaMd z_;YC+DVA$mpk56eQ-Qq^9WS<>zMRCFbZSmXxICl_r*?rX&O9z9nZC7o%E| zZWNJgY+mUUR_dFZpXVA;8kuZvQkoR$?U-9pRpA?zA7v7l?BbK3Vd<5WT#%pBkX&4v zgla@-X?C_zakfi@Phg(8pSQ7VhGmv-UP@$InTu0JMRrMgL1v1VV_}lHuW> Date: Wed, 20 Nov 2024 12:34:24 +0100 Subject: [PATCH 11/13] 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 From aea1880386d8801e9988be8c5177b41bdc043e81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Wed, 20 Nov 2024 14:55:48 +0100 Subject: [PATCH 12/13] tests: lots of small asjustement --- did/didtest/crypto.go | 114 ++++----- pkg/policy/policytest/policy.go | 20 -- .../data/TokenCarolDanInvalidExpired.dagcbor | Bin 337 -> 0 bytes ...enCarolDan_InvalidExpandedCommand.dagcbor} | Bin .../data/TokenCarolDan_InvalidExpired.dagcbor | Bin 0 -> 337 bytes ... => TokenCarolDan_InvalidInactive.dagcbor} | Bin ...r => TokenCarolDan_InvalidSubject.dagcbor} | Bin ...enCarolDan_ValidAttenuatedCommand.dagcbor} | Bin .../data/TokenDanErinInvalidExpired.dagcbor | Bin 337 -> 0 bytes ...kenDanErin_InvalidExpandedCommand.dagcbor} | Bin .../data/TokenDanErin_InvalidExpired.dagcbor | Bin 0 -> 337 bytes ...r => TokenDanErin_InvalidInactive.dagcbor} | Bin ...or => TokenDanErin_InvalidSubject.dagcbor} | Bin ...kenDanErin_ValidAttenuatedCommand.dagcbor} | Bin .../data/TokenErinFrankInvalidExpired.dagcbor | Bin 337 -> 0 bytes ...nErinFrank_InvalidExpandedCommand.dagcbor} | Bin .../TokenErinFrank_InvalidExpired.dagcbor | Bin 0 -> 337 bytes ...=> TokenErinFrank_InvalidInactive.dagcbor} | Bin ... => TokenErinFrank_InvalidSubject.dagcbor} | Bin ...nErinFrank_ValidAttenuatedCommand.dagcbor} | Bin token/delegation/delegationtest/doc.go | 8 +- token/delegation/delegationtest/generator.go | 227 ++++++++++++++++++ .../delegationtest/generator_test.go | 204 +--------------- token/delegation/delegationtest/token.go | 28 +-- token/delegation/delegationtest/token_gen.go | 150 ++++++------ token/delegation/delegationtest/token_test.go | 2 - token/invocation/invocation_test.go | 19 +- 27 files changed, 376 insertions(+), 396 deletions(-) delete mode 100644 pkg/policy/policytest/policy.go delete mode 100644 token/delegation/delegationtest/data/TokenCarolDanInvalidExpired.dagcbor rename token/delegation/delegationtest/data/{TokenCarolDanInvalidExpandedCommand.dagcbor => TokenCarolDan_InvalidExpandedCommand.dagcbor} (100%) create mode 100644 token/delegation/delegationtest/data/TokenCarolDan_InvalidExpired.dagcbor rename token/delegation/delegationtest/data/{TokenCarolDanInvalidInactive.dagcbor => TokenCarolDan_InvalidInactive.dagcbor} (100%) rename token/delegation/delegationtest/data/{TokenCarolDanInvalidSubject.dagcbor => TokenCarolDan_InvalidSubject.dagcbor} (100%) rename token/delegation/delegationtest/data/{TokenCarolDanValidAttenuatedCommand.dagcbor => TokenCarolDan_ValidAttenuatedCommand.dagcbor} (100%) delete mode 100644 token/delegation/delegationtest/data/TokenDanErinInvalidExpired.dagcbor rename token/delegation/delegationtest/data/{TokenDanErinInvalidExpandedCommand.dagcbor => TokenDanErin_InvalidExpandedCommand.dagcbor} (100%) create mode 100644 token/delegation/delegationtest/data/TokenDanErin_InvalidExpired.dagcbor rename token/delegation/delegationtest/data/{TokenDanErinInvalidInactive.dagcbor => TokenDanErin_InvalidInactive.dagcbor} (100%) rename token/delegation/delegationtest/data/{TokenDanErinInvalidSubject.dagcbor => TokenDanErin_InvalidSubject.dagcbor} (100%) rename token/delegation/delegationtest/data/{TokenDanErinValidAttenuatedCommand.dagcbor => TokenDanErin_ValidAttenuatedCommand.dagcbor} (100%) delete mode 100644 token/delegation/delegationtest/data/TokenErinFrankInvalidExpired.dagcbor rename token/delegation/delegationtest/data/{TokenErinFrankInvalidExpandedCommand.dagcbor => TokenErinFrank_InvalidExpandedCommand.dagcbor} (100%) create mode 100644 token/delegation/delegationtest/data/TokenErinFrank_InvalidExpired.dagcbor rename token/delegation/delegationtest/data/{TokenErinFrankInvalidInactive.dagcbor => TokenErinFrank_InvalidInactive.dagcbor} (100%) rename token/delegation/delegationtest/data/{TokenErinFrankInvalidSubject.dagcbor => TokenErinFrank_InvalidSubject.dagcbor} (100%) rename token/delegation/delegationtest/data/{TokenErinFrankValidAttenuatedCommand.dagcbor => TokenErinFrank_ValidAttenuatedCommand.dagcbor} (100%) create mode 100644 token/delegation/delegationtest/generator.go diff --git a/did/didtest/crypto.go b/did/didtest/crypto.go index b06928e..3d9de8d 100644 --- a/did/didtest/crypto.go +++ b/did/didtest/crypto.go @@ -4,27 +4,28 @@ package didtest import ( - "sync" + "fmt" "testing" "github.com/libp2p/go-libp2p/core/crypto" "github.com/stretchr/testify/require" + "github.com/ucan-wg/go-ucan/did" ) const ( - alicePrivKeyCfg = "CAESQHdNJLBBiuc1AdwPHBkubB2KS1p0cv2JEF7m8tfwtrcm5ajaYPm+XmVCmtcHOF2lGDlmaiDA7emfwD3IrcyES0M=" - bobPrivKeyCfg = "CAESQHBz+AIop1g+9iBDj+ufUc/zm9/ry7c6kDFO8Wl/D0+H63V9hC6s9l4npf3pYEFCjBtlR0AMNWMoFQKSlYNKo20=" - carolPrivKeyCfg = "CAESQPrCgkcHnYFXDT9AlAydhPECBEivEuuVx9dJxLjVvDTmJIVNivfzg6H4mAiPfYS+5ryVVUZTHZBzvMuvvvG/Ks0=" - danPrivKeyCfg = "CAESQCgNhzofKhC+7hW6x+fNd7iMPtQHeEmKRhhlduf/I7/TeOEFYAEflbJ0sAhMeDJ/HQXaAvsWgHEbJ3ZLhP8q2B0=" - erinPrivKeyCfg = "CAESQKhCJo5UBpQcthko8DKMFsbdZ+qqQ5oc01CtLCqrE90dF2GfRlrMmot3WPHiHGCmEYi5ZMEHuiSI095e/6O4Bpw=" - frankPrivKeyCfg = "CAESQDlXPKsy3jHh7OWTWQqyZF95Ueac5DKo7xD0NOBE5F2BNr1ZVxRmJ2dBELbOt8KP9sOACcO9qlCB7uMA1UQc7sk=" + alicePrivKeyB64 = "CAESQHdNJLBBiuc1AdwPHBkubB2KS1p0cv2JEF7m8tfwtrcm5ajaYPm+XmVCmtcHOF2lGDlmaiDA7emfwD3IrcyES0M=" + bobPrivKeyB64 = "CAESQHBz+AIop1g+9iBDj+ufUc/zm9/ry7c6kDFO8Wl/D0+H63V9hC6s9l4npf3pYEFCjBtlR0AMNWMoFQKSlYNKo20=" + carolPrivKeyB64 = "CAESQPrCgkcHnYFXDT9AlAydhPECBEivEuuVx9dJxLjVvDTmJIVNivfzg6H4mAiPfYS+5ryVVUZTHZBzvMuvvvG/Ks0=" + danPrivKeyB64 = "CAESQCgNhzofKhC+7hW6x+fNd7iMPtQHeEmKRhhlduf/I7/TeOEFYAEflbJ0sAhMeDJ/HQXaAvsWgHEbJ3ZLhP8q2B0=" + erinPrivKeyB64 = "CAESQKhCJo5UBpQcthko8DKMFsbdZ+qqQ5oc01CtLCqrE90dF2GfRlrMmot3WPHiHGCmEYi5ZMEHuiSI095e/6O4Bpw=" + frankPrivKeyB64 = "CAESQDlXPKsy3jHh7OWTWQqyZF95Ueac5DKo7xD0NOBE5F2BNr1ZVxRmJ2dBELbOt8KP9sOACcO9qlCB7uMA1UQc7sk=" ) // Persona is a generic participant used for cryptographic testing. type Persona int -// The provided Personas were selected from the first few generic +// The provided Personas were selected from the first few generic // participants listed in this [table]. // // [table]: https://en.wikipedia.org/wiki/Alice_and_Bob#Cryptographic_systems @@ -37,26 +38,36 @@ const ( PersonaFrank ) -var ( - once sync.Once +var privKeys map[Persona]crypto.PrivKey + +func init() { privKeys = make(map[Persona]crypto.PrivKey, 6) - err error -) + for persona, privKeyCfg := range privKeyB64() { + privKeyMar, err := crypto.ConfigDecodeKey(privKeyCfg) + if err != nil { + return + } + + privKey, err := crypto.UnmarshalPrivateKey(privKeyMar) + if err != nil { + return + } + + privKeys[persona] = privKey + } +} // DID returns a did.DID based on the Persona's Ed25519 public key. -func (p Persona) DID(t *testing.T) did.DID { - t.Helper() - - did, err := did.FromPrivKey(p.PrivKey(t)) - require.NoError(t, err) - - return did +func (p Persona) DID() did.DID { + d, err := did.FromPrivKey(p.PrivKey()) + if err != nil { + panic(err) + } + return d } // Name returns the username of the Persona. -func (p Persona) Name(t *testing.T) string { - t.Helper() - +func (p Persona) Name() string { name, ok := map[Persona]string{ PersonaAlice: "Alice", PersonaBob: "Bob", @@ -66,78 +77,45 @@ func (p Persona) Name(t *testing.T) string { PersonaFrank: "Frank", }[p] if !ok { - t.Fatal("Unknown persona:", p) + panic(fmt.Sprintf("Unknown persona: %v", p)) } return name } // PrivKey returns the Ed25519 private key for the Persona. -func (p Persona) PrivKey(t *testing.T) crypto.PrivKey { - t.Helper() - - once.Do(func() { - for persona, privKeyCfg := range privKeyCfgs(t) { - privKeyMar, err := crypto.ConfigDecodeKey(privKeyCfg) - if err != nil { - return - } - - privKey, err := crypto.UnmarshalPrivateKey(privKeyMar) - if err != nil { - return - } - - privKeys[persona] = privKey - } - }) - require.NoError(t, err) - +func (p Persona) PrivKey() crypto.PrivKey { return privKeys[p] } -// PrivKeyConfig returns the marshaled and encoded Ed25519 private key -// for the Persona. -func (p Persona) PrivKeyConfig(t *testing.T) string { - t.Helper() - - return privKeyCfgs(t)[p] -} - // PubKey returns the Ed25519 public key for the Persona. -func (p Persona) PubKey(t *testing.T) crypto.PubKey { - t.Helper() - - return p.PrivKey(t).GetPublic() +func (p Persona) PubKey() crypto.PubKey { + return p.PrivKey().GetPublic() } // PubKeyConfig returns the marshaled and encoded Ed25519 public key // for the Persona. func (p Persona) PubKeyConfig(t *testing.T) string { - pubKeyMar, err := crypto.MarshalPublicKey(p.PrivKey(t).GetPublic()) + pubKeyMar, err := crypto.MarshalPublicKey(p.PrivKey().GetPublic()) require.NoError(t, err) return crypto.ConfigEncodeKey(pubKeyMar) } -func privKeyCfgs(t *testing.T) map[Persona]string { - t.Helper() - +func privKeyB64() map[Persona]string { return map[Persona]string{ - PersonaAlice: alicePrivKeyCfg, - PersonaBob: bobPrivKeyCfg, - PersonaCarol: carolPrivKeyCfg, - PersonaDan: danPrivKeyCfg, - PersonaErin: erinPrivKeyCfg, - PersonaFrank: frankPrivKeyCfg, + PersonaAlice: alicePrivKeyB64, + PersonaBob: bobPrivKeyB64, + PersonaCarol: carolPrivKeyB64, + PersonaDan: danPrivKeyB64, + PersonaErin: erinPrivKeyB64, + PersonaFrank: frankPrivKeyB64, } } // Personas returns an (alphabetically) ordered list of the defined // Persona values. -func Personas(t *testing.T) []Persona { - t.Helper() - +func Personas() []Persona { return []Persona{ PersonaAlice, PersonaBob, diff --git a/pkg/policy/policytest/policy.go b/pkg/policy/policytest/policy.go deleted file mode 100644 index 4afd5f3..0000000 --- a/pkg/policy/policytest/policy.go +++ /dev/null @@ -1,20 +0,0 @@ -// Package policytest provides values and functions that are useful when -// testing code that relies on Policies. -package policytest - -import ( - "testing" - - "github.com/stretchr/testify/require" - "github.com/ucan-wg/go-ucan/pkg/policy" -) - -// EmptyPolicy provides a policy with no statements for testing purposes. -func EmptyPolicy(t *testing.T) policy.Policy { - t.Helper() - - pol, err := policy.FromDagJson("[]") - require.NoError(t, err) - - return pol -} diff --git a/token/delegation/delegationtest/data/TokenCarolDanInvalidExpired.dagcbor b/token/delegation/delegationtest/data/TokenCarolDanInvalidExpired.dagcbor deleted file mode 100644 index 09d95dcca90d7ecd2474493649d8c9d752dc9e7e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 337 zcmZpQaF}#CL`ctjOY+TQU8>tEK6312Ub@t<=;>Pb@7o^NF8WzyI8&ne-1oM{n)?kB zjV6Em^~`qJ3grWOpS7GHz21I*3g@E43>TBPjD^Le$%%RTDLLs5hI$5i2D(MbdWOrB z6H8MnEK)L4tg=%pt*Xp?v-6V7E3vularHkQwsG{D+&_xQc_d&^YU{u^AdBCfdW$LHkb60GmDE+4NosENY2PG z^(Zg(NX!T}k1VcqwMg$S z+o(9(CBi2#&)m=3*fqm4%Qr72GOf(TsiGpgB)uRr#mliU$=uhtAT=*PFFDnRfsu)s Mg_Vt+gOiIJ0Ajs?3IG5A diff --git a/token/delegation/delegationtest/data/TokenCarolDanInvalidExpandedCommand.dagcbor b/token/delegation/delegationtest/data/TokenCarolDan_InvalidExpandedCommand.dagcbor similarity index 100% rename from token/delegation/delegationtest/data/TokenCarolDanInvalidExpandedCommand.dagcbor rename to token/delegation/delegationtest/data/TokenCarolDan_InvalidExpandedCommand.dagcbor diff --git a/token/delegation/delegationtest/data/TokenCarolDan_InvalidExpired.dagcbor b/token/delegation/delegationtest/data/TokenCarolDan_InvalidExpired.dagcbor new file mode 100644 index 0000000000000000000000000000000000000000..c67b5e2230da340ac9a723647aba761bd0ef6416 GIT binary patch literal 337 zcmZpQaCm=rW5#p2-6YzA^jn)idp@_c*RQcuVDvb;P8-hvQDl-??Bg zpKZ5GldCV^mgR17-`HmdMVM%&GA~NZa4~tySXf+|oS3Jdl9TRWsAr&Opj(uzXSh5$ zu{5Q^A|*4$Dm%5(s>;kaJ1@z+GRr4FvC`B%(aR^>qSDaS%q%}3+u6}9HOn~L(L6QV zBFi*4$h06kIXO8urBFY$q98FZB{fAqFF!XkFEJ+>C?J(?`_eZ#v$z=5@buDxBHLT{A4ReDhKw)5=_&Dk`!|(hD+Eyc`RY%zcduQuFfjl2d&c7@3$^ LSlQS)IJvk16f=Iw literal 0 HcmV?d00001 diff --git a/token/delegation/delegationtest/data/TokenCarolDanInvalidInactive.dagcbor b/token/delegation/delegationtest/data/TokenCarolDan_InvalidInactive.dagcbor similarity index 100% rename from token/delegation/delegationtest/data/TokenCarolDanInvalidInactive.dagcbor rename to token/delegation/delegationtest/data/TokenCarolDan_InvalidInactive.dagcbor diff --git a/token/delegation/delegationtest/data/TokenCarolDanInvalidSubject.dagcbor b/token/delegation/delegationtest/data/TokenCarolDan_InvalidSubject.dagcbor similarity index 100% rename from token/delegation/delegationtest/data/TokenCarolDanInvalidSubject.dagcbor rename to token/delegation/delegationtest/data/TokenCarolDan_InvalidSubject.dagcbor diff --git a/token/delegation/delegationtest/data/TokenCarolDanValidAttenuatedCommand.dagcbor b/token/delegation/delegationtest/data/TokenCarolDan_ValidAttenuatedCommand.dagcbor similarity index 100% rename from token/delegation/delegationtest/data/TokenCarolDanValidAttenuatedCommand.dagcbor rename to token/delegation/delegationtest/data/TokenCarolDan_ValidAttenuatedCommand.dagcbor diff --git a/token/delegation/delegationtest/data/TokenDanErinInvalidExpired.dagcbor b/token/delegation/delegationtest/data/TokenDanErinInvalidExpired.dagcbor deleted file mode 100644 index ba90840528194bed2e6f34278b25790b0b0cf819..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 337 zcmZpQaHz{%VJ!2j-chUWZP($%8&5b5rG8zSQ2$l5BW+#9p44?umWx)`-kJV*`6N~E z_%~nosRw=U)A?FIMe+1g8>60h&P9nCE+%go3yVvW6Z7;_a?%|P^$he3bc>Sp43{S- zmZnr#q-3U8Wv5nJRhju_ryE7&8k<)-g_Zi|=I6Oaltw0-o0KL+dOPM;R8{y!m6zZo|6eQ-Qq^9WS<>zMRCFUdp1*Fn#F6kv_78j!$o|j}^ndOt8 zSZV5>=;f1bQEBLEW|kk2?d)ilnq{2rXr7vFk!6}2WLl7&oLrEf(~w+TnuKaZX=!%0 zQE|3Qgim0exu3VOYldZ(Z(d4dTA7PeMMZW=dO>E2mt$d)xvz0SYF>U`a;gslBNH3j(FG|dCF?q{aSX`Q%n5UnTlkQ-sXP{@GTa>J4xI8(r zG^N5KB{Rh;JGIiP%FH)A-6$g0*u2sytkgF*KhHIyG&0%Tq%lelFQ06SN<&vOv;2T;XGgQtEaPlP^VDpMEYsW|(}L{eVGhCjW zSejB{k&>BWm7Q8?Rb}RzooP`X;T9TRXlWdpRGt->SXdG1?BGBt{IkDzIiE;X=N@>6&2Yf=>?f7UXF!H=Dx-Ssd@Q%$*DdJj7-cd LtZeKYoLt-h2%UcO diff --git a/token/delegation/delegationtest/data/TokenErinFrankInvalidExpandedCommand.dagcbor b/token/delegation/delegationtest/data/TokenErinFrank_InvalidExpandedCommand.dagcbor similarity index 100% rename from token/delegation/delegationtest/data/TokenErinFrankInvalidExpandedCommand.dagcbor rename to token/delegation/delegationtest/data/TokenErinFrank_InvalidExpandedCommand.dagcbor diff --git a/token/delegation/delegationtest/data/TokenErinFrank_InvalidExpired.dagcbor b/token/delegation/delegationtest/data/TokenErinFrank_InvalidExpired.dagcbor new file mode 100644 index 0000000000000000000000000000000000000000..ef55b22ca20ba3d320764a8b914a88eb4c0027de GIT binary patch literal 337 zcmZpQa5%j^+gjtV-l>g?7xc_r!FbH8^^}4Ui$wIFx4i~iVk;QzPi}Xab+FEKrNKGV z_IbO%@y+9Stp2^_+uz(ZmbzB=7#1aFxR|_UEG#ZfPR!F!$w_xG)HBdC&@D>VGhCjW zSejB{k&>BWm7Q8?Rb}RzooP`X;T9TRXlWdpRGt->SXdG1?BGBt{IkDzIiE;X=N@>6&2Yf=>?f7UXF!H=Dx-Ssd@Q%$*DdJj7-cd LtZeKYoLt-hqBwrL literal 0 HcmV?d00001 diff --git a/token/delegation/delegationtest/data/TokenErinFrankInvalidInactive.dagcbor b/token/delegation/delegationtest/data/TokenErinFrank_InvalidInactive.dagcbor similarity index 100% rename from token/delegation/delegationtest/data/TokenErinFrankInvalidInactive.dagcbor rename to token/delegation/delegationtest/data/TokenErinFrank_InvalidInactive.dagcbor diff --git a/token/delegation/delegationtest/data/TokenErinFrankInvalidSubject.dagcbor b/token/delegation/delegationtest/data/TokenErinFrank_InvalidSubject.dagcbor similarity index 100% rename from token/delegation/delegationtest/data/TokenErinFrankInvalidSubject.dagcbor rename to token/delegation/delegationtest/data/TokenErinFrank_InvalidSubject.dagcbor diff --git a/token/delegation/delegationtest/data/TokenErinFrankValidAttenuatedCommand.dagcbor b/token/delegation/delegationtest/data/TokenErinFrank_ValidAttenuatedCommand.dagcbor similarity index 100% rename from token/delegation/delegationtest/data/TokenErinFrankValidAttenuatedCommand.dagcbor rename to token/delegation/delegationtest/data/TokenErinFrank_ValidAttenuatedCommand.dagcbor diff --git a/token/delegation/delegationtest/doc.go b/token/delegation/delegationtest/doc.go index be389d1..cb54e64 100644 --- a/token/delegation/delegationtest/doc.go +++ b/token/delegation/delegationtest/doc.go @@ -7,14 +7,14 @@ // // Delegation proof-chain names contain each didtest.Persona name in // order starting with the root delegation (which will always be generated -// by Alice.) This is opposite of the list of cic.Cids that represent the +// by Alice). This is the opposite of the list of cic.Cids that represent the // proof chain. // // For both the generated delegation tokens granted to Carol's Persona and // the proof chains containing Carol's delegations to Dan, if there is no // suffix, the proof chain will be deemed valid. If there is a suffix, it // will consist of either the word "Valid" or "Invalid" and the name of the -// field that has been altered. Only optional fields will generate proof +// field that has been altered. Only optional fields will generate proof // chains with Valid suffixes. // // If changes are made to the list of Personas included in the chain, or @@ -25,9 +25,9 @@ // go test . -update // // Generated delegation Tokens are stored in the data/ directory and loaded -// into the DelegationLoader on the first call to GetDelegationLoader. +// into the delegation.Loader. // Generated references to these tokens and the tokens themselves are -// created in the token_gen.go file. See /token/invocation/invocation_test.go +// created in the token_gen.go file. See /token/invocation/invocation_test.go // for an example of how these delegation tokens and proof-chains can // be used during testing. package delegationtest diff --git a/token/delegation/delegationtest/generator.go b/token/delegation/delegationtest/generator.go new file mode 100644 index 0000000..06fcdc4 --- /dev/null +++ b/token/delegation/delegationtest/generator.go @@ -0,0 +1,227 @@ +package delegationtest + +import ( + "os" + "path/filepath" + "slices" + "time" + + "github.com/dave/jennifer/jen" + "github.com/ipfs/go-cid" + "github.com/libp2p/go-libp2p/core/crypto" + + "github.com/ucan-wg/go-ucan/did" + "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/token/delegation" +) + +const ( + tokenNamePrefix = "Token" + proorChainNamePrefix = "Proof" +) + +var constantNonce = []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b} + +type newDelegationParams struct { + privKey crypto.PrivKey + aud did.DID + sub did.DID + cmd command.Command + pol policy.Policy + opts []delegation.Option +} + +type token struct { + name string + id cid.Cid +} + +type proof struct { + name string + prf []cid.Cid +} + +type acc struct { + name string + chain []cid.Cid +} + +type variant struct { + name string + variant func(*newDelegationParams) +} + +func noopVariant() variant { + return variant{ + name: "", + variant: func(_ *newDelegationParams) {}, + } +} + +type generator struct { + dlgs []token + chains []proof +} + +func (g *generator) chainPersonas(personas []didtest.Persona, acc acc, vari variant) error { + acc.name += personas[0].Name() + + proofName := acc.name + if len(vari.name) > 0 { + proofName += "_" + vari.name + } + g.createProofChain(proofName, acc.chain) + + if len(personas) < 2 { + return nil + } + + name := personas[0].Name() + personas[1].Name() + + params := newDelegationParams{ + privKey: personas[0].PrivKey(), + aud: personas[1].DID(), + cmd: NominalCommand, + pol: policy.Policy{}, + opts: []delegation.Option{ + delegation.WithSubject(didtest.PersonaAlice.DID()), + delegation.WithNonce(constantNonce), + }, + } + + // Create each nominal token and continue the chain + id, err := g.createDelegation(params, name, vari) + if err != nil { + return err + } + acc.chain = append(acc.chain, id) + err = g.chainPersonas(personas[1:], acc, vari) + if err != nil { + return err + } + + // If the user is Carol, create variants for each invalid and/or optional + // parameter and also continue the chain + if personas[0] == didtest.PersonaCarol { + variants := []variant{ + {name: "InvalidExpandedCommand", variant: func(p *newDelegationParams) { + p.cmd = ExpandedCommand + }}, + {name: "ValidAttenuatedCommand", variant: func(p *newDelegationParams) { + p.cmd = AttenuatedCommand + }}, + {name: "InvalidSubject", variant: func(p *newDelegationParams) { + p.opts = append(p.opts, delegation.WithSubject(didtest.PersonaBob.DID())) + }}, + {name: "InvalidExpired", variant: func(p *newDelegationParams) { + // Note: this makes the generator not deterministic + p.opts = append(p.opts, delegation.WithExpiration(time.Now().Add(time.Second))) + }}, + {name: "InvalidInactive", variant: func(p *newDelegationParams) { + nbf, err := time.Parse(time.RFC3339, "2070-01-01T00:00:00Z") + if err != nil { + panic(err) + } + p.opts = append(p.opts, delegation.WithNotBefore(nbf)) + }}, + } + + // Start a branch in the recursion for each of the variants + for _, v := range variants { + id, err := g.createDelegation(params, name, v) + if err != nil { + return err + } + + // replace the previous Carol token id with the one from the variant + acc.chain[len(acc.chain)-1] = id + err = g.chainPersonas(personas[1:], acc, v) + if err != nil { + return err + } + } + } + return nil +} + +func (g *generator) createDelegation(params newDelegationParams, name string, vari variant) (cid.Cid, error) { + vari.variant(¶ms) + + tkn, err := delegation.New(params.privKey, params.aud, params.cmd, params.pol, params.opts...) + if err != nil { + return cid.Undef, err + } + + data, id, err := tkn.ToSealed(params.privKey) + if err != nil { + return cid.Undef, err + } + + dlgName := tokenNamePrefix + name + if len(vari.name) > 0 { + dlgName += "_" + vari.name + } + + err = os.WriteFile(filepath.Join(tokenDir, dlgName+tokenExt), data, 0o644) + if err != nil { + return cid.Undef, err + } + + g.dlgs = append(g.dlgs, token{ + name: dlgName, + id: id, + }) + + return id, nil +} + +func (g *generator) createProofChain(name string, prf []cid.Cid) { + if len(prf) < 1 { + return + } + + clone := make([]cid.Cid, len(prf)) + copy(clone, prf) + + g.chains = append(g.chains, proof{ + name: proorChainNamePrefix + name, + prf: clone, + }) +} + +func (g *generator) writeGoFile() error { + file := jen.NewFile("delegationtest") + file.HeaderComment("Code generated by delegationtest - DO NOT EDIT.") + + refs := map[cid.Cid]string{} + + 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() + } + + 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]) + } + }) + + file.Var().Id(c.name).Op("=").Index().Qual("github.com/ipfs/go-cid", "Cid").Values(g) + file.Line() + } + + return file.Save("token_gen.go") +} diff --git a/token/delegation/delegationtest/generator_test.go b/token/delegation/delegationtest/generator_test.go index f5878c2..2c79608 100644 --- a/token/delegation/delegationtest/generator_test.go +++ b/token/delegation/delegationtest/generator_test.go @@ -1,32 +1,14 @@ package delegationtest import ( - "os" - "path/filepath" - "slices" "testing" - "time" - "github.com/dave/jennifer/jen" - "github.com/ipfs/go-cid" - "github.com/libp2p/go-libp2p/core/crypto" "github.com/stretchr/testify/require" - "github.com/ucan-wg/go-ucan/did" - "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" "gotest.tools/v3/golden" -) -const ( - tokenNamePrefix = "Token" - proorChainNamePrefix = "Proof" + "github.com/ucan-wg/go-ucan/did/didtest" ) -var constantNonce = []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b} - // TestUpdate doesn't actually run a test but uses the Go testing library // to trigger generation of the delegation tokens and associated Go file. func TestUpdate(t *testing.T) { @@ -35,190 +17,10 @@ func TestUpdate(t *testing.T) { } } -type newDelegationParams struct { - privKey crypto.PrivKey - aud did.DID - sub did.DID - cmd command.Command - pol policy.Policy - opts []delegation.Option -} - -type newDelegationParamsVariant func(*newDelegationParams) - -type token struct { - name string - id cid.Cid -} - -type proof struct { - name string - prf []cid.Cid -} - -type generator struct { - dlgs []token - chains []proof -} - -type acc struct { - name string - chain []cid.Cid -} - -type variant struct { - name string - variant func(*newDelegationParams) -} - -func noopVariant() variant { - return variant{ - name: "", - variant: func(_ *newDelegationParams) {}, - } -} - func update(t *testing.T) { t.Helper() gen := &generator{} - gen.chainPersonas(t, didtest.Personas(t), acc{}, noopVariant()) - gen.writeGoFile(t) -} - -func (g *generator) chainPersonas(t *testing.T, personas []didtest.Persona, acc acc, vari variant) { - t.Helper() - - acc.name += personas[0].Name(t) - g.createProofChain(t, acc.name+vari.name, acc.chain) - - if len(personas) < 2 { - return - } - - name := personas[0].Name(t) + personas[1].Name(t) - - params := newDelegationParams{ - privKey: personas[0].PrivKey(t), - aud: personas[1].DID(t), - cmd: NominalCommand, - pol: policytest.EmptyPolicy(t), - opts: []delegation.Option{ - delegation.WithSubject(didtest.PersonaAlice.DID(t)), - delegation.WithNonce(constantNonce), - }, - } - - // Create each nominal token and continue the chain - id := g.createDelegation(t, params, name, vari) - acc.chain = append(acc.chain, id) - g.chainPersonas(t, personas[1:], acc, vari) - - // If the user is Carol, create variants for each invalid and/or optional - // parameter and also continue the chain - if personas[0] == didtest.PersonaCarol { - variants := []variant{ - {name: "InvalidExpandedCommand", variant: func(p *newDelegationParams) { - p.cmd = ExpandedCommand - }}, - {name: "ValidAttenuatedCommand", variant: func(p *newDelegationParams) { - p.cmd = AttenuatedCommand - }}, - {name: "InvalidSubject", variant: func(p *newDelegationParams) { - p.opts = append(p.opts, delegation.WithSubject(didtest.PersonaBob.DID(t))) - }}, - {name: "InvalidExpired", variant: func(p *newDelegationParams) { - p.opts = append(p.opts, delegation.WithExpiration(time.Now().Add(time.Second))) - }}, - {name: "InvalidInactive", variant: func(p *newDelegationParams) { - nbf, err := time.Parse(time.RFC3339, "2070-01-01T00:00:00Z") - require.NoError(t, err) - - p.opts = append(p.opts, delegation.WithNotBefore(nbf)) - }}, - } - - // Start a branch in the recursion for each of the variants - for _, v := range variants { - id := g.createDelegation(t, params, name, v) - - // replace the previous Carol token id with the one from the variant - acc.chain[len(acc.chain)-1] = id - g.chainPersonas(t, personas[1:], acc, v) - } - } -} - -func (g *generator) createDelegation(t *testing.T, params newDelegationParams, name string, vari variant) cid.Cid { - t.Helper() - - vari.variant(¶ms) - - tkn, err := delegation.New(params.privKey, params.aud, params.cmd, params.pol, params.opts...) - require.NoError(t, err) - - data, id, err := tkn.ToSealed(params.privKey) - require.NoError(t, err) - - require.NoError(t, os.WriteFile(filepath.Join(tokenDir, tokenNamePrefix+name+vari.name+tokenExt), data, 0o644)) - - g.dlgs = append(g.dlgs, token{ - name: tokenNamePrefix + name + vari.name, - id: id, - }) - - return id -} - -func (g *generator) createProofChain(t *testing.T, name string, prf []cid.Cid) { - t.Helper() - - if len(prf) < 1 { - return - } - - clone := make([]cid.Cid, len(prf)) - copy(clone, prf) - - g.chains = append(g.chains, proof{ - name: proorChainNamePrefix + name, - prf: clone, - }) -} - -func (g *generator) writeGoFile(t *testing.T) { - t.Helper() - - file := jen.NewFile("delegationtest") - file.HeaderComment("Code generated by delegationtest - DO NOT EDIT.") - - refs := map[cid.Cid]string{} - - 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() - } - - 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]) - } - }) - - file.Var().Id(c.name).Op("=").Index().Qual("github.com/ipfs/go-cid", "Cid").Values(g) - file.Line() - } - - require.NoError(t, file.Save("token_gen.go")) + require.NoError(t, gen.chainPersonas(didtest.Personas(), acc{}, noopVariant())) + require.NoError(t, gen.writeGoFile()) } diff --git a/token/delegation/delegationtest/token.go b/token/delegation/delegationtest/token.go index 47a77fd..7f3eb7b 100644 --- a/token/delegation/delegationtest/token.go +++ b/token/delegation/delegationtest/token.go @@ -38,27 +38,29 @@ var ProofEmpty = []cid.Cid{} //go:embed data var fs embed.FS -var ( - once sync.Once - ldr delegation.Loader - err error -) - var _ delegation.Loader = (*delegationLoader)(nil) type delegationLoader struct { tokens map[cid.Cid]*delegation.Token } +var ( + once sync.Once + ldr delegation.Loader +) + // GetDelegationLoader returns a singleton instance of a test // DelegationLoader containing all the tokens present in the data/ // directory. -func GetDelegationLoader() (delegation.Loader, error) { +func GetDelegationLoader() delegation.Loader { once.Do(func() { + var err error ldr, err = loadDelegations() + if err != nil { + panic(err) + } }) - - return ldr, err + return ldr } // GetDelegation implements invocation.DelegationLoader. @@ -101,12 +103,7 @@ func loadDelegations() (delegation.Loader, error) { // GetDelegation is a shortcut that gets (or creates) the DelegationLoader // and attempts to return the token referenced by the provided CID. func GetDelegation(id cid.Cid) (*delegation.Token, error) { - ldr, err := GetDelegationLoader() - if err != nil { - return nil, err - } - - return ldr.GetDelegation(id) + return GetDelegationLoader().GetDelegation(id) } func mustGetDelegation(id cid.Cid) *delegation.Token { @@ -114,6 +111,5 @@ func mustGetDelegation(id cid.Cid) *delegation.Token { if err != nil { panic(err) } - return tkn } diff --git a/token/delegation/delegationtest/token_gen.go b/token/delegation/delegationtest/token_gen.go index 469f385..2a24424 100644 --- a/token/delegation/delegationtest/token_gen.go +++ b/token/delegation/delegationtest/token_gen.go @@ -30,78 +30,78 @@ var ( ) var ( - TokenCarolDanInvalidExpandedCommandCID = gocid.MustParse("bafyreid3m3pk53gqgp5rlzqhvpedbwsqbidqlp4yz64vknwbzj7bxrmsr4") - TokenCarolDanInvalidExpandedCommand = mustGetDelegation(TokenCarolDanInvalidExpandedCommandCID) + TokenCarolDan_InvalidExpandedCommandCID = gocid.MustParse("bafyreid3m3pk53gqgp5rlzqhvpedbwsqbidqlp4yz64vknwbzj7bxrmsr4") + TokenCarolDan_InvalidExpandedCommand = mustGetDelegation(TokenCarolDan_InvalidExpandedCommandCID) ) var ( - TokenDanErinInvalidExpandedCommandCID = gocid.MustParse("bafyreifn4sy5onwajx3kqvot5mib6m6xarzrqjozqbzgmzpmc5ox3g2uzm") - TokenDanErinInvalidExpandedCommand = mustGetDelegation(TokenDanErinInvalidExpandedCommandCID) + TokenDanErin_InvalidExpandedCommandCID = gocid.MustParse("bafyreifn4sy5onwajx3kqvot5mib6m6xarzrqjozqbzgmzpmc5ox3g2uzm") + TokenDanErin_InvalidExpandedCommand = mustGetDelegation(TokenDanErin_InvalidExpandedCommandCID) ) var ( - TokenErinFrankInvalidExpandedCommandCID = gocid.MustParse("bafyreidmpgd36jznmq42bs34o4qi3fcbrsh4idkg6ejahudejzwb76fwxe") - TokenErinFrankInvalidExpandedCommand = mustGetDelegation(TokenErinFrankInvalidExpandedCommandCID) + TokenErinFrank_InvalidExpandedCommandCID = gocid.MustParse("bafyreidmpgd36jznmq42bs34o4qi3fcbrsh4idkg6ejahudejzwb76fwxe") + TokenErinFrank_InvalidExpandedCommand = mustGetDelegation(TokenErinFrank_InvalidExpandedCommandCID) ) var ( - TokenCarolDanValidAttenuatedCommandCID = gocid.MustParse("bafyreiekhtm237vyapk3c6voeb5lnz54crebqdqi3x4wn4u4cbrrhzsqfe") - TokenCarolDanValidAttenuatedCommand = mustGetDelegation(TokenCarolDanValidAttenuatedCommandCID) + TokenCarolDan_ValidAttenuatedCommandCID = gocid.MustParse("bafyreiekhtm237vyapk3c6voeb5lnz54crebqdqi3x4wn4u4cbrrhzsqfe") + TokenCarolDan_ValidAttenuatedCommand = mustGetDelegation(TokenCarolDan_ValidAttenuatedCommandCID) ) var ( - TokenDanErinValidAttenuatedCommandCID = gocid.MustParse("bafyreicrvzqferyy7rgo75l5rn6r2nl7zyeexxjmu3dm4ff7rn2coblj4y") - TokenDanErinValidAttenuatedCommand = mustGetDelegation(TokenDanErinValidAttenuatedCommandCID) + TokenDanErin_ValidAttenuatedCommandCID = gocid.MustParse("bafyreicrvzqferyy7rgo75l5rn6r2nl7zyeexxjmu3dm4ff7rn2coblj4y") + TokenDanErin_ValidAttenuatedCommand = mustGetDelegation(TokenDanErin_ValidAttenuatedCommandCID) ) var ( - TokenErinFrankValidAttenuatedCommandCID = gocid.MustParse("bafyreie6fhspk53kplcc2phla3e7z7fzldlbmmpuwk6nbow5q6s2zjmw2q") - TokenErinFrankValidAttenuatedCommand = mustGetDelegation(TokenErinFrankValidAttenuatedCommandCID) + TokenErinFrank_ValidAttenuatedCommandCID = gocid.MustParse("bafyreie6fhspk53kplcc2phla3e7z7fzldlbmmpuwk6nbow5q6s2zjmw2q") + TokenErinFrank_ValidAttenuatedCommand = mustGetDelegation(TokenErinFrank_ValidAttenuatedCommandCID) ) var ( - TokenCarolDanInvalidSubjectCID = gocid.MustParse("bafyreifgksz6756if42tnc6rqsnbaa2u3fdrveo7ek44lnj2d64d5sw26u") - TokenCarolDanInvalidSubject = mustGetDelegation(TokenCarolDanInvalidSubjectCID) + TokenCarolDan_InvalidSubjectCID = gocid.MustParse("bafyreifgksz6756if42tnc6rqsnbaa2u3fdrveo7ek44lnj2d64d5sw26u") + TokenCarolDan_InvalidSubject = mustGetDelegation(TokenCarolDan_InvalidSubjectCID) ) var ( - TokenDanErinInvalidSubjectCID = gocid.MustParse("bafyreibdwew5nypsxrm4fq73wu6hw3lgwwiolj3bi33xdrbgcf3ogm6fty") - TokenDanErinInvalidSubject = mustGetDelegation(TokenDanErinInvalidSubjectCID) + TokenDanErin_InvalidSubjectCID = gocid.MustParse("bafyreibdwew5nypsxrm4fq73wu6hw3lgwwiolj3bi33xdrbgcf3ogm6fty") + TokenDanErin_InvalidSubject = mustGetDelegation(TokenDanErin_InvalidSubjectCID) ) var ( - TokenErinFrankInvalidSubjectCID = gocid.MustParse("bafyreicr364mj3n7x4iyhcksxypelktcqkkw3ptg7ggxtqegw3p3mr6zc4") - TokenErinFrankInvalidSubject = mustGetDelegation(TokenErinFrankInvalidSubjectCID) + TokenErinFrank_InvalidSubjectCID = gocid.MustParse("bafyreicr364mj3n7x4iyhcksxypelktcqkkw3ptg7ggxtqegw3p3mr6zc4") + TokenErinFrank_InvalidSubject = mustGetDelegation(TokenErinFrank_InvalidSubjectCID) ) var ( - TokenCarolDanInvalidExpiredCID = gocid.MustParse("bafyreibgtlioorouqpwr6olk6boc3pprl5tx5xs6zpfnv3pvxtggueofii") - TokenCarolDanInvalidExpired = mustGetDelegation(TokenCarolDanInvalidExpiredCID) + TokenCarolDan_InvalidExpiredCID = gocid.MustParse("bafyreigenypixaxvhzlry5rjnywvjyl4xvzlzxz2ui74uzys7qdhos4bbu") + TokenCarolDan_InvalidExpired = mustGetDelegation(TokenCarolDan_InvalidExpiredCID) ) var ( - TokenDanErinInvalidExpiredCID = gocid.MustParse("bafyreidhq3hjsfrucbecgcjf2nkcgmq3sh3m5gjxz23vzcaynozs5p3uh4") - TokenDanErinInvalidExpired = mustGetDelegation(TokenDanErinInvalidExpiredCID) + TokenDanErin_InvalidExpiredCID = gocid.MustParse("bafyreifvnfb7zqocpdysedcvjkb4y7tqfuziuqjhbbdoay4zg33pwpbzqi") + TokenDanErin_InvalidExpired = mustGetDelegation(TokenDanErin_InvalidExpiredCID) ) var ( - TokenErinFrankInvalidExpiredCID = gocid.MustParse("bafyreido4om3y3ttkmp4c4gxm6pqug76vu3aekb666vdp6zewpvir5zs7u") - TokenErinFrankInvalidExpired = mustGetDelegation(TokenErinFrankInvalidExpiredCID) + TokenErinFrank_InvalidExpiredCID = gocid.MustParse("bafyreicvydzt3obkqx7krmoi3zu4tlirlksibxfks5jc7vlvjxjamv2764") + TokenErinFrank_InvalidExpired = mustGetDelegation(TokenErinFrank_InvalidExpiredCID) ) var ( - TokenCarolDanInvalidInactiveCID = gocid.MustParse("bafyreicea5y2nvlitvxijkupeavtg23i7ktjk3uejnaquguurzptiabk4u") - TokenCarolDanInvalidInactive = mustGetDelegation(TokenCarolDanInvalidInactiveCID) + TokenCarolDan_InvalidInactiveCID = gocid.MustParse("bafyreicea5y2nvlitvxijkupeavtg23i7ktjk3uejnaquguurzptiabk4u") + TokenCarolDan_InvalidInactive = mustGetDelegation(TokenCarolDan_InvalidInactiveCID) ) var ( - TokenDanErinInvalidInactiveCID = gocid.MustParse("bafyreifsgqzkmxj2vexuts3z766mwcjreiisjg2jykyzf7tbj5sclutpvq") - TokenDanErinInvalidInactive = mustGetDelegation(TokenDanErinInvalidInactiveCID) + TokenDanErin_InvalidInactiveCID = gocid.MustParse("bafyreifsgqzkmxj2vexuts3z766mwcjreiisjg2jykyzf7tbj5sclutpvq") + TokenDanErin_InvalidInactive = mustGetDelegation(TokenDanErin_InvalidInactiveCID) ) var ( - TokenErinFrankInvalidInactiveCID = gocid.MustParse("bafyreifbfegon24c6dndiqyktahzs65vhyasrygbw7nhsvojn6distsdre") - TokenErinFrankInvalidInactive = mustGetDelegation(TokenErinFrankInvalidInactiveCID) + TokenErinFrank_InvalidInactiveCID = gocid.MustParse("bafyreifbfegon24c6dndiqyktahzs65vhyasrygbw7nhsvojn6distsdre") + TokenErinFrank_InvalidInactive = mustGetDelegation(TokenErinFrank_InvalidInactiveCID) ) var ProofAliceBob = []gocid.Cid{ @@ -134,107 +134,107 @@ var ProofAliceBobCarolDanErinFrank = []gocid.Cid{ TokenAliceBobCID, } -var ProofAliceBobCarolDanInvalidExpandedCommand = []gocid.Cid{ - TokenCarolDanInvalidExpandedCommandCID, +var ProofAliceBobCarolDan_InvalidExpandedCommand = []gocid.Cid{ + TokenCarolDan_InvalidExpandedCommandCID, TokenBobCarolCID, TokenAliceBobCID, } -var ProofAliceBobCarolDanErinInvalidExpandedCommand = []gocid.Cid{ - TokenDanErinInvalidExpandedCommandCID, - TokenCarolDanInvalidExpandedCommandCID, +var ProofAliceBobCarolDanErin_InvalidExpandedCommand = []gocid.Cid{ + TokenDanErin_InvalidExpandedCommandCID, + TokenCarolDan_InvalidExpandedCommandCID, TokenBobCarolCID, TokenAliceBobCID, } -var ProofAliceBobCarolDanErinFrankInvalidExpandedCommand = []gocid.Cid{ - TokenErinFrankInvalidExpandedCommandCID, - TokenDanErinInvalidExpandedCommandCID, - TokenCarolDanInvalidExpandedCommandCID, +var ProofAliceBobCarolDanErinFrank_InvalidExpandedCommand = []gocid.Cid{ + TokenErinFrank_InvalidExpandedCommandCID, + TokenDanErin_InvalidExpandedCommandCID, + TokenCarolDan_InvalidExpandedCommandCID, TokenBobCarolCID, TokenAliceBobCID, } -var ProofAliceBobCarolDanValidAttenuatedCommand = []gocid.Cid{ - TokenCarolDanValidAttenuatedCommandCID, +var ProofAliceBobCarolDan_ValidAttenuatedCommand = []gocid.Cid{ + TokenCarolDan_ValidAttenuatedCommandCID, TokenBobCarolCID, TokenAliceBobCID, } -var ProofAliceBobCarolDanErinValidAttenuatedCommand = []gocid.Cid{ - TokenDanErinValidAttenuatedCommandCID, - TokenCarolDanValidAttenuatedCommandCID, +var ProofAliceBobCarolDanErin_ValidAttenuatedCommand = []gocid.Cid{ + TokenDanErin_ValidAttenuatedCommandCID, + TokenCarolDan_ValidAttenuatedCommandCID, TokenBobCarolCID, TokenAliceBobCID, } -var ProofAliceBobCarolDanErinFrankValidAttenuatedCommand = []gocid.Cid{ - TokenErinFrankValidAttenuatedCommandCID, - TokenDanErinValidAttenuatedCommandCID, - TokenCarolDanValidAttenuatedCommandCID, +var ProofAliceBobCarolDanErinFrank_ValidAttenuatedCommand = []gocid.Cid{ + TokenErinFrank_ValidAttenuatedCommandCID, + TokenDanErin_ValidAttenuatedCommandCID, + TokenCarolDan_ValidAttenuatedCommandCID, TokenBobCarolCID, TokenAliceBobCID, } -var ProofAliceBobCarolDanInvalidSubject = []gocid.Cid{ - TokenCarolDanInvalidSubjectCID, +var ProofAliceBobCarolDan_InvalidSubject = []gocid.Cid{ + TokenCarolDan_InvalidSubjectCID, TokenBobCarolCID, TokenAliceBobCID, } -var ProofAliceBobCarolDanErinInvalidSubject = []gocid.Cid{ - TokenDanErinInvalidSubjectCID, - TokenCarolDanInvalidSubjectCID, +var ProofAliceBobCarolDanErin_InvalidSubject = []gocid.Cid{ + TokenDanErin_InvalidSubjectCID, + TokenCarolDan_InvalidSubjectCID, TokenBobCarolCID, TokenAliceBobCID, } -var ProofAliceBobCarolDanErinFrankInvalidSubject = []gocid.Cid{ - TokenErinFrankInvalidSubjectCID, - TokenDanErinInvalidSubjectCID, - TokenCarolDanInvalidSubjectCID, +var ProofAliceBobCarolDanErinFrank_InvalidSubject = []gocid.Cid{ + TokenErinFrank_InvalidSubjectCID, + TokenDanErin_InvalidSubjectCID, + TokenCarolDan_InvalidSubjectCID, TokenBobCarolCID, TokenAliceBobCID, } -var ProofAliceBobCarolDanInvalidExpired = []gocid.Cid{ - TokenCarolDanInvalidExpiredCID, +var ProofAliceBobCarolDan_InvalidExpired = []gocid.Cid{ + TokenCarolDan_InvalidExpiredCID, TokenBobCarolCID, TokenAliceBobCID, } -var ProofAliceBobCarolDanErinInvalidExpired = []gocid.Cid{ - TokenDanErinInvalidExpiredCID, - TokenCarolDanInvalidExpiredCID, +var ProofAliceBobCarolDanErin_InvalidExpired = []gocid.Cid{ + TokenDanErin_InvalidExpiredCID, + TokenCarolDan_InvalidExpiredCID, TokenBobCarolCID, TokenAliceBobCID, } -var ProofAliceBobCarolDanErinFrankInvalidExpired = []gocid.Cid{ - TokenErinFrankInvalidExpiredCID, - TokenDanErinInvalidExpiredCID, - TokenCarolDanInvalidExpiredCID, +var ProofAliceBobCarolDanErinFrank_InvalidExpired = []gocid.Cid{ + TokenErinFrank_InvalidExpiredCID, + TokenDanErin_InvalidExpiredCID, + TokenCarolDan_InvalidExpiredCID, TokenBobCarolCID, TokenAliceBobCID, } -var ProofAliceBobCarolDanInvalidInactive = []gocid.Cid{ - TokenCarolDanInvalidInactiveCID, +var ProofAliceBobCarolDan_InvalidInactive = []gocid.Cid{ + TokenCarolDan_InvalidInactiveCID, TokenBobCarolCID, TokenAliceBobCID, } -var ProofAliceBobCarolDanErinInvalidInactive = []gocid.Cid{ - TokenDanErinInvalidInactiveCID, - TokenCarolDanInvalidInactiveCID, +var ProofAliceBobCarolDanErin_InvalidInactive = []gocid.Cid{ + TokenDanErin_InvalidInactiveCID, + TokenCarolDan_InvalidInactiveCID, TokenBobCarolCID, TokenAliceBobCID, } -var ProofAliceBobCarolDanErinFrankInvalidInactive = []gocid.Cid{ - TokenErinFrankInvalidInactiveCID, - TokenDanErinInvalidInactiveCID, - TokenCarolDanInvalidInactiveCID, +var ProofAliceBobCarolDanErinFrank_InvalidInactive = []gocid.Cid{ + TokenErinFrank_InvalidInactiveCID, + TokenDanErin_InvalidInactiveCID, + TokenCarolDan_InvalidInactiveCID, TokenBobCarolCID, TokenAliceBobCID, } diff --git a/token/delegation/delegationtest/token_test.go b/token/delegation/delegationtest/token_test.go index fde6749..b24a02c 100644 --- a/token/delegation/delegationtest/token_test.go +++ b/token/delegation/delegationtest/token_test.go @@ -12,8 +12,6 @@ import ( ) func TestGetDelegation(t *testing.T) { - t.Parallel() - t.Run("passes with valid CID", func(t *testing.T) { t.Parallel() diff --git a/token/invocation/invocation_test.go b/token/invocation/invocation_test.go index ea0ba2a..20ce554 100644 --- a/token/invocation/invocation_test.go +++ b/token/invocation/invocation_test.go @@ -39,7 +39,7 @@ func TestToken_ExecutionAllowed(t *testing.T) { t.Run("passes - proof chain attenuates command", func(t *testing.T) { t.Parallel() - testPasses(t, didtest.PersonaFrank, delegationtest.AttenuatedCommand, emptyArguments, delegationtest.ProofAliceBobCarolDanErinFrankValidAttenuatedCommand) + testPasses(t, didtest.PersonaFrank, delegationtest.AttenuatedCommand, emptyArguments, delegationtest.ProofAliceBobCarolDanErinFrank_ValidAttenuatedCommand) }) t.Run("passes - invocation attenuates command", func(t *testing.T) { @@ -67,14 +67,14 @@ func TestToken_ExecutionAllowed(t *testing.T) { t.Run("fails - referenced delegation expired", func(t *testing.T) { t.Parallel() - testFails(t, invocation.ErrTokenInvalidNow, didtest.PersonaFrank, delegationtest.NominalCommand, emptyArguments, delegationtest.ProofAliceBobCarolDanErinFrankInvalidExpired) + testFails(t, invocation.ErrTokenInvalidNow, didtest.PersonaFrank, delegationtest.NominalCommand, emptyArguments, delegationtest.ProofAliceBobCarolDanErinFrank_InvalidExpired) }) t.Run("fails - referenced delegation inactive", func(t *testing.T) { t.Parallel() - testFails(t, invocation.ErrTokenInvalidNow, didtest.PersonaFrank, delegationtest.NominalCommand, emptyArguments, delegationtest.ProofAliceBobCarolDanErinFrankInvalidInactive) + testFails(t, invocation.ErrTokenInvalidNow, didtest.PersonaFrank, delegationtest.NominalCommand, emptyArguments, delegationtest.ProofAliceBobCarolDanErinFrank_InvalidInactive) }) t.Run("fails - last (or only) delegation not root", func(t *testing.T) { @@ -101,7 +101,7 @@ func TestToken_ExecutionAllowed(t *testing.T) { t.Run("fails - proof chain expands command", func(t *testing.T) { t.Parallel() - testFails(t, invocation.ErrCommandNotCovered, didtest.PersonaFrank, delegationtest.NominalCommand, emptyArguments, delegationtest.ProofAliceBobCarolDanErinFrankInvalidExpandedCommand) + testFails(t, invocation.ErrCommandNotCovered, didtest.PersonaFrank, delegationtest.NominalCommand, emptyArguments, delegationtest.ProofAliceBobCarolDanErinFrank_InvalidExpandedCommand) }) t.Run("fails - invocation expands command", func(t *testing.T) { @@ -113,20 +113,19 @@ func TestToken_ExecutionAllowed(t *testing.T) { t.Run("fails - inconsistent subject", func(t *testing.T) { t.Parallel() - testFails(t, invocation.ErrWrongSub, didtest.PersonaFrank, delegationtest.ExpandedCommand, emptyArguments, delegationtest.ProofAliceBobCarolDanErinFrankInvalidSubject) + testFails(t, invocation.ErrWrongSub, didtest.PersonaFrank, delegationtest.ExpandedCommand, emptyArguments, delegationtest.ProofAliceBobCarolDanErinFrank_InvalidSubject) }) } 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...) + // TODO: use the args and add minimal test to check that they are verified against the policy + + tkn, err := invocation.New(persona.DID(), didtest.PersonaAlice.DID(), cmd, prf, opts...) require.NoError(t, err) - ldr, err := delegationtest.GetDelegationLoader() - require.NoError(t, err) - - return tkn.ExecutionAllowed(ldr) + return tkn.ExecutionAllowed(delegationtest.GetDelegationLoader()) } func testFails(t *testing.T, expErr error, persona didtest.Persona, cmd command.Command, args *args.Args, prf []cid.Cid, opts ...invocation.Option) { From c19e38356dc6b7725a86c7b92a27ac815177d0ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Wed, 20 Nov 2024 15:35:33 +0100 Subject: [PATCH 13/13] delegationtest: make the generator a main() --- token/delegation/delegationtest/doc.go | 2 +- .../{ => generator}/generator.go | 14 +++++----- .../delegationtest/generator/main.go | 17 ++++++++++++ .../delegationtest/generator_test.go | 26 ------------------- token/delegation/delegationtest/token.go | 11 +++----- 5 files changed, 30 insertions(+), 40 deletions(-) rename token/delegation/delegationtest/{ => generator}/generator.go (93%) create mode 100644 token/delegation/delegationtest/generator/main.go delete mode 100644 token/delegation/delegationtest/generator_test.go diff --git a/token/delegation/delegationtest/doc.go b/token/delegation/delegationtest/doc.go index cb54e64..526c6fa 100644 --- a/token/delegation/delegationtest/doc.go +++ b/token/delegation/delegationtest/doc.go @@ -22,7 +22,7 @@ // tokens stored in the data/ directory should be regenerated by running // the following command in this directory: // -// go test . -update +// cd generator && go run . // // Generated delegation Tokens are stored in the data/ directory and loaded // into the delegation.Loader. diff --git a/token/delegation/delegationtest/generator.go b/token/delegation/delegationtest/generator/generator.go similarity index 93% rename from token/delegation/delegationtest/generator.go rename to token/delegation/delegationtest/generator/generator.go index 06fcdc4..2eaacb2 100644 --- a/token/delegation/delegationtest/generator.go +++ b/token/delegation/delegationtest/generator/generator.go @@ -1,4 +1,4 @@ -package delegationtest +package main import ( "os" @@ -15,11 +15,13 @@ import ( "github.com/ucan-wg/go-ucan/pkg/command" "github.com/ucan-wg/go-ucan/pkg/policy" "github.com/ucan-wg/go-ucan/token/delegation" + "github.com/ucan-wg/go-ucan/token/delegation/delegationtest" ) const ( tokenNamePrefix = "Token" proorChainNamePrefix = "Proof" + tokenExt = ".dagcbor" ) var constantNonce = []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b} @@ -83,7 +85,7 @@ func (g *generator) chainPersonas(personas []didtest.Persona, acc acc, vari vari params := newDelegationParams{ privKey: personas[0].PrivKey(), aud: personas[1].DID(), - cmd: NominalCommand, + cmd: delegationtest.NominalCommand, pol: policy.Policy{}, opts: []delegation.Option{ delegation.WithSubject(didtest.PersonaAlice.DID()), @@ -107,10 +109,10 @@ func (g *generator) chainPersonas(personas []didtest.Persona, acc acc, vari vari if personas[0] == didtest.PersonaCarol { variants := []variant{ {name: "InvalidExpandedCommand", variant: func(p *newDelegationParams) { - p.cmd = ExpandedCommand + p.cmd = delegationtest.ExpandedCommand }}, {name: "ValidAttenuatedCommand", variant: func(p *newDelegationParams) { - p.cmd = AttenuatedCommand + p.cmd = delegationtest.AttenuatedCommand }}, {name: "InvalidSubject", variant: func(p *newDelegationParams) { p.opts = append(p.opts, delegation.WithSubject(didtest.PersonaBob.DID())) @@ -164,7 +166,7 @@ func (g *generator) createDelegation(params newDelegationParams, name string, va dlgName += "_" + vari.name } - err = os.WriteFile(filepath.Join(tokenDir, dlgName+tokenExt), data, 0o644) + err = os.WriteFile(filepath.Join("..", delegationtest.TokenDir, dlgName+tokenExt), data, 0o644) if err != nil { return cid.Undef, err } @@ -223,5 +225,5 @@ func (g *generator) writeGoFile() error { file.Line() } - return file.Save("token_gen.go") + return file.Save("../token_gen.go") } diff --git a/token/delegation/delegationtest/generator/main.go b/token/delegation/delegationtest/generator/main.go new file mode 100644 index 0000000..994ae42 --- /dev/null +++ b/token/delegation/delegationtest/generator/main.go @@ -0,0 +1,17 @@ +package main + +import ( + "github.com/ucan-wg/go-ucan/did/didtest" +) + +func main() { + gen := &generator{} + err := gen.chainPersonas(didtest.Personas(), acc{}, noopVariant()) + if err != nil { + panic(err) + } + err = gen.writeGoFile() + if err != nil { + panic(err) + } +} diff --git a/token/delegation/delegationtest/generator_test.go b/token/delegation/delegationtest/generator_test.go deleted file mode 100644 index 2c79608..0000000 --- a/token/delegation/delegationtest/generator_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package delegationtest - -import ( - "testing" - - "github.com/stretchr/testify/require" - "gotest.tools/v3/golden" - - "github.com/ucan-wg/go-ucan/did/didtest" -) - -// TestUpdate doesn't actually run a test but uses the Go testing library -// to trigger generation of the delegation tokens and associated Go file. -func TestUpdate(t *testing.T) { - if golden.FlagUpdate() { - update(t) - } -} - -func update(t *testing.T) { - t.Helper() - - gen := &generator{} - require.NoError(t, gen.chainPersonas(didtest.Personas(), acc{}, noopVariant())) - require.NoError(t, gen.writeGoFile()) -} diff --git a/token/delegation/delegationtest/token.go b/token/delegation/delegationtest/token.go index 7f3eb7b..e425733 100644 --- a/token/delegation/delegationtest/token.go +++ b/token/delegation/delegationtest/token.go @@ -11,11 +11,6 @@ import ( "github.com/ucan-wg/go-ucan/token/delegation" ) -const ( - tokenDir = "data" - tokenExt = ".dagcbor" -) - var ( // ExpandedCommand is the parent of the NominalCommand and represents // the cases where the delegation proof-chain or invocation token tries @@ -35,6 +30,8 @@ var ( // ProofEmpty provides an empty proof chain for testing purposes. var ProofEmpty = []cid.Cid{} +const TokenDir = "data" + //go:embed data var fs embed.FS @@ -74,7 +71,7 @@ func (l *delegationLoader) GetDelegation(id cid.Cid) (*delegation.Token, error) } func loadDelegations() (delegation.Loader, error) { - dirEntries, err := fs.ReadDir("data") + dirEntries, err := fs.ReadDir(TokenDir) if err != nil { return nil, err } @@ -82,7 +79,7 @@ func loadDelegations() (delegation.Loader, error) { tkns := make(map[cid.Cid]*delegation.Token, len(dirEntries)) for _, dirEntry := range dirEntries { - data, err := fs.ReadFile(filepath.Join(tokenDir, dirEntry.Name())) + data, err := fs.ReadFile(filepath.Join(TokenDir, dirEntry.Name())) if err != nil { return nil, err }